/*
 * Copyright (c) 2002-2016, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * http://www.opensource.org/licenses/bsd-license.php
 */
package jline.console.completer;

import jline.internal.Log;

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

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 ArgumentCompleter
    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 ArgumentCompleter(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 ArgumentCompleter(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 ArgumentCompleter(final Completer... completers) {
        this(new WhitespaceArgumentDelimiter(), completers);
    }

    /**
     * Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
     *
     * @param completers    The embedded completers
     */
    public ArgumentCompleter(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.
     */
    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;
    }

    /**
     * @since 2.3
     */
    public ArgumentDelimiter getDelimiter() {
        return delimiter;
    }

    /**
     * @since 2.3
     */
    public List<Completer> getCompleters() {
        return completers;
    }

    public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) {
        // buffer can be null
        checkNotNull(candidates);

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

        if (argIndex < 0) {
            return -1;
        }

        List<Completer> completers = getCompleters();
        Completer completer;

        // if we are beyond the end of the completers, just use the last one
        if (argIndex >= completers.size()) {
            completer = completers.get(completers.size() - 1);
        }
        else {
            completer = completers.get(argIndex);
        }

        // ensure that all the previous completers are successful before allowing this completer to pass (only if strict).
        for (int i = 0; isStrict() && (i < argIndex); i++) {
            Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i);
            String[] args = list.getArguments();
            String arg = (args == null || i >= args.length) ? "" : args[i];

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

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

            if (!subCandidates.contains(arg)) {
                return -1;
            }
        }

        int ret = completer.complete(list.getCursorArgument(), argpos, candidates);

        if (ret == -1) {
            return -1;
        }

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

        // Special case: when completing in the middle of a line, and the area under the cursor is a delimiter,
        // then trim any delimiters from the candidates, since we do not need to have an extra delimiter.
        //
        // E.g., if we have a completion for "foo", and we enter "f bar" into the buffer, and move to after the "f"
        // and hit TAB, we want "foo bar" instead of "foo  bar".

        if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) {
            for (int i = 0; i < candidates.size(); i++) {
                CharSequence val = candidates.get(i);

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

                candidates.set(i, val);
            }
        }

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

        return pos;
    }

    /**
     * The {@link ArgumentCompleter.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 static 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
    {
        private char[] quoteChars = {'\'', '"'};

        private char[] escapeChars = {'\\'};

        public void setQuoteChars(final char[] chars) {
            this.quoteChars = chars;
        }

        public char[] getQuoteChars() {
            return this.quoteChars;
        }

        public void setEscapeChars(final char[] chars) {
            this.escapeChars = chars;
        }

        public char[] getEscapeChars() {
            return this.escapeChars;
        }

        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;
            int quoteStart = -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 (quoteStart < 0 && isQuoteChar(buffer, i)) {
                    // Start a quote block
                    quoteStart = i;
                } else if (quoteStart >= 0) {
                    // In a quote block
                    if (buffer.charAt(quoteStart) == buffer.charAt(i) && !isEscaped(buffer, i)) {
                        // End the block; arg could be empty, but that's fine
                        args.add(arg.toString());
                        arg.setLength(0);
                        quoteStart = -1;
                    } else if (!isEscapeChar(buffer, i)) {
                        // Take the next character
                        arg.append(buffer.charAt(i));
                    }
                } else {
                    // Not in a quote block
                    if (isDelimiter(buffer, i)) {
                        if (arg.length() > 0) {
                            args.add(arg.toString());
                            arg.setLength(0); // reset the arg
                        }
                    } else if (!isEscapeChar(buffer, i)) {
                        arg.append(buffer.charAt(i));
                    }
                }
            }

            if (cursor == buffer.length()) {
                bindex = args.size();
                // the position in the current argument is just the
                // length of the current argument
                argpos = arg.length();
            }
            if (arg.length() > 0) {
                args.add(arg.toString());
            }

            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
         */
        public boolean isDelimiter(final CharSequence buffer, final int pos) {
            return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
        }

        public boolean isQuoted(final CharSequence buffer, final int pos) {
            return false;
        }

        public boolean isQuoteChar(final CharSequence buffer, final int pos) {
            if (pos < 0) {
                return false;
            }

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

            return false;
        }

        /**
         * Check if this character is a valid escape char (i.e. one that has not been escaped)
         */
        public boolean isEscapeChar(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); // escape escape
                }
            }

            return false;
        }

        /**
         * Check if a character is escaped (i.e. if the previous character is an escape)
         *
         * @param buffer
         *          the buffer to check in
         * @param pos
         *          the position of the character to check
         * @return true if the character at the specified position in the given buffer is an escape character and the character immediately preceding it is not an
         *         escape character.
         */
        public boolean isEscaped(final CharSequence buffer, final int pos) {
            if (pos <= 0) {
                return false;
            }

            return isEscapeChar(buffer, pos - 1);
        }

        /**
         * 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.
         */
        public abstract boolean isDelimiterChar(CharSequence buffer, int pos);
    }

    /**
     * {@link ArgumentCompleter.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
    {
        /**
         * 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;

        /**
         * @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) {
            this.arguments = checkNotNull(arguments);
            this.cursorArgumentIndex = cursorArgumentIndex;
            this.argumentPosition = argumentPosition;
            this.bufferPosition = bufferPosition;
        }

        public void setCursorArgumentIndex(final int i) {
            this.cursorArgumentIndex = i;
        }

        public int getCursorArgumentIndex() {
            return this.cursorArgumentIndex;
        }

        public String getCursorArgument() {
            if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) {
                return null;
            }

            return arguments[cursorArgumentIndex];
        }

        public void setArgumentPosition(final int pos) {
            this.argumentPosition = pos;
        }

        public int getArgumentPosition() {
            return this.argumentPosition;
        }

        public void setArguments(final String[] arguments) {
            this.arguments = arguments;
        }

        public String[] getArguments() {
            return this.arguments;
        }

        public void setBufferPosition(final int pos) {
            this.bufferPosition = pos;
        }

        public int getBufferPosition() {
            return this.bufferPosition;
        }
    }
}
