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 final String inputString;
    private final Syntax syntax;
    private final Parser yyparser;

    private UnitExpr parseResult = null;

    private Yylex helperLexer;  // for debugging, only

    /**
     * Create a new unit parser.
     * 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
     * @throws UnitParserException if no parser can be created (typically because the required syntax is unknown)
     */
    public UnitParser(Syntax syntax, String unitString)
            throws UnitParserException {
        if (syntax == null) {
            throw new IllegalArgumentException("null parser type");
        }
        this.syntax = syntax;

        inputString = unitString;

        yyparser = SyntaxFactory.createParser(this.syntax,
                                              new java.io.StringReader(inputString));
        // createParser must succeed for all valid syntaxes
        assert yyparser != null;

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

    /**
     * Retrieve the parsed result
     * @throws UnitParserException if the parse fails
     */
    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();
        }
        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;
    }        

    /**
     * Return successive lexemes from the input.
     * Primarily for debugging.
     * @return a Lexeme object, or null when the lexemes are exhausted
     */
    public UnitParser.Lexeme getLexeme()
            throws UnitParserException {
        if (parseResult != null) {
            throw new IllegalStateException("Can't mix getLexeme and getParsed in UnitParser");
        }

        if (helperLexer == null) {
            helperLexer = yyparser.lexer;
        }

        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
     */
    public static void main(String[] args) {
        Syntax inputSyntax = Syntax.FITS;
        Syntax outputSyntax = Syntax.FITS;
        boolean showSyntaxes = false;
        boolean showLexemes = false;
        boolean validateParse = false;
        String unitExpression = null;

        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 'S':
                    showSyntaxes = true;
                    break;
                  case 'l':
                    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 {
                if (unitExpression != null) {
                    Usage();
                } else {
                    unitExpression = arg;
                }
            }
        }
        if (! (showSyntaxes || unitExpression != null)) {
            Usage();
        }

        if (showSyntaxes) {
            for (Syntax syntax : Syntax.values()) {
                System.out.print(" " + syntax);
            }
            System.out.println();
        } else if (showLexemes) {
            try {
                UnitParser p = new UnitParser(inputSyntax, unitExpression);
                UnitParser.Lexeme l;
                while ((l = p.getLexeme()) != null) {
                    System.out.println(l);
                }
            } catch (UnitParserException e) {
                System.err.println("Can't lex expression \"" + unitExpression + "\": " + e);
            }
        } else {
            try {
                UnitParser p = new UnitParser(inputSyntax, unitExpression);
                UnitExpr e = p.getParsed();
                System.out.println(e.toString(outputSyntax));
                if (validateParse) {
                    System.out.println("check: all units recognised?        " 
                                       + (e.allUnitsRecognised(inputSyntax) ? "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"));
                }
            } 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] unit-expression");
        System.err.println("    UnitParser -S     : show syntaxes");
        System.err.println("    UnitParser -V     : show version");
        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() {
            if (value.type() == UnitVal.Type.EMPTY) {
                return Token.lookup(token);
            } else {
                return Token.lookup(token) + ": " + value.toString();
            }
        }
    }
}
