/**
 * 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.
 */

// Package
///////////////

package org.apache.jena.tools.schemagen;


// Imports
///////////////

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jena.schemagen;
import jena.schemagen.SchemagenOptions.OPT;
import org.apache.jena.rdf.model.Resource ;
import org.apache.jena.rdf.model.ResourceFactory ;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.DirectoryScanner;


/**
 * <p>Maven plugin to execute Jena schemagen as part of a Jena-based
 * project build cycle
 * </p>
*/
@Mojo(name="translate", defaultPhase=LifecyclePhase.GENERATE_SOURCES)
public class SchemagenMojo
    extends AbstractMojo
{
    /* ******************************* */
    /* Constants                       */
    /* ******************************* */

    /** Default output location */
    public static final String GENERATED_SOURCES = File.separator + "generated-sources";

    /** Default pattern for includes */

    /** Name of default options element */
    public static final String DEFAULT_OPTIONS_ELEM = "default";

    /* ******************************* */
    /* Static variables                */
    /* ******************************* */

    /* ******************************* */
    /* Instance variables              */
    /* ******************************* */

    /**
     * @parameter property="project.build.directory"
     */

    @Parameter(property="project.build.directory")
    private String projectBuildDir;


    /**
     * Array of file patterns to include in processing
     */
    @Parameter
    private String[] includes = new String[0];

    /**
     * Array of file patterns to exclude from processing
     */
    @Parameter
    private String[] excludes = new String[0];

    /**
     * Options for individual files
     */
    @Parameter
    private List<Source> fileOptions;

    /**
     * The current base directory of the project
     */
    @Parameter(property="basedir")
    private File baseDir;

    /** The default options object, if any */
    private SchemagenOptions defaultOptions;

    /** Map of source options, indexed by name */
    private Map<String, SchemagenOptions> optIndex = new HashMap<>();

    /* ******************************* */
    /* Constructors                    */
    /* ******************************* */

    /* ******************************* */
    /* External signature methods      */
    /* ******************************* */

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            // set the default defaults
            defaultOptions = new SchemagenOptions(getDefaultOutputDir());
            getLog().info( "Starting schemagen execute() ...");

            // next process the various options specs
            if( fileOptions != null ){
                for (Source s: fileOptions) {
                    if (s.isDefaultOptions()) {
                        handleDefaultOptions( s );
                    }
                    else {
                        handleOption( s );
                    }
                }
            }

            if( defaultOptions == null ){
                handleDefaultOptions( new Source() );
            }

            // then the files themselves
            for (String fileName: matchFileNames()) {
                processFile( fileName );
            }
        } catch (SchemagenOptionsConfigurationException e) {
            throw new MojoExecutionException(
                    "Error during default schemagen options creation", e);
        }
    }

    /**
     * Return a list of the file names to be processed by schemagen. These are
     * determined by processing the Ant style paths given in the <code>includes</code>
     * and <code>excludes</code> parameters.
     *
     * @return Non-null but possibly empty list of files to process, sorted into lexical order
     */
    protected List<String> matchFileNames() {
        DirectoryScanner ds = new DirectoryScanner();
        ds.setExcludes( excludes );
        ds.setIncludes( includes );
        ds.setBasedir( getBaseDir() );
        ds.scan();

        List<String> files = new ArrayList<>( Arrays.asList( ds.getIncludedFiles() ) );
        Collections.sort( files );

        //add http includes
        for( String include : includes ){
            if( include.startsWith("http:") || include.startsWith("https:")){
                files.add( include );
            }
        }
        return files;
    }


    /**
     * Return the default options structure, or null
     * @return The default options
     */
    protected SchemagenOptions getDefaultOptions() {
        return defaultOptions;
    }

    /** Return the value of <code>${project.build.directory}</code> */
    public String getProjectBuildDir() {
        return projectBuildDir;
    }

    /**
     * Handle the default options by creating a default options object and assigning
     * the options values from the given source object.
     * @param defOptionsSource The source object containing the default options
     * @throws SchemagenOptionsConfigurationException
     */
    protected void handleDefaultOptions( Source defOptionsSource )
            throws SchemagenOptionsConfigurationException {
        SchemagenOptions defSo = new SchemagenOptions(getDefaultOutputDir(),
                defOptionsSource);
        if (defaultOptions != null) {
            defSo.setParent( defaultOptions );
        }
        defaultOptions = defSo;
    }

    /**
     * Process the given options specification for one of the input files
     * by attaching the default options and indexing.
     *
     * @param optionSpec Specification of the options for a given file
     * @throws SchemagenOptionsConfigurationException
     */
    protected void handleOption( Source optionSpec ) throws SchemagenOptionsConfigurationException {
        SchemagenOptions so = new SchemagenOptions(getDefaultOutputDir(), optionSpec);
        if (optionSpec.getInput() != null && !optionSpec.getInput().isEmpty()) {
            so.setParent( getDefaultOptions() );
            optIndex.put( optionSpec.getInput(), so );
        }
        else {
            getLog().info( "ignoring <source> element because the fileName is not specified" );
        }
    }

    /**
     * Delegate the processing of the given file to schemagen itself
     * @param fileName
     * @throws SchemagenOptionsConfigurationException
     */
    protected void processFile( String fileName )
        throws MojoExecutionException, SchemagenOptionsConfigurationException
    {
        getLog().info( "processFile with " + fileName );
        getLog().info( optIndex.keySet().toString() );
        SchemagenOptions so = optIndex.get( fileName );
        getLog().info( "so = " + so );

        // if we have no options carrier for this file, we create one to contain
        // the name of the input file, and link it to the defaults
        String soFileName;
        if (so == null) {
            so = new SchemagenOptions(getDefaultOptions().getOutputOption());
            soFileName = fileName;
            so.setParent( getDefaultOptions() );
        } else {
            soFileName = so.getOption( OPT.INPUT ).asLiteral().getString();
        }

        getLog().info( "input before adjustment: " + soFileName );

        boolean relative = !(soFileName.startsWith( "http:" ) || soFileName.startsWith( "https:" )
                || soFileName.startsWith( "file:" ));
        getLog().info( "relative = " + relative );
        getLog().info( "baseDir = " + baseDir );
        getLog().info( "getBaseDir() = " + getBaseDir() );
        soFileName = relative ? "file:" + baseDir + File.separator + soFileName : soFileName;
        //fix windows paths
        if( File.separator.equals("\\") ){
            soFileName = soFileName.replaceAll( "\\\\", "/" );
        }
        getLog().info( "input after adjustment: " + soFileName );
        Resource input = ResourceFactory.createResource( soFileName );
        so.setOption( OPT.INPUT, input );

        getLog().info( "about to call run(): " );
        ensureTargetDirectory( so );
        new SchemagenAdapter().run( so );
    }


    /* ******************************* */
    /* Internal implementation methods */
    /* ******************************* */

    public void setExcludes( String[] excludes ) {
        this.excludes = excludes;
    }

    public void setIncludes( String[] includes ) {
        this.includes = includes;
    }

    /**
     * Append the given string to the array of included file patterns
     * @param incl File pattern string to append to <code>this.includes</code>
     */
    public void addIncludes( String incl ) {
        String[] incls = new String[this.includes.length + 1];
        int i = 0;
        for (String s: this.includes) {
            incls[i++] = s;
        }
        incls[i] = incl;

        this.includes = incls;
    }

    /**
     * Append the given string to the array of excluded file patterns
     * @param excl File pattern string to append to <code>this.excludes</code>
     */
    public void addExcludes( String excl ) {
        String[] excls = new String[this.excludes.length + 1];
        int i = 0;
        for (String s: this.excludes) {
            excls[i++] = s;
        }
        excls[i] = excl;
        this.excludes = excls;
    }

    /**
     * Return the base directory for the plugin, which should be supplied
     * by plexus, but if not we default to the current working directory.
     *
     * @return The base directory as a file
     */
    protected File getBaseDir() {
        return (baseDir == null) ? new File(".").getAbsoluteFile() : baseDir;
    }

    /**
     * Ensure that the output directory exists
     */
    protected void ensureTargetDirectory( SchemagenOptions so )
        throws MojoExecutionException
    {
        File gs = new File( so.getOutputOption() );

        if (!gs.exists()) {
            gs.mkdirs();
        }
        else if (!gs.isDirectory()) {
            getLog().error( "The output location is not a directory: " + gs.getPath() );
            throw new MojoExecutionException( "Already exists as file: " + gs.getPath() );
        }
        else if (!gs.canWrite()) {
            getLog().error( "Output directory exists but is not writable: " + gs.getPath() );
            throw new MojoExecutionException( "Not writable: " + gs.getPath() );
        }
    }

    protected String getDefaultOutputDir(){
        return projectBuildDir + GENERATED_SOURCES;
    }

    /* ******************************* */
    /* Inner classes                   */
    /* ******************************* */

    /**
     * Adapter class to invoke the schemagen tool with a given set of options
     */
    protected class SchemagenAdapter
        extends schemagen
    {
        public void run( SchemagenOptions options ) {
            go( options );
        }
    }
}

