/*
 * Copyright 2001-2005 The Apache Software Foundation.
 *
 * Licensed 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 com.javaforge.bobber.plugin.archetype;

import com.javaforge.bobber.archetype.model.Template;
import com.javaforge.bobber.archetype.model.Variable;
import com.javaforge.bobber.archetype.model.io.xpp3.BobberArchetypeXpp3Reader;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import org.apache.maven.archetype.Archetype;
import org.apache.maven.archetype.ArchetypeDescriptorException;
import org.apache.maven.archetype.ArchetypeNotFoundException;
import org.apache.maven.archetype.ArchetypeTemplateProcessingException;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.settings.MavenSettingsBuilder;
import org.apache.velocity.VelocityContext;
import org.codehaus.plexus.components.interactivity.InputHandler;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.codehaus.plexus.velocity.VelocityComponent;

//source blatantly copied from org.apache.maven.archetype.DefaultArchetype. Also stole code from DefaultPluginVersionManager.java

//and maven-model for the mdo
public class BobberArchetype
        extends AbstractLogEnabled
        implements Archetype
{

    private static final String NEW_LINE = System.getProperty("line.separator");

    // ----------------------------------------------------------------------
    // Components
    // ----------------------------------------------------------------------

    /** @component */
    private VelocityComponent velocity;

    /** @component */
    private ArtifactResolver artifactResolver;

    /** @component */
    private InputHandler inputHandler;

    /** @component */
    private ArtifactFactory artifactFactory;


    /** @component */
    private MavenSettingsBuilder settingsBuilder;
    private static final int MESSAGE_LINE_LENGTH = 80;


    public void createArchetype(String archetypeGroupId, String archetypeArtifactId, String archetypeVersion,
                                ArtifactRepository localRepository, List remoteRepositories, Map parameters)
            throws ArchetypeNotFoundException, ArchetypeDescriptorException, ArchetypeTemplateProcessingException
    {

        // ---------------------------------------------------------------------
        //locate the archetype file
        // ---------------------------------------------------------------------
        Artifact archetypeArtifact = artifactFactory.createArtifact(archetypeGroupId, archetypeArtifactId,
                archetypeVersion, Artifact.SCOPE_RUNTIME, "jar");

        try
        {
            artifactResolver.resolve(archetypeArtifact, remoteRepositories, localRepository);
        }
        catch (ArtifactResolutionException e)
        {
            throw new ArchetypeDescriptorException("Error attempting to download archetype: " + e.getMessage(), e);
        }

        // ----------------------------------------------------------------------
        // Load the archetype descriptor
        // ----------------------------------------------------------------------

        BobberArchetypeXpp3Reader builder = new BobberArchetypeXpp3Reader();

        com.javaforge.bobber.archetype.model.BobberArchetype archetype;

        JarFile archetypeJarFile;

        try
        {

            archetypeJarFile = new JarFile(archetypeArtifact.getFile());

            final ZipEntry zipEntry = archetypeJarFile.getEntry(ARCHETYPE_DESCRIPTOR);
            InputStream is = archetypeJarFile.getInputStream(zipEntry);

            if (is == null)
            {
                throw new ArchetypeDescriptorException(
                        "The " + ARCHETYPE_DESCRIPTOR + " descriptor cannot be found.");
            }

            archetype = builder.read(new InputStreamReader(is));

            archetypeJarFile.close();
        }
        catch (IOException e)
        {
            throw new ArchetypeDescriptorException("Error reading the " + ARCHETYPE_DESCRIPTOR + " descriptor.", e);
        }
        catch (XmlPullParserException e)
        {
            throw new ArchetypeDescriptorException("Error reading the " + ARCHETYPE_DESCRIPTOR + " descriptor.", e);
        }

        // ----------------------------------------------------------------------
        //
        // ----------------------------------------------------------------------

        String basedir = (String) parameters.get("basedir");

        String artifactId = (String) parameters.get("artifactId");

        File pomFile = new File(basedir, ARCHETYPE_POM);

        File outputDirectoryFile;

        if (pomFile.exists() && archetype.isAllowPartial())
        {
            outputDirectoryFile = new File(basedir);
        }
        else
        {
            outputDirectoryFile = new File(basedir, artifactId);

            // TODO temporarily allow partial generation, remove it later
            if (!archetype.isAllowPartial() &&
                    outputDirectoryFile.exists() &&
                    outputDirectoryFile.listFiles().length > 0)
            {
                throw new ArchetypeTemplateProcessingException(
                        outputDirectoryFile.getName() + " already exists - please run from a clean directory");
            }

            outputDirectoryFile.mkdir();

        }

        String outputDirectory = outputDirectoryFile.getAbsolutePath();

        // ----------------------------------------------------------------------
        // Set up the Velocity context
        // ----------------------------------------------------------------------

        VelocityContext context = new VelocityContext();

        String packageName = (String) parameters.get("package");

        addParamToContext("package", packageName, context);
        addParamToContext("packagePath", StringUtils.replace(packageName, ".", "/"), context);

        for (Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();)
        {
            String key = (String) iterator.next();

            Object value = parameters.get(key);
            addParamToContext(key, value, context);
        }

        //add in the specified system properties
        //if this were a mojo could have set the settings using the ${settings} expression. Since it is not need to get it from the settings builder


        boolean inInteractiveMode = false;
        try
        {
            inInteractiveMode = settingsBuilder.buildSettings().getInteractiveMode().booleanValue();
            //TODO there must be a cleaner way of doing this
            String temp = System.getProperty("interactive", null);
            if (temp != null)
            {
                inInteractiveMode = Boolean.valueOf(temp).booleanValue();
            }
            getLogger().info("Interactive is: " + inInteractiveMode);
        }
        catch (Exception ie)
        {
            throw new ArchetypeTemplateProcessingException("unable to read settings ", ie);
        }
        if (inInteractiveMode)
        {
            getLogger().info("Please enter the values for the following archetype variables:");
        }


        final List variables = archetype.getVariables();
        processVariables(variables.iterator(), context, inInteractiveMode);

        // ---------------------------------------------------------------------
        // Get Logger and display all parameters used
        // ---------------------------------------------------------------------
        if (getLogger().isInfoEnabled())
        {
            Object[] keys = context.getKeys();
            if (keys.length > 0)
            {
                getLogger().info("----------------------------------------------------------------------------");

                getLogger().info("Using following parameters for creating Archetype: " + archetypeArtifactId + ":" +
                        archetypeVersion);

                getLogger().info("----------------------------------------------------------------------------");

                for (int i = 0; i < keys.length; i++)
                {

                    String parameterName = (String) keys[i];

                    Object parameterValue = context.get(parameterName);

                    getLogger().info("Parameter: " + parameterName + " = " + parameterValue);
                }
            }
            else
            {
                getLogger().info("No Parameters found for creating Archetype");
            }
        }

        // ----------------------------------------------------------------------
        // Extract the archetype to the chosen directory
        // ----------------------------------------------------------------------

        try
        {
            archetypeJarFile = new JarFile(archetypeArtifact.getFile());
            Enumeration entries = archetypeJarFile.entries();

            while (entries.hasMoreElements())
            {
                JarEntry entry = (JarEntry) entries.nextElement();
                String path = entry.getName();
                if (!path.startsWith(ARCHETYPE_RESOURCES) || path.endsWith(".vm"))
                {
                    continue;
                }

                File t = new File(outputDirectory, path.substring(19));
                if (entry.isDirectory())
                {
                    // Assume directories are stored parents first then children.
                    getLogger().debug("Extracting directory: " + entry.getName() + " to " + t.getAbsolutePath());
                    t.mkdir();
                    continue;
                }

                getLogger().debug("Extracting file: " + entry.getName() + " to " + t.getAbsolutePath());
                t.createNewFile();
                IOUtil.copy(archetypeJarFile.getInputStream(entry), new FileOutputStream(t));
            }

            archetypeJarFile.close();

            //remove the archetype descriptor
            File t = new File(outputDirectory, ARCHETYPE_DESCRIPTOR);
            t.delete();
        }
        catch (IOException ioe)
        {
            throw new ArchetypeTemplateProcessingException("Error extracting archetype", ioe);
        }

        // ----------------------------------------------------------------------
        // Process the templates
        // ----------------------------------------------------------------------

        // use the out of the box codehaus velocity component that loads templates
        //from the class path
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try
        {
            URL[] urls = new URL[1];
            urls[0] = archetypeArtifact.getFile().toURI().toURL();
            URLClassLoader archetypeJarLoader = new URLClassLoader(urls);

            Thread.currentThread().setContextClassLoader(archetypeJarLoader);
            for (Iterator i = archetype.getTemplates().iterator(); i.hasNext();)
            {
                final Template template = (Template) i.next();

                // Check the optional 'condition' property on the template.
                // If present and the variable it points to is 'true', then
                // continue processing. If it's false, then skip.
                // If condition is not specified, assume the template should
                // be processed.
                boolean shouldProcess = true;
                String condition = template.getDependsOnVar();
                String requiredValue = null;
                List options = new ArrayList();
                if (StringUtils.isNotEmpty(condition))
                {
                    //Crappy logic processing -- for now
                    boolean not = false;
                    //Allow very simple matching logic to match templates against variable values
                    int x = condition.indexOf("!=");
                    getLogger().debug("Processing Condition : " + condition);
                    if (x > -1)
                    {
                        not = true;
                        requiredValue = condition.substring(x + 2).trim();
                        options = getListOfValues(requiredValue);
                        condition = condition.substring(0, x).trim();
                    }
                    else
                    {
                        x = condition.indexOf("=");
                        if (x > -1)
                        {
                            requiredValue = condition.substring(x + 1);
                            options = getListOfValues(requiredValue);
                            condition = condition.substring(0, x);
                        }
                    }
                    getLogger().debug("Not Expr: " + not);
                    getLogger().debug("Condition Value: '" + condition + "'");
                    getLogger().debug("Required Value: '" + requiredValue + "'");
                    final Variable var = (Variable) findVariable(condition, variables);
                    if (var != null)
                    {
                        final String strValue = (String) context.get(var.getName());
                        getLogger().debug("Variable Value is: '" + strValue + "'");
                        if (requiredValue == null)
                        {
                            if (!Boolean.valueOf(strValue).booleanValue())
                            {
                                shouldProcess = false;
                            }
                        }
                        else
                        {
                            if (!options.contains(strValue))
                            {
                                shouldProcess = false;
                            }
                        }

                    }
                    else
                    {
                        getLogger().debug("Variable Value is: null");
                        shouldProcess = false;
                    }
                    if (not)
                    {
                        shouldProcess = !shouldProcess;
                    }
                }

                if (shouldProcess)
                {
                    processTemplate(template, outputDirectory, context);
                }
                else
                {
                    getLogger().debug("Condition not met, skipping " + template.getOutput());
                }
            }

        }
        catch (MalformedURLException mfe)
        {
            throw new ArchetypeTemplateProcessingException("Error loading archetype resources into the classpath", mfe);
        }
        finally
        {
            Thread.currentThread().setContextClassLoader(old);
        }

        // ----------------------------------------------------------------------
        // Log message on Archetype creation
        // ----------------------------------------------------------------------
        if (getLogger().isInfoEnabled())
        {
            getLogger().info("Archetype created in dir: " + outputDirectory);
        }

    }

    protected void addParamToContext(String key, Object value, VelocityContext context)
    {
        getLogger().info("Adding Parameter to template Context: " + key + "=" + value);
        context.put(key, value);

    }

    protected void processVariables(Iterator variables, VelocityContext context, final boolean interactiveMode) throws ArchetypeTemplateProcessingException
    {
        while (variables.hasNext())
        {
            Variable var = (Variable) variables.next();
            String val = System.getProperty(var.getName(), var.getDefvalue());

            if (interactiveMode)
            {

                StringBuffer message = new StringBuffer();
                message.append(var.getName()).append(": ")
                        .append(NEW_LINE)
                        .append(StringUtils.repeat("*", MESSAGE_LINE_LENGTH))
                        .append(NEW_LINE)
                        .append(NEW_LINE)
                        .append(StringUtils.center(var.getDescription(), MESSAGE_LINE_LENGTH))
                        .append(NEW_LINE)
                        .append(StringUtils.leftPad("[default: " + val + "]", MESSAGE_LINE_LENGTH))
                        .append(NEW_LINE)
                        .append(StringUtils.repeat("*", MESSAGE_LINE_LENGTH));
                getLogger().info(message.toString());
                try
                {
                    String answer = inputHandler.readLine();
                    if (!StringUtils.isEmpty(answer))
                    {
                        val = answer;
                    }
                }
                catch (IOException ie)
                {
                    throw new ArchetypeTemplateProcessingException(ie);
                }
                context.put(var.getName(), val);
            }
            else
            {
                context.put(var.getName(), val);
            }

            if (val.toLowerCase().equals("false") || val.toLowerCase().equals("n"))
            {
                if (var.getVariables() != null)
                {
                    //keep processing the variables picking up the default values
                    processVariables(var.getVariables().iterator(), context, interactiveMode);

                }
            }
            else if (var.getVariables() != null)
            {
                //keep processing the variables picking up the default values
                processVariables(var.getVariables().iterator(), context, interactiveMode);

            }
        }

    }

    protected List getListOfValues(String s)
    {
        List options = new ArrayList();
        for (StringTokenizer stringTokenizer = new StringTokenizer(s, "|"); stringTokenizer.hasMoreTokens();)
        {
            options.add(stringTokenizer.nextToken());
        }
        return options;
    }

    protected void processTemplate(Template template, String outputDirectory, VelocityContext context)
            throws ArchetypeTemplateProcessingException
    {
        File outFile;


        try
        {
            StringWriter wout = new StringWriter();

            velocity.getEngine().evaluate(context, wout, "output value", template.getOutput());
            outFile = new File(outputDirectory, wout.toString());
            getLogger().debug(outFile.getAbsolutePath());
            FileUtils.forceMkdir(outFile.getParentFile());
            getLogger().debug("Created directory: " + outFile.getParentFile() + ", Dir exists = " + outFile.getParentFile().exists());

        }
        catch (Exception e)
        {
            e.printStackTrace();
            throw new ArchetypeTemplateProcessingException("error evaluating output file name " + template.getOutput(), e);
        }


        Writer writer = null;
        try
        {
            getLogger().info("Processing Template: " + template.getFile());
            String templateLocation = ARCHETYPE_RESOURCES + "/" + template.getFile();

            writer = new FileWriter(outFile);
            velocity.getEngine().mergeTemplate(templateLocation, context, writer);
            writer.flush();

        }
        catch (Exception e)
        {
            throw new ArchetypeTemplateProcessingException("Error merging velocity templates", e);
        }
        finally
        {
            IOUtil.close(writer);
            getLogger().info("Written Template to: " + outFile + ", file exists = " + outFile.exists());
        }

        // Delete archetype-originated folders in case the output path is also templated.
        // Otherwise, there will be a processed folder AND the original folder.
        try
        {
            final File templateFile = new File(outputDirectory, template.getFile());
            final String templateDir = FileUtils.dirname(templateFile.getCanonicalPath());
            final String outputDir = FileUtils.dirname(outFile.getCanonicalPath());
            if (getLogger().isDebugEnabled())
            {
                getLogger().debug("TemplateDir=" + templateDir);
                getLogger().debug("OutputDir=" + outputDir);
            }
            if (!outputDir.startsWith(templateDir))
            {
                getLogger().debug("Deleting Template Dir:" + templateDir);
                FileUtils.forceDelete(templateDir);
            }
        }
        catch (IOException e)
        {
            throw new ArchetypeTemplateProcessingException("Failed to cleanup the working dir.", e);
        }
    }


    /**
     * Find the  variable.
     *
     * @param variableName name
     * @param variables    all variables of the artifact
     * @return variable value or null of not found
     */
    protected Object findVariable(String variableName, List variables)
    {

        for (int i = 0; i < variables.size(); i++)
        {
            Variable var = (Variable) variables.get(i);
            if (variableName.equals(var.getName()))
            {
                return var;
            }
            else if (var.getVariables() != null)
            {
                Object o = findVariable(variableName, var.getVariables());
                if (o != null)
                {
                    return o;
                }
            }
        }

        return null;
    }

}



