package uk.me.nxg.unity;

import java.util.List;

/**
 * A parser for unit strings.  The parser will parse a single string.
 *
 * <p>There are a number of different parsers available,
 * enumerated in {@link Syntax}.
 *
 * <p>See the {@link uk.me.nxg.unity} package documentation
 * for fuller discussion of the grammars.
 */
public class UnitParser {

    private String inputString;
    private final Syntax syntax;
    private final Parser yyparser;

    private UnitExpr parseResult = null;

    private Yylex helperLexer;  // for debugging, only

    protected boolean guessUnits = false; // should we guess units when parsing?

    /**
     * Create a new unit parser and parse a unit string.
     *
     * <p>The possible parser syntaxes are available from
     * {@link Syntax#values()}, or can alternatively be specified using the
     * values enumerated in {@link Syntax}.
     *
     * @param syntax one of the known syntaxes
     * @param unitString the string to be parsed, which must be non-null
     * @throws UnitParserException if no parser can be created
     * (typically because the required syntax is unknown),
     * or if the string is invalid
     */
    public UnitParser(Syntax syntax, String unitString)
            throws UnitParserException {
        this(syntax);

        parse(unitString);

        assert helperLexer == null;
        assert inputString != null;
        assert parseResult != null;
    }

    /**
     * Create a new unit parser.
     *
     * <p>The possible parser syntaxes are available from
     * {@link Syntax#values()}, or can alternatively be specified using the
     * values enumerated in {@link Syntax}.
     *
     * @param syntax one of the known syntaxes
     * @throws UnitParserException if no parser can be created
     * (typically because the required syntax is unknown)
     */
    public UnitParser(Syntax syntax)
            throws UnitParserException {
        if (syntax == null) {
            throw new IllegalArgumentException("null parser type");
        }
        this.syntax = syntax;
        inputString = null;

        yyparser = SyntaxFactory.createParser(this.syntax, null);
        // createParser must succeed for all valid syntaxes
        assert yyparser != null;
    }

    /**
     * Parse a unit string.
     * @param unitString the non-null string to be parsed, such as {@code m/s-1}
     * @return a parsed unit
     * @throws UnitParserException if the parse fails
     */
    public UnitExpr parse(String unitString)
            throws UnitParserException {
        if (unitString == null) {
            throw new IllegalArgumentException("parse must have a non-null argument");
        }
        if (helperLexer != null) {
            throw new IllegalStateException("Can't mix getLexeme and getParsed in UnitParser");
        }

        inputString = unitString;

        yyparser.setReader(new java.io.StringReader(unitString), syntax);
        yyparser.setGuessing(guessUnits);
        yyparser.parse();
        parseResult = yyparser.getParseResult();
        assert parseResult != null;

        return parseResult;
    }

    /**
     * Prepare to lex a unit string.
     * After this is called, then {@link #getLexeme} may be repeatedly
     * called to return one lexeme after another.
     * This is primarily a debugging method.
     * @param unitString the string to return lexemes from
     */
    public void lex(String unitString) {
        if (parseResult != null) {
            throw new IllegalStateException("Can't mix getLexeme and getParsed in UnitParser");
        }
        inputString = unitString;
        yyparser.setReader(new java.io.StringReader(unitString), syntax);
        helperLexer = yyparser.lexer;
    }

    /**
     * Retrieve the parsed result.
     * <p>This will either be the result of the last call of the {@link parse} method,
     * or the parsed result if the object was created with {@link #UnitParser(Syntax,String)}.
     *
     * @throws UnitParserException if the parse fails
     * @return a unit expression, which will not be null
     */
    public UnitExpr getParsed()
            throws UnitParserException
    {
        if (helperLexer != null) {
            throw new IllegalStateException("Can't mix getLexeme and getParsed in UnitParser");
        }

        if (parseResult == null) {
            yyparser.parse();
            parseResult = yyparser.getParseResult();
            assert parseResult != null;
        }
        return parseResult;
    }

    /**
     * Indicates whether the argument is the name of a known parser.
     * @param parser the string name of a parser (eg "fits")
     * @return true if the parser corresponds to the name of one of the elements of {@link Syntax}
     */
    public static boolean isKnownParser(String parser) {
        return Syntax.lookup(parser) != null;
    }

    /**
     * When parsing units, should we try to guess units?
     * By default, we don't guess.  But if we are asked to guess, then
     * unrecognised units will be re-examined with some set of
     * heuristics so that, for example, ‘degrees’ will be recognised
     * as the degree of angle.  The set of heuristics is not specified
     * here.
     *
     * <p>The guessing process is agnostic about which syntax it gets
     * its guesses from.  That is, even when parsing in the CDS
     * syntax, we will still guess {@code ergs} as the Erg, which the
     * CDS syntax doesn't otherwise recognise.
     *
     * <p>A guessed unit will always be a recognised unit in one or
     * other syntax, but not necessarily the syntax this parser is
     * configured to use.  A quoted unit is never guessed (because
     * it's by definition not a recognised unit, and because quoting
     * is not attempted for such explicitly-marked units).
     *
     * @param guess_p true to turn on guessing
     */
    public void setGuessing(boolean guess_p) {
        guessUnits = guess_p;
    }

    /**
     * Return successive lexemes from the input.
     * This must follow a call to {@link #lex}.
     * Primarily for debugging.
     * @return a Lexeme object, or null when the lexemes are exhausted
     * @throws UnitParserException if there is a lexical error within the input
     */
    public UnitParser.Lexeme getLexeme()
            throws UnitParserException {
        if (parseResult != null) {
            throw new IllegalStateException("Can't mix getLexeme and getParsed in UnitParser");
        }

        int token;
        try {
            if ((token = helperLexer.yylex()) == 0) {
                helperLexer = null;
                return null;
            } else {
                return new UnitParser.Lexeme(token, yyparser.yylval);
            }
        } catch (java.io.IOException e) {
            System.err.println("IOException: " + e);
            helperLexer = null;
            return null;
        }
    }

    /**
     * The library main program, for exploring the library's functionality.
     * <p>Usage:
     * <pre>
     * UnitParser [-isyntax] [-osyntax] [-v] unit-expression
     * </pre>
     * <p>Parse and redisplay the string expression given as argument.
     * The <code>-i</code> and <code>-o</code> flags specify the
     * input and output syntaxes.
     * <pre>
     * UnitParser -S
     * </pre>
     * <p>List the set of available syntaxes
     * @param args the command-line arguments and options
     */
    public static void main(String[] args) {
        Syntax inputSyntax = Syntax.FITS;
        Syntax outputSyntax = Syntax.FITS;
        boolean showSyntaxes = false;
        boolean showLexemes = false;
        boolean validateParse = false;
        boolean guessUnits = false;
        List<String> unitExpressions = new java.util.ArrayList<String>();

        for (String arg : args) {
            if (arg.charAt(0) == '-') {
                // Yes, there are getopt packages for Java, but (a) I
                // don't want anything fancy, and (b) the one I tried
                // was GPL and, on reflection, I think I'd best
                // release this as BSD.
                if (arg.length() < 2) {
                    Usage();
                }
                switch (arg.charAt(1)) {
                  case 'i':
                    inputSyntax = Syntax.lookup(arg.substring(2));
                    if (inputSyntax == null) {
                        System.err.println("Unrecognised input syntax: " + arg.substring(2));
                        System.exit(1);
                    }
                    break;
                  case 'o':
                    outputSyntax = Syntax.lookup(arg.substring(2));
                    if (outputSyntax == null) {
                        System.err.println("Unrecognised output syntax: " + arg.substring(2));
                        System.exit(1);
                    }
                    break;
                  case 'g':
                    guessUnits = true;
                    break;
                  case 'S':
                    showSyntaxes = true;
                    break;
                  case 'l': // undocumented option
                    showLexemes = true;
                    break;
                  case 'v':
                    validateParse = true;
                    break;
                  case 'V':
                    System.out.println(Version.versionString());
                    System.exit(0);
                    break;      // redundant, but keeps the compiler happy
                  default:
                    Usage();
                    break;
                }
            } else {
                unitExpressions.add(arg);
            }
        }

        if (!(showSyntaxes || unitExpressions.size() > 0)) {
            Usage();
        }

        if (showSyntaxes) {
            for (Syntax syntax : Syntax.values()) {
                System.out.print(" " + syntax);
            }
            System.out.println();
        } else if (showLexemes) {
            try {
                UnitParser p = new UnitParser(inputSyntax);
                p.lex(unitExpressions.get(0));
                UnitParser.Lexeme l;
                while ((l = p.getLexeme()) != null) {
                    System.out.println(l);
                }
            } catch (UnitParserException e) {
                System.err.println("Can't lex expression \"" + unitExpressions.get(0) + "\": " + e);
            }
        } else {
            try {
                UnitParser p = new UnitParser(inputSyntax);
                p.setGuessing(guessUnits);

                for (String expr : unitExpressions) {
                    UnitExpr e = p.parse(expr);
                    System.out.println(e.toString(outputSyntax));
                    if (validateParse) {
                        boolean allRecognised = e.allUnitsRecognised(inputSyntax);
                        boolean allRecognisedWithGuess = e.allUnitsRecognised(inputSyntax, true);

                        System.out.println("Checking <" + expr + ">, in input syntax "
                                           + inputSyntax + ":");
                        System.out.println("check: all units recognised?        "
                                           + (allRecognised ? "yes" : "no"));
                        if (!allRecognised && guessUnits) {
                            System.out.println("       ...with guessing?            "
                                               + (allRecognisedWithGuess ? "yes" : "no"));
                        }
                        System.out.println("check: all units recommended?       "
                                           + (e.allUnitsRecommended(inputSyntax) ? "yes" : "no"));
                        System.out.println("check: all constraints satisfied?   "
                                           + (e.allUsageConstraintsSatisfied(inputSyntax) ? "yes" : "no"));

                        System.out.println("Result:");
                        for (OneUnit u : e) {
                            StringBuilder sb = new StringBuilder();
                            java.util.Formatter f = new java.util.Formatter(sb);
                            f.format("  %-20s (%s%s)%s%s", u,
                                     (u.getPrefix() == 0.0 ? "" : "10^"+u.getPrefix()+" "),
                                     u.getBaseUnitName(),
                                     (u.getExponent() == 1.0 ? "" : "^"+u.getExponent()),
                                     (u.wasGuessed() ? ", guessed" : ""));
                            UnitDefinition ud = u.getBaseUnitDefinition();
                            if (ud == null) {
                                f.format("\n    (unrecognised)");
                            } else {
                                f.format("\n    %s / %s", ud, ud.getRepresentation(Syntax.VOUNITS));
                            }
                            System.out.println(f.toString());
                        }
                    }
                }
            } catch (UnitParserException e) {
                    System.err.println("Error parsing units: " + e.getMessage());
            }
        }
    }

    private static void Usage()
    {
        System.err.println(Version.versionString());
        System.err.println("Usage:");
        System.err.println("    UnitParser [-isyntax] [-osyntax] [-v] [-g] unit-expression ...");
        System.err.println("    UnitParser -S     : show syntaxes");
        System.err.println("    UnitParser -V     : show version");
        System.err.println();
        System.err.println("Options:");
        System.err.println("  -i, -o   : input or output syntax");
        System.err.println("  -g       : try guessing unrecognised units");
        System.err.println("  -v       : validate the parsed expression");
        System.exit(1);
    }

    /**
     * A single lexeme.
     * This is primarily for debugging, and may be removed or hidden in future.
     */
    public static class Lexeme {
        public final int token;
        public final UnitVal value;
        private Lexeme(int token, UnitVal value) {
            this.token = token;
            this.value = value;
        }
        public String toString() {
            Token t = Token.lookup(token);
            if (t.type == null) {
                return t.name;
            } else {
                return String.format("%s: %s", t.name, value.toString());
                //return String.format("%s: %s (%s)", t.name, value.toString(), t.type);
            }
        }
    }
}
