package de.renew.console;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch;

import jline.console.ConsoleReader;
import jline.console.completer.AggregateCompleter;
import jline.console.completer.CandidateListCompletionHandler;
import jline.console.completer.Completer;
import jline.console.completer.CompletionHandler;
import jline.console.completer.FileNameCompleter;
import jline.console.completer.NullCompleter;
import jline.console.completer.StringsCompleter;
import jline.console.history.FileHistory;

import de.renew.console.completer.CompleterComposition;
import de.renew.console.completer.DrawingsCompleter;
import de.renew.console.completer.FirstWhitespaceArgumentDelimiter;
import de.renew.console.completer.LocationsCompleter;
import de.renew.console.completer.PluginCompleter;
import de.renew.console.completer.PropertyCompleter;
import de.renew.console.completer.RenewArgumentCompleter;
import de.renew.plugin.CommandsListener;
import de.renew.plugin.PluginAdapter;
import de.renew.plugin.PluginException;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PluginProperties;
import de.renew.plugin.SoftDependency;
import de.renew.plugin.command.CLCommand;
import de.renew.plugin.command.InteractiveCLCommand;
import de.renew.util.StringUtil;


/*
 * This is a generated file. Generated by PluginDevelopment
 * plugin (version 0.4)
 * Representative for the Console plug-in.
 */


/**
 * This plugin provides an interactive command line user interface.
 *
 * @author Joern Schumacher
 * @author Michael Duvigneau
 * @author Lawrence Cabac
 * @author David Mosteller
 */
public class ConsolePlugin extends PluginAdapter implements CommandsListener {
    private static final String COLOR_PROP_NAME = "de.renew.console.color";
    private static final String DRAWING_NAMES = "drawingNames";
    private static final String PROPERTY_NAMES = "propertyNames";
    private static final String LOCATION_NAMES = "locationNames";
    private static final String PLUGIN_NAMES = "pluginNames";
    private static final String FILE_NAMES = "fileNames";
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(ConsolePlugin.class);
    private static final String KEEPALIVE = "keepalive";
    private static final String PROMPT_ALIVE = "Prompt will keep plugin system alive.";
    private static final String PROMPT_NOT_ALIVE =
        "Prompt will not prevent plugin system from automatic termination.";


    private static final String DONT_PROP_NAME = "de.renew.console.dont";
    private static final String ALIVE_PROP_NAME = "de.renew.console.keepalive";
    private boolean _coloredPrompt = false;
    private PromptThread _promptThread;
    private boolean _blockingState = false;
    private Map<String, Completer> _completers = new HashMap<String, Completer>();
    private CountDownLatch _latch;
    private boolean _promtThreadReady = false;
    private SoftDependency _chDependency;

    /**
     * Creates a new instance of ConsolePlugin using the provided URL.
     * This constructor initializes the plugin with the given URL and performs any necessary setup.
     *
     * @param url the URL associated with the plugin, used for initialization
     * @throws PluginException if there is an error during the plugin setup
     */
    public ConsolePlugin(URL url) throws PluginException {
        super(url);
    }

    /**
     * Creates a new instance of ConsolePlugin using the provided plugin properties.
     * This constructor initializes the plugin with the given properties, typically loaded from a configuration.
     *
     * @param props the properties used to configure the plugin, such as settings and options
     */
    public ConsolePlugin(PluginProperties props) {
        super(props);
    }


    @Override
    public synchronized void init() {
        // create a command prompt
        boolean startPrompt = !getProperties().getBoolProperty(DONT_PROP_NAME);
        if (startPrompt) {
            this._latch = new CountDownLatch(1);
            prompt();
            setBlockingState(getProperties().getBoolProperty(ALIVE_PROP_NAME));
            PluginManager pm = PluginManager.getInstance();
            pm.addCommandListener(this);
            _coloredPrompt = getProperties().getBoolProperty(COLOR_PROP_NAME);

            // add commands that have been added to the pm before
            for (Entry<String, CLCommand> entry : pm.getCLCommands().entrySet()) {
                String commandName = entry.getKey();
                CLCommand command = entry.getValue();
                commandAdded(commandName, command);
            }
            pm.addCLCommand(KEEPALIVE, new BlockingPromptCommand());
        } else {
            LOGGER.debug("ConsolePlugin: " + DONT_PROP_NAME + " is set to true. Not prompting.");
        }
        _chDependency =
            new SoftDependency(this, "ch.ifa.draw", "de.renew.console.CHDependencyListener");
    }

    @Override
    public synchronized boolean cleanup() {
        if (_promptThread != null) {
            LOGGER.debug("shutting down prompt thread " + _promptThread);
            _promptThread.setStop();
            PluginManager.getInstance().removeCLCommand(KEEPALIVE);
            _promptThread = null;
        }
        PluginManager pm = PluginManager.getInstance();
        pm.removeCLCommand(KEEPALIVE);
        pm.removeCLCommand("reinit");
        pm.removeCommandListener(this);

        //NOTICEnull
        if (_chDependency != null) {
            _chDependency.discard();
            _chDependency = null;
        }
        return true;
    }

    private void prompt() {
        _promptThread = new PromptThread();
        _promptThread.start();
    }

    /**
     * Sets the blocking state of the object. If the new state is different from the current state,
     * it updates the blocking state and performs corresponding actions.
     *
     * @param newState The new state to set for blocking. A value of {@code true} indicates that the
     *                 object is in a blocking state, while {@code false} indicates it is not.
     *
     * <p>If the new state is {@code true}, the method will register an exit block. If the new state
     * is {@code false}, it will register an exit OK.</p>
     *
     * <p>This method is synchronized to ensure thread safety when accessing or modifying the
     * blocking state.</p>
     */
    public synchronized void setBlockingState(boolean newState) {
        if (newState != _blockingState) {
            _blockingState = newState;
            if (_blockingState) {
                registerExitBlock();
            } else {
                registerExitOk();
            }
        }
    }

    /**
     * This class represents a thread waiting for input.
     * If a string is entered, the thread compares it
     * to the available commands of the manager; if one is
     * available, its execute method is invoked.
     */
    private class PromptThread extends Thread {
        private static final String HISTORY_PROPS = "history.props";
        private boolean _stop = false;
        private ConsoleReader _reader;
        private File _prefsFile = null;
        private CompleterComposition _ac = new CompleterComposition();

        PromptThread() {
            super("Plugin-Prompt-Thread");
            File dotRenew = PluginManager.getPreferencesLocation();
            _prefsFile = new File(dotRenew, HISTORY_PROPS);
        }

        @Override
        public void run() {
            LOGGER.debug("Prompt thread running.");
            _stop = false;
            try {
                initializeReader();
                handleUserInput();
            } catch (ThreadDeath death) {
                LOGGER.debug("Prompt thread exiting!");
            } catch (IOException e) {
                handleIOException(e);
            }
            cleanup();
        }

        /**
         * Initializes the console reader and sets up necessary configurations such as
         * the completion handler, prompt, history, and input completer.
         *
         * @throws IOException If an I/O error occurs while initializing the reader.
         */
        private void initializeReader() throws IOException {
            _reader = new ConsoleReader();
            CompletionHandler completionHandler = _reader.getCompletionHandler();
            if (completionHandler instanceof CandidateListCompletionHandler) {
                ((CandidateListCompletionHandler) completionHandler)
                    .setPrintSpaceAfterFullCompletion(false);
            }
            _reader.setPrompt(getPrompt());
            FileHistory history = new FileHistory(_prefsFile);
            _reader.setHistory(history);
            _reader.addCompleter(_ac);
            ConsolePlugin.this._latch.countDown(); // prompt thread is ready
        }

        /**
         * Handles reading user input and processing it in a loop. It processes each
         * line read from the console and executes commands accordingly.
         */
        private void handleUserInput() {
            String line;
            PrintWriter out = new PrintWriter(_reader.getOutput());

            try {
                line = _reader.readLine();
                while (!_stop && line != null) {
                    processLine(out, line);
                    line = readNextLine();
                }
            } catch (IllegalArgumentException | IOException e) {
                handleInputException(e);
            }
        }

        /**
         * Processes a single line of user input by logging it (if debugging is enabled),
         * executing the commands within the line, and then flushing the output.
         *
         * @param out The PrintWriter to flush after processing the line.
         * @param line The user input line to be processed.
         */
        private void processLine(PrintWriter out, String line) {
            if (LOGGER.isDebugEnabled()) {
                logLine(line);
            }
            out.flush();
            executeCommands(line);
        }

        /**
         * Executes the commands found in the given input line. The line is split into
         * individual commands, and each command is executed accordingly.
         *
         * @param line The user input line containing commands to execute.
         */
        private void executeCommands(String line) {
            try {
                Map<String, CLCommand> commands = PluginManager.getInstance().getCLCommands();
                String[] cmds = line.split(PluginManager.COMMAND_SEPERATOR);
                for (String cmd : cmds) {
                    executeCommand(commands, cmd);
                }
            } catch (RuntimeException e) {
                LOGGER.error("PromptThread: an exception occurred: " + e);
                LOGGER.error(e.getMessage(), e);
            }
        }

        /**
         * Executes an individual command by retrieving it from the command map and
         * invoking its execution method. If the command is not found, it prints an
         * error message.
         *
         * @param commands The map of available commands.
         * @param cmd The individual command to execute.
         */
        private void executeCommand(Map<String, CLCommand> commands, String cmd) {
            cmd = cmd.trim();
            String[] cl = StringUtil.splitStringWithEscape(cmd);
            if (cl.length == 0) {
                return;
            }

            CLCommand c = commands.get(cl[0]);
            if (c == null) {
                System.out.println("unknown command.");
            } else {
                String[] nc = Arrays.copyOfRange(cl, 1, cl.length);
                if (c instanceof InteractiveCLCommand ic) {
                    ic.execute(nc, System.out, this::readAdditionalUserInput);
                } else {
                    c.execute(nc, System.out);
                }
            }
        }

        /**
         * Reads the next line of user input from the console. Ensures the reader is in
         * a valid state before reading input again.
         *
         * @return The next line of input, or null if the input reading should stop.
         */
        private String readNextLine() {
            try {
                // ensure that reader is in a definite state each time
                _reader.getTerminal().init();
                return _reader.readLine();
            } catch (Exception e) {
                handleInputException(e);
                return null;
            }
        }

        /**
         * Handles exceptions that occur during user input processing.
         * This method can be used to log errors and take additional actions if needed.
         *
         * @param e The exception that was thrown during input reading or processing.
         */
        private void handleInputException(Exception e) {
            LOGGER.error("Error reading input: " + e.getMessage());
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Exception: ", e);
            }
        }

        /**
         * Handles IOExceptions that may occur during console input/output operations.
         * Logs the error and provides debug information if enabled.
         *
         * @param e The IOException to handle.
         */
        private void handleIOException(IOException e) {
            LOGGER.error(e.getMessage());
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(ConsolePlugin.PromptThread.class.getSimpleName() + ": " + e);
            }
        }

        /**
         * Logs the given line if debugging is enabled. If the colored prompt is used,
         * the line is logged with a special color code.
         *
         * @param line The line to log.
         */
        private void logLine(String line) {
            if (_coloredPrompt) {
                LOGGER.debug("\u001B[33m=>\u001B[0m\"" + line + "\"");
            } else {
                LOGGER.debug("=>\"" + line + "\"");
            }
        }

        /**
         * Performs any necessary cleanup after the thread execution. This includes
         * resetting the blocking state and printing an exit message.
         */
        private void cleanup() {
            setBlockingState(false);
            System.out.println("exiting 3");
        }


        private String readAdditionalUserInput() throws IOException {
            try (ConsoleReader reader = new ConsoleReader()) {
                return reader.readLine();
            }
        }

        private String getPrompt() {
            if (_coloredPrompt) {
                return "\u001B[34mRenew > \u001B[0m";
            } else {
                return "Renew > ";
            }
        }

        protected void setStop() {
            FileHistory history = (FileHistory) _reader.getHistory();
            try {
                history.flush();
            } catch (IOException e) {
                LOGGER.error(e.getMessage());
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug(
                        ConsolePlugin.PromptThread.class.getSimpleName()
                            + ": could not save prompt history" + e);
                }
            }
            _stop = true;
            interrupt();
        }
    }

    private class BlockingPromptCommand implements CLCommand {
        @Override
        public void execute(String[] args, PrintStream response) {
            if (args.length == 0) {
                if (_blockingState) {
                    response.println(PROMPT_ALIVE);
                } else {
                    response.println(PROMPT_NOT_ALIVE);
                }
            } else if ("on".equals(args[0])) {
                setBlockingState(true);
                response.println(PROMPT_ALIVE);
            } else if ("off".equals(args[0])) {
                setBlockingState(false);
                response.println(PROMPT_NOT_ALIVE);
            } else {
                response.println(
                    "Controls the keep-alive feature of the Renew Prompt plugin.\n" + "Arguments:\n"
                        + " - \"on\" prevents the plugin system from automatic termination.\n"
                        + " - \"off\" allows automatic termination as long as no other plugin prevents it.\n"
                        + " - no argument displays the current keep-alive mode.");
            }
        }

        @Override
        public String getDescription() {
            return "controls the keep-alive feature of the Renew Prompt plugin.";
        }

        /**
         * @see de.renew.plugin.command.CLCommand#getArguments()
         */
        @Override
        public String getArguments() {
            return null;
        }
    }

    @Override
    public void commandAdded(String name, CLCommand command) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                "[" + ConsolePlugin.class.getSimpleName() + "]: Command added: " + name + " "
                    + command.getClass().getSimpleName());
        }
        String arguments = command.getArguments();
        String[] argumentsArray = null;
        if (arguments != null) {
            // replace multiple whitespaces with a single one and split
            argumentsArray = arguments.replaceAll(" +", " ").split(" ");
        }
        Completer completer = getCompleterForCommand(name, argumentsArray);

        // wait until thread is initialized; shouln't take too long
        if (!this._promtThreadReady) {
            try {
                LOGGER.debug(
                    "[" + ConsolePlugin.class.getSimpleName() + "]: Waiting for Prompt thread.");
                this._latch.await();
                this._promtThreadReady = true;
                LOGGER.debug("[" + ConsolePlugin.class.getSimpleName() + "]: Prompt thread ready.");
            } catch (InterruptedException e) {
                // Ignore
            }
        }
        _promptThread._ac.addCompleter(completer);
        _completers.put(name, completer);
    }

    @Override
    public void commandRemoved(String name) {
        if (_promptThread != null && _promptThread._ac != null) {
            _promptThread._ac.removeCompleter(_completers.remove(name));
        }
    }

    /**
     * creates a completer for the given command and its arguments. The
     * arguments are already separated, but each argument may use a special
     * syntax to denote optional, alternative or recurring arguments (see
     * {@link de.renew.plugin.command.CLCommand#getArguments()} for details)
     *
     * @param commandName the name of the command
     * @param arguments the arguments for the command as array
     * @return a completer for completion of the command and the arguments
     */
    private Completer getCompleterForCommand(String commandName, String[] arguments) {
        Completer completer = new NullCompleter();
        if (arguments != null) {
            Completer followUpCompleter = new NullCompleter();

            // Iterate over arguments from right to left
            for (int i = arguments.length - 1; i >= 0; i--) {
                String arg = arguments[i];
                Completer completerForArgument = getArgumentCompleter(arg);

                // Handle optional arguments ([]) and alternative arguments (())
                completer = handleArgumentCompleter(arg, completerForArgument, followUpCompleter);
                followUpCompleter = completer;
            }
        }

        // Build the completer for the command name and delegate to the argument completer
        return new RenewArgumentCompleter(
            new FirstWhitespaceArgumentDelimiter(), new StringsCompleter(commandName + " "),
            completer);
    }

    /**
     * Determines the appropriate completer for the given argument.
     * Handles optional and alternative arguments.
     *
     * @param arg The argument string.
     * @return The completer for the argument.
     */
    private Completer getArgumentCompleter(String arg) {
        // Check if the argument is optional or alternative
        if (isOptionalOrAlternative(arg)) {
            return createComplexArgumentCompleter(arg);
        } else {
            // Simple argument or keyword
            if (isKeyword(arg)) {
                return getCompleterForKeyword(arg);
            } else {
                return new StringsCompleter(arg + " ");
            }
        }
    }

    /**
     * Checks if the argument is optional or alternative.
     *
     * @param arg The argument string.
     * @return true if the argument is optional or alternative, false otherwise.
     */
    private boolean isOptionalOrAlternative(String arg) {
        return arg.matches("\\[.*\\]\\*?") || arg.matches("\\(.*\\)\\*?");
    }

    /**
     * Creates a completer for complex arguments that may contain multiple alternatives.
     * For example, an argument like "[arg1|arg2]".
     *
     * @param arg The argument string.
     * @return The completer for the complex argument.
     */
    private Completer createComplexArgumentCompleter(String arg) {
        boolean loop = arg.matches(".*\\*");

        // Strip enclosing brackets and possible "*" suffix
        arg = arg.replaceAll("^[\\[\\(]|[\\]\\)]\\*$", "");
        String[] argSplit = arg.split("\\|");
        List<String> strings = new ArrayList<>();
        List<Completer> completers = new ArrayList<>();

        for (String s : argSplit) {
            if (isKeyword(s)) {
                completers.add(getCompleterForKeyword(s));
            } else {
                strings.add(s + " ");
            }
        }

        if (!strings.isEmpty()) {
            completers.add(new StringsCompleter(strings));
        }

        Completer completerForArgument = new CompleterComposition(completers);
        if (loop) {
            // Handle looping argument (argument repeated)
            return createLoopingCompleter(completerForArgument);
        } else {
            return completerForArgument;
        }
    }

    /**
     * Creates a completer for looping arguments where the argument may be repeated.
     *
     * @param completerForArgument The completer for the current argument.
     * @return The completer for looping arguments.
     */
    private Completer createLoopingCompleter(Completer completerForArgument) {
        CompleterComposition followUpCompleterWithBackwardsOption =
            new CompleterComposition(new NullCompleter());
        return new RenewArgumentCompleter(
            new FirstWhitespaceArgumentDelimiter(), completerForArgument,
            followUpCompleterWithBackwardsOption);
    }

    /**
     * Handles the addition of the completer for an argument, managing optional and
     * looping cases.
     *
     * @param arg The argument string.
     * @param completerForArgument The completer for the current argument.
     * @param followUpCompleter The next completer to delegate to.
     * @return The updated completer.
     */
    private Completer handleArgumentCompleter(
        String arg, Completer completerForArgument, Completer followUpCompleter)
    {
        boolean optional = arg.matches("\\[.*\\]\\*?");
        boolean loop = arg.matches(".*\\*");

        if (loop) {
            return new RenewArgumentCompleter(
                new FirstWhitespaceArgumentDelimiter(), completerForArgument, followUpCompleter);
        } else if (optional) {
            // Argument is optional, so delegate to the next completer if skipped
            return new AggregateCompleter(completerForArgument, followUpCompleter);
        } else {
            // Standard completer delegation
            return new RenewArgumentCompleter(
                new FirstWhitespaceArgumentDelimiter(), completerForArgument, followUpCompleter);
        }
    }


    private Completer getCompleterForKeyword(String keyword) {
        Completer result;
        if (keyword.equals(FILE_NAMES)) {
            result = new FileNameCompleter();
        } else if (keyword.equals(PLUGIN_NAMES)) {
            result = new PluginCompleter();
        } else if (keyword.equals(LOCATION_NAMES)) {
            result = new LocationsCompleter();
        } else if (keyword.equals(PROPERTY_NAMES)) {
            result = new PropertyCompleter();
        } else if (keyword.equals(DRAWING_NAMES)) {
            result = new DrawingsCompleter();
        } else {
            result = new NullCompleter();
        }
        return result;
    }

    private boolean isKeyword(String candidate) {
        return Arrays
            .asList(FILE_NAMES, PLUGIN_NAMES, LOCATION_NAMES, PROPERTY_NAMES, DRAWING_NAMES)
            .contains(candidate);
    }
}