/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.karaf.shell.impl.action.command;

import java.io.File;
import java.lang.reflect.Field;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.karaf.shell.api.action.Argument;
import org.apache.karaf.shell.api.action.Command;
import org.apache.karaf.shell.api.action.Completion;
import org.apache.karaf.shell.api.action.Option;
import org.apache.karaf.shell.api.console.Candidate;
import org.apache.karaf.shell.api.console.CommandLine;
import org.apache.karaf.shell.api.console.Completer;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.support.completers.ArgumentCommandLine;
import org.apache.karaf.shell.support.completers.FileCompleter;
import org.apache.karaf.shell.support.completers.NullCompleter;
import org.apache.karaf.shell.support.completers.StringsCompleter;
import org.apache.karaf.shell.support.completers.UriCompleter;
import org.apache.karaf.shell.support.converter.GenericType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ArgumentCompleter implements Completer {

    private static final Logger LOGGER = LoggerFactory.getLogger(ArgumentCompleter.class);

    final ActionCommand command;
    final CandidateCompleter commandCompleter;
    final CandidateCompleter optionsCompleter;
    final List<Completer> argsCompleters;
    final Map<String, Completer> optionalCompleters;
    final Map<Option, Field> fields = new HashMap<>();
    final Map<String, Option> options = new HashMap<>();
    final Map<Integer, Field> arguments = new HashMap<>();

    @SuppressWarnings("rawtypes")
    public ArgumentCompleter(ActionCommand command, boolean scoped) {
        this.command = command;
        Class<?> actionClass = command.getActionClass();
        // Command name completer
        Command cmd = actionClass.getAnnotation(Command.class);
        String[] names = scoped || Session.SCOPE_GLOBAL.equals(cmd.scope()) ? new String[] { cmd.name() } : new String[] { cmd.name(), cmd.scope() + ":" + cmd.name() };
        commandCompleter = new CandidateCompleter();
        for (String name : names) {
            commandCompleter.addCandidate(name, cmd.description(), actionClass.getName());
        }
        // Build options completer
        for (Class<?> type = actionClass; type != null; type = type.getSuperclass()) {
            for (Field field : type.getDeclaredFields()) {
                Option option = field.getAnnotation(Option.class);
                if (option != null) {
                    fields.put(option, field);
                    options.put(option.name(), option);
                    String[] aliases = option.aliases();
                    if (aliases != null) {
                        for (String alias : aliases) {
                            options.put(alias, option);
                        }
                    }
                }
                Argument argument = field.getAnnotation(Argument.class);
                if (argument != null) {
                    Integer key = argument.index();
                    if (arguments.containsKey(key)) {
                        LOGGER.warn("Duplicate @Argument annotations on class " + type.getName() + " for index: " + key + " see: " + field);
                    } else {
                        arguments.put(key, field);
                    }
                }
            }
        }
        options.put(HelpOption.HELP.name(), HelpOption.HELP);

        optionsCompleter = new CandidateCompleter();
        for (Map.Entry<String, Option> entry : options.entrySet()) {
            optionsCompleter.addCandidate(
                    entry.getKey(),
                    entry.getValue().description(),
                    actionClass.getName() + "/" + entry.getValue().name());
        }

        argsCompleters = new ArrayList<>();

        boolean multi = false;
        for (int key = 0; key < arguments.size(); key++) {
            Completer completer = null;
            Field field = arguments.get(key);
            if (field != null) {
                Argument argument = field.getAnnotation(Argument.class);
                multi = (argument != null && argument.multiValued());
                Completion ann = field.getAnnotation(Completion.class);
                if (ann != null) {
                    Class<?> clazz = ann.value();
                    String[] value = ann.values();
                    if (clazz != null) {
                        if (value.length > 0 && clazz == StringsCompleter.class) {
                            completer = new StringsCompleter(value, ann.caseSensitive());
                        } else {
                            completer = command.getCompleter(clazz);
                        }
                    }
                } else {
                    completer = getDefaultCompleter(field, multi);
                }
            }
            if (completer == null) {
                completer = NullCompleter.INSTANCE;
            }
            argsCompleters.add(completer);
        }
        if (argsCompleters.isEmpty() || !multi) {
            argsCompleters.add(NullCompleter.INSTANCE);
        }
        optionalCompleters = new HashMap<>();
        for (Option option : fields.keySet()) {
            Completer completer = null;
            Field field = fields.get(option);
            if (field != null) {
                Completion ann = field.getAnnotation(Completion.class);
                if (ann != null) {
                    try {
                        Class clazz = ann.value();
                        String[] value = ann.values();
                        if (clazz != null) {
                            if (clazz == StringsCompleter.class) {
                                completer = new StringsCompleter(value, ann.caseSensitive());
                            } else {
                                completer = command.getCompleter(clazz);
                            }
                        }
                    } catch (Throwable t) {
                        // Ignore in case the completer class is not even available
                    }
                } else {
                    completer = getDefaultCompleter(field, option.multiValued());
                }
            }
            if (completer == null) {
                completer = NullCompleter.INSTANCE;
            }
            optionalCompleters.put(option.name(), completer);
            if (option.aliases() != null) {
                for (String alias : option.aliases()) {
                    optionalCompleters.put(alias, completer);
                }
            }
        }
    }

    @SuppressWarnings({
     "unchecked", "rawtypes"
    })
    private Completer getDefaultCompleter(Field field, boolean multi) {
        Completer completer = null;
        Class<?> type = field.getType();
        GenericType genericType = new GenericType(field.getGenericType());
        if (Collection.class.isAssignableFrom(genericType.getRawClass()) && multi) {
            type = genericType.getActualTypeArgument(0).getRawClass();
        }
        if (type.isAssignableFrom(URI.class)) {
            completer = new UriCompleter();
        } else if (type.isAssignableFrom(File.class)) {
            completer = new FileCompleter();
        } else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) {
            completer = new StringsCompleter(new String[] {"false", "true"}, false);
        } else if (Enum.class.isAssignableFrom(type)) {
            Set<String> values = new HashSet<>();
            for (Object o : EnumSet.allOf((Class<Enum>) type)) {
                values.add(o.toString());
            }
            completer = new StringsCompleter(values, false);
        }
        return completer;
    }

    public int complete(Session session, final CommandLine commandLine, final List<String> candidates) {
        List<Candidate> cands = new ArrayList<>();
        completeCandidates(session, commandLine, cands);
        for (Candidate cand : cands) {
            candidates.add(cand.value());
        }
        return 0;
    }

    @Override
    public void completeCandidates(Session session, final CommandLine list, List<Candidate> candidates) {
        int argIndex = list.getCursorArgumentIndex();

        Completer comp = null;
        String[] args = list.getArguments();
        int index = 0;
        // First argument is command name
        if (index < argIndex) {
            // Verify scope
            if (!Session.SCOPE_GLOBAL.equals(command.getScope()) && !session.resolveCommand(args[index]).equals(command.getScope() + ":" + command.getName())) {
                return;
            }
            // Verify command name
            if (!verifyCompleter(session, commandCompleter, args[index])) {
                return;
            }
            index++;
        } else {
            comp = commandCompleter;
        }
        // Now, check options
        if (comp == null) {
            while (index < argIndex && args[index].startsWith("-")) {
                if (!verifyCompleter(session, optionsCompleter, args[index])) {
                    return;
                }
                Option option = options.get(args[index]);
                if (option == null) {
                    return;
                }
                Field field = fields.get(option);
                if (field != null && field.getType() != boolean.class && field.getType() != Boolean.class) {
                    if (++index == argIndex) {
                        comp = NullCompleter.INSTANCE;
                    }
                }
                index++;
            }
            if (comp == null && index >= argIndex && index < args.length && args[index].startsWith("-")) {
                comp = optionsCompleter;
            }
        }
        // Now check for if last Option has a completer
        int lastAgurmentIndex = argIndex - 1;
        if (lastAgurmentIndex >= 1) {
            Option lastOption = options.get(args[lastAgurmentIndex]);
            if (lastOption != null) {
                Field lastField = fields.get(lastOption);
                if (lastField != null && lastField.getType() != boolean.class && lastField.getType() != Boolean.class) {
                    Option option = lastField.getAnnotation(Option.class);
                    if (option != null) {
                        Completer optionValueCompleter = null;
                        String name = option.name();
                        if (name != null) {
                            optionValueCompleter = optionalCompleters.get(name);
                            if (optionValueCompleter == null) {
                                String[] aliases = option.aliases();
                                if (aliases.length > 0) {
                                    for (int i = 0; i < aliases.length && optionValueCompleter == null; i++) {
                                        optionValueCompleter = optionalCompleters.get(option.aliases()[i]);
                                    }
                                }
                            }
                        }
                        if(optionValueCompleter != null) {
                            comp = optionValueCompleter;
                        }
                    }
                }
            }
        }

        // Check arguments
        if (comp == null) {
            int indexArg = 0;
            while (index < argIndex) {
                Completer sub = argsCompleters.get(indexArg >= argsCompleters.size() ? argsCompleters.size() - 1 : indexArg);
                if (!verifyCompleter(session, sub, args[index])) {
                    return;
                }
                index++;
                indexArg++;
            }
            comp = argsCompleters.get(indexArg >= argsCompleters.size() ? argsCompleters.size() - 1 : indexArg);
        }

        comp.completeCandidates(session, list, candidates);

        /* TODO:JLINE
        if (pos == -1) {
            return -1;
        }
        */

        /**
         *  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".
         */

        /* TODO:JLINE
        String buffer = list.getBuffer();
        int cursor = list.getBufferPosition();
        if ((buffer != null) && (cursor != buffer.length()) && isDelimiter(buffer, cursor)) {
            for (int i = 0; i < candidates.size(); i++) {
                String val = candidates.get(i);

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

                candidates.set(i, val);
            }
        }

        return pos;
        */
    }

    protected boolean verifyCompleter(Session session, Completer completer, String argument) {
        List<Candidate> candidates = new ArrayList<>();
        completer.completeCandidates(session, new ArgumentCommandLine(argument, argument.length()), candidates);
        return !candidates.isEmpty();
    }

    /**
     * Returns true if the specified character is a whitespace
     * parameter. Check to ensure that the character is not
     * escaped 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, false else.
     */
    public boolean isDelimiter(final String buffer, final int pos) {
        return !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
    }

    public boolean isEscaped(final String buffer, final int pos) {
        return pos > 0 && buffer.charAt(pos) == '\\' && !isEscaped(buffer, pos - 1);
    }

    /**
     * The character is a delimiter if it is whitespace, and the
     * preceding character is not an escape character.
     *
     * @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, false else.
     */
    public boolean isDelimiterChar(String buffer, int pos) {
        return Character.isWhitespace(buffer.charAt(pos));
    }

    static class CandidateCompleter implements Completer {

        private final List<Candidate> candidates = new ArrayList<>();

        public void addCandidate(String value, String desc) {
            addCandidate(value, desc, null);
        }

        public void addCandidate(String value, String desc, String key) {
            if (desc.endsWith(".")) {
                desc = desc.substring(0, desc.length() - 1);
            }
            candidates.add(new Candidate(value, value, null, desc, null, key, true));
        }

        @Override
        public int complete(Session session, CommandLine commandLine, List<String> candidates) {
            List<Candidate> cands = new ArrayList<>();
            completeCandidates(session, commandLine, cands);
            for (Candidate cand : cands) {
                candidates.add(cand.value());
            }
            return 0;
        }

        @Override
        public void completeCandidates(Session session, CommandLine commandLine, List<Candidate> candidates) {
            candidates.addAll(this.candidates);
        }
    }

}
