/*
 * 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.jackrabbit.vault.util.console;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Properties;

import org.apache.commons.cli2.CommandLine;
import org.apache.commons.cli2.DisplaySetting;
import org.apache.commons.cli2.Group;
import org.apache.commons.cli2.Option;
import org.apache.commons.cli2.OptionException;
import org.apache.commons.cli2.builder.ArgumentBuilder;
import org.apache.commons.cli2.builder.DefaultOptionBuilder;
import org.apache.commons.cli2.builder.GroupBuilder;
import org.apache.commons.cli2.commandline.Parser;
import org.apache.commons.cli2.option.Command;
import org.apache.commons.cli2.util.HelpFormatter;
import org.apache.jackrabbit.vault.util.console.util.CliHelpFormatter;
import org.apache.jackrabbit.vault.util.console.util.Log4JConfig;
import org.apache.jackrabbit.vault.util.console.util.PomProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@code Console}...
 */
public abstract class AbstractApplication {

    /**
     * the default logger
     */
    static final Logger log = LoggerFactory.getLogger(AbstractApplication.class);

    private static final String LOG4J_PROPERTIES = "/org/apache/jackrabbit/vault/util/console/log4j.properties";

    public static final String DEFAULT_CONF_FILENAME = "console.properties";

    public static final String KEY_PROMPT = "prompt";
    public static final String KEY_USER = "user";
    public static final String KEY_PATH = "path";
    public static final String KEY_HOST = "host";
    public static final String KEY_LOGLEVEL = "loglevel";

    /**
     * The global env can be loaded and saved into the console properties.
     */
    private Properties globalEnv = new Properties();

    //private Option optPropertyFile;
    private Option optLogLevel;
    private Option optVersion;
    private Option optHelp;

    public String getVersion() {
        return getPomProperties().getVersion();
    }

    public PomProperties getPomProperties() {
        return new PomProperties("org.apache.jackrabbit.vault", "vault-cli");
    }
    
    public String getCopyrightLine() {
        return "copyright 2013-2020 by Apache Software Foundation. See LICENSE.txt for more information.";
    }
    
    public String getVersionString() {
        return getApplicationName() + " [version " + getVersion() + "] " + getCopyrightLine();
    }

    public void printVersion() {
        System.out.println(getVersionString());
    }

    /**
     * Returns the name of this application
     *
     * @return the name of this application
     */
    public abstract String getApplicationName();


    /**
     * Returns the name of the shell command
     *
     * @return the name of the shell command
     */
    public abstract String getShellCommand();

    public void printHelp(String cmd) {
        if (cmd == null) {
            getAppHelpFormatter().print();
        } else {
            getDefaultContext().printHelp(cmd);
        }
    }

    protected HelpFormatter getAppHelpFormatter() {
        CliHelpFormatter hf = CliHelpFormatter.create();
        StringBuffer sep = new StringBuffer(hf.getPageWidth());
        while (sep.length() < hf.getPageWidth()) {
            sep.append("-");
        }
        hf.setHeader(getVersionString());
        hf.setDivider(sep.toString());
        hf.setShellCommand("  " + getShellCommand() + " [options] <command> [arg1 [arg2 [arg3] ..]]");
        hf.setGroup(getApplicationCLGroup());
        hf.setSkipToplevel(true);
        hf.getFullUsageSettings().removeAll(DisplaySetting.ALL);

        hf.getDisplaySettings().remove(DisplaySetting.DISPLAY_GROUP_ARGUMENT);
        hf.getDisplaySettings().remove(DisplaySetting.DISPLAY_PARENT_CHILDREN);
        hf.getDisplaySettings().add(DisplaySetting.DISPLAY_OPTIONAL);

        hf.getLineUsageSettings().add(DisplaySetting.DISPLAY_PROPERTY_OPTION);
        hf.getLineUsageSettings().add(DisplaySetting.DISPLAY_PARENT_ARGUMENT);
        hf.getLineUsageSettings().add(DisplaySetting.DISPLAY_ARGUMENT_BRACKETED);
        return hf;
    }

    public Group getApplicationCLGroup() {
        return new GroupBuilder()
                .withName("")
                .withOption(addApplicationOptions(new GroupBuilder()).create())
                .withOption(getDefaultContext().getCommandsGroup())
                .withMinimum(0)
                .create();
    }

    public GroupBuilder addApplicationOptions(GroupBuilder gbuilder) {
        final DefaultOptionBuilder obuilder = new DefaultOptionBuilder();
        final ArgumentBuilder abuilder = new ArgumentBuilder();
        /*
        optPropertyFile =
                obuilder
                        .withShortName("F")
                        .withLongName("console-settings")
                        .withDescription(
                                "The console settings property file. " +
                                        "This is only required for interactive mode.")
                        .withArgument(abuilder
                                .withDescription("defaults to ...")
                                .withMinimum(1)
                                .withMaximum(1)
                                .create()
                        )
                        .create();
        optInteractive =
                obuilder
                        .withShortName("i")
                        .withLongName("interactive")
                        .withDescription("runs this application in an interactive mode.")
                        .create();
        */
        optVersion =
                obuilder
                        .withLongName("version")
                        .withDescription("print the version information and exit")
                        .create();
        optHelp =
                obuilder
                        .withShortName("h")
                        .withLongName("help")
                        .withDescription("print this help")
                        .withArgument(abuilder
                                .withName("command")
                                .withMaximum(1)
                                .create()
                        )
                        .create();

        optLogLevel =
                obuilder
                        .withLongName("log-level")
                        .withDescription("the log4j log level")
                        .withArgument(abuilder
                                .withName("level")
                                .withMaximum(1)
                                .create()
                        )
                        .create();

        gbuilder
                .withName("Global options:")
                //.withOption(optPropertyFile)
                .withOption(CliCommand.OPT_VERBOSE)
                .withOption(CliCommand.OPT_QUIET)
                .withOption(optVersion)
                .withOption(optLogLevel)
                .withOption(optHelp)
                .withMinimum(0);
        /*
        if (getConsole() != null) {
            gbuilder.withOption(optInteractive);
        }
        */
        return gbuilder;
    }

    protected void init() {
        globalEnv.setProperty(KEY_PROMPT,
                "[${" + KEY_USER + "}@${" + KEY_HOST + "} ${" + KEY_PATH  +"}]$ ");
    }

    protected void initLogging() {
        Log4JConfig.init(LOG4J_PROPERTIES);
    }

    protected void run(String[] args) {
        // setup logging
        try {
            initLogging();
        } catch (Throwable e) {
            System.err.println("Error while initializing logging: " + e);
        }

        // setup and start
        init();

        Parser parser = new Parser();
        parser.setGroup(getApplicationCLGroup());
        parser.setHelpOption(optHelp);
        try {
            CommandLine cl = parser.parse(args);
            String logLevel = getEnv().getProperty(KEY_LOGLEVEL);
            if (cl.hasOption(optLogLevel)) {
                logLevel = (String) cl.getValue(optLogLevel);
            }
            if (logLevel != null) {
                Log4JConfig.setLevel(logLevel);
            }
            prepare(cl);
            execute(cl);
        } catch (OptionException e) {
            log.error("{}. Type --help for more information.", e.getMessage());
        } catch (ExecutionException e) {
            log.error("Error while starting: {}", e.getMessage());
        } finally {
            close();
        }
    }

    public void setLogLevel(String level) {
        try {
            Log4JConfig.setLevel(level);
            getEnv().setProperty(KEY_LOGLEVEL, level);
            System.out.println("Log level set to '" + Log4JConfig.getLevel() + "'");
        } catch (Throwable e) {
            System.err.println("Error while setting log level: " + e);
        }
    }

    public void prepare(CommandLine cl) throws ExecutionException {
        /*
        try {
            loadConfig((String) cl.getValue(optPropertyFile));
        } catch (IOException e) {
            throw new ExecutionException("Error while loading property file.", e);
        }
        */
    }

    public void execute(CommandLine cl) throws ExecutionException {
        if (cl.hasOption(optVersion)) {
            printVersion();
        //} else if (cl.hasOption(optInteractive)) {
        //    getConsole().run();
        } else if (cl.hasOption(optHelp)) {
            String cmd = (String) cl.getValue(optHelp);
            if (cmd == null) {
                // in this case, the --help is specified after the command
                // eg: vlt checkout --help
                Iterator iter = cl.getOptions().iterator();
                while (iter.hasNext()) {
                    Object o = iter.next();                    
                    if (o instanceof Command) {
                        cmd = ((Command) o).getPreferredName();
                        break;
                    }
                }
            }
            printHelp(cmd);
        } else {
            if (!getDefaultContext().execute(cl)) {
                log.error("Unknown command. Type '--help' for more information.");
            }
        }
    }

    public void saveConfig(String path) throws IOException {
        File file = new File(path == null ? DEFAULT_CONF_FILENAME : path);
        /*
        Properties props = new Properties();
        Iterator iter = globalEnv.keySet().iterator();
        while (iter.hasNext()) {
            String key = (String) iter.next();
            if (key.startsWith("conf.") || key.startsWith("macro.")) {
                props.put(key, globalEnv.getProperty(key));
            }
        }
        props.store(out, "Console Configuration");
        */
        try (FileOutputStream out = new FileOutputStream(file)) {
            globalEnv.store(out, "Console Configuration");
        }
        log.info("Configuration saved to {}", file.getCanonicalPath());
    }

    public void loadConfig(String path) throws IOException {
        File file = new File(path == null ? DEFAULT_CONF_FILENAME : path);
        if (!file.canRead() && path == null) {
            // ignore errors for default config
            return;
        }
        Properties props = new Properties();
        try (FileInputStream in = new FileInputStream(file)) {
            props.load(in);
        }
        Iterator iter = globalEnv.keySet().iterator();
        while (iter.hasNext()) {
            String key = (String) iter.next();
            if (!props.containsKey(key)) {
                props.put(key, globalEnv.getProperty(key));
            }
        }
        globalEnv = props;
        log.info("Configuration loaded from {}", file.getCanonicalPath());
    }

    protected void close() {
    }

    public Properties getEnv() {
        return globalEnv;
    }

    public void setProperty(String key, String value) {
        if (value == null) {
            globalEnv.remove(key);
        } else {
            if (key.equals(KEY_LOGLEVEL)) {
                setLogLevel(value);
            } else {
                globalEnv.setProperty(key, value);
            }
        }
    }

    public String getProperty(String key) {
        return globalEnv.getProperty(key);
    }

    protected abstract ExecutionContext getDefaultContext();

    public abstract Console getConsole();
}