/*
 * Grammatica.java
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the BSD license.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * LICENSE.txt file for more details.
 *
 * Copyright (c) 2003-2015 Per Cederberg. All rights reserved.
 */

package net.percederberg.grammatica;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

import net.percederberg.grammatica.output.CSharpParserGenerator;
import net.percederberg.grammatica.output.JavaParserGenerator;
import net.percederberg.grammatica.output.VisualBasicParserGenerator;
import net.percederberg.grammatica.parser.Analyzer;
import net.percederberg.grammatica.parser.Node;
import net.percederberg.grammatica.parser.ParseException;
import net.percederberg.grammatica.parser.Parser;
import net.percederberg.grammatica.parser.ParserCreationException;
import net.percederberg.grammatica.parser.ParserLogException;
import net.percederberg.grammatica.parser.Token;
import net.percederberg.grammatica.parser.Tokenizer;

/**
 * The main application. This class provides the command-line
 * interface for invoking the application. See separate documentation
 * for information on usage and command-line parameters.
 *
 * @author   Per Cederberg
 * @version  1.5
 */
public class Grammatica extends Object {

    /**
     * The command-line help output.
     */
    private static final String COMMAND_HELP =
        "Generates source code for a C#, Java or Visual Basic parser from\n" +
        "a grammar file. This program comes with ABSOLUTELY NO WARRANTY;\n" +
        "for details see the LICENSE.txt file.\n" +
        "\n" +
        "Syntax: Grammatica <grammarfile> <action> [<options>]\n" +
        "\n" +
        "Actions:\n" +
        "  --debug\n" +
        "      Debugs the grammar by validating it and printing the\n" +
        "      internal representation.\n" +
        "  --tokenize <file>\n" +
        "      Debugs the grammar by using it to tokenize the specified\n" +
        "      file. No code has to be generated for this.\n" +
        "  --parse <file>\n" +
        "      Debugs the grammar by using it to parse the specified\n" +
        "      file. No code has to be generated for this.\n" +
        "  --profile <file(s)>\n" +
        "      Profiles the grammar by using it to parse the specified\n" +
        "      file(s) and printing a statistic summary.\n" +
        "  --csoutput <dir>\n" +
        "      Creates a C# parser for the grammar (in source code).\n" +
        "      The specified directory will be used as output directory\n" +
        "      for the source code files.\n" +
        "  --javaoutput <dir>\n" +
        "      Creates a Java parser for the grammar (in source code).\n" +
        "      The specified directory will be used as the base output\n" +
        "      directory for the source code files.\n" +
        "  --vboutput <dir>\n" +
        "      Creates a Visual Basic (.NET) parser for the grammar (in\n" +
        "      source code). The specified directory will be used as\n" +
        "      output directory for the source code files.\n" +
        "\n" +
        "C# Output Options:\n" +
        "  --csnamespace <package>\n" +
        "      Sets the C# namespace to use in generated source code\n" +
        "      files. By default no namespace declaration is included.\n" +
        "  --csclassname <name>\n" +
        "      Sets the C# class name prefix to use in generated source\n" +
        "      code files. By default the grammar file name is used.\n" +
        "  --cspublic\n" +
        "      Sets public access for all C# types generated. By default\n" +
        "      type access is internal.\n" +
        "\n" +
        "Java Output Options:\n" +
        "  --javapackage <package>\n" +
        "      Sets the Java package to use in generated source code\n" +
        "      files. By default no package declaration is included.\n" +
        "  --javaclassname <name>\n" +
        "      Sets the Java class name prefix to use in generated source\n" +
        "      code files. By default the grammar file name is used.\n" +
        "  --javapublic\n" +
        "      Sets public access for all Java types. By default type\n" +
        "      access is package local.\n" +
        "\n" +
        "Visual Basic Output Options:\n" +
        "  --vbnamespace <package>\n" +
        "      Sets the namespace to use in generated source code files.\n" +
        "      By default no namespace declaration is included.\n" +
        "  --vbclassname <name>\n" +
        "      Sets the class name prefix to use in generated source code\n" +
        "      files. By default the grammar file name is used.\n" +
        "  --vbpublic\n" +
        "      Sets public access for all types generated. By default type\n" +
        "      access is friend.";

    /**
     * The internal error message.
     */
    private static final String INTERNAL_ERROR =
        "INTERNAL ERROR: An internal error in Grammatica has been found.\n" +
        "    Please report this error to the maintainers (see the web\n" +
        "    site for instructions). Be sure to include the Grammatica\n" +
        "    version number, as well as the information below:\n";

    /**
     * The application entry point.
     *
     * @param args           the command-line parameters
     */
    public static void main(String[] args) {
        Grammar  grammar = null;

        // Parse command-line arguments
        if (args.length == 1 && args[0].equals("--help")) {
            printHelp(null);
            System.exit(1);
        }
        if (args.length < 2) {
            printHelp("Missing grammar file and/or action");
            System.exit(1);
        }

        // Read grammar file
        try {
            grammar = new Grammar(new File(args[0]));
        } catch (FileNotFoundException e) {
            printError(args[0], e);
            System.exit(1);
        } catch (ParserLogException e) {
            printError(args[0], e);
            System.exit(1);
        } catch (GrammarException e) {
            printError(e);
            System.exit(1);
        } catch (SecurityException e) {
            throw e;
        } catch (RuntimeException e) {
            printInternalError(e);
            System.exit(2);
        }

        // Check action parameter
        try {
            if (args[1].equals("--debug")) {
                debug(grammar);
            } else if (args.length < 3) {
                printHelp("missing action file parameter");
                System.exit(1);
            } else if (args[1].equals("--tokenize")) {
                tokenize(grammar, new File(args[2]));
            } else if (args[1].equals("--parse")) {
                parse(grammar, new File(args[2]));
            } else if (args[1].equals("--profile")) {
                profile(grammar, args, 2);
            } else if (args[1].equals("--javaoutput")) {
                writeJavaCode(args, grammar);
            } else if (args[1].equals("--csoutput")) {
                writeCSharpCode(args, grammar);
            } else if (args[1].equals("--vboutput")) {
                writeVisualBasicCode(args, grammar);
            } else {
                printHelp("unrecognized option: " + args[1]);
                System.exit(1);
            }
        } catch (SecurityException e) {
            throw e;
        } catch (RuntimeException e) {
            printInternalError(e);
            System.exit(2);
        }
    }

    /**
     * Prints command-line help information.
     *
     * @param error          an optional error message, or null
     */
    private static void printHelp(String error) {
        System.err.println(COMMAND_HELP);
        System.err.println();
        if (error != null) {
            System.err.print("Error: ");
            System.err.println(error);
            System.err.println();
        }
    }

    /**
     * Prints a general error message.
     *
     * @param e              the detailed exception
     */
    private static void printError(Exception e) {
        StringBuffer  buffer = new StringBuffer();

        buffer.append("Error: ");
        buffer.append(e.getMessage());
        System.err.println(buffer.toString());
    }

    /**
     * Prints a file not found error message.
     *
     * @param file           the file name not found
     * @param e              the detailed exception
     */
    private static void printError(String file, FileNotFoundException e) {
        StringBuffer  buffer = new StringBuffer();

        buffer.append("Error: couldn't open file:");
        buffer.append("\n    ");
        buffer.append(file);
        System.err.println(buffer.toString());
    }

    /**
     * Prints a parse error message.
     *
     * @param file           the input file name
     * @param e              the detailed exception
     */
    private static void printError(String file, ParseException e) {
        StringBuffer  buffer = new StringBuffer();
        String        line;

        // Handle normal parse error
        buffer.append("Error: in ");
        buffer.append(file);
        if (e.getLine() > 0) {
            buffer.append(": line ");
            buffer.append(e.getLine());
        }
        buffer.append(":\n");
        buffer.append(linebreakString(e.getErrorMessage(), "    ", 70));
        line = readLines(file, e.getLine(), e.getLine());
        if (line != null) {
            buffer.append("\n\n");
            buffer.append(line);
            for (int i = 1; i < e.getColumn(); i++) {
                if (line.charAt(i - 1) == '\t') {
                    buffer.append("\t");
                } else {
                    buffer.append(" ");
                }
            }
            buffer.append("^");
        }
        System.err.println(buffer.toString());
    }

    /**
     * Prints a list of parse error messages.
     *
     * @param file           the input file name
     * @param e              the parser log exception
     */
    private static void printError(String file, ParserLogException e) {
        for (int i = 0; i < e.getErrorCount(); i++) {
            printError(file, e.getError(i));
        }
    }

    /**
     * Prints a grammar error message.
     *
     * @param e              the detailed exception
     */
    private static void printError(GrammarException e) {
        StringBuffer  buffer = new StringBuffer();
        String        lines;

        buffer.append("Error: in ");
        buffer.append(e.getFile());
        if (e.getStartLine() > 0) {
            if (e.getStartLine() == e.getEndLine()) {
                buffer.append(": line ");
                buffer.append(e.getStartLine());
            } else {
                buffer.append(": lines ");
                buffer.append(e.getStartLine());
                buffer.append("-");
                buffer.append(e.getEndLine());
            }
        }
        buffer.append(":\n");
        buffer.append(linebreakString(e.getErrorMessage(), "    ", 70));
        lines = readLines(e.getFile(), e.getStartLine(), e.getEndLine());
        if (lines != null) {
            buffer.append("\n\n");
            buffer.append(lines);
        }
        System.err.println(buffer.toString());
    }

    /**
     * Prints an internal error message. This type of error should
     * only be reported when run-time exceptions occur, such as null
     * pointer and the likes. All these error should be reported as
     * bugs to the program maintainers.
     *
     * @param e              the exception to be reported
     */
    private static void printInternalError(Exception e) {
        System.err.println(INTERNAL_ERROR);
        e.printStackTrace();
    }

    /**
     * Breaks a string into multiple lines. This method will also add
     * a prefix to each line in the resulting string. The prefix
     * length will be taken into account when breaking the line. Line
     * breaks will only be inserted as replacements for space
     * characters.
     *
     * @param str            the string to line break
     * @param prefix         the prefix to add to each line
     * @param length         the maximum line length
     *
     * @return the new formatted string
     */
    private static String linebreakString(String str,
                                          String prefix,
                                          int length) {

        StringBuffer  buffer = new StringBuffer();
        int           pos;

        while (str.length() + prefix.length() > length) {
            pos = str.lastIndexOf(' ', length - prefix.length());
            if (pos < 0) {
                pos = str.indexOf(' ');
                if (pos < 0) {
                    break;
                }
            }
            buffer.append(prefix);
            buffer.append(str.substring(0, pos));
            str = str.substring(pos + 1);
            buffer.append("\n");
        }
        buffer.append(prefix);
        buffer.append(str);
        return buffer.toString();
    }

    /**
     * Reads a number of lines from a file. In the file couldn't be
     * opened or read correctly, null will be returned.
     *
     * @param file           the name of the file to read
     * @param start          the first line number to read, from one (1)
     * @param end            the last line number to read, from one (1)
     *
     * @return the lines read including newline characters
     */
    private static String readLines(String file, int start, int end) {
        BufferedReader  input;
        StringBuffer    buffer = new StringBuffer();
        String          str;

        // Check invalid line number
        if (start < 1 || end < start) {
            return null;
        }

        // Read line from file
        try {
            input = new BufferedReader(new FileReader(file));
            for (int i = 0; i < end; i++) {
                str = input.readLine();
                if (str == null) {
                    input.close();
                    return null;
                } else if (start <= i + 1) {
                    buffer.append(str);
                    buffer.append("\n");
                }
            }
            input.close();
        } catch (IOException e) {
            return null;
        }

        return buffer.toString();
    }

    /**
     * Debugs a grammar by printing the internal representation.
     *
     * @param grammar        the grammar to use
     */
    private static void debug(Grammar grammar) {
        Tokenizer  tokenizer = null;
        Parser     parser = null;

        // Create tokenizer and parser
        try {
            tokenizer = grammar.createTokenizer(null);
            parser = grammar.createParser(tokenizer);
        } catch (GrammarException e) {
            printInternalError(e);
            System.exit(2);
        }

        // Print tokenizer and parser
        System.out.println("Contents of " + grammar.getFileName() + ":");
        System.out.println();
        System.out.println("Token Declarations:");
        System.out.println("-------------------");
        System.out.print(tokenizer);
        System.out.println("Production Declarations:");
        System.out.println("------------------------");
        System.out.print(parser);
    }

    /**
     * Tokenizes the specified file with the token patterns from the
     * grammar.
     *
     * @param grammar        the grammar to use
     * @param file           the file to parse
     */
    private static void tokenize(Grammar grammar, File file) {
        Tokenizer  tokenizer;
        Token      token;

        try {
            tokenizer = grammar.createTokenizer(new FileReader(file));
            System.out.println("Tokens from " + file + ":");
            while ((token = tokenizer.next()) != null) {
                System.out.println(token);
            }
        } catch (FileNotFoundException e) {
            printError(file.toString(), e);
            System.exit(1);
        } catch (GrammarException e) {
            printInternalError(e);
            System.exit(2);
        } catch (ParseException e) {
            printError(file.toString(), e);
            System.exit(1);
        }
    }

    /**
     * Parses the specified file with the grammar.
     *
     * @param grammar        the grammar to use
     * @param file           the file to parse
     */
    private static void parse(Grammar grammar, File file) {
        Tokenizer  tokenizer;
        Analyzer   analyzer;
        Parser     parser;

        try {
            tokenizer = grammar.createTokenizer(new FileReader(file));
            analyzer = new TreePrinter(System.out);
            parser = grammar.createParser(tokenizer, analyzer);
            System.out.println("Parse tree from " + file + ":");
            parser.parse();
        } catch (FileNotFoundException e) {
            printError(file.toString(), e);
            System.exit(1);
        } catch (GrammarException e) {
            printInternalError(e);
            System.exit(2);
        } catch (ParserCreationException e) {
            printInternalError(e);
            System.exit(2);
        } catch (ParserLogException e) {
            printError(file.toString(), e);
            System.exit(1);
        }
    }

    /**
     * Parses the specified file with the grammar and prints
     * profiling information.
     *
     * @param grammar        the grammar to use
     * @param files          the files to parse
     * @param first          the index of the first file
     */
    private static void profile(Grammar grammar, String[] files, int first) {
        File       file = new File(files[first]);
        Tokenizer  tokenizer;
        Parser     parser;
        Node       node;
        int        fileCount = files.length - first;
        long       time;
        int        counter;

        // Profile tokenizer
        try {
            System.out.println("Tokenizing " + fileCount + " file(s)...");
            tokenizer = grammar.createTokenizer(new FileReader(file));
            time = System.currentTimeMillis();
            counter = 0;
            for (int i = first; i < files.length; i++) {
                if (i > first) {
                    file = new File(files[i]);
                    tokenizer.reset(new FileReader(file));
                }
                while (tokenizer.next() != null) {
                    counter++;
                }
            }
            time = System.currentTimeMillis() - time + 1;
            System.out.println("  Time elapsed:  " + time + " millisec");
            System.out.println("  Tokens found:  " + counter);
            System.out.println("  Average speed: " + (counter / time) +
                               " tokens/millisec");
            System.out.println();
        } catch (FileNotFoundException e) {
            printError(file.toString(), e);
            System.exit(1);
        } catch (GrammarException e) {
            printInternalError(e);
            System.exit(2);
        } catch (ParseException e) {
            printError(file.toString(), e);
            System.exit(1);
        }

        // Profile parser
        try {
            System.out.println("Parsing " + fileCount + " file(s)...");
            file = new File(files[first]);
            tokenizer = grammar.createTokenizer(new FileReader(file));
            parser = grammar.createParser(tokenizer);
            time = System.currentTimeMillis();
            counter = 0;
            for (int i = first; i < files.length; i++) {
                if (i > first) {
                    file = new File(files[i]);
                    parser.reset(new FileReader(file));
                }
                node = parser.parse();
                counter += 1 + node.getDescendantCount();
            }
            time = System.currentTimeMillis() - time + 1;
            System.out.println("  Time elapsed:  " + time + " millisec");
            System.out.println("  Nodes found:   " + counter);
            System.out.println("  Average speed: " + (counter / time) +
                               " nodes/millisec");
            System.out.println();
        } catch (FileNotFoundException e) {
            printError(file.toString(), e);
            System.exit(1);
        } catch (GrammarException e) {
            printInternalError(e);
            System.exit(2);
        } catch (ParserCreationException e) {
            printInternalError(e);
            System.exit(2);
        } catch (ParserLogException e) {
            printError(file.toString(), e);
            System.exit(1);
        }
    }

    /**
     * Parses the command-line arguments and generates the Java source
     * code for a parser.
     *
     * @param args           the command-line arguments
     * @param grammar        the grammar to use
     */
    private static void writeJavaCode(String[] args, Grammar grammar) {
        JavaParserGenerator gen = new JavaParserGenerator(grammar);

        // Read command-line arguments
        for (int i = 1; i < args.length; i++) {
            if (args[i].equals("--javaoutput")) {
                gen.setBaseDir(new File(args[++i]));
            } else if (args[i].equals("--javapackage")) {
                gen.setBasePackage(args[++i]);
            } else if (args[i].equals("--javaclassname")) {
                gen.setBaseName(args[++i]);
            } else if (args[i].equals("--javapublic")) {
                gen.setPublicAccess(true);
            } else {
                printHelp("unrecognized option: " + args[i]);
                System.exit(1);
            }
        }

        // Write parser source code
        try {
            System.out.println("Writing Java parser source code...");
            gen.write();
            System.out.println("Done.");
        } catch (IOException e) {
            printError(e);
            System.exit(1);
        }
    }

    /**
     * Parses the command-line arguments and generates the C# source
     * code for a parser.
     *
     * @param args           the command-line arguments
     * @param grammar        the grammar to use
     */
    private static void writeCSharpCode(String[] args, Grammar grammar) {
        CSharpParserGenerator gen = new CSharpParserGenerator(grammar);

        // Read command-line arguments
        for (int i = 1; i < args.length; i++) {
            if (args[i].equals("--csoutput")) {
                gen.setBaseDir(new File(args[++i]));
            } else if (args[i].equals("--csnamespace")) {
                gen.setNamespace(args[++i]);
            } else if (args[i].equals("--csclassname")) {
                gen.setBaseName(args[++i]);
            } else if (args[i].equals("--cspublic")) {
                gen.setPublicAccess(true);
            } else {
                printHelp("unrecognized option: " + args[i]);
                System.exit(1);
            }
        }

        // Write parser source code
        try {
            System.out.println("Writing C# parser source code...");
            gen.write();
            System.out.println("Done.");
        } catch (IOException e) {
            printError(e);
            System.exit(1);
        }
    }

    /**
     * Parses the command-line arguments and generates the Visual
     * Basic source code for a parser.
     *
     * @param args           the command-line arguments
     * @param grammar        the grammar to use
     */
    private static void writeVisualBasicCode(String[] args, Grammar grammar) {
        VisualBasicParserGenerator gen;

        // Read command-line arguments
        gen = new VisualBasicParserGenerator(grammar);
        for (int i = 1; i < args.length; i++) {
            if (args[i].equals("--vboutput")) {
                gen.setBaseDir(new File(args[++i]));
            } else if (args[i].equals("--vbnamespace")) {
                gen.setNamespace(args[++i]);
            } else if (args[i].equals("--vbclassname")) {
                gen.setBaseName(args[++i]);
            } else if (args[i].equals("--vbpublic")) {
                gen.setPublicAccess(true);
            } else {
                printHelp("unrecognized option: " + args[i]);
                System.exit(1);
            }
        }

        // Write parser source code
        try {
            System.out.println("Writing Visual Basic parser source code...");
            gen.write();
            System.out.println("Done.");
        } catch (IOException e) {
            printError(e);
            System.exit(1);
        }
    }
}
