/*
 * This module performs the tests described in src/grammar/testcases.csv.
 *
 * Usage: java -cp ... ParseAll -t -pxxx ../../grammar/testcases-xxx.csv
 *
 * Here, 'xxx' is one of the supported parsers: fits, ogip, cds or vounit.
 * The -p option may appear more than once, which causes the test
 * cases to be run for each of the indicated syntaxes.
 *
 * The structure of this program is tightly coupled to the the set of
 * tests in src/grammar/testcases.csv, and you should consult the
 * documentation at the top of that file to understand what this
 * program is trying to do.
 *
 * Note that the testcases.csv file must be run through the stripat
 * program, to generate the per-syntax tests which are the actual
 * input to this file.  See the testcases-%.csv target in
 * src/grammar/Makefile for an indication of how to do that.
 */

import uk.me.nxg.unity.*;
import uk.me.nxg.unity.util.SimpleCSVReader;

import java.util.List;

public class ParseAll
{
    enum Action {
        UNKNOWN,
        LIST_LEXEMES,
        DISPLAY_PARSE,
        RUN_TESTS };

    public static void main(String[] args)
    {
        Action action = Action.UNKNOWN;

        String inputString = null;
        char optionChar;

        // list of parsers to test
        List<String> parserNames = new java.util.ArrayList<String>();

        boolean finalStatusOK = true;

        for (int i=0; i<args.length; i++) {
            if (args[i].charAt(0) == '-') {
                switch (args[i].charAt(1)) {
                  case 'l':
                    action = Action.LIST_LEXEMES;
                    break;
                  case 'd':
                    action = Action.DISPLAY_PARSE;
                    break;
                  case 't':
                    action = Action.RUN_TESTS;
                    break;
                  case 'p':
                    parserNames.add(args[i].substring(2));
                    break;
                  case 'S':
                    for (Syntax s : Syntax.values()) {
                        System.out.println("parser: " + s);
                    }
                    System.exit(0);
                    break;
                  default:
                    Usage();
                    break;
                }
            } else {
                inputString = args[i];
            }
        }

        if (inputString == null || action == Action.UNKNOWN) {
            Usage();
        }

        if (parserNames.isEmpty()) {
            parserNames.add("fits");
        }

        try {
            for (String p : parserNames) {
                boolean ok = true;

                switch (action) {
                  case LIST_LEXEMES:
                    ok = listLexemes(inputString, p);
                    break;
                  case DISPLAY_PARSE:
                    // parse the input string
                    // and display the contents in an unambiguous way
                    ok = displayParse(inputString, p);
                    break;
                  case RUN_TESTS:
                    // if the inputString (indicating a filename) has a
                    // '%' in it, that's substituted by the corresponding
                    // parser name
                    ok = runTestsInFile(inputString, p);
                    break;
                  default:
                    Usage();
                }
                if (! ok) {
                    finalStatusOK = false;
                }
            }
        } catch (UnitParserException e) {
            System.err.println("Unit parser exception: " + e);
        }

        System.exit(finalStatusOK ? 0 : 1);
    }

    /**
     * List lexemes in the inputString, for debugging
     */
    private static boolean listLexemes(String inputString, String syntaxName)
            throws UnitParserException {
        Syntax syntax = Syntax.lookup(syntaxName);
        if (syntax == null) {
            System.err.println("Unrecognised syntax: " + syntaxName);
            return false;
        }
        UnitParser p = new UnitParser(syntax, inputString);
        UnitParser.Lexeme lexeme;
        while ((lexeme = p.getLexeme()) != null) {
            System.out.println(lexeme);
        }
        return true;
    }

    /**
     * Display the parsed version of the input string in an unambiguous way
     */
    private static boolean displayParse(String inputString, String parser) {
        Syntax stx = Syntax.lookup(parser);
        if (stx == null) {
            System.err.println("Bad parser name: " + parser);
            return false;
        }

        try {
            UnitExpr units = new UnitParser(stx, inputString).getParsed();
            System.out.print(units + ":");
            for (OneUnit u : units) {
                System.out.print("  " + u.toDebugString());
            }
            System.out.println();
        } catch (UnitParserException e) {
            System.err.println("Failed to parse that: " + e);
            return false;
        }
        return true;
    }

    private enum ExpectedStatus {
        FINE,                   // no further tests
        UNRECOGNISED_UNITS,     // includes unrecognised units
        DEPRECATED_UNIT,        // all units recognised, but at least one deprecated
        FORBIDDEN_SI,           // uses SI prefixes where forbidden
        FORBIDDEN_BINARY,       // uses binary prefixes where forbidden
        ALL_CONSTRAINTS_OK      // all constraints satisfied
    };

    static String lineIdent = "Line ???";
    private static void report_result(String msg, java.io.PrintStream s) {
        s.println(lineIdent + ":\n  " + msg);
    }
    private static void report_result(String msg) {
        report_result(msg, System.out);
    }

    /**
     * Run the tests in the named CSV file. If the file name has a '%'
     * in it, that's substituted by the corresponding parser name
     */
    private static boolean runTestsInFile(String initialFilename, String parser)
            throws UnitParserException {
        String filename = initialFilename.replaceAll("%", parser);
        int nok = 0;
        int nfail = 0;
        try {
            java.io.BufferedReader in = new java.io.BufferedReader(new java.io.FileReader(filename));
            String line;

            // Set the locale to something non-default, so we can
            // check that the testing code is locale-independent (ie
            // that it doesn't just work by accident in a particular
            // locale).  The library as a whole should be
            // locale-independent.  This isn't
            // a very thorough test, because the French locale seems
            // to differ from the non-locale only in the use of commas
            // for the decimal separator.  However I can't find a
            // locale which differs more than this in relevant
            // respects -- all the ones I would have expected to be
            // different (japaense, chinese, arabic, thai, ...) seem to have
            // 'us' digits (pragmatically, I suppose).
            java.util.Locale.setDefault(new java.util.Locale("FR"));

            Syntax stx = Syntax.lookup(parser);
            if (stx == null) {
                System.err.println("Bad parser name: " + parser);
                return false;
            }

            UnitParser theParser = new UnitParser(stx);

            // Create a 'sloppy' CSV reader (ie it skips blank and '#'
            // lines, and skips whitespace after CSV commas)
            for (List<String> fields : new SimpleCSVReader(in, true)) {

                String unitString;
                boolean shouldBeOK;
                String failureReason;
                String parseResult;
                String expected = null;
                UnitExpr expr;

                // expectedStatus indicates further tests:
                ExpectedStatus expectedStatus = ExpectedStatus.FINE;

                // column 0
                // The thing to parse; parse it into parseResult
                {
                    unitString = fields.get(0);
                    StringBuffer sb = new StringBuffer();
                    try {
                        //expr = new UnitParser(stx, fields.get(0)).getParsed();
                        expr = theParser.parse(fields.get(0));
                        parseResult = expr.toString(Syntax.DEBUG);
                    } catch (UnitParserException e) {
                        // that's OK -- some of the tests are expected
                        // to fail, and we check that they do below
                        parseResult = null;
                        expr = null;
                    }
                }

                lineIdent = filename + '(' + unitString + ')';

                // column 1
                // non-empty if this should fail
                if (fields.get(1).length() == 0) {
                    shouldBeOK = true;
                    failureReason = null;
                } else {
                    shouldBeOK = false;
                    failureReason = fields.get(1);
                }

                // column 2
                // what it should parse to, as a .toDebugString() string
                if (fields.size() > 2) {
                    char leadingChar = fields.get(2).charAt(0);
                    if (Character.isDigit(leadingChar) || leadingChar == '-') {
                        // OK
                        expectedStatus = ExpectedStatus.FINE;
                        expected = fields.get(2);
                    } else {
                        expected = fields.get(2).substring(1);
                        switch (leadingChar) {
                          case 'u':
                            expectedStatus = ExpectedStatus.UNRECOGNISED_UNITS;
                            break;
                          case '?':
                            expectedStatus = ExpectedStatus.DEPRECATED_UNIT;
                            break;
                          case 's':
                            expectedStatus = ExpectedStatus.FORBIDDEN_SI;
                            break;
                          case 'b':
                            expectedStatus = ExpectedStatus.FORBIDDEN_BINARY;
                            break;
                          case '!':
                            expectedStatus = ExpectedStatus.ALL_CONSTRAINTS_OK;
                            break;
                          default:
                            // oops -- the above are the only allowed flags
                            report_result("malformed test: unrecognised flag '" + leadingChar + "' in " + fields.get(2),
                                          System.err);
                            System.exit(1);
                        }
                    }
                }

                // we've gathered all the information -- now do the check

                if (shouldBeOK) {
                    if (parseResult == null) {
                        report_result("parsing \"" + unitString
                                      + "\" should have worked, but didn't");
                        nfail++;
                    } else if (parseResult.equals(expected)) {
                        // this test passed
                        nok++;
                        switch (expectedStatus) {
                          case FINE:
                            break; // no other checks required
                          case UNRECOGNISED_UNITS:
                            // this string had non-recognised units
                            if (expr.allUnitsRecognised(stx)) {
                                report_result("parsing \"" + unitString
                                              + "\"(" + parser + ") has non-recognised units: didn't spot them!");
                                nfail++;
                            } else {
                                nok++;
                            }
                            break;
                          case DEPRECATED_UNIT:
                            // all units recognised, but at least one deprecated
                            if (expr.allUnitsRecognised(stx) && !expr.allUnitsRecommended(stx)) {
                                nok++;
                            } else {
                                report_result("parsing \"" + unitString
                                              + "\"(" + parser + ") has deprecated units: didn't spot them");
                                nfail++;
                            }
                            break;
                          case FORBIDDEN_SI:
                          case FORBIDDEN_BINARY:
                            // at least one unit was used with an SI
                            // prefix.  The unit was recognised but is
                            // known not to permit this.  We don't (I
                            // think) have to distinguish these two
                            // cases, since they're bot aliases for
                            // the same underlying check:
                            if (expr.allUsageConstraintsSatisfied(stx)) {
                                report_result("parsing \"" + unitString
                                              + "\"(" + parser + ") does not conform to usage constraints: didn't spot that");
                                nfail++;
                            } else {
                                nok++;
                            }
                            break;
                          case ALL_CONSTRAINTS_OK:
                            // fully conformant
                            if (expr.isFullyConformant(stx)) {
                                nok++;
                            } else {
                                report_result("parsing \"" + unitString
                                              + "\"(" + parser + ") is fully conformant: didn't spot that");
                                nfail++;
                            }
                            break;
                          default:
                            throw new AssertionError("Impossible expectedStatus: " + expectedStatus);
                        }
                    } else {
                        report_result("parsing \"" + unitString
                                      + "\": expected \""  + expected
                                      + "\" got \"" + parseResult + "\"");
                        nfail++;
                    }

                    // separately, check the formatted versions, if they're present
                    if (expr != null && fields.size() > 3) {
                        for (int i=3; i<fields.size(); i++) {
                            String[] parts = fields.get(i).split(":");
                            if (parts.length != 2) {
                                report_result("malformed formatting specifier: "
                                              + fields.get(i) + " on line: ",
                                              System.err);
                                for (int ii=0; ii<fields.size(); ii++) {
                                    System.err.print(fields.get(ii) + ", ");
                                }
                                System.err.println();
                            } else if (parts[1].charAt(0) == '!') {
                                // we're not to try re-parsing this
                                // ...so do nothing
                            } else {
                                Syntax reparseSyntax = Syntax.lookup(parts[0]);
                                if (reparseSyntax == null) {
                                    report_result("bad test: field "
                                                  + fields.get(i)
                                                  + " mentions invalid parser",
                                                  System.err);
                                    System.exit(1);
                                }

                                String actualFormat = expr.toString(reparseSyntax);
                                String expectedFormat = parts[1];
                                if (actualFormat.equals(expectedFormat)) {
                                    nok++;
                                } else {
                                    report_result("formatting (" + reparseSyntax
                                                  + "): expected \"" + expectedFormat
                                                  + "\", got \"" + actualFormat + "\"");
                                    nfail++;
                                }

                                // now re-parse the result
                                UnitParser p;
                                try {
                                    p = new UnitParser(reparseSyntax, actualFormat);
                                } catch (UnitParserException e) {
                                    // that's OK: this is a write-only syntax
                                    p = null;
                                }
                                if (p != null) {
                                    try {
                                        UnitExpr reExpr = p.getParsed();
                                        String reParseResult = reExpr.toString(Syntax.DEBUG);
                                        if (! reParseResult.equals(expected)) {
                                            report_result("re-parsing " + actualFormat
                                                          + " with syntax " + reparseSyntax
                                                          + ": produced " + reParseResult
                                                          + ", expected " + expected);
                                            nfail++;
                                        }
                                    } catch (UnitParserException e) {
                                        // parsing failed
                                        report_result("re-parsing " + actualFormat
                                                      + " with syntax " + reparseSyntax
                                                      + ": FAILED [" + e + "]");
                                        nfail++;
                                    }
                                }
                            }
                        }
                    }
                } else {
                    // should have failed
                    if (parseResult == null) {
                        // this test was expected to fail
                        nok++;
                    } else {
                        report_result("parsing \"" + unitString
                                      + "\" should have failed ("
                                      + failureReason
                                      + "), but it produced: "
                                      + parseResult);
                        nfail++;
                    }
                }
            }
            StringBuilder sb = new StringBuilder("        ");
            String syntaxName = stx.toString();
            sb.replace(sb.length()-syntaxName.length(), sb.length(), syntaxName);
            if (nfail == 0) {
                System.out.println(sb + " : ALL " + nok + " tests pass");
            } else {
                System.out.println(sb + " : passes: " + nok);
                System.out.println("           FAILS:  " + nfail);
            }
        } catch (UnwritableExpression e) {
            System.err.println("Unwritable expression: " + e);
        } catch (java.io.IOException e) {
            System.err.println("IOException reading " + filename + ": " + e);
        }

        return (nfail == 0);           // zero if all went well
    }

    private static void Usage() {
        System.err.println("Usage: ParseAll -l [-pstx]* unitstring (list lexemes)");
        System.err.println("       ParseAll -d [-pstx]* unitstring (display parse)");
        System.err.println("       ParseAll -t [-pstx]* filename   (run tests)");
        System.err.println("       ParseAll -S                     (list syntaxes)");
        System.err.println("-pstx : parse with given syntax");
        System.exit(1);
    }
}
