package org.codehaus.plexus.tools.cli;

/*
 * Copyright 2006 The Codehaus 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.
 */

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.codehaus.plexus.ContainerConfiguration;
import org.codehaus.plexus.DefaultContainerConfiguration;
import org.codehaus.plexus.DefaultPlexusContainer;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.PlexusContainerException;
import org.codehaus.plexus.classworlds.ClassWorld;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;

/**
 * @author jason van zyl
 */
public abstract class AbstractCli implements Cli {
    // ----------------------------------------------------------------------------
    // These are standard options that we would want to use for all our projects.
    // ----------------------------------------------------------------------------

    public static final char QUIET = 'q';

    public static final char DEBUG = 'X';

    public static final char ERRORS = 'e';

    public static final char HELP = 'h';

    public static final char VERSION = 'v';

    public static final char SET_SYSTEM_PROPERTY = 'D';

    // ----------------------------------------------------------------------------
    // Abstract methods
    // ----------------------------------------------------------------------------

    public abstract Options buildCliOptions(Options options);

    public abstract void invokePlexusComponent(CommandLine cli, PlexusContainer container) throws Exception;

    public String getPomPropertiesPath() {
        return null;
    }

    public int execute(String[] args) {
        ClassWorld classWorld =
                new ClassWorld("plexus.core", Thread.currentThread().getContextClassLoader());

        return execute(args, classWorld);
    }

    public int execute(String[] args, ClassWorld classWorld) {
        CommandLine cli;

        try {
            cli = parse(args);
        } catch (ParseException e) {
            System.err.println("Unable to parse command line options: " + e.getMessage());

            displayHelp();

            return 1;
        }

        if (System.getProperty("java.class.version", "44.0").compareTo("48.0") < 0) {
            System.err.println("Sorry, but JDK 1.4 or above is required to execute Maven");

            System.err.println(
                    "You appear to be using Java version: " + System.getProperty("java.version", "<unknown>"));

            return 1;
        }

        boolean debug = cli.hasOption(DEBUG);

        boolean quiet = !debug && cli.hasOption(QUIET);

        boolean showErrors = debug || cli.hasOption(ERRORS);

        if (showErrors) {
            System.out.println("+ Error stacktraces are turned on.");
        }

        // ----------------------------------------------------------------------------
        // Logging
        // ----------------------------------------------------------------------------

        int loggingLevel;

        if (debug) {
            loggingLevel = 0;
        } else if (quiet) {
            loggingLevel = 0;
        } else {
            loggingLevel = 0;
        }

        // ----------------------------------------------------------------------
        // Process particular command line options
        // ----------------------------------------------------------------------

        if (cli.hasOption(HELP)) {
            displayHelp();

            return 0;
        }

        if (cli.hasOption(VERSION)) {
            showVersion();

            return 0;
        } else if (debug) {
            showVersion();
        }

        // ----------------------------------------------------------------------------
        // This is what we will generalize for the invocation of the command line.
        // ----------------------------------------------------------------------------

        try {
            ContainerConfiguration configuration = new DefaultContainerConfiguration().setClassWorld(classWorld);

            customizeContainerConfiguration(configuration, cli);

            PlexusContainer plexus = new DefaultPlexusContainer(configuration);

            invokePlexusComponent(cli, plexus);
        } catch (PlexusContainerException e) {
            showFatalError("Cannot create Plexus container.", e, true);
        } catch (ComponentLookupException e) {
            showError("Cannot lookup application component.", e, true);
        } catch (Exception e) {
            showError("Problem executing command line.", e, true);
        }

        return 0;
    }

    protected void customizeContainerConfiguration(ContainerConfiguration configuration, CommandLine cli) {}

    protected int showFatalError(String message, Exception e, boolean show) {
        System.err.println("FATAL ERROR: " + message);

        if (show) {
            System.err.println("Error stacktrace:");

            e.printStackTrace();
        } else {
            System.err.println("For more information, run with the -e flag");
        }

        return 1;
    }

    protected void showError(String message, Exception e, boolean show) {
        System.err.println(message);

        if (show) {
            System.err.println("Error stacktrace:");

            e.printStackTrace();
        }
    }

    // Need to get the versions of the application in a general way, so that I need a way to get the
    // specifics of the application so that I can do this in a general way.
    private void showVersion() {
        InputStream is;

        try {
            Properties properties = new Properties();

            String pomPropertiesPath = getPomPropertiesPath();

            if (pomPropertiesPath == null) {
                System.err.println("Unable determine version from JAR file.");

                return;
            }

            is = AbstractCli.class.getClassLoader().getResourceAsStream(pomPropertiesPath);

            if (is == null) {
                System.err.println("Unable determine version from JAR file.");

                return;
            }

            properties.load(is);

            if (properties.getProperty("builtOn") != null) {
                System.out.println("Version: " + properties.getProperty("version", "unknown") + " built on "
                        + properties.getProperty("builtOn"));
            } else {
                System.out.println("Version: " + properties.getProperty("version", "unknown"));
            }
        } catch (IOException e) {
            System.err.println("Unable determine version from JAR file: " + e.getMessage());
        }
    }

    // ----------------------------------------------------------------------
    // System properties handling
    // ----------------------------------------------------------------------

    private Properties getExecutionProperties(CommandLine commandLine) {
        Properties executionProperties = new Properties();

        // ----------------------------------------------------------------------
        // Options that are set on the command line become system properties
        // and therefore are set in the session properties. System properties
        // are most dominant.
        // ----------------------------------------------------------------------

        if (commandLine.hasOption(SET_SYSTEM_PROPERTY)) {
            String[] defStrs = commandLine.getOptionValues(SET_SYSTEM_PROPERTY);

            for (int i = 0; i < defStrs.length; ++i) {
                setCliProperty(defStrs[i], executionProperties);
            }
        }

        executionProperties.putAll(System.getProperties());

        return executionProperties;
    }

    private void setCliProperty(String property, Properties executionProperties) {
        String name;

        String value;

        int i = property.indexOf("=");

        if (i <= 0) {
            name = property.trim();

            value = "true";
        } else {
            name = property.substring(0, i).trim();

            value = property.substring(i + 1).trim();
        }

        executionProperties.setProperty(name, value);

        // ----------------------------------------------------------------------
        // I'm leaving the setting of system properties here as not to break
        // the SystemPropertyProfileActivator. This won't harm embedding. jvz.
        // ----------------------------------------------------------------------

        System.setProperty(name, value);
    }

    private Options options;

    public Options buildDefaultCliOptions() {
        options = new Options();

        options.addOption(OptionBuilder.withLongOpt("define")
                .hasArg()
                .withDescription("Define a system property")
                .create(SET_SYSTEM_PROPERTY));
        options.addOption(OptionBuilder.withLongOpt("help")
                .withDescription("Display help information")
                .create(HELP));
        options.addOption(OptionBuilder.withLongOpt("version")
                .withDescription("Display version information")
                .create(VERSION));
        options.addOption(OptionBuilder.withLongOpt("quiet")
                .withDescription("Quiet output - only show errors")
                .create(QUIET));
        options.addOption(OptionBuilder.withLongOpt("debug")
                .withDescription("Produce execution debug output")
                .create(DEBUG));
        options.addOption(OptionBuilder.withLongOpt("errors")
                .withDescription("Produce execution error messages")
                .create(ERRORS));

        return buildCliOptions(options);
    }

    public CommandLine parse(String[] args) throws ParseException {
        // We need to eat any quotes surrounding arguments...
        String[] cleanArgs = cleanArgs(args);

        CommandLineParser parser = new GnuParser();

        return parser.parse(buildDefaultCliOptions(), cleanArgs);
    }

    private static String[] cleanArgs(String[] args) {
        List cleaned = new ArrayList();

        StringBuffer currentArg = null;

        for (int i = 0; i < args.length; i++) {
            String arg = args[i];

            boolean addedToBuffer = false;

            if (arg.startsWith("\"")) {
                // if we're in the process of building up another arg, push it and start over.
                // this is for the case: "-Dfoo=bar "-Dfoo2=bar two" (note the first unterminated quote)
                if (currentArg != null) {
                    cleaned.add(currentArg.toString());
                }

                // start building an argument here.
                currentArg = new StringBuffer(arg.substring(1));

                addedToBuffer = true;
            }

            // this has to be a separate "if" statement, to capture the case of: "-Dfoo=bar"
            if (arg.endsWith("\"")) {
                String cleanArgPart = arg.substring(0, arg.length() - 1);

                // if we're building an argument, keep doing so.
                if (currentArg != null) {
                    // if this is the case of "-Dfoo=bar", then we need to adjust the buffer.
                    if (addedToBuffer) {
                        currentArg.setLength(currentArg.length() - 1);
                    }
                    // otherwise, we trim the trailing " and append to the buffer.
                    else {
                        // TODO: introducing a space here...not sure what else to do but collapse whitespace
                        currentArg.append(' ').append(cleanArgPart);
                    }

                    // we're done with this argument, so add it.
                    cleaned.add(currentArg.toString());
                } else {
                    // this is a simple argument...just add it.
                    cleaned.add(cleanArgPart);
                }

                // the currentArg MUST be finished when this completes.
                currentArg = null;

                continue;
            }

            // if we haven't added this arg to the buffer, and we ARE building an argument
            // buffer, then append it with a preceding space...again, not sure what else to
            // do other than collapse whitespace.
            // NOTE: The case of a trailing quote is handled by nullifying the arg buffer.
            if (!addedToBuffer) {
                // append to the argument we're building, collapsing whitespace to a single space.
                if (currentArg != null) {
                    currentArg.append(' ').append(arg);
                }
                // this is a loner, just add it directly.
                else {
                    cleaned.add(arg);
                }
            }
        }

        // clean up.
        if (currentArg != null) {
            cleaned.add(currentArg.toString());
        }

        int cleanedSz = cleaned.size();
        String[] cleanArgs = null;

        if (cleanedSz == 0) {
            // if we didn't have any arguments to clean, simply pass the original array through
            cleanArgs = args;
        } else {
            cleanArgs = (String[]) cleaned.toArray(new String[cleanedSz]);
        }

        return cleanArgs;
    }

    public void displayHelp() {
        System.out.println();

        HelpFormatter formatter = new HelpFormatter();

        formatter.printHelp("mvn [options] [<goal(s)>] [<phase(s)>]", "\nOptions:", options, "\n");
    }
}
