/*
    This file is a part of the SteamWar software.

    Copyright (C) 2020  SteamWar.de-Serverteam

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package de.steamwar.bausystem.world;

import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.FlatteningWrapper;
import de.steamwar.bausystem.commands.CommandScript;
import de.steamwar.bausystem.commands.CommandTNT;
import de.steamwar.bausystem.tracer.record.RecordStateMachine;
import de.steamwar.bausystem.world.regions.Region;
import de.steamwar.inventory.SWAnvilInv;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;

import java.util.*;
import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.IntUnaryOperator;
import java.util.logging.Level;

public class ScriptListener implements Listener {

    public static final Map<Player, Map<String, Integer>> GLOBAL_VARIABLES = new HashMap<>();
    private static final Map<String, Function<Player, Integer>> CONSTANTS = new HashMap<>();

    static {
        CONSTANTS.put("trace", player -> RecordStateMachine.getRecordStatus().isTracing() ? 1 : 0);
        CONSTANTS.put("autotrace", player -> RecordStateMachine.getRecordStatus().isAutoTrace() ? 1 : 0);
        CONSTANTS.put("tnt", player -> Region.getRegion(player.getLocation()).getTntMode() == CommandTNT.TNTMode.OFF ? 0 : 1);
        CONSTANTS.put("freeze", player -> Region.getRegion(player.getLocation()).isFreeze() ? 1 : 0);
        CONSTANTS.put("fire", player -> Region.getRegion(player.getLocation()).isFire() ? 1 : 0);
        CONSTANTS.put("x", player -> player.getLocation().getBlockX());
        CONSTANTS.put("y", player -> player.getLocation().getBlockY());
        CONSTANTS.put("z", player -> player.getLocation().getBlockZ());
    }

    private Set<Player> playerSet = new HashSet<>();

    public ScriptListener() {
        Bukkit.getScheduler().runTaskTimer(BauSystem.getPlugin(), playerSet::clear, 5, 5);
    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onLeftClick(PlayerInteractEvent event) {
        ItemStack item = event.getItem();
        if (item == null || isNoBook(item) || item.getItemMeta() == null)
            return;

        if (CommandScript.BOOK.getItemMeta() != null && item.getItemMeta().hasDisplayName() && item.getItemMeta().getDisplayName().equals(CommandScript.BOOK.getItemMeta().getDisplayName())) {
            return;
        }

        if (event.getAction() != Action.LEFT_CLICK_AIR && event.getAction() != Action.LEFT_CLICK_BLOCK) {
            if (event.getAction() == Action.RIGHT_CLICK_AIR) {
                playerSet.add(event.getPlayer());
            }
            return;
        }
        if (playerSet.remove(event.getPlayer())) {
            return;
        }

        event.setCancelled(true);
        new ScriptExecutor((BookMeta) item.getItemMeta(), event.getPlayer());
    }

    @EventHandler
    public void onPlayerQuit(PlayerQuitEvent event) {
        GLOBAL_VARIABLES.remove(event.getPlayer());
    }

    private boolean isNoBook(ItemStack item) {
        return FlatteningWrapper.impl.isNoBook(item);
    }

    private static class ScriptExecutor {

        private final Player player;
        private final List<String> commands = new ArrayList<>();
        private final Map<String, Integer> jumpPoints = new HashMap<>();
        private Map<String, Integer> variables = new HashMap<>();

        private int index = 0;

        public ScriptExecutor(BookMeta bookMeta, Player player) {
            this.player = player;

            for (String page : bookMeta.getPages()) {
                for (String command : page.split("\n")) {
                    command = command.replaceAll(" +", " ");
                    if (command.startsWith("#") || command.trim().isEmpty()) continue;
                    if (command.startsWith(".")) {
                        jumpPoints.put(command.substring(1), commands.size());
                        continue;
                    }
                    commands.add(command);
                }
            }
            if (commands.isEmpty()) return;
            resume();
        }

        private void resume() {
            if (!player.isOnline()) {
                return;
            }

            int executionPoints = 0;

            while (index < commands.size()) {
                String command = commands.get(index++);
                if (executionPoints++ > 200) {
                    player.sendMessage(BauSystem.PREFIX + "§cFüge ein sleep in dein Script ein");
                    return;
                }

                String firstArg = command;
                if (command.contains(" ")) {
                    firstArg = command.substring(0, command.indexOf(' '));
                }
                switch (firstArg.toLowerCase()) {
                    case "sleep":
                        ScriptListener.sleepCommand(this, generateArgumentArray("sleep", this, command));
                        return;
                    case "exit":
                        return;
                    case "jump":
                        int jumpIndex = ScriptListener.jumpCommand(this, generateArgumentArray("jump", this, command));
                        if (jumpIndex != -1) {
                            index = jumpIndex;
                        } else {
                            player.sendMessage(BauSystem.PREFIX + "§cUnbekannter Jump Punkt: " + command);
                        }
                        continue;
                    case "echo":
                        ScriptListener.echoCommand(this, generateArgumentArray("echo", this, command));
                        continue;
                    case "input":
                        ScriptListener.inputCommand(this, generateArgumentArray("input", this, command));
                        return;
                    case "var":
                        ScriptListener.variableCommand(this, generateArgumentArray("var", this, command));
                        continue;
                    case "unvar":
                        ScriptListener.unvariableCommand(this, generateArgumentArray("unvar", this, command));
                        continue;
                    case "global":
                        ScriptListener.globalCommand(this, generateArgumentArray("global", this, command));
                        continue;
                    case "unglobal":
                        ScriptListener.unglobalCommand(this, generateArgumentArray("unglobal", this, command));
                        continue;
                    case "add":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("add", this, command), (left, right) -> left + right);
                        continue;
                    case "sub":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("sub", this, command), (left, right) -> left - right);
                        continue;
                    case "mul":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("mul", this, command), (left, right) -> left * right);
                        continue;
                    case "div":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("div", this, command), (left, right) -> left / right);
                        continue;
                    case "equal":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("equal", this, command), (left, right) -> left == right ? 1 : 0);
                        continue;
                    case "less":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("less", this, command), (left, right) -> left < right ? 1 : 0);
                        continue;
                    case "greater":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("greater", this, command), (left, right) -> left > right ? 1 : 0);
                        continue;
                    case "not":
                        ScriptListener.arithmeticInfixCommand(this, generateArgumentArray("not", this, command), (left) -> left == 1 ? 0 : 1);
                        continue;
                    case "and":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("and", this, command), (left, right) -> left == 1 && right == 1 ? 1 : 0);
                        continue;
                    case "or":
                        ScriptListener.arithmeticCommand(this, generateArgumentArray("or", this, command), (left, right) -> left == 1 || right == 1 ? 1 : 0);
                        continue;
                    case "if":
                        int ifJumpIndex = ScriptListener.ifCommand(this, generateArgumentArray("if", this, command));
                        if (ifJumpIndex != -1) {
                            index = ifJumpIndex;
                        }
                        continue;
                    default:
                        break;
                }

                PlayerCommandPreprocessEvent preprocessEvent = new PlayerCommandPreprocessEvent(player, "/" + command);
                Bukkit.getServer().getPluginManager().callEvent(preprocessEvent);
                if (preprocessEvent.isCancelled()) {
                    continue;
                }

                // Variable Replaces in commands.
                command = String.join(" ", generateArgumentArray("", this, command));

                Bukkit.getLogger().log(Level.INFO, player.getName() + " dispatched command: " + command);
                Bukkit.getServer().dispatchCommand(player, command);
            }
        }

    }

    private static String[] generateArgumentArray(String command, ScriptExecutor scriptExecutor, String fullCommand) {
        String[] strings;
        if (command.isEmpty()) {
            strings = fullCommand.split(" ");
        } else {
            strings = fullCommand.substring(command.length()).trim().split(" ");
        }
        return replaceVariables(scriptExecutor, strings);
    }

    private static void sleepCommand(ScriptExecutor scriptExecutor, String[] args) {
        int sleepTime = 1;
        if (args.length > 0) {
            try {
                sleepTime = Integer.parseInt(args[0]);
                if (sleepTime <= 0) {
                    scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cDie Zeit muss eine Zahl großer 0 sein.");
                    sleepTime = 1;
                }
            } catch (NumberFormatException e) {
                scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cDie Zeit darf nur aus Zahlen bestehen.");
            }
        }
        Bukkit.getScheduler().runTaskLater(BauSystem.getPlugin(), scriptExecutor::resume, sleepTime);
    }

    private static int jumpCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cEin Jump Punkt muss angegeben sein.");
            return -1;
        }
        return scriptExecutor.jumpPoints.getOrDefault(args[0], -1);
    }

    private static String[] replaceVariables(ScriptExecutor scriptExecutor, String[] args) {
        // Legacy '$' notation for variable reference, could be removed later on.
        for (int i = 0; i < args.length; i++) {
            if (args[i].startsWith("$") && isVariable(scriptExecutor, args[i].substring(1))) {
                args[i] = getValue(scriptExecutor, args[i].substring(1)) + "";
            }
        }

        String s = String.join(" ", args);
        Set<String> variables = new HashSet<>(scriptExecutor.variables.keySet());
        variables.addAll(CONSTANTS.keySet());
        if (GLOBAL_VARIABLES.containsKey(scriptExecutor.player)) {
            variables.addAll(GLOBAL_VARIABLES.get(scriptExecutor.player).keySet());
        }
        for (String variable : variables) {
            s = s.replace("<" + variable + ">", getValue(scriptExecutor, variable) + "");
        }
        for (String constVariable : CONSTANTS.keySet()) {
            s = s.replace("<const." + constVariable + ">", getConstantValue(scriptExecutor, constVariable) + "");
        }
        for (String localVariable : scriptExecutor.variables.keySet()) {
            s = s.replace("<local." + localVariable + ">", getLocalValue(scriptExecutor, localVariable) + "");
        }
        for (String globalVariable : GLOBAL_VARIABLES.getOrDefault(scriptExecutor.player, new HashMap<>()).keySet()) {
            s = s.replace("<global." + globalVariable + ">", getGlobalValue(scriptExecutor, globalVariable) + "");
        }
        return s.split(" ");
    }

    private static void echoCommand(ScriptExecutor scriptExecutor, String[] args) {
        scriptExecutor.player.sendMessage("§eInfo§8» §7" + ChatColor.translateAlternateColorCodes('&', String.join(" ", replaceVariables(scriptExecutor, args))));
    }

    private static void variableCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariablen Namen und Zahlen/Boolsche Wert fehlt.");
            return;
        }
        if (args.length < 2) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cZahlen/Boolsche Wert fehlt.");
            return;
        }
        switch (args[1]) {
            case "inc":
            case "increment":
            case "++":
                add(scriptExecutor, args[0], 1);
                return;
            case "dec":
            case "decrement":
            case "--":
                add(scriptExecutor, args[0], -1);
                return;
            default:
                break;
        }
        setValue(scriptExecutor, args[0], parseValue(args[1]));
    }

    private static void unvariableCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariablen Namen fehlt.");
            return;
        }
        if (!isLocalVariable(scriptExecutor, args[0])) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariable is nicht definiert");
            return;
        }
        scriptExecutor.variables.remove(args[0]);
    }

    private static void globalCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariablen Namen fehlt.");
            return;
        }
        if (!isLocalVariable(scriptExecutor, args[0])) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariable is nicht definiert");
            return;
        }
        GLOBAL_VARIABLES.computeIfAbsent(scriptExecutor.player, player -> new HashMap<>()).put(args[0], scriptExecutor.variables.getOrDefault(args[0], 0));
    }

    private static void unglobalCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariablen Namen fehlt.");
            return;
        }
        if (!isGlobalVariable(scriptExecutor, args[0])) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cVariable is nicht definiert");
            return;
        }
        if (GLOBAL_VARIABLES.containsKey(scriptExecutor.player)) {
            GLOBAL_VARIABLES.get(scriptExecutor.player).remove(args[0]);
        }
    }

    private static int ifCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 2) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cDie ersten beiden Argumente sind Zahlen/Boolsche Werte oder Variablen.");
            return -1;
        }
        if (args.length < 3) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cDas dritte Argument muss ein JumpPoint sein.");
            return -1;
        }

        int jumpTruePoint = scriptExecutor.jumpPoints.getOrDefault(args[2], -1);
        int jumpFalsePoint = args.length > 3 ? scriptExecutor.jumpPoints.getOrDefault(args[3], -1) : -1;

        if (args[1].equals("exists")) {
            if (isVariable(scriptExecutor, args[0])) {
                return jumpTruePoint;
            } else {
                return jumpFalsePoint;
            }
        }
        int firstValue = getValueOrParse(scriptExecutor, args[0]);
        int secondValue = getValueOrParse(scriptExecutor, args[1]);
        if (firstValue == secondValue) {
            return jumpTruePoint;
        } else {
            return jumpFalsePoint;
        }
    }

    private static void arithmeticCommand(ScriptExecutor scriptExecutor, String[] args, IntBinaryOperator operation) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cAls erstes Argument fehlt eine Variable");
            return;
        }
        if (args.length < 2) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cAls zweites Argument fehlt eine Zahl oder Variable");
            return;
        }

        int firstValue;
        int secondValue;
        if (args.length < 3) {
            if (!isVariable(scriptExecutor, args[0])) {
                scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cDas erste Argument muss eine Variable sein");
                return;
            }
            firstValue = getValue(scriptExecutor, args[0]);
            secondValue = getValueOrParse(scriptExecutor, args[1]);
        } else {
            firstValue = getValue(scriptExecutor, args[1]);
            secondValue = getValueOrParse(scriptExecutor, args[2]);
        }
        setValue(scriptExecutor, args[0], operation.applyAsInt(firstValue, secondValue));
    }

    private static void arithmeticInfixCommand(ScriptExecutor scriptExecutor, String[] args, IntUnaryOperator operation) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cAls erstes Argument fehlt eine Variable");
            return;
        }

        int firstValue;
        if (args.length < 2) {
            if (!isVariable(scriptExecutor, args[0])) {
                scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cDas erste Argument muss eine Variable sein");
                return;
            }
            firstValue = getValue(scriptExecutor, args[0]);
        } else {
            firstValue = getValue(scriptExecutor, args[1]);
        }
        setValue(scriptExecutor, args[0], operation.applyAsInt(firstValue));
    }

    private static void inputCommand(ScriptExecutor scriptExecutor, String[] args) {
        if (args.length < 1) {
            scriptExecutor.player.sendMessage(BauSystem.PREFIX + "§cAls erstes Argument fehlt eine Variable");
            return;
        }
        String varName = args[0];
        StringBuilder st = new StringBuilder();
        for (int i = 1; i < args.length; i++) {
            if (i != 1) {
                st.append(" ");
            }
            st.append(args[i]);
        }

        SWAnvilInv swAnvilInv = new SWAnvilInv(scriptExecutor.player, ChatColor.translateAlternateColorCodes('&', st.toString()));
        swAnvilInv.setCallback(s -> {
            int value = parseValue(s);
            setValue(scriptExecutor, varName, value);
            scriptExecutor.resume();
        });
        swAnvilInv.open();
    }

    private static void setValue(ScriptExecutor scriptExecutor, String key, int value) {
        scriptExecutor.variables.put(key, value);
    }

    private static void add(ScriptExecutor scriptExecutor, String key, int value) {
        if (!isVariable(scriptExecutor, key)) {
            scriptExecutor.variables.put(key, 0);
        }
        scriptExecutor.variables.put(key, scriptExecutor.variables.get(key) + value);
    }

    private static int getValueOrParse(ScriptExecutor scriptExecutor, String key) {
        if (isVariable(scriptExecutor, key)) {
            return getValue(scriptExecutor, key);
        } else {
            return parseValue(key);
        }
    }

    private static int getValue(ScriptExecutor scriptExecutor, String key) {
        if (CONSTANTS.containsKey(key)) {
            return CONSTANTS.get(key).apply(scriptExecutor.player);
        }
        if (GLOBAL_VARIABLES.containsKey(scriptExecutor.player) && GLOBAL_VARIABLES.get(scriptExecutor.player).containsKey(key)) {
            return GLOBAL_VARIABLES.get(scriptExecutor.player).get(key);
        }
        return scriptExecutor.variables.getOrDefault(key, 0);
    }

    private static int getConstantValue(ScriptExecutor scriptExecutor, String key) {
        return CONSTANTS.getOrDefault(key, player -> 0).apply(scriptExecutor.player);
    }

    private static int getGlobalValue(ScriptExecutor scriptExecutor, String key) {
        if (!GLOBAL_VARIABLES.containsKey(scriptExecutor.player)) {
            return 0;
        }
        return GLOBAL_VARIABLES.get(scriptExecutor.player).getOrDefault(key, 0);
    }

    private static int getLocalValue(ScriptExecutor scriptExecutor, String key) {
        return scriptExecutor.variables.getOrDefault(key, 0);
    }

    private static boolean isVariable(ScriptExecutor scriptExecutor, String key) {
        if (CONSTANTS.containsKey(key)) {
            return true;
        }
        if (GLOBAL_VARIABLES.containsKey(scriptExecutor.player) && GLOBAL_VARIABLES.get(scriptExecutor.player).containsKey(key)) {
            return true;
        }
        return scriptExecutor.variables.containsKey(key);
    }

    private static boolean isLocalVariable(ScriptExecutor scriptExecutor, String key) {
        return isVariable(scriptExecutor, key) && !isGlobalVariable(scriptExecutor, key);
    }

    private static boolean isGlobalVariable(ScriptExecutor scriptExecutor, String key) {
        if (!GLOBAL_VARIABLES.containsKey(scriptExecutor.player)) {
            return false;
        }
        return GLOBAL_VARIABLES.get(scriptExecutor.player).containsKey(key);
    }

    public static int parseValue(String value) {
        if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("yes")) {
            return 1;
        } else if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("no")) {
            return 0;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return 0;
        }
    }

}
