/*
 * 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.kafka.shell.command;

import org.apache.kafka.shell.InteractiveShell;
import org.apache.kafka.shell.state.MetadataShellState;

import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;
import net.sourceforge.argparse4j.internal.HelpScreenException;

import org.jline.reader.Candidate;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.TreeMap;

/**
 * The commands for the Kafka metadata tool.
 */
public final class Commands {
    /**
     * A map from command names to command types.
     */
    public static final NavigableMap<String, Type> TYPES;

    static {
        TreeMap<String, Type> typesMap = new TreeMap<>();
        for (Type type : List.of(
            CatCommandHandler.TYPE,
            CdCommandHandler.TYPE,
            ExitCommandHandler.TYPE,
            FindCommandHandler.TYPE,
            HelpCommandHandler.TYPE,
            HistoryCommandHandler.TYPE,
            LsCommandHandler.TYPE,
            ManCommandHandler.TYPE,
            PwdCommandHandler.TYPE,
            TreeCommandHandler.TYPE
        )) {
            typesMap.put(type.name(), type);
        }
        TYPES = Collections.unmodifiableNavigableMap(typesMap);
    }

    /**
     * Command handler objects are instantiated with specific arguments to
     * execute commands.
     */
    public interface Handler {
        void run(
            Optional<InteractiveShell> shell,
            PrintWriter writer,
            MetadataShellState state
        ) throws Exception;
    }

    /**
     * An object which describes a type of command handler. This includes
     * information like its name, help text, and whether it should be accessible
     * from non-interactive mode.
     */
    public interface Type {
        String name();
        String description();
        boolean shellOnly();
        void addArguments(ArgumentParser parser);
        Handler createHandler(Namespace namespace);
        void completeNext(
            MetadataShellState nodeManager,
            List<String> nextWords,
            List<Candidate> candidates
        ) throws Exception;
    }

    private final ArgumentParser parser;

    /**
     * Create the commands instance.
     *
     * @param addShellCommands  True if we should include the shell-only commands.
     */
    public Commands(boolean addShellCommands) {
        this.parser = ArgumentParsers.newArgumentParser("", false);
        Subparsers subparsers = this.parser.addSubparsers().dest("command");
        for (Type type : TYPES.values()) {
            if (addShellCommands || !type.shellOnly()) {
                Subparser subParser = subparsers.addParser(type.name());
                subParser.help(type.description());
                type.addArguments(subParser);
            }
        }
    }

    ArgumentParser parser() {
        return parser;
    }

    /**
     * Handle the given command.
     *
     * In general this function should not throw exceptions. Instead, it should
     * return ErroneousCommandHandler if the input was invalid.
     *
     * @param arguments     The command line arguments.
     * @return              The command handler.
     */
    public Handler parseCommand(List<String> arguments) {
        List<String> trimmedArguments = new ArrayList<>(arguments);
        while (true) {
            if (trimmedArguments.isEmpty()) {
                return new NoOpCommandHandler();
            }
            String last = trimmedArguments.get(trimmedArguments.size() - 1);
            if (!last.isEmpty()) {
                break;
            }
            trimmedArguments.remove(trimmedArguments.size() - 1);
        }
        Namespace namespace;
        try {
            namespace = parser.parseArgs(trimmedArguments.toArray(new String[0]));
        } catch (HelpScreenException e) {
            return new NoOpCommandHandler();
        } catch (ArgumentParserException e) {
            return new ErroneousCommandHandler(e.getMessage());
        }
        String command = namespace.get("command");
        if (!command.equals(trimmedArguments.get(0))) {
            return new ErroneousCommandHandler("invalid choice: '" +
                trimmedArguments.get(0) + "': did you mean '" + command + "'?");
        }
        Type type = TYPES.get(command);
        if (type == null) {
            return new ErroneousCommandHandler("Unknown command specified: " + command);
        } else {
            return type.createHandler(namespace);
        }
    }
}
