/**
 * Copyright (c) 2010-2012 EBM WebSourcing, 2012-2013 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library 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 Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see <http://www.gnu.org/licenses/>
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.cli.shell;

import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.ow2.petals.admin.api.ContainerAdministration;
import org.ow2.petals.admin.api.PetalsAdministrationFactory;
import org.ow2.petals.admin.api.exception.ContainerAdministrationException;
import org.ow2.petals.admin.api.exception.NoConnectionException;
import org.ow2.petals.cli.api.command.Command;
import org.ow2.petals.cli.api.command.exception.CommandException;
import org.ow2.petals.cli.api.command.exception.CommandInvalidException;
import org.ow2.petals.cli.api.connection.ConnectionParameters;
import org.ow2.petals.cli.api.exception.DuplicatedCommandException;
import org.ow2.petals.cli.api.exception.NoInteractiveShellException;
import org.ow2.petals.cli.api.exception.ShellException;
import org.ow2.petals.cli.api.http.EmbeddedHttpServer;
import org.ow2.petals.cli.api.shell.Shell;
import org.ow2.petals.cli.connection.PreferenceFileException;
import org.ow2.petals.cli.connection.PreferenceFileManager;
import org.ow2.petals.cli.http.EmbeddedHttpServerConfig;
import org.ow2.petals.cli.http.EmbeddedHttpServerImpl;
import org.ow2.petals.cli.shell.exception.UnknownCommandException;
import org.ow2.petals.cli.shell.util.Utils;

/**
 * A shell registers and evaluates commands
 *
 * @author Sebastien Andre - EBM WebSourcing
 * @author Christophe DENEUX - EBM WebSourcing
 */
public abstract class AbstractShell implements Shell {

    private Logger log = Logger.getLogger(AbstractShell.class.getName());

    /**
     * Regular expression that matches comments in scripts
     */
    public static final Pattern REGEX_COMMENT = Pattern.compile("^\\s*(#.*)?$");

    /**
     * Regular expression that matches variables in expressions
     */
    public static final Pattern REGEX_VARIABLE = Pattern
            .compile("(\\\\|\\B)?(\\$\\{([-_\\.\\w]+)\\})");

    private final Map<String, Command> commands = new HashMap<String, Command>();

    protected final PrintStream printStream;

    protected final PrintStream errStream;

    /**
     * Flag to stop reading command on the input stream
     */
    protected boolean isCommandRead = true;

    private int exitStatus = 0;

    /**
     * Flag indicating if the debug mode is enabled
     */
    protected final boolean isDebugModeEnable;

    /**
     * Flag indicating if the automatic confirmation is enabled
     */
    protected final boolean isYesFlagEnabled;

    protected ConnectionParameters connectionParameters;

    private final AtomicInteger asynchronousCommandsInProgress = new AtomicInteger(0);

    /**
     * The embedded HTTP server
     */
    private EmbeddedHttpServer embeddedHttpServer;

    /**
     * 
     * @param printStream
     * @param errStream
     * @param isDebugModeEnable
     * @param isYesFlagEnable
     *            Flag to enable the automatic confirmation
     */
    public AbstractShell(final PrintStream printStream, final PrintStream errStream,
            final boolean isDebugModeEnable, final boolean isYesFlagEnable) {
        this.printStream = printStream;
        this.errStream = errStream;
        this.isDebugModeEnable = isDebugModeEnable;
        this.isYesFlagEnabled = isYesFlagEnable;

        EmbeddedHttpServerConfig embeddedHttpServerConfig;
        try {
            embeddedHttpServerConfig = PreferenceFileManager.getEmbeddedHttpServerParameters();
            this.embeddedHttpServer = new EmbeddedHttpServerImpl(embeddedHttpServerConfig);
        } catch (final PreferenceFileException e) {
            this.log.log(Level.WARNING, e.getMessage(), e);
            this.embeddedHttpServer = new EmbeddedHttpServerImpl(new EmbeddedHttpServerConfig(
                    PreferenceFileManager.DEFAULT_EMBEDDED_HTTP_PORT));
        }

    }

    /**
     * Get the available commands
     *
     * @return a set of commands mapped with their name
     */
    @Override
	public final Map<String, Command> getCommands() {
        return this.commands;
    }

    /**
     * Registers a command.
     *
     * @param command
     *            . Not <code>null</code>.
     * @throws IllegalArgumentException
     *             if the command is <code>null</code>.
     * @throws DuplicatedCommandException
     *             if the command is already defined
     */
    @Override
	public void registersCommand(final Command command) throws DuplicatedCommandException,
            IllegalArgumentException {
        assert command != null;

        String name = command.getName();

        if (this.commands.containsKey(name)) {
            throw new DuplicatedCommandException(name);
        } else {
            this.commands.put(name, command);
        }
    }

    /**
     * <p>
     * Executes the shell.
     * </p>
     * <p>
     * No exception is thrown. The implementation of the method is responsible
     * for printing error messages and set the right exit code.
     * </p>
     */
    public abstract void run();

    /**
     * Tests whether a line is a comment or not.
     *
     * @param line
     *            the string to be tested
     * @return true if the line is a comment, false otherwise
     */
    protected boolean isComment(String line) {
        return REGEX_COMMENT.matcher(line).matches();
    }

    /**
     * Interpolates variables and special characters in a string.
     *
     * @param str
     *            the string to be interpolated
     * @return a new string with interpolated variables
     */
    @Override
	public String interpolate(String str) {
        Matcher m = REGEX_VARIABLE.matcher(str);
        StringBuffer sb = new StringBuffer();

        while (m.find()) {
            String backslash = m.group(1);
            String property = m.group(2).replace("$", "\\$");
            String name = m.group(3);
            String value = System.getProperty(name);

            if (value == null || backslash.length() > 0) {
                m.appendReplacement(sb, property);
            } else {
                m.appendReplacement(sb, value);
            }
        }

        m.appendTail(sb);
        return sb.toString();
    }

    /**
     * Sets an exit code and exit the current shell.
     *
     * @param status
     *            the optional result value
     */
    @Override
	public void exit(int status) {
        this.exitStatus = status;
        this.isCommandRead = false;

        // Wait the end of asynchronous commands
        try {
            while (this.isAsynchronousCommandInProgress()) {
                Thread.sleep(1000);
            }
        } catch (final InterruptedException e) {
            // No operation to exit the shell
        }
    }

    /**
     * Disconnect, if needed, Petals CLI from the current Petals ESB server.
     */
    public void disconnectIfNeeded() {
        // Disconnect if needed to prevent resource consumption
        // if the command 'disconnect' was not used
        try {
            final ContainerAdministration containerAdministration = PetalsAdministrationFactory
                    .newInstance().newContainerAdministration();
            if (containerAdministration.isConnected()) {
                try {
                    containerAdministration.disconnect();
                } catch (final NoConnectionException e) {
                    // NOP: No connection previously established
                } catch (final ContainerAdministrationException e) {
                    this.errStream
                            .println("WARNING: The following error occurs disconnection Petals CLI from the Petals ESB container: "
                                    + e.getMessage());
                    e.printStackTrace(this.errStream);
                }
            }
        } catch (final Exception e) {
            this.errStream
                    .println("WARNING: The following error occurs checking if a connection is established before to force the disconnection: "
                            + e.getMessage());
            e.printStackTrace(this.errStream);
        }
    }

    /**
     * Evaluates a list of arguments.
     * <p>Does nothing if args id null of empty.</p>
     *
     * @param args
     *            the command line. Not empty and not null.
     * @throws UnknownCommandException
     *             when the command was not found
     * @throws CommandException
     *             when an error occurs during the execution of the command
     */
    protected void evaluate(String[] args) throws UnknownCommandException, CommandException {
        assert (args != null && args.length > 0);

        String cmdName = args[0];
        Command cmd;
        if (this.commands.containsKey(cmdName)) {
            cmd = this.commands.get(cmdName);
        } else {
            throw new UnknownCommandException(cmdName);
        }

        String[] nargs = new String[args.length - 1];
        for (int i = 0; i < nargs.length; i++) {
            nargs[i] = this.interpolate(args[i + 1]);
        }
        try {
            cmd.execute(nargs);
        } finally {
            cmd.resetOptions();
        }
    }

    /**
     * Evaluates a command line.
     * <p>Does nothing if the line is a comment.</p>
     *
     * @see #isComment(String)
     * @param line
     *            the line to be evaluated
     * @throws UnknownCommandException
     *             when the command was not found
     * @throws CommandException
     *             when an error occurs during the execution of the command
     */
    protected final void evaluate(String line) throws UnknownCommandException, CommandException {
        if (!this.isComment(line)) {
            String[] args = Utils.getLineArgs(line);
            this.evaluate(args);
        }
    }

    /**
     * Gets the output stream where anything can be written, mainly by commands.
     * @return The output stream.
     */
    @Override
	public PrintStream getPrintStream() {
        return this.printStream;
    }

    public PrintStream getErrorStream() {
        return this.errStream;
    }

    public int getExitStatus() {
        return this.exitStatus;
    }

    @Override
	public void setExitStatus(int exitStatus) {
        this.exitStatus = exitStatus;
    }

    /**
     * Prints the error message of a command syntax error.
     * <p>
     * The message is made up of the command name,
     * the syntax error message and the command usage.
     * </p>
     *
     * @param ce
     *            The command syntax error to print
     */
    public void printCommandSyntaxError(final CommandInvalidException ce) {
        final Command command = ce.getCommand();
        if (command != null) {
            this.errStream.println("ERROR on command '" + ce.getCommand().getName() + "': "
                    + ce.getMessage());
            this.errStream.println(ce.getCommand().getUsage());
        } else {
            this.errStream.println("ERROR: " + ce.getMessage());
        }
    }

    /**
     * Prints the error message of a command execution error.
     * <p>
     * The message is made up of the command name, the execution error message and, if debug
     * mode is enable, the stack trace.
     * </p>
     *
     * @param ce
     *            The command syntax error to print
     */
    public void printCommandExecutionError(final CommandException ce) {
        final Command command = ce.getCommand();
        if (command != null) {
            this.errStream.println("ERROR on command '" + ce.getCommand().getName() + "': "
                    + ce.getMessage());
        } else {
            this.errStream.println("ERROR: " + ce.getMessage());
        }
        if (this.isDebugModeEnable) {
            ce.printStackTrace(this.errStream);
        }
    }

    /**
     * Prints an error message.
     * <p>If the debug mode is enabled, the stack trace is also printed.</p>
     *
     * @param error
     *            The error. The message to print will be extracted from
     *            {@link Throwable#getMessage()}.
     */
    public void printError(final Throwable error) {
        this.printError(error, "ERROR: " + error.getMessage());
    }

    /**
     * Prints an error message.
     * <p>If the debug mode is enabled, the stack trace is also printed.</p>
     *
     * @param error
     *            The error
     * @param msg
     *            The message to print.
     */
    public void printError(final Throwable error, final String msg) {
        this.errStream.println(msg);
        if (this.isDebugModeEnable) {
            error.printStackTrace(this.errStream);
        }
    }

    /**
     * @return true if the debug mode is enabled, false otherwise
     */
    @Override
    public boolean isDebugModeEnable() {
        return this.isDebugModeEnable;
    }

    @Override
    public String askQuestion(String question, boolean isReplyPassword)
            throws NoInteractiveShellException, ShellException {
        throw new NoInteractiveShellException();
    }

    /**
     * {@inheritDoc}
     * 
     * <p>
     * Special notes for the implementation {@link AbstractShell}:
     * <ul>
     * <li>no message is displayed,</li>
     * <li>the return value is <code>true</code> if the 'yes' flag is enable.</li>
     * </ul>
     * </p>
     */
    @Override
    public boolean confirms(final String message) throws ShellException {
        return this.isYesFlagEnabled;
    }

    @Override
    public boolean isInteractive() {
        return false;
    }

    @Override
    public void setPrompt(final String prompt) {
        // NOP: By default, shell has no prompt
    }

    @Override
    public void setConnectionParameters(final ConnectionParameters connectionParameters) {
        this.connectionParameters = connectionParameters;
    }

    @Override
    public ConnectionParameters getConnectionParameters() {
        return this.connectionParameters;
    }

    @Override
    public boolean isAsynchronousCommandInProgress() {
        return this.asynchronousCommandsInProgress.get() != 0;
    }

    @Override
    public void addAsynchronousCommand() {
        this.asynchronousCommandsInProgress.incrementAndGet();
    }

    @Override
    public void removeAsynchronousCommand() {
        this.asynchronousCommandsInProgress.decrementAndGet();
    }

    @Override
    public EmbeddedHttpServer getEmbeddedHttpServer() {
        return this.embeddedHttpServer;
    }
}
