/*
 * 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.compat;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
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.felix.gogo.commands.Action;
import org.apache.felix.service.command.CommandSession;
import org.apache.karaf.shell.commands.Argument;
import org.apache.karaf.shell.commands.CommandWithAction;
import org.apache.karaf.shell.commands.CompleterValues;
import org.apache.karaf.shell.commands.HelpOption;
import org.apache.karaf.shell.commands.Option;
import org.apache.karaf.shell.console.CommandSessionHolder;
import org.apache.karaf.shell.console.CompletableFunction;
import org.apache.karaf.shell.console.Completer;
import org.apache.karaf.shell.console.completer.FileCompleter;
import org.apache.karaf.shell.console.completer.NullCompleter;
import org.apache.karaf.shell.console.completer.StringsCompleter;
import org.apache.karaf.shell.api.console.CommandLine;
import org.apache.karaf.shell.api.console.Session;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ArgumentCompleter {

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

    public static final String ARGUMENTS_LIST = "ARGUMENTS_LIST";

    public static final String COMMANDS = ".commands";

    final String scope;
    final String name;
    final Completer commandCompleter;
    final Completer optionsCompleter;
    final List<Completer> argsCompleters;
    final Map<String, Completer> optionalCompleters;
    final CommandWithAction function;
    final Map<Option, Field> fields = new HashMap<>();
    final Map<String, Option> options = new HashMap<>();
    final Map<Integer, Field> arguments = new HashMap<>();
    boolean strict = true;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public ArgumentCompleter(CommandWithAction function, String scope, String name, boolean scoped) {
        this.function = function;
        this.scope = scope;
        this.name = name;
        // Command name completer
        String[] names = scoped ? new String[] { name } : new String[] { name, scope + ":" + name };
        commandCompleter = new StringsCompleter(names);
        // Build options completer
        for (Class<?> type = function.getActionClass(); 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 StringsCompleter(options.keySet());

        // Build arguments completers
        List<Completer> argsCompleters = null;
        Map<String, Completer> optionalCompleters = null;

        if (function instanceof CompletableFunction) {
            Map<String, Completer> focl = ((CompletableFunction) function).getOptionalCompleters();
            List<Completer> fcl = ((CompletableFunction) function).getCompleters();
            if (focl != null || fcl != null) {
                argsCompleters = new ArrayList<>();
                if (fcl != null) {
                    for (Completer c : fcl) {
                        argsCompleters.add(c == null ? NullCompleter.INSTANCE : c);
                    }
                }
                optionalCompleters = focl;
            }
        }
        if (argsCompleters == null) {
            final Map<Integer, Object> values = getCompleterValues(function);
            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());
                    org.apache.karaf.shell.commands.Completer ann = field.getAnnotation(org.apache.karaf.shell.commands.Completer.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 {
                                BundleContext context = FrameworkUtil.getBundle(function.getClass()).getBundleContext();
                                completer = new ProxyServiceCompleter(context, clazz);
                            }
                        }
                    } else if (values.containsKey(key)) {
                        Object value = values.get(key);
                        if (value instanceof String[]) {
                            completer = new StringsCompleter((String[]) value);
                        } else if (value instanceof Collection) {
                            completer = new StringsCompleter((Collection<String>) value);
                        } else {
                            LOGGER.warn("Could not use value " + value + " as set of completions!");
                        }
                    } else {
                        completer = getDefaultCompleter(field);
                    }
                }
                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) {
                    org.apache.karaf.shell.commands.Completer ann = field.getAnnotation(org.apache.karaf.shell.commands.Completer.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 {
                                BundleContext context = FrameworkUtil.getBundle(function.getClass()).getBundleContext();
                                completer = new ProxyServiceCompleter(context, clazz);
                            }
                        }
                    }
                }
                if (completer == null) {
                    completer = NullCompleter.INSTANCE;
                }
                optionalCompleters.put(option.name(), completer);
                if (option.aliases() != null) {
                    for (String alias : option.aliases()) {
                        optionalCompleters.put(alias, completer);
                    }
                }
            }
        }
        this.argsCompleters = argsCompleters;
        this.optionalCompleters = optionalCompleters;
    }

    private Map<Integer, Object> getCompleterValues(CommandWithAction function) {
        final Map<Integer, Object> values = new HashMap<>();
        Action action = null;
        try {
            for (Class<?> type = function.getActionClass(); type != null; type = type.getSuperclass()) {
                for (Method method : type.getDeclaredMethods()) {
                    CompleterValues completerMethod = method.getAnnotation(CompleterValues.class);
                    if (completerMethod != null) {
                        int index = completerMethod.index();
                        Integer key = index;
                        if (index >= arguments.size() || index < 0) {
                            LOGGER.warn("Index out of range on @CompleterValues on class " + type.getName() + " for index: " + key + " see: " + method);
                        } else if (values.containsKey(key)) {
                            LOGGER.warn("Duplicate @CompleterMethod annotations on class " + type.getName() + " for index: " + key + " see: " + method);
                        } else {
                            try {
                                Object value;
                                if (Modifier.isStatic(method.getModifiers())) {
                                    value = method.invoke(null);
                                } else {
                                    if (action == null) {
                                        action = function.createNewAction();
                                    }
                                    value = method.invoke(action);
                                }
                                values.put(key, value);
                            } catch (IllegalAccessException e) {
                                LOGGER.warn("Could not invoke @CompleterMethod on " + function + ". " + e, e);
                            } catch (InvocationTargetException e) {
                                Throwable target = e.getTargetException();
                                if (target == null) {
                                    target = e;
                                }
                                LOGGER.warn("Could not invoke @CompleterMethod on " + function + ". " + target, target);
                            }
                        }
                    }
                }
            }
        } finally {
            if (action != null) {
                try {
                    function.releaseAction(action);
                } catch (Exception e) {
                    LOGGER.warn("Failed to release action: " + action + ". " + e, e);
                }
            }
        }
        return values;
    }

    private Completer getDefaultCompleter(Field field) {
        Completer completer = null;
        Class<?> type = field.getType();
        if (type.isAssignableFrom(File.class)) {
            completer = new FileCompleter(null);
        } else if (type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class)) {
            completer = new StringsCompleter(new String[] {"false", "true"}, false);
        } else if (type.isAssignableFrom(Enum.class)) {
            Set<String> values = new HashSet<>();
            for (Object o : EnumSet.allOf((Class<Enum>) type)) {
                values.add(o.toString());
            }
            completer = new StringsCompleter(values, false);
        } else {
            // TODO any other completers we can add?
        }
        return completer;
    }

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

    /**
     * Return whether a completion at argument index N will success
     * if all the completions from arguments 0-(N-1) also succeed.
     *
     * @return The value of the strict flag.
     */
    public boolean getStrict() {
        return this.strict;
    }

    public int complete(final Session session, final CommandLine list, final List<String> candidates) {
        int argpos = list.getArgumentPosition();
        int argIndex = list.getCursorArgumentIndex();

        //Store the argument list so that it can be used by completers.
        CommandSession commandSession = CommandSessionHolder.getSession();
        if(commandSession != null) {
            commandSession.put(ARGUMENTS_LIST,list);
        }

        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(scope) && !session.resolveCommand(args[index]).equals(scope + ":" + name)) {
                return -1;
            }
            // Verify command name
            if (!verifyCompleter(commandCompleter, args[index])) {
                return -1;
            }
            index++;
        } else {
            comp = commandCompleter;
        }
        // Now, check options
        if (comp == null) {
            while (index < argIndex && args[index].startsWith("-")) {
                if (!verifyCompleter(optionsCompleter, args[index])) {
                    return -1;
                }
                Option option = options.get(args[index]);
                if (option == null) {
                    return -1;
                }
                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 (optionalCompleters != null && 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(sub, args[index])) {
                    return -1;
                }
                index++;
                indexArg++;
            }
            comp = argsCompleters.get(indexArg >= argsCompleters.size() ? argsCompleters.size() - 1 : indexArg);
        }

        int ret = comp.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".
         */
        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(Completer completer, String argument) {
        List<String> candidates = new ArrayList<>();
        return completer.complete(argument, argument.length(), candidates) != -1 && !candidates.isEmpty();
    }

    /**
     * Return 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 is a delimiter, false else.
     */
    public boolean isDelimiterChar(String buffer, int pos) {
        return Character.isWhitespace(buffer.charAt(pos));
    }

    public static class ProxyServiceCompleter implements Completer {
        private final BundleContext context;
        private final Class<? extends Completer> clazz;

        public ProxyServiceCompleter(BundleContext context, Class<? extends Completer> clazz) {
            this.context = context;
            this.clazz = clazz;
        }

        @Override
        public int complete(String buffer, int cursor, List<String> candidates) {
            ServiceReference<? extends Completer> ref = context.getServiceReference(clazz);
            if (ref != null) {
                Completer completer = context.getService(ref);
                if (completer != null) {
                    try {
                        return completer.complete(buffer, cursor, candidates);
                    } finally {
                        context.ungetService(ref);
                    }
                }
            }
            return -1;
        }
    }
}
