package net.glowstone.util;

import net.glowstone.GlowServer;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.PluginIdentifiableCommand;
import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.help.GenericCommandHelpTopic;
import org.bukkit.help.HelpMap;
import org.bukkit.help.HelpTopic;
import org.bukkit.help.HelpTopicComparator;
import org.bukkit.help.HelpTopicFactory;
import org.bukkit.help.IndexHelpTopic;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * An implementation of {@link HelpMap}.
 *
 * <p>See <a href="http://wiki.bukkit.org/Help.yml">http://wiki.bukkit.org/Help.yml</a>
 */
public final class GlowHelpMap implements HelpMap {

    private static final Comparator<String> NAME_COMPARE = HelpTopicComparator
        .topicNameComparatorInstance();
    private static final Comparator<HelpTopic> TOPIC_COMPARE = HelpTopicComparator
        .helpTopicComparatorInstance();

    private final GlowServer server;

    private final Map<String, HelpTopic> helpTopics;
    private final Map<Class, HelpTopicFactory<Command>> topicFactoryMap = new HashMap<>();
    private final Set<String> ignoredPlugins = new HashSet<>();

    private final Set<HelpTopic> indexTopics = new TreeSet<>(TOPIC_COMPARE);
    private HelpTopic defaultTopic;
    private boolean commandsInIndex = true;

    /**
     * Creates the instance for the given server.
     *
     * @param server the server
     */
    public GlowHelpMap(GlowServer server) {
        this.server = server;
        helpTopics = new TreeMap<>(NAME_COMPARE);
        defaultTopic
            = new IndexHelpTopic("Index", null, null, indexTopics, "Use /help [n] to get page"
            + " n of help.");
    }

    @Override
    public synchronized HelpTopic getHelpTopic(String topicName) {
        if (topicName.isEmpty()) {
            return defaultTopic;
        }

        return helpTopics.get(topicName);
    }

    @Override
    public Collection<HelpTopic> getHelpTopics() {
        return helpTopics.values();
    }

    @Override
    public synchronized void addTopic(HelpTopic topic) {
        indexTopics.add(topic);
        addPrivateTopic(topic);
    }

    @Override
    public void clear() {
        helpTopics.clear();
        topicFactoryMap.clear();
        ignoredPlugins.clear();
        indexTopics.clear();
    }

    @Override
    @SuppressWarnings("unchecked")
    public void registerHelpTopicFactory(Class commandClass, HelpTopicFactory factory) {
        if (!Command.class.isAssignableFrom(commandClass) && !CommandExecutor.class
            .isAssignableFrom(commandClass)) {
            throw new IllegalArgumentException("commandClass must implement either Command or "
                + "CommandExecutor!");
        }
        topicFactoryMap.put(commandClass, factory);
    }

    @Override
    public List<String> getIgnoredPlugins() {
        return new ArrayList<>(ignoredPlugins);
    }

    ////////////////////////////////////////////////////////////////////////////
    // Internals

    private void addCommandTopic(HelpTopic topic) {
        if (commandsInIndex) {
            addTopic(topic);
        } else {
            addPrivateTopic(topic);
        }
    }

    private void addPrivateTopic(HelpTopic topic) {
        if (!helpTopics.containsKey(topic.getName())) {
            helpTopics.put(topic.getName(), topic);
        }
    }

    private String color(String text) {
        return text == null ? null : ChatColor.translateAlternateColorCodes('&', text);
    }

    /**
     * Reads the general topics from help.yml and adds them to the help index.
     *
     * @param config The configuration to read from.
     */
    public void loadConfig(ConfigurationSection config) {
        // general topics
        ConfigurationSection general = config.getConfigurationSection("general-topics");
        if (general != null) {
            for (String key : general.getKeys(false)) {
                ConfigurationSection topic = general.getConfigurationSection(key);
                if (topic != null) {
                    String shortText = topic.getString("shortText", "");
                    String fullText = topic.getString("fullText", "");
                    if (!shortText.isEmpty()) {
                        if (fullText.isEmpty()) {
                            fullText = shortText;
                        } else {
                            fullText = shortText + "\n" + ChatColor.RESET + fullText;
                        }
                    }
                    addTopic(new GeneralHelpTopic(key, color(shortText), color(fullText), topic
                        .getString("permission", null)));
                }
            }
        }

        // index topics
        ConfigurationSection index = config.getConfigurationSection("index-topics");
        if (index != null) {
            for (String key : index.getKeys(false)) {
                ConfigurationSection topic = index.getConfigurationSection(key);
                if (topic != null) {
                    String shortText = color(topic.getString("shortText", ""));
                    String preamble = color(topic.getString("preamble", null));
                    String permission = topic.getString("permission", null);
                    HelpTopic helpTopic = new LazyIndexTopic(key, shortText, permission, topic
                        .getStringList("commands"), preamble);
                    if (key.equals("Default")) {
                        defaultTopic = helpTopic;
                    } else {
                        addTopic(helpTopic);
                    }
                }
            }
        }

        // ignore plugins and command topics settings
        ignoredPlugins.addAll(config.getStringList("ignore-plugins"));
        commandsInIndex = config.getBoolean("command-topics-in-master-index", true);
    }

    /**
     * Processes all the commands registered in the server and creates help topics for them.
     */
    public synchronized void initializeCommands() {
        // Don't load any automatic help topics if All is ignored
        if (ignoredPlugins.contains("All")) {
            return;
        }

        Collection<Command> commands = server.getCommandMap().getCommands();
        // Initialize help topics from the server's command map
        outer:
        for (Command command : commands) {
            if (commandInIgnoredPlugin(command)) {
                continue;
            }

            // Register a topic
            for (Entry<Class, HelpTopicFactory<Command>> entry : topicFactoryMap.entrySet()) {
                if (((Class<?>) entry.getKey()).isAssignableFrom(command.getClass())) {
                    HelpTopic t = entry.getValue().createTopic(command);
                    if (t != null) {
                        addCommandTopic(t);
                    }
                    continue outer;
                }
                if (command instanceof PluginCommand && ((Class<?>) entry.getKey())
                    .isAssignableFrom(((PluginCommand) command).getExecutor().getClass())) {
                    HelpTopic t = entry.getValue().createTopic(command);
                    if (t != null) {
                        addCommandTopic(t);
                    }
                    continue outer;
                }
            }
            addCommandTopic(new GenericCommandHelpTopic(command));
        }

        // Alias topics for commands
        Set<HelpTopic> aliases = new TreeSet<>(TOPIC_COMPARE);
        for (Command command : commands) {
            if (commandInIgnoredPlugin(command)) {
                continue;
            }
            HelpTopic original = getHelpTopic("/" + command.getLabel());
            if (original != null) {
                for (String alias : command.getAliases()) {
                    HelpTopic aliasTopic = new AliasTopic("/" + alias, original);
                    if (!helpTopics.containsKey(aliasTopic.getName())) {
                        aliases.add(aliasTopic);
                        addPrivateTopic(aliasTopic);
                    }
                }
            }
        }

        // Aliases index topic
        if (!aliases.isEmpty()) {
            addTopic(new IndexHelpTopic("Aliases", "Lists command aliases", null, aliases, null));
        }

        // Initialize plugin-level sub-topics
        Map<String, Set<HelpTopic>> pluginIndexes = new HashMap<>();
        for (Command command : commands) {
            String pluginName = getCommandPluginName(command);
            if (pluginName != null) {
                HelpTopic topic = getHelpTopic("/" + command.getLabel());
                if (topic != null) {
                    if (!pluginIndexes.containsKey(pluginName)) {
                        pluginIndexes.put(pluginName, new TreeSet<>(TOPIC_COMPARE));
                    }
                    pluginIndexes.get(pluginName).add(topic);
                }
            }
        }
        for (Entry<String, Set<HelpTopic>> entry : pluginIndexes.entrySet()) {
            String key = entry.getKey();
            addTopic(new IndexHelpTopic(key,
                "All commands for " + key, null, entry.getValue(),
                "Below is a list of all " + key + " commands:"));
        }
    }

    /**
     * Process topic amendments from help.yml.
     *
     * @param config The configuration to read from.
     */
    public void amendTopics(ConfigurationSection config) {
        ConfigurationSection amendedTopics = config.getConfigurationSection("amended-topics");
        if (amendedTopics != null) {
            for (String key : amendedTopics.getKeys(false)) {
                HelpTopic target = getHelpTopic(key);
                if (target == null) {
                    continue;
                }
                ConfigurationSection amend = amendedTopics.getConfigurationSection(key);
                if (amend == null) {
                    continue;
                }

                target.amendTopic(color(amend.getString("shortText")), color(amend
                    .getString("fullText")));
                String perm = amend.getString("permission", null);
                if (perm != null) {
                    // empty string can be specified to remove permission
                    target.amendCanSee(perm.isEmpty() ? null : perm);
                }
            }
        }
    }

    private String getCommandPluginName(Command command) {
        if (command instanceof BukkitCommand) {
            return "Bukkit";
        }
        if (command instanceof PluginIdentifiableCommand) {
            return ((PluginIdentifiableCommand) command).getPlugin().getName();
        }
        return null;
    }

    private boolean commandInIgnoredPlugin(Command command) {
        String name = getCommandPluginName(command);
        return name != null && ignoredPlugins.contains(name);
    }

    ////////////////////////////////////////////////////////////////////////////
    // Help topic subclasses

    private static class GeneralHelpTopic extends HelpTopic {

        public GeneralHelpTopic(String name, String shortText, String fullText, String permission) {
            this.name = name;
            this.shortText = shortText;
            this.fullText = fullText;
            amendedPermission = permission;
        }

        @Override
        public boolean canSee(CommandSender sender) {
            return sender instanceof ConsoleCommandSender || amendedPermission == null || sender
                .hasPermission(amendedPermission);
        }
    }

    private static class AliasTopic extends HelpTopic {

        private final HelpTopic original;

        public AliasTopic(String name, HelpTopic original) {
            this.name = name;
            shortText = ChatColor.YELLOW + "Alias for " + ChatColor.WHITE + original.getName();
            this.original = original;
        }

        @Override
        public boolean canSee(CommandSender player) {
            return original.canSee(player);
        }

        @Override
        public String getFullText(CommandSender sender) {
            return shortText + "\n" + original.getFullText(sender);
        }
    }

    private class LazyIndexTopic extends IndexHelpTopic {

        private Collection<String> topics;

        public LazyIndexTopic(String name, String shortText, String permission,
                              Collection<String> topics, String preamble) {
            super(name, shortText, permission, Collections.emptyList(), preamble);
            this.topics = topics;
        }

        @Override
        public String getFullText(CommandSender sender) {
            if (topics != null) {
                List<HelpTopic> list = new ArrayList<>(topics.size());
                for (String name : topics) {
                    HelpTopic topic = getHelpTopic(name);
                    if (topic != null) {
                        list.add(topic);
                    }
                }
                setTopicsCollection(list);
                topics = null;
            }
            return super.getFullText(sender);
        }
    }
}
