/*
 * 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 org.apache.brooklyn.cli;

import io.airlift.airline.Arguments;
import io.airlift.airline.Cli;
import io.airlift.airline.Cli.CliBuilder;
import io.airlift.airline.Command;
import io.airlift.airline.Help;
import io.airlift.airline.Option;
import io.airlift.airline.OptionType;
import io.airlift.airline.ParseException;

import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.inject.Inject;

import org.apache.brooklyn.api.catalog.BrooklynCatalog;
import org.apache.brooklyn.cli.Main.LaunchCommand;
import org.apache.brooklyn.core.BrooklynVersion;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.FatalConfigurationRuntimeException;
import org.apache.brooklyn.util.exceptions.FatalRuntimeException;
import org.apache.brooklyn.util.exceptions.UserFacingException;
import org.apache.brooklyn.util.text.KeyValueParser;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;

/**
 * This class is the primary CLI for brooklyn.
 * Run with the `help` argument for help.
 * <p>
 * This class is designed for subclassing, with subclasses typically:
 * <li> providing their own static {@link #main(String...)} (of course) which need simply invoke 
 *      {@link #execCli(String[])} with the arguments 
 * <li> returning their CLI name (e.g. "start.sh") in an overridden {@link #cliScriptName()}
 * <li> providing an overridden {@link LaunchCommand} via {@link #cliLaunchCommand()} if desired
 * <li> providing any other CLI customisations by overriding {@link #cliBuilder()}
 *      (typically calling the parent and then customizing the builder)
 * <li> populating a custom catalog using {@link LaunchCommand#populateCatalog(BrooklynCatalog)}
 */
public abstract class AbstractMain {

    private static final Logger log = LoggerFactory.getLogger(AbstractMain.class);

    // Launch banner
    public static final String DEFAULT_BANNER =
        " _                     _    _             \n" +
        "| |__  _ __ ___   ___ | | _| |_   _ _ __ (R)\n" +
        "| '_ \\| '__/ _ \\ / _ \\| |/ / | | | | '_ \\ \n" +
        "| |_) | | | (_) | (_) |   <| | |_| | | | |\n" +
        "|_.__/|_|  \\___/ \\___/|_|\\_\\_|\\__, |_| |_|\n" +
        "                              |___/             "+BrooklynVersion.get()+"\n";

    // Error codes
    public static final int SUCCESS = 0;
    public static final int PARSE_ERROR = 1;
    public static final int EXECUTION_ERROR = 2;
    public static final int CONFIGURATION_ERROR = 3;

    /**
     * Field intended for sub-classes (with their own {@code main()}) to customize the banner.
     * All accesses to the banner are done through this field, to ensure consistent customization.
     * 
     * Note that a {@code getBanner()} method is not an option for supporting this, because
     * it is accessed from static inner-classes (such as {@link InfoCommand}, so non-static
     * methods are not an option (and one can't override static methods).
     */
    protected static volatile String banner = DEFAULT_BANNER;
    
    /** abstract superclass for commands defining global options, but not arguments,
     * as that prevents Help from being injectable in the {@link HelpCommand} subclass */
    public static abstract class BrooklynCommand implements Callable<Void> {

        @Option(type = OptionType.GLOBAL, name = { "-v", "--verbose" }, description = "Verbose mode")
        public boolean verbose;

        @Option(type = OptionType.GLOBAL, name = { "-q", "--quiet" }, description = "Quiet mode")
        public boolean quiet;

        @VisibleForTesting
        protected PrintStream stdout = System.out;
        
        @VisibleForTesting
        protected PrintStream stderr = System.err;

        @VisibleForTesting
        protected InputStream stdin = System.in;

        public ToStringHelper string() {
            return Objects.toStringHelper(getClass())
                    .add("verbose", verbose)
                    .add("quiet", quiet);
        }
        
        @Override
        public String toString() {
            return string().toString();
        }
    }
    
    /** common superclass for commands, defining global options (in our super) and extracting the arguments */
    public static abstract class BrooklynCommandCollectingArgs extends BrooklynCommand {

        /** extra arguments */
        @Arguments
        public List<String> arguments = new ArrayList<String>();
        
        /** @return true iff there are arguments; it also sys.errs a warning in that case  */
        protected boolean warnIfArguments() {
            if (arguments.isEmpty()) return false;
            stderr.println("Invalid subcommand arguments: "+Strings.join(arguments, " "));
            return true;
        }
        
        /** throw {@link ParseException} iff there are arguments */
        protected void failIfArguments() {
            if (arguments.isEmpty()) return ;
            throw new ParseException("Invalid subcommand arguments '"+Strings.join(arguments, " ")+"'");
        }
        
        @Override
        public ToStringHelper string() {
            return super.string()
                    .add("arguments", arguments);
        }
    }
    
    /** superclass which reads `-D` system property definitions and applies them
     * <p>
     * useful when scripting, e.g. where brooklyn.sh encodes `java o.a.b.Main "$@"` 
     * but we want the caller to be able to pass system properties
     */
    public static abstract class BrooklynCommandWithSystemDefines extends BrooklynCommandCollectingArgs {
        @Option(type = OptionType.GLOBAL, name = { "-D" }, description = "Set java system property")
        public List<String> defines1;

        @Option(name = { "-D" }, description = "Set java system property")
        public List<String> defines2;

        public List<String> getDefines() { return MutableList.copyOf(defines1).appendAll(defines2); }
        public Map<String,String> getDefinesAsMap() { return KeyValueParser.parseMap(Strings.join(getDefines(),",")); }
        public void applyDefinesAsSystemProperties() { System.getProperties().putAll(getDefinesAsMap()); }
        
        @Override
        public ToStringHelper string() {
            return super.string()
                    .add("defines", getDefines());
        }
        
        @Override
        public Void call() throws Exception {
            applyDefinesAsSystemProperties();
            return null;
        }
    }

    @Command(name = "help", description = "Display help for available commands")
    public static class HelpCommand extends BrooklynCommand {

        @Inject
        public Help help;

        @Override
        public Void call() throws Exception {
            if (log.isDebugEnabled()) log.debug("Invoked help command: {}", this);
            return help.call();
        }
    }

    @Command(name = "info", description = "Display information about brooklyn")
    public static class InfoCommand extends BrooklynCommandCollectingArgs {
        
        @Override
        public Void call() throws Exception {
            if (log.isDebugEnabled()) log.debug("Invoked info command: {}", this);
            warnIfArguments();

            System.out.println(banner);
            System.out.println("Version:  " + BrooklynVersion.get());
            if (BrooklynVersion.INSTANCE.isSnapshot()) {
                System.out.println("Git SHA1: " + BrooklynVersion.INSTANCE.getSha1FromOsgiManifest());
            }
            System.out.println("Website:  http://brooklyn.apache.org");
            System.out.println("Source:   https://github.com/apache/brooklyn");
            System.out.println();
            System.out.println("Copyright 2011-2016 The Apache Software Foundation.");
            System.out.println("Licensed under the Apache 2.0 License");
            System.out.println();

            return null;
        }
    }

    public static class DefaultInfoCommand extends InfoCommand {
        @Override
        public Void call() throws Exception {
            super.call();
            System.out.println("ERROR: No command specified.");
            System.out.println();
            throw new ParseException("No command specified.");
        }
    }

    /** method intended for overriding when the script filename is different 
     * @return the name of the script the user has invoked */
    protected String cliScriptName() {
        return "brooklyn";
    }

    /** 
     * Build the commands.
     */
    protected abstract CliBuilder<BrooklynCommand> cliBuilder();
    
    protected void execCli(String ...args) {
        execCli(cliBuilder().build(), args);
    }
    
    protected void execCli(Cli<BrooklynCommand> parser, String ...args) {
        try {
            log.debug("Parsing command line arguments: {}", Arrays.asList(args));
            BrooklynCommand command = parser.parse(args);
            log.debug("Executing command: {}", command);
            command.call();
            System.exit(SUCCESS);
        } catch (ParseException pe) { // looks like the user typed it wrong
            System.err.println("Parse error: " + pe.getMessage()); // display
                                                                   // error
            System.err.println(getUsageInfo(parser)); // display cli help
            System.exit(PARSE_ERROR);
        } catch (FatalConfigurationRuntimeException e) {
            log.error("Configuration error: "+e.getMessage(), e.getCause());
            System.err.println("Configuration error: " + e.getMessage());
            System.exit(CONFIGURATION_ERROR);
        } catch (FatalRuntimeException e) { // anticipated non-configuration error
            log.error("Startup error: "+e.getMessage(), e.getCause());
            System.err.println("Startup error: "+e.getMessage());
            System.exit(EXECUTION_ERROR);
        } catch (Exception e) { // unexpected error during command execution
            log.error("Execution error: " + e.getMessage(), e);
            System.err.println("Execution error: " + e.getMessage());
            if (!(e instanceof UserFacingException))
                e.printStackTrace();
            System.exit(EXECUTION_ERROR);
        }
    }

    protected String getUsageInfo(Cli<BrooklynCommand> parser) {
        StringBuilder help = new StringBuilder();
        help.append("\n");
        Help.help(parser.getMetadata(), Collections.<String>emptyList(), help);
        return help.toString();
    }

}
