package de.renew.console.completer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import jline.console.completer.Completer;
import jline.internal.Log;

import static jline.internal.Preconditions.checkNotNull;


/**
 * A {@link Completer} implementation that invokes a child completer using the appropriate <i>separator</i> argument.
 * This can be used instead of the individual completers having to know about argument parsing semantics.
 *
 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @since 2.3
 */
public class RenewArgumentCompleter implements Completer {
    private final ArgumentDelimiter _delimiter;
    private final List<Completer> _completers = new ArrayList<Completer>();
    private boolean _strict = true;

    /**
     * Create a new completer with the specified argument delimiter.
     *
     * @param delimiter The delimiter for parsing arguments
     * @param completers The embedded completers
     */
    public RenewArgumentCompleter(
        final ArgumentDelimiter delimiter, final Collection<Completer> completers)
    {
        this._delimiter = checkNotNull(delimiter);
        checkNotNull(completers);
        this._completers.addAll(completers);
    }

    /**
     * Create a new completer with the specified argument delimiter.
     *
     * @param delimiter The delimiter for parsing arguments
     * @param completers The embedded completers
     */
    public RenewArgumentCompleter(
        final ArgumentDelimiter delimiter, final Completer... completers)
    {
        this(delimiter, Arrays.asList(completers));
    }

    /**
     * Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
     *
     * @param completers The embedded completers
     */
    public RenewArgumentCompleter(final Completer... completers) {
        this(new WhitespaceArgumentDelimiter(), completers);
    }

    /**
     * Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
     *
     * @param completers The embedded completers
     */
    public RenewArgumentCompleter(final List<Completer> completers) {
        this(new WhitespaceArgumentDelimiter(), completers);
    }

    /**
     * If true, a completion at argument index N will only succeed
     * if all the completions from 0-(N-1) also succeed.
     *
     * @param strict The boolean if strict should be enabled
     */
    public void setStrict(final boolean strict) {
        this._strict = strict;
    }

    /**
     * Returns whether a completion at argument index N will success
     * if all the completions from arguments 0-(N-1) also succeed.
     *
     * @return True if strict.
     * @since 2.3
     */
    public boolean isStrict() {
        return this._strict;
    }

    /**
     * Returns the delimiter.
     * @return The delimiter.
     * @since 2.3
     */
    public ArgumentDelimiter getDelimiter() {
        return _delimiter;
    }

    /**
     * Returns a  list of completers.
     * @return The completers.
     * @since 2.3
     */
    public List<Completer> getCompleters() {
        return _completers;
    }

    @Override
    public int complete(
        final String buffer, final int cursor, final List<CharSequence> candidates)
    {
        if (candidates == null) {
            throw new IllegalArgumentException("Candidates list cannot be null.");
        }

        List<Completer> completers = getCompleters();
        if (completers.isEmpty()) {
            return -1;
        }

        ArgumentDelimiter delim = getDelimiter();
        ArgumentList list = delim.delimit(buffer, cursor);
        int argpos = list.getArgumentPosition();
        int argIndex = list.getCursorArgumentIndex();

        Completer completer = chooseCompleter(argIndex, completers);

        if (isStrict()) {
            if (!validatePreviousCompleters(completers, argIndex, list)) {
                return -1;
            }
        }

        int ret = completer.complete(list.getCursorArgument(), argpos, candidates);
        if (ret == -1) {
            return -1;
        }

        int pos = ret + list.getBufferPosition() - argpos;

        adjustCandidatesForDelimiters(buffer, cursor, delim, candidates);

        Log.trace(
            "Completing ", buffer, " (pos=", cursor, ") with: ", candidates, ": offset=", pos);

        return pos;
    }

    /**
     * Chooses the appropriate completer based on the argument index.
     *
     * @param argIndex the index of the current argument
     * @param completers the list of completers
     * @return the selected completer
     */
    private Completer chooseCompleter(int argIndex, List<Completer> completers) {
        if (argIndex < 0) {
            return completers.get(0);
        } else if (argIndex >= completers.size()) {
            return completers.get(completers.size() - 1);
        } else {
            return completers.get(argIndex);
        }
    }

    /**
     * Validates all previous completers if strict mode is enabled.
     *
     * @param completers the list of completers
     * @param argIndex the index of the current argument
     * @param list the argument list
     * @return true if all previous completers were successful, false otherwise
     */
    private boolean validatePreviousCompleters(
        List<Completer> completers, int argIndex, ArgumentList list)
    {
        for (int i = 0; i < argIndex; i++) {
            Completer sub = completers.get(Math.min(i, completers.size() - 1));
            String[] args = list.getArguments();
            String arg = (args == null || i >= args.length) ? "" : args[i];

            List<CharSequence> subCandidates = new LinkedList<>();

            if (sub.complete(arg, arg.length(), subCandidates) == -1) {
                return false;
            }

            if (subCandidates.isEmpty()) {
                return false;
            }

            boolean exactMatch = false;
            for (CharSequence candidate : subCandidates) {
                if (candidate.toString().trim().equals(arg.trim())) {
                    exactMatch = true;
                    break;
                }
            }
            if (!exactMatch) {
                return false;
            }
        }
        return true;
    }

    /**
     * Adjusts the completion candidates by removing any trailing delimiters.
     *
     * @param buffer the current input buffer
     * @param cursor the current cursor position
     * @param delim the delimiter used for argument parsing
     * @param candidates the list of completion candidates
     */
    private void adjustCandidatesForDelimiters(
        String buffer, int cursor, ArgumentDelimiter delim, List<CharSequence> candidates)
    {
        if (buffer != null && cursor != buffer.length() && delim.isDelimiter(buffer, cursor)) {
            for (int i = 0; i < candidates.size(); i++) {
                CharSequence val = candidates.get(i);

                while (!val.isEmpty() && delim.isDelimiter(val, val.length() - 1)) {
                    val = val.subSequence(0, val.length() - 1);
                }

                candidates.set(i, val);
            }
        }
    }


    /**
     * The {@link RenewArgumentCompleter.ArgumentDelimiter} allows custom breaking up of a {@link String} into individual
     * arguments in order to dispatch the arguments to the nested {@link Completer}.
     *
     * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
     */
    public interface ArgumentDelimiter {

        /**
         * Break the specified buffer into individual tokens that can be completed on their own.
         *
         * @param buffer The buffer to split
         * @param pos The current position of the cursor in the buffer
         * @return The tokens
         */
        ArgumentList delimit(CharSequence buffer, int pos);

        /**
         * Returns true if the specified character is a whitespace parameter.
         *
         * @param buffer The complete command buffer
         * @param pos The index of the character in the buffer
         * @return True if the character should be a delimiter
         */
        boolean isDelimiter(CharSequence buffer, int pos);
    }

    /**
     * Abstract implementation of a delimiter that uses the {@link #isDelimiter} method to determine if a particular
     * character should be used as a delimiter.
     *
     * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
     */
    public abstract static class AbstractArgumentDelimiter implements ArgumentDelimiter {
        // TODO: handle argument quoting and escape characters
        private char[] _quoteChars = { '\'', '"' };
        private char[] _escapeChars = { '\\' };

        /**
         * Constructs an {@code AbstractArgumentDelimiter}.
         * This abstract class serves as a base for argument delimiters that implement the ArgumentDelimiter interface.
         */
        public AbstractArgumentDelimiter() {}

        /**
         * Sets the quote characters.
         *
         * @param chars An array of characters.
         */
        public void setQuoteChars(final char[] chars) {
            this._quoteChars = chars;
        }

        /**
         * Gets the quote characters.
         *
         * @return The quote characters.
         */
        public char[] getQuoteChars() {
            return this._quoteChars;
        }

        /**
         * Sets the escape characters.
         *
         * @param chars An array of characters.
         */
        public void setEscapeChars(final char[] chars) {
            this._escapeChars = chars;
        }

        /**
         * Gets the escape characters.
         *
         * @return The quote characters.
         */
        public char[] getEscapeChars() {
            return this._escapeChars;
        }

        @Override
        public ArgumentList delimit(final CharSequence buffer, final int cursor) {
            List<String> args = new LinkedList<String>();
            StringBuilder arg = new StringBuilder();
            int argpos = -1;
            int bindex = -1;

            for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) {
                // once we reach the cursor, set the
                // position of the selected index
                if (i == cursor) {
                    bindex = args.size();
                    // the position in the current argument is just the
                    // length of the current argument
                    argpos = arg.length();
                }

                if ((i == buffer.length()) || isDelimiter(buffer, i)) {
                    if (!arg.isEmpty()) {
                        args.add(arg.toString());
                        arg.setLength(0); // reset the arg
                    }
                } else {
                    arg.append(buffer.charAt(i));
                }
            }

            return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor);
        }

        /**
         * Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not
         * escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and
         * returns true from {@link #isDelimiterChar}.
         *
         * @param buffer The complete command buffer
         * @param pos The index of the character in the buffer
         * @return True if the character should be a delimiter
         */
        @Override
        public boolean isDelimiter(final CharSequence buffer, final int pos) {
            return !isQuoted(buffer, pos) && !isEscaped(buffer, pos)
                && isDelimiterChar(buffer, pos);
        }

        /**
         * Determines if the character at a specified position in a sequence is within quotes.
         *
         * @param buffer The character sequence to inspect.
         * @param pos The position within the sequence to check.
         * @return The boolean if the character at the specified position is quoted;
         */
        public boolean isQuoted(final CharSequence buffer, final int pos) {
            return false;
        }

        /**
         * Determines if the character at a specified position in a sequence is escaped.
         *
         * @param buffer The character sequence to inspect.
         * @param pos The position within the sequence to check.
         * @return The boolean if the character at the specified position is escaped;
         */
        public boolean isEscaped(final CharSequence buffer, final int pos) {
            if (pos <= 0) {
                return false;
            }

            for (int i = 0; (_escapeChars != null) && (i < _escapeChars.length); i++) {
                if (buffer.charAt(pos) == _escapeChars[i]) {
                    return !isEscaped(buffer, pos - 1); // escape escape
                }
            }

            return false;
        }

        /**
         * Returns true if the character at the specified position if a delimiter. This method will only be called if
         * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the
         * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead.
         *
         *  @param buffer The character sequence (typically a String or StringBuilder) containing the characters to be checked.
         *  @param pos The position within the buffer where the character should be checked.
         *
         *  @return {@code true} if the character at the specified position is considered a delimiter;
         *          {@code false} otherwise.
         */
        public abstract boolean isDelimiterChar(CharSequence buffer, int pos);
    }

    /**
     * {@link RenewArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by
     * {@link Character#isWhitespace}) as being a delimiter.
     *
     * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
     */
    public static class WhitespaceArgumentDelimiter extends AbstractArgumentDelimiter {

        /**
         * Constructs a {@code WhitespaceArgumentDelimiter}.
         * This delimiter treats whitespace characters as argument separators.
         */
        public WhitespaceArgumentDelimiter() {}

        /**
         * The character is a delimiter if it is whitespace, and the
         * preceding character is not an escape character.
         */
        @Override
        public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
            return Character.isWhitespace(buffer.charAt(pos));
        }
    }

    /**
     * The result of a delimited buffer.
     *
     * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
     */
    public static class ArgumentList {
        private String[] _arguments;
        private int _cursorArgumentIndex;
        private int _argumentPosition;
        private int _bufferPosition;

        /**
         * Initializes Argumentlist.
         * @param arguments The array of tokens
         * @param cursorArgumentIndex The token index of the cursor
         * @param argumentPosition The position of the cursor in the current token
         * @param bufferPosition The position of the cursor in the whole buffer
         */
        public ArgumentList(
            final String[] arguments, final int cursorArgumentIndex, final int argumentPosition,
            final int bufferPosition)
        {
            _arguments = checkNotNull(arguments);
            _cursorArgumentIndex = cursorArgumentIndex;
            _argumentPosition = argumentPosition;
            _bufferPosition = bufferPosition;
        }

        /**
         * Sets the index of the current cursor argument.
         *
         * @param i The index to set as the cursor argument index.
         */
        public void setCursorArgumentIndex(final int i) {
            _cursorArgumentIndex = i;
        }

        /**
         * Retrieves the index of the current cursor argument.
         *
         * @return The index of the cursor argument.
         */
        public int getCursorArgumentIndex() {
            return _cursorArgumentIndex;
        }

        /**
         * Retrieves the argument at the current cursor position.
         *
         * Returns {@code null} if the cursor argument index is out of bounds.
         *
         * @return The argument at the cursor position, or {@code null} if out of bounds.
         */
        public String getCursorArgument() {
            if ((_cursorArgumentIndex < 0) || (_cursorArgumentIndex >= _arguments.length)) {
                return null;
            }

            return _arguments[_cursorArgumentIndex];
        }

        /**
         * Sets the position of the current argument.
         *
         * @param pos The position to set for the argument.
         */
        public void setArgumentPosition(final int pos) {
            _argumentPosition = pos;
        }

        /**
         * Retrieves the position of the current argument.
         *
         * @return The position of the argument.
         */
        public int getArgumentPosition() {
            return _argumentPosition;
        }


        /**
         * Sets the array of arguments.
         *
         * @param arguments The array of arguments to set.
         */
        public void setArguments(final String[] arguments) {
            _arguments = arguments;
        }

        /**
         * Retrieves the array of arguments.
         *
         * @return The array of arguments.
         */
        public String[] getArguments() {
            return _arguments;
        }

        /**
         * Sets the position of the buffer.
         *
         * @param pos The position to set for the buffer.
         */
        public void setBufferPosition(final int pos) {
            _bufferPosition = pos;
        }

        /**
         * Retrieves the position of the buffer.
         *
         * @return The position of the buffer.
         */
        public int getBufferPosition() {
            return _bufferPosition;
        }
    }


    /**
     * Adds a completer to the list of completers.
     *
     * @param completer The completer to be added.
     */
    public void addCompleter(Completer completer) {
        _completers.add(completer);
    }

    /**
     * Removes a completer from the list of completers.
     *
     * @param completer The completer to be removed.
     */
    public void removeCompleter(Completer completer) {
        _completers.remove(completer);
    }
}