/**
[The "BSD licence"]

ANTLR        - Copyright (c) 2005-2008 Terence Parr
Maven Plugin - Copyright (c) 2009      Jim Idle

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* ========================================================================
 * This is the definitive ANTLR3 Mojo set. All other sets are belong to us.
 */
package org.antlr.mojo.antlr3;

import antlr.RecognitionException;
import antlr.TokenStreamException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.antlr.Tool;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping;
import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping;

/**
 * Goal that picks up all the ANTLR grammars in a project and moves those that
 * are required for generation of the compilable sources into the location
 * that we use to compile them, such as target/generated-sources/antlr3 ...
 *
 * @goal antlr
 * 
 * @phase process-sources
 * @requiresDependencyResolution compile
 * @requiresProject true
 * 
 * @author <a href="mailto:jimi@temporal-wave.com">Jim Idle</a>
 */
public class Antlr3Mojo
        extends AbstractMojo {

    // First, let's deal with the options that the ANTLR tool itself
    // can be configured by.
    //
    /**
     * If set to true, then after the tool has processed an input grammar file
     * it will report variaous statistics about the parser, such as information
     * on cyclic DFAs, which rules may use backtracking, and so on.
     *
     * @parameter default-value="false"
     */
    protected boolean report;
    /**
     * If set to true, then the ANTLR tool will print a version of the input
     * grammar which is devoid of any actions that may be present in the input file.
     *
     * @parameter default-value="false"
     */
    protected boolean printGrammar;
    /**
     * If set to true, then the code generated by the ANTLR code generator will
     * be set to debug mode. This means that when run, the code will 'hang' and
     * wait for a debug connection on a TCP port (49100 by default).
     *
     * @parameter default-value="false"
     */
    protected boolean debug;
    /**
     * If set to true, then then the generated parser will compute and report on
     * profile information at runtime.
     *
     * @parameter default-value="false"
     */
    protected boolean profile;
    /**
     * If set to true then the ANTLR tool will generate a description of the nfa
     * for each rule in <a href="http://www.graphviz.org">Dot format</a>
     * 
     * @parameter default-value="false"
     */
    protected boolean nfa;
    /**
     * If set to true then the ANTLR tool will generate a description of the DFA
     * for each decision in the grammar in <a href="http://www.graphviz.org">Dot format</a>
     * 
     * @parameter default-value="false"
     */
    protected boolean dfa;
    /**
     * If set to true, the generated parser code will log rule entry and exit points
     * to stdout as an aid to debugging.
     *
     * @parameter default-value="false"
     */
    protected boolean trace;
    /**
     * If this parameter is set, it indicates that any warning or error messages returned
     * by ANLTR, shoould be formatted in the specified way. Currently, ANTLR suports the
     * built-in formats of antlr, gnu and vs2005.
     *
     * @parameter default-value="antlr"
     */
    protected String messageFormat;
    /**
     * If this parameter is set to true, then ANTLR will report all sorts of things
     * about what it is doing such as the names of files and the version of ANTLR and so on.
     *
     * @parameter default-value="true"
     */
    protected boolean verbose;
    /**
     * The number of milliseconds ANTLR will wait for analysis of each
     * alternative in the grammar to complete before giving up. You may raise
     * this value if ANTLR gives up on a complicated alt and tells you that
     * there are lots of ambiguties, but you know that it just needed to spend
     * more time on it. Note that this is an absolute time and not CPU time.
     *
     * @parameter default-value="10000"
     */
    private int conversionTimeout;

    /**
     * The number of alts, beyond which ANTLR will not generate a switch statement
     * for the DFA.
     *
     * @parameter default-value="300"
     */
    private int maxSwitchCaseLabels;

    /**
     * The number of alts, below which ANTLR will not choose to generate a switch
     * statement over an if statement.
     */
    private int minSwitchAlts;

    /* --------------------------------------------------------------------
     * The following are Maven specific parameters, rather than specificlly
     * options that the ANTLR tool can use.
     */
    /**
     * Provides an explicit list of all the grammars that should
     * be included in the generate phase of the plugin. Note that the plugin
     * is smart enough to realize that imported grammars should be included but
     * not acted upon directly by the ANTLR Tool.
     *
     * Unless otherwise specified, the include list scans for and includes all
     * files that end in ".g" in any directory beneath src/main/antlr3. Note that
     * this version of the plugin looks for the directory antlr3 and not the directory
     * antlr, so as to avoid clashes and confusion for projects that use both v2 and v3 grammars
     * such as ANTLR itself.
     *
     * @parameter
     */
    protected Set includes = new HashSet();
    /**
     * Provides an explicit list of any grammars that should be excluded from
     * the generate phase of the plugin. Files listed here will not be sent for
     * processing by the ANTLR tool.
     *
     * @parameter 
     */
    protected Set excludes = new HashSet();
    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;
    /**
     * Specifies the Antlr directory containing grammar files. For
     * antlr version 3.x we default this to a directory in the tree
     * called antlr3 because the antlr directory is occupied by version
     * 2.x grammars.
     *
     * @parameter default-value="${basedir}/src/main/antlr3"
     * @required
     */
    private File sourceDirectory;
    /**
     * Location for generated Java files. For antlr version 3.x we default
     * this to a directory in the tree called antlr3 because the antlr
     * directory is occupied by version 2.x grammars.
     *
     * @parameter default-value="${project.build.directory}/generated-sources/antlr3"
     * @required
     */
    private File outputDirectory;
    /**
     * Location for imported token files, e.g. <code>.tokens</code> and imported grammars.
     * Note that ANTLR will not try to process grammars that it finds to be imported
     * into other grammars (in the same processing session).
     *
     * @parameter default-value="${basedir}/src/main/antlr3/imports"
     */
    private File libDirectory;

    public File getSourceDirectory() {
        return sourceDirectory;
    }

    public File getOutputDirectory() {
        return outputDirectory;
    }

    public File getLibDirectory() {
        return libDirectory;
    }

    void addSourceRoot(File outputDir) {
        project.addCompileSourceRoot(outputDir.getPath());
    }
    /**
     * An instance of the ANTLR tool build
     */
    protected Tool tool;

    /**
     * The main entry point for this Mojo, it is responsible for converting
     * ANTLR 3.x grammars into the target language specified by the grammar.
     * 
     * @throws org.apache.maven.plugin.MojoExecutionException When something is disvocered such as a missing source
     * @throws org.apache.maven.plugin.MojoFailureException When something really bad happesn such as not being able to create the ANTLR Tool
     */
    public void execute()
            throws MojoExecutionException, MojoFailureException {

        Log log = getLog();

        // Check to see if the user asked for debug information, then dump all the
        // parameters we have picked up if they did.
        //
        if (log.isDebugEnabled()) {

            // Excludes
            //
            for (String e : (Set<String>) excludes) {

                log.debug("ANTLR: Exclude: " + e);
            }

            // Includes
            //
            for (String e : (Set<String>) includes) {

                log.debug("ANTLR: Include: " + e);
            }

            // Output location
            //
            log.debug("ANTLR: Output: " + outputDirectory);

            // Library directory
            //
            log.debug("ANTLR: Library: " + libDirectory);

            // Flags
            //
            log.debug("ANTLR: report              : " + report);
            log.debug("ANTLR: printGrammar        : " + printGrammar);
            log.debug("ANTLR: debug               : " + debug);
            log.debug("ANTLR: profile             : " + profile);
            log.debug("ANTLR: nfa                 : " + nfa);
            log.debug("ANTLR: dfa                 : " + dfa);
            log.debug("ANTLR: trace               : " + trace);
            log.debug("ANTLR: messageFormat       : " + messageFormat);
            log.debug("ANTLR: conversionTimeout   : " + conversionTimeout);
            log.debug("ANTLR: maxSwitchCaseLabels : " + maxSwitchCaseLabels);
            log.debug("ANTLR: minSwitchAlts       : " + minSwitchAlts);
            log.debug("ANTLR: verbose             : " + verbose);
        }

        // Ensure that the output directory path is all in tact so that
        // ANTLR can just write into it.
        //
        File outputDir = getOutputDirectory();

        if (!outputDir.exists()) {
            outputDir.mkdirs();
        }

        // First thing we need is an instance of the ANTLR 3.1 build tool
        //
        try {
            // ANTLR Tool buld interface
            //
            tool = new Tool();
        } catch (Exception e) {
            log.error("The attempt to create the ANTLR build tool failed, see exception report for details");

            throw new MojoFailureException("Jim failed you!");
        }

        // Next we need to set the options given to us in the pom into the
        // tool instance we have created.
        //
        tool.setConversionTimeout(conversionTimeout);
        tool.setDebug(debug);
        tool.setGenerate_DFA_dot(dfa);
        tool.setGenerate_NFA_dot(nfa);
        tool.setProfile(profile);
        tool.setReport(report);
        tool.setPrintGrammar(printGrammar);
        tool.setTrace(trace);
        tool.setVerbose(verbose);
        tool.setMessageFormat(messageFormat);
        tool.setMaxSwitchCaseLabels(maxSwitchCaseLabels);
        tool.setMinSwitchAlts(minSwitchAlts);

        // Where do we want ANTLR to produce its output? (Base directory)
        //
        if (log.isDebugEnabled())
        {
            log.debug("Output directory base will be " + outputDirectory.getAbsolutePath());
        }
        tool.setOutputDirectory(outputDirectory.getAbsolutePath());

        // Tell ANTLR that we always want the output files to be produced in the output directory
        // using the same relative path as the input file was to the input directory.
        //
        tool.setForceRelativeOutput(true);

        // Where do we want ANTLR to look for .tokens and import grammars?
        //
        tool.setLibDirectory(libDirectory.getAbsolutePath());

        if (!sourceDirectory.exists()) {
            if (log.isInfoEnabled()) {
                log.info("No ANTLR grammars to compile in " + sourceDirectory.getAbsolutePath());
            }
            return;
        } else {
            if (log.isInfoEnabled()) {
                log.info("ANTLR: Processing source directory " + sourceDirectory.getAbsolutePath());
            }
        }

        // Set working directory for ANTLR to be the base source directory
        //
        tool.setInputDirectory(sourceDirectory.getAbsolutePath());

        try {

            // Now pick up all the files and process them with the Tool
            //
            processGrammarFiles(sourceDirectory, outputDirectory);

        } catch (InclusionScanException ie) {

            log.error(ie);
            throw new MojoExecutionException("Fatal error occured while evaluating the names of the grammar files to analyze");

        } catch (Exception e) {

            getLog().error(e);
            throw new MojoExecutionException(e.getMessage());
        }



        tool.process();

        // If any of the grammar files caused errors but did nto throw exceptions
        // then we should have accumulated errors in the counts
        //
        if (tool.getNumErrors() > 0) {
            throw new MojoExecutionException("ANTLR caught " + tool.getNumErrors() + " build errors.");
        }

        // All looks good, so we need to tel Maven about the sources that
        // we just created.
        //
        if (project != null) {
            // Tell Maven that there are some new source files underneath
            // the output directory.
            //
            addSourceRoot(this.getOutputDirectory());
        }

    }


    /**
     *
     * @param sourceDirectory
     * @param outputDirectory
     * @throws antlr.TokenStreamException
     * @throws antlr.RecognitionException
     * @throws java.io.IOException
     * @throws org.codehaus.plexus.compiler.util.scan.InclusionScanException
     */
    private void processGrammarFiles(File sourceDirectory, File outputDirectory)
            throws TokenStreamException, RecognitionException, IOException, InclusionScanException {
        // Which files under the source set should we be looking for as grammar files
        //
        SourceMapping mapping = new SuffixMapping("g", Collections.EMPTY_SET);

        // What are the sets of includes (defaulted or otherwise).
        //
        Set includes = getIncludesPatterns();

        // Now, to the excludes, we need to add the imports directory
        // as this is autoscanned for importd grammars and so is auto-excluded from the
        // set of gramamr fiels we shuold be analyzing.
        //
        excludes.add("imports/**");

        SourceInclusionScanner scan = new SimpleSourceInclusionScanner(includes, excludes);

        scan.addSourceMapping(mapping);
        Set grammarFiles = scan.getIncludedSources(sourceDirectory, null);

        if (grammarFiles.isEmpty()) {
            if (getLog().isInfoEnabled()) {
                getLog().info("No grammars to process");
            }
        } else {

            // Tell the ANTLR tool that we want sorted build mode
            //
            tool.setMake(true);
            
            // Iterate each grammar file we were given and add it into the tool's list of
            // grammars to process.
            //
            for (File grammar : (Set<File>) grammarFiles) {

                if (getLog().isDebugEnabled()) {
                    getLog().debug("Grammar file '" + grammar.getPath() + "' detected.");
                }


                String relPath = findSourceSubdir(sourceDirectory, grammar.getPath()) + grammar.getName();

                if (getLog().isDebugEnabled()) {
                    getLog().debug("  ... relative path is: " + relPath);
                }
                tool.addGrammarFile(relPath);

            }

        }


    }

    public Set getIncludesPatterns() {
        if (includes == null || includes.isEmpty()) {
            return Collections.singleton("**/*.g");
        }
        return includes;
    }

    /**
     * Given the source directory File object and the full PATH to a
     * grammar, produce the path to the named grammar file in relative
     * terms to the sourceDirectory. This will then allow ANTLR to
     * produce output relative to the base of the output directory and
     * reflect the input organization of the grammar files.
     *
     * @param sourceDirectory The source directory File object
     * @param grammarFileName The full path to the input grammar file
     * @return The path to the grammar file relative to the source directory
     */
    private String findSourceSubdir(File sourceDirectory, String grammarFileName) {
        String srcPath = sourceDirectory.getPath() + File.separator;

        if (!grammarFileName.startsWith(srcPath)) {
            throw new IllegalArgumentException("expected " + grammarFileName + " to be prefixed with " + sourceDirectory);
        }

        File unprefixedGrammarFileName = new File(grammarFileName.substring(srcPath.length()));

        return unprefixedGrammarFileName.getParent() + File.separator;
    }
}
