/*
 * Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License
 *
 *  Permission is hereby granted, free of charge, to any person obtaining
 *  a copy of this software and associated documentation files (the
 *  "Software"), to deal in the Software without restriction, including
 *  without limitation the rights to use, copy, modify, merge, publish,
 *  distribute, sublicense, and/or sell copies of the Software, and to
 *  permit persons to whom the Software is furnished to do so, subject to
 *  the following conditions:
 *
 *  The above copyright notice and this permission notice shall be
 *  included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 *  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 *  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 *  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package co.aikar.commands;

import co.aikar.timings.lib.TimingManager;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandException;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.bukkit.command.SimpleCommandMap;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.plugin.Plugin;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

@SuppressWarnings("WeakerAccess")
public class BukkitCommandManager extends CommandManager {

    @SuppressWarnings("WeakerAccess")
    protected final Plugin plugin;
    private final CommandMap commandMap;
    private final TimingManager timingManager;
    protected Map<String, Command> knownCommands = new HashMap<>();
    protected Map<String, BaseCommand> registeredCommands = new HashMap<>();
    protected CommandContexts contexts;
    protected CommandCompletions completions;

    public BukkitCommandManager(Plugin plugin) {
        this.plugin = plugin;
        this.timingManager = TimingManager.of(plugin);
        CommandMap commandMap = null;
        try {
            Server server = Bukkit.getServer();
            Method getCommandMap = server.getClass().getDeclaredMethod("getCommandMap");
            getCommandMap.setAccessible(true);
            commandMap = (CommandMap) getCommandMap.invoke(server);
            if (!SimpleCommandMap.class.isAssignableFrom(commandMap.getClass())) {
                ACFLog.severe("ERROR: CommandMap has been hijacked! Offending command map is located at: " + commandMap.getClass().getName());
                ACFLog.severe("We are going to try to hijack it back and resolve this, but you are now in dangerous territory.");
                ACFLog.severe("We can not guarantee things are going to work.");
                Field cmField = server.getClass().getDeclaredField("commandMap");
                cmField.set(server, commandMap = new ProxyCommandMap(commandMap));
                ACFLog.info("Injected Proxy Command Map... good luck...");
            }
            Field knownCommands = commandMap.getClass().getDeclaredField("knownCommands");
            knownCommands.setAccessible(true);
            //noinspection unchecked
            this.knownCommands = (Map<String, Command>) knownCommands.get(commandMap);
        } catch (Exception e) {
            ACFLog.severe("Failed to get Command Map. ACF will not function.");
            ACFUtil.sneaky(e);
        }
        this.commandMap = commandMap;
        Bukkit.getPluginManager().registerEvents(new ACFBukkitListener(plugin), plugin);
    }

    public Plugin getPlugin() {
        return this.plugin;
    }

    @Override
    public synchronized CommandContexts getCommandContexts() {
        if (this.contexts == null) {
            this.contexts = new BukkitCommandContexts(this);
        }
        return contexts;
    }

    @Override
    public synchronized CommandCompletions getCommandCompletions() {
        if (this.completions == null) {
            this.completions = new BukkitCommandCompletions(this);
        }
        return completions;
    }

    @Override
    public void registerCommand(BaseCommand command) {
        final String plugin = this.plugin.getName().toLowerCase();
        command.onRegister(this);
        for (Map.Entry<String, RootCommand> entry : command.registeredCommands.entrySet()) {
            String key = entry.getKey().toLowerCase();
            RootCommand value = entry.getValue();
            if (!value.isRegistered) {
                commandMap.register(key, plugin, value);
            }
            value.isRegistered = true;
            registeredCommands.put(key, command);
        }
    }

    public void unregisterCommand(BaseCommand command) {
        final String plugin = this.plugin.getName().toLowerCase();
        command.registeredCommands.entrySet().removeIf(entry -> {
            Command cmd = entry.getValue();
            cmd.unregister(commandMap);
            String key = entry.getKey();
            Command registered = knownCommands.get(key);
            if (registered == command) {
                knownCommands.remove(key);
            }
            knownCommands.remove(plugin + ":" + key);
            return true;
        });
    }

    public void unregisterCommands() {
        for (Map.Entry<String, BaseCommand> entry : registeredCommands.entrySet()) {
            unregisterCommand(entry.getValue());
        }
    }

    private class ACFBukkitListener implements Listener {
        private final Plugin plugin;

        public ACFBukkitListener(Plugin plugin) {
            this.plugin = plugin;
        }

        @EventHandler
        public void onPluginDisable(PluginDisableEvent event) {
            if (!(plugin.getName().equalsIgnoreCase(event.getPlugin().getName()))) {
                return;
            }
            unregisterCommands();
        }
    }

    @Override
    public TimingManager getTimings() {
        return timingManager;
    }

    class ProxyCommandMap extends SimpleCommandMap {

        CommandMap proxied;

        ProxyCommandMap(CommandMap proxied) {
            super(Bukkit.getServer());
            this.proxied = proxied;
        }

        @Override
        public void registerAll(String fallbackPrefix, List<Command> commands) {
            proxied.registerAll(fallbackPrefix, commands);
        }

        @Override
        public boolean register(String label, String fallbackPrefix, Command command) {
            if (isOurCommand(command)) {
                return super.register(label, fallbackPrefix, command);
            } else {
                return proxied.register(label, fallbackPrefix, command);
            }
        }

        boolean isOurCommand(String cmdLine) {
            String[] args = ACFPatterns.SPACE.split(cmdLine);
            return args.length != 0 && isOurCommand(knownCommands.get(args[0].toLowerCase(Locale.ENGLISH)));

        }
        boolean isOurCommand(Command command) {
            return command instanceof BaseCommand && ((BaseCommand) command).manager == BukkitCommandManager.this;
        }

        @Override
        public boolean register(String fallbackPrefix, Command command) {
            if (isOurCommand(command)) {
                return super.register(fallbackPrefix, command);
            } else {
                return proxied.register(fallbackPrefix, command);
            }
        }

        @Override
        public boolean dispatch(CommandSender sender, String cmdLine) throws CommandException {
            if (isOurCommand(cmdLine)) {
                return super.dispatch(sender, cmdLine);
            } else {
                return proxied.dispatch(sender, cmdLine);
            }
        }

        @Override
        public void clearCommands() {
            super.clearCommands();;
            proxied.clearCommands();
        }

        @Override
        public Command getCommand(String name) {
            if (isOurCommand(name)) {
                return super.getCommand(name);
            } else {
                return proxied.getCommand(name);
            }
        }

        @Override
        public List<String> tabComplete(CommandSender sender, String cmdLine) throws IllegalArgumentException {
            if (isOurCommand(cmdLine)) {
                return super.tabComplete(sender, cmdLine);
            } else {
                return proxied.tabComplete(sender, cmdLine);
            }
        }
    }
}

@@ -3375,17 +3375,23 @@
 mands = 
-c
+SimpleC
 ommandMa
@@ -3388,34 +3388,29 @@
 eCommandMap.
-getC
+c
 lass
-()
 .getDeclared

Use the correct class for knownFields lookup - Fixes #25