package org.codehaus.mojo.javacc;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file 
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, 
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied.  See the License for the 
 * specific language governing permissions and limitations 
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.codehaus.plexus.util.DirectoryScanner;

/**
 * Scans source directories for JavaCC grammar files.
 * 
 * @author Benjamin Bentmann
 * @version $Id: GrammarDirectoryScanner.java 6282 2008-02-09 23:49:06Z bentmann $
 */
class GrammarDirectoryScanner
{

    /**
     * The directory scanner used to scan the source directory for files.
     */
    private DirectoryScanner scanner;

    /**
     * The absolute path to the output directory used to detect stale target files by timestamp checking, may be
     * <code>null</code> if no stale detection should be performed.
     */
    private File outputDirectory;

    // TODO: Once the parameter "packageName" from the javacc mojo has been deleted, remove this field, too.
    /**
     * The package name for the generated parser, may be <code>null</code> to use the package declaration from the
     * grammar file.
     */
    private String parserPackage;

    /**
     * The granularity in milliseconds of the last modification date for testing whether a grammar file needs
     * recompilation because its corresponding target file is stale.
     */
    private int staleMillis;

    /**
     * A set of grammar infos describing the included grammar files, must never be <code>null</code>.
     */
    private List includedGrammars;

    /**
     * Creates a new grammar directory scanner.
     */
    public GrammarDirectoryScanner()
    {
        this.scanner = new DirectoryScanner();
        this.scanner.setFollowSymlinks( true );
        this.includedGrammars = new ArrayList();
    }

    /**
     * Sets the absolute path to the source directory to scan for grammar files. This directory must exist or the
     * scanner will report an error.
     * 
     * @param directory The absolute path to the source directory to scan, must not be <code>null</code>.
     */
    public void setSourceDirectory( File directory )
    {
        if ( !directory.isAbsolute() )
        {
            throw new IllegalArgumentException( "source directory is not absolute: " + directory );
        }
        this.scanner.setBasedir( directory );
    }

    /**
     * Sets the package name for the generated parser.
     * 
     * @param packageName The package name for the generated parser, may be <code>null</code> to use the package
     *            declaration from the grammar file.
     */
    public void setParserPackage( String packageName )
    {
        this.parserPackage = packageName;
    }

    /**
     * Sets the Ant-like inclusion patterns.
     * 
     * @param includes The set of Ant-like inclusion patterns, may be <code>null</code> to include all files.
     */
    public void setIncludes( String[] includes )
    {
        this.scanner.setIncludes( includes );
    }

    /**
     * Sets the Ant-like exclusion patterns.
     * 
     * @param excludes The set of Ant-like exclusion patterns, may be <code>null</code> to exclude no files.
     */
    public void setExcludes( String[] excludes )
    {
        this.scanner.setExcludes( excludes );
        this.scanner.addDefaultExcludes();
    }

    /**
     * Sets the absolute path to the output directory used to detect stale target files.
     * 
     * @param directory The absolute path to the output directory used to detect stale target files by timestamp
     *            checking, may be <code>null</code> if no stale detection should be performed.
     */
    public void setOutputDirectory( File directory )
    {
        if ( directory != null && !directory.isAbsolute() )
        {
            throw new IllegalArgumentException( "output directory is not absolute: " + directory );
        }
        this.outputDirectory = directory;
    }

    /**
     * Sets the granularity in milliseconds of the last modification date for stale file detection.
     * 
     * @param milliseconds The granularity in milliseconds of the last modification date for testing whether a grammar
     *            file needs recompilation because its corresponding target file is stale.
     */
    public void setStaleMillis( int milliseconds )
    {
        this.staleMillis = milliseconds;
    }

    /**
     * Scans the source directory for grammar files that match at least one inclusion pattern but no exclusion pattern,
     * optionally performing timestamp checking to exclude grammars whose corresponding parser files are up to date.
     * 
     * @throws IOException If a grammar file could not be analyzed for metadata.
     */
    public void scan()
        throws IOException
    {
        this.includedGrammars.clear();
        this.scanner.scan();

        String[] includedFiles = this.scanner.getIncludedFiles();
        for ( int i = 0; i < includedFiles.length; i++ )
        {
            String includedFile = includedFiles[i];
            GrammarInfo grammarInfo = new GrammarInfo( this.scanner.getBasedir(), includedFile, this.parserPackage );
            if ( this.outputDirectory != null )
            {
                File sourceFile = grammarInfo.getGrammarFile();
                File[] targetFiles = getTargetFiles( this.outputDirectory, includedFile, grammarInfo );
                for ( int j = 0; j < targetFiles.length; j++ )
                {
                    File targetFile = targetFiles[j];
                    if ( !targetFile.exists()
                        || targetFile.lastModified() + this.staleMillis < sourceFile.lastModified() )
                    {
                        this.includedGrammars.add( grammarInfo );
                        break;
                    }
                }
            }
            else
            {
                this.includedGrammars.add( grammarInfo );
            }
        }
    }

    /**
     * Determines the output files corresponding to the specified grammar file.
     * 
     * @param targetDirectory The absolute path to the output directory for the target files, must not be
     *            <code>null</code>.
     * @param grammarFile The path to the grammar file, relative to the scanned source directory, must not be
     *            <code>null</code>.
     * @param grammarInfo The grammar info describing the grammar file, must not be <code>null</code>
     * @return A file array with target files, never <code>null</code>.
     */
    protected File[] getTargetFiles( File targetDirectory, String grammarFile, GrammarInfo grammarInfo )
    {
        File parserFile = new File( targetDirectory, grammarInfo.getParserFile() );
        return new File[] { parserFile };
    }

    /**
     * Gets the grammar files that were included by the scanner during the last invocation of {@link #scan()}.
     * 
     * @return An array of grammar infos describing the included grammar files, will be empty if no files were included
     *         but is never <code>null</code>.
     */
    public GrammarInfo[] getIncludedGrammars()
    {
        return (GrammarInfo[]) this.includedGrammars.toArray( new GrammarInfo[this.includedGrammars.size()] );
    }

}
