/*
 This file is part of GNUnet.
 (C) 2011, 2012 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.util;

import org.apache.log4j.*;
import org.gnunet.util.getopt.Argument;
import org.gnunet.util.getopt.ArgumentAction;
import org.gnunet.util.getopt.Parser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;


/**
 * Program is the entry point class for everything that uses GNUnet services or APIs.
 *
 * Also specifies the default command line arguments using the org.gnunet.util.getopt annotations.
 *
 * @see Service
 */
public abstract class Program {
    private static final Logger logger = LoggerFactory
            .getLogger(Program.class);


    protected final Configuration cfg = new Configuration();

    @Argument(shortname = "c", longname = "config",
            description = "path of the configuration file",
            argumentName = "FILENAME",
            action = ArgumentAction.STORE_STRING)
    public String cfgFileName;

    @Argument(shortname = "h", longname = "help",
            description = "print this help message",
            action = ArgumentAction.SET)
    public boolean printHelp;

    @Argument(shortname = "v", longname = "version",
            description = "print version",
            action = ArgumentAction.SET)
    public boolean showVersion;


    @Argument(shortname = "L", longname = "log",
            description = "configure logging to use LOGLEVEL",
            argumentName = "LOGLEVEL",
            action = ArgumentAction.STORE_STRING)
    public String logLevel;

    @Argument(shortname = "l", longname = "logfile",
            description = "configure logging to write logs to LOGFILE",
            argumentName = "LOGFILE",
            action = ArgumentAction.STORE_STRING)
    public String logFile;

    /**
     * Positional arguments, excluding those that have been processed
     * by the command line parser.
     */
    protected String[] unprocessedArgs;

    /**
     * Return value for the program.
     * We prefer setting the return value, as System.exit(...) does bad things sometimes.
     * (In JUnit test cases, for instance)
     */
    private int returnValue = 0;

    /**
     * Configure logging with the given log level and log file.
     *
     * @param logLevel one of DEBUG,INFO,WARN,ERROR,OFF
     * @param logFile logfile, absolute or relative to the current working directory
     */
    public static void configureLogging(String logLevel, String logFile) {
        org.apache.log4j.Logger rootLogger = LogManager.getRootLogger();

        rootLogger.removeAllAppenders();

        // %c{2}: category 2 levels
        Layout layout = new PatternLayout("%d{dd MMM yyyy HH:mm:ss-SSS} %c{2} %p: %m%n");

        if (logFile == null) {
            rootLogger.addAppender(new ConsoleAppender(layout, ConsoleAppender.SYSTEM_OUT));
        } else {
            Appender appender = null;
            try {
                appender = new FileAppender(layout, logFile);
            } catch (IOException e) {
                logger.warn("could not open log file {}", logFile);
            }
            if (appender!= null) {
                rootLogger.removeAllAppenders();
                rootLogger.addAppender(appender);
            }
        }
        if (logLevel == null) {
            rootLogger.setLevel(Level.INFO);
        } else if (logLevel.equalsIgnoreCase("debug")) {
            rootLogger.setLevel(Level.DEBUG);
        } else if (logLevel.equalsIgnoreCase("info")) {
            rootLogger.setLevel(Level.INFO);
        } else if (logLevel.equalsIgnoreCase("warn") || logLevel.equalsIgnoreCase("warning")) {
            rootLogger.setLevel(Level.WARN);
        } else if (logLevel.equalsIgnoreCase("error")) {
            rootLogger.setLevel(Level.ERROR);
        } else if (logLevel.equalsIgnoreCase("off")) {
            rootLogger.setLevel(Level.OFF);
        } else {
            rootLogger.setLevel(Level.INFO);
            logger.info("unknown log level '{}'; defaulting to INFO", logLevel);
        }
    }

    public static void configureLogging(String logLevel) {
        configureLogging(logLevel, null);
    }

    public static void configureLogging() {
        configureLogging(null, null);
    }


    /**
     * Override to display a different help text on "-h/--help"
     *
     * @return the help text
     */
    protected String makeHelpText() {
        return "gnunet-java tool";
    }

    /**
     * Override to display a different version description on "-h/--help"
     *
     * @return version description
     */
    protected String makeVersionDescription() {
        return "development version of gnunet-java";
    }

    final protected void setReturnValue(int x) {
        returnValue = x;
    }

    /**
     * Overridden by specializations of Program, like Service.
     *
     * Allows for start() to be final.
     */
    /* package-private */
    void runHook() {
        run();
    }

    /**
     * Override to implement the behavior of the Program.
     */
    protected abstract void run();

    protected final Configuration getConfiguration() {
        return cfg;
    }


    /**
     * Start the Program.  Invokes the run method.
     *
     * @param withScheduler determines whether the run-method
     *                      is invoked inside a scheduler task or not
     * @return the exit value of the program
     */
    public final int start(boolean withScheduler, String... args) {
        Parser optParser = new Parser(this);
        unprocessedArgs = optParser.parse(args);

        configureLogging(logLevel, logFile);

        cfg.loadDefaults();

        if (cfgFileName != null) {
            cfg.parse(cfgFileName);
        }

        Resolver.getInstance().setConfiguration(cfg);

        if (showVersion) {
            System.out.println(makeVersionDescription());
        } else if (printHelp) {
            System.out.println(makeHelpText());
            System.out.print(optParser.getHelp());
        } else {
            if (withScheduler) {
                Scheduler.run(new Scheduler.Task() {
                    public void run(Scheduler.RunContext c) {
                        Program.this.runHook();
                    }
                });
            } else {
                Program.this.runHook();
            }
        }

        if (Scheduler.hasTasks()) {
            logger.error("scheduler still has pending tasks after program returned");
        }

        return returnValue;
    }


    /**
     * Start the Program as the initial task of the Scheduler.
     *
     * @return the exit value of the program
     */
    public final int start(String... args) {
        return start(true, args);
    }


    /**
     * Start the program without setting up the scheduler.
     *
     * @return the exit value of the program
     */
    public final int startWithoutScheduler(String... args) {
        return start(false, args);
    }
}
