package net.coreprotect.config;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CompletableFuture;

import org.bukkit.Bukkit;
import org.bukkit.World;

import net.coreprotect.CoreProtect;
import net.coreprotect.language.Language;
import net.coreprotect.thread.Scheduler;

public class Config extends Language {

    private static final Map<String, String[]> HEADERS = new HashMap<>();
    private static final Map<String, String> DEFAULT_VALUES = new LinkedHashMap<>();
    private static final Map<String, Config> CONFIG_BY_WORLD_NAME = new HashMap<>();
    private static final WeakHashMap<World, Config> CONFIG_BY_WORLD = new WeakHashMap<>();
    private static final String DEFAULT_FILE_HEADER = "# CoreProtect Config";
    public static final String LINE_SEPARATOR = "\n";

    private static final Config GLOBAL = new Config();
    private final HashMap<String, String> config;
    private Config defaults;

    public String DONATION_KEY;
    public String PREFIX;
    public String MYSQL_HOST;
    public String MYSQL_DATABASE;
    public String MYSQL_USERNAME;
    public String MYSQL_PASSWORD;
    public String LANGUAGE;
    public boolean ENABLE_AWE;
    public boolean ENABLE_SSL;
    public boolean DISABLE_WAL;
    public boolean HOVER_EVENTS;
    public boolean DATABASE_LOCK;
    public boolean LOG_CANCELLED_CHAT;
    public boolean HOPPER_FILTER_META;
    public boolean EXCLUDE_TNT;
    public boolean NETWORK_DEBUG;
    public boolean MYSQL;
    public boolean CHECK_UPDATES;
    public boolean API_ENABLED;
    public boolean VERBOSE;
    public boolean ROLLBACK_ITEMS;
    public boolean ROLLBACK_ENTITIES;
    public boolean SKIP_GENERIC_DATA;
    public boolean BLOCK_PLACE;
    public boolean BLOCK_BREAK;
    public boolean NATURAL_BREAK;
    public boolean BLOCK_MOVEMENT;
    public boolean PISTONS;
    public boolean BLOCK_BURN;
    public boolean BLOCK_IGNITE;
    public boolean EXPLOSIONS;
    public boolean ENTITY_CHANGE;
    public boolean ENTITY_KILLS;
    public boolean SIGN_TEXT;
    public boolean BUCKETS;
    public boolean LEAF_DECAY;
    public boolean TREE_GROWTH;
    public boolean MUSHROOM_GROWTH;
    public boolean VINE_GROWTH;
    public boolean SCULK_SPREAD;
    public boolean PORTALS;
    public boolean WATER_FLOW;
    public boolean LAVA_FLOW;
    public boolean LIQUID_TRACKING;
    public boolean ITEM_TRANSACTIONS;
    public boolean ITEM_DROPS;
    public boolean ITEM_PICKUPS;
    public boolean HOPPER_TRANSACTIONS;
    public boolean PLAYER_INTERACTIONS;
    public boolean PLAYER_MESSAGES;
    public boolean PLAYER_COMMANDS;
    public boolean PLAYER_SESSIONS;
    public boolean UNKNOWN_LOGGING;
    public boolean USERNAME_CHANGES;
    public boolean WORLDEDIT;
    public int MAXIMUM_POOL_SIZE;
    public int MYSQL_PORT;
    public int DEFAULT_RADIUS;
    public int MAX_RADIUS;

    static {
        DEFAULT_VALUES.put("donation-key", "");
        DEFAULT_VALUES.put("use-mysql", "false");
        DEFAULT_VALUES.put("table-prefix", "co_");
        DEFAULT_VALUES.put("mysql-host", "127.0.0.1");
        DEFAULT_VALUES.put("mysql-port", "3306");
        DEFAULT_VALUES.put("mysql-database", "database");
        DEFAULT_VALUES.put("mysql-username", "root");
        DEFAULT_VALUES.put("mysql-password", "");
        DEFAULT_VALUES.put("language", "en");
        DEFAULT_VALUES.put("check-updates", "true");
        DEFAULT_VALUES.put("api-enabled", "true");
        DEFAULT_VALUES.put("verbose", "true");
        DEFAULT_VALUES.put("default-radius", "10");
        DEFAULT_VALUES.put("max-radius", "100");
        DEFAULT_VALUES.put("rollback-items", "true");
        DEFAULT_VALUES.put("rollback-entities", "true");
        DEFAULT_VALUES.put("skip-generic-data", "true");
        DEFAULT_VALUES.put("block-place", "true");
        DEFAULT_VALUES.put("block-break", "true");
        DEFAULT_VALUES.put("natural-break", "true");
        DEFAULT_VALUES.put("block-movement", "true");
        DEFAULT_VALUES.put("pistons", "true");
        DEFAULT_VALUES.put("block-burn", "true");
        DEFAULT_VALUES.put("block-ignite", "true");
        DEFAULT_VALUES.put("explosions", "true");
        DEFAULT_VALUES.put("entity-change", "true");
        DEFAULT_VALUES.put("entity-kills", "true");
        DEFAULT_VALUES.put("sign-text", "true");
        DEFAULT_VALUES.put("buckets", "true");
        DEFAULT_VALUES.put("leaf-decay", "true");
        DEFAULT_VALUES.put("tree-growth", "true");
        DEFAULT_VALUES.put("mushroom-growth", "true");
        DEFAULT_VALUES.put("vine-growth", "true");
        DEFAULT_VALUES.put("sculk-spread", "true");
        DEFAULT_VALUES.put("portals", "true");
        DEFAULT_VALUES.put("water-flow", "true");
        DEFAULT_VALUES.put("lava-flow", "true");
        DEFAULT_VALUES.put("liquid-tracking", "true");
        DEFAULT_VALUES.put("item-transactions", "true");
        DEFAULT_VALUES.put("item-drops", "true");
        DEFAULT_VALUES.put("item-pickups", "true");
        DEFAULT_VALUES.put("hopper-transactions", "true");
        DEFAULT_VALUES.put("player-interactions", "true");
        DEFAULT_VALUES.put("player-messages", "true");
        DEFAULT_VALUES.put("player-commands", "true");
        DEFAULT_VALUES.put("player-sessions", "true");
        DEFAULT_VALUES.put("username-changes", "true");
        DEFAULT_VALUES.put("worldedit", "true");

        HEADERS.put("donation-key", new String[] { "# CoreProtect is donationware. Obtain a donation key from coreprotect.net/donate/" });
        HEADERS.put("use-mysql", new String[] { "# MySQL is optional and not required.", "# If you prefer to use MySQL, enable the following and fill out the fields." });
        HEADERS.put("language", new String[] { "# If modified, will automatically attempt to translate languages phrases.", "# List of language codes: https://coreprotect.net/languages/" });
        HEADERS.put("check-updates", new String[] { "# If enabled, CoreProtect will check for updates when your server starts up.", "# If an update is available, you'll be notified via your server console.", });
        HEADERS.put("api-enabled", new String[] { "# If enabled, other plugins will be able to utilize the CoreProtect API.", });
        HEADERS.put("verbose", new String[] { "# If enabled, extra data is displayed during rollbacks and restores.", "# Can be manually triggered by adding \"#verbose\" to your rollback command." });
        HEADERS.put("default-radius", new String[] { "# If no radius is specified in a rollback or restore, this value will be", "# used as the radius. Set to \"0\" to disable automatically adding a radius." });
        HEADERS.put("max-radius", new String[] { "# The maximum radius that can be used in a command. Set to \"0\" to disable.", "# To run a rollback or restore without a radius, you can use \"r:#global\"." });
        HEADERS.put("rollback-items", new String[] { "# If enabled, items taken from containers (etc) will be included in rollbacks." });
        HEADERS.put("rollback-entities", new String[] { "# If enabled, entities, such as killed animals, will be included in rollbacks." });
        HEADERS.put("skip-generic-data", new String[] { "# If enabled, generic data, like zombies burning in daylight, won't be logged." });
        HEADERS.put("block-place", new String[] { "# Logs blocks placed by players." });
        HEADERS.put("block-break", new String[] { "# Logs blocks broken by players." });
        HEADERS.put("natural-break", new String[] { "# Logs blocks that break off of other blocks; for example, a sign or torch", "# falling off of a dirt block that a player breaks. This is required for", "# beds/doors to properly rollback." });
        HEADERS.put("block-movement", new String[] { "# Properly track block movement, such as sand or gravel falling." });
        HEADERS.put("pistons", new String[] { "# Properly track blocks moved by pistons." });
        HEADERS.put("block-burn", new String[] { "# Logs blocks that burn up in a fire." });
        HEADERS.put("block-ignite", new String[] { "# Logs when a block naturally ignites, such as from fire spreading." });
        HEADERS.put("explosions", new String[] { "# Logs explosions, such as TNT and Creepers." });
        HEADERS.put("entity-change", new String[] { "# Track when an entity changes a block, such as an Enderman destroying blocks." });
        HEADERS.put("entity-kills", new String[] { "# Logs killed entities, such as killed cows and enderman." });
        HEADERS.put("sign-text", new String[] { "# Logs text on signs. If disabled, signs will be blank when rolled back." });
        HEADERS.put("buckets", new String[] { "# Logs lava and water sources placed/removed by players who are using buckets." });
        HEADERS.put("leaf-decay", new String[] { "# Logs natural tree leaf decay." });
        HEADERS.put("tree-growth", new String[] { "# Logs tree growth. Trees are linked to the player who planted the sapling." });
        HEADERS.put("mushroom-growth", new String[] { "# Logs mushroom growth." });
        HEADERS.put("vine-growth", new String[] { "# Logs natural vine growth." });
        HEADERS.put("sculk-spread", new String[] { "# Logs the spread of sculk blocks from sculk catalysts." });
        HEADERS.put("portals", new String[] { "# Logs when portals such as Nether portals generate naturally." });
        HEADERS.put("water-flow", new String[] { "# Logs water flow. If water destroys other blocks, such as torches,", "# this allows it to be properly rolled back." });
        HEADERS.put("lava-flow", new String[] { "# Logs lava flow. If lava destroys other blocks, such as torches,", "# this allows it to be properly rolled back." });
        HEADERS.put("liquid-tracking", new String[] { "# Allows liquid to be properly tracked and linked to players.", "# For example, if a player places water which flows and destroys torches,", "# it can all be properly restored by rolling back that single player." });
        HEADERS.put("item-transactions", new String[] { "# Track item transactions, such as when a player takes items from", "# a chest, furnace, or dispenser." });
        HEADERS.put("item-drops", new String[] { "# Logs items dropped by players." });
        HEADERS.put("item-pickups", new String[] { "# Logs items picked up by players." });
        HEADERS.put("hopper-transactions", new String[] { "# Track all hopper transactions, such as when a hopper removes items from a", "# chest, furnace, or dispenser." });
        HEADERS.put("player-interactions", new String[] { "# Track player interactions, such as when a player opens a door, presses", "# a button, or opens a chest. Player interactions can't be rolled back." });
        HEADERS.put("player-messages", new String[] { "# Logs messages that players send in the chat." });
        HEADERS.put("player-commands", new String[] { "# Logs all commands used by players." });
        HEADERS.put("player-sessions", new String[] { "# Logs the logins and logouts of players." });
        HEADERS.put("username-changes", new String[] { "# Logs when a player changes their Minecraft username." });
        HEADERS.put("worldedit", new String[] { "# Logs changes made via the plugin \"WorldEdit\" if it's in use on your server." });
    }

    private void readValues() {
        this.ENABLE_AWE = this.getBoolean("enable-awe", false);
        this.ENABLE_SSL = this.getBoolean("enable-ssl", false);
        this.DISABLE_WAL = this.getBoolean("disable-wal", false);
        this.HOVER_EVENTS = this.getBoolean("hover-events", true);
        this.DATABASE_LOCK = this.getBoolean("database-lock", true);
        this.LOG_CANCELLED_CHAT = this.getBoolean("log-cancelled-chat", true);
        this.HOPPER_FILTER_META = this.getBoolean("hopper-filter-meta", false);
        this.EXCLUDE_TNT = this.getBoolean("exclude-tnt", false);
        this.NETWORK_DEBUG = this.getBoolean("network-debug", false);
        this.UNKNOWN_LOGGING = this.getBoolean("unknown-logging", false);
        this.MAXIMUM_POOL_SIZE = this.getInt("maximum-pool-size", 10);
        this.DONATION_KEY = this.getString("donation-key");
        this.MYSQL = this.getBoolean("use-mysql");
        this.PREFIX = this.getString("table-prefix");
        this.MYSQL_HOST = this.getString("mysql-host");
        this.MYSQL_PORT = this.getInt("mysql-port");
        this.MYSQL_DATABASE = this.getString("mysql-database");
        this.MYSQL_USERNAME = this.getString("mysql-username");
        this.MYSQL_PASSWORD = this.getString("mysql-password");
        this.LANGUAGE = this.getString("language");
        this.CHECK_UPDATES = this.getBoolean("check-updates");
        this.API_ENABLED = this.getBoolean("api-enabled");
        this.VERBOSE = this.getBoolean("verbose");
        this.DEFAULT_RADIUS = this.getInt("default-radius");
        this.MAX_RADIUS = this.getInt("max-radius");
        this.ROLLBACK_ITEMS = this.getBoolean("rollback-items");
        this.ROLLBACK_ENTITIES = this.getBoolean("rollback-entities");
        this.SKIP_GENERIC_DATA = this.getBoolean("skip-generic-data");
        this.BLOCK_PLACE = this.getBoolean("block-place");
        this.BLOCK_BREAK = this.getBoolean("block-break");
        this.NATURAL_BREAK = this.getBoolean("natural-break");
        this.BLOCK_MOVEMENT = this.getBoolean("block-movement");
        this.PISTONS = this.getBoolean("pistons");
        this.BLOCK_BURN = this.getBoolean("block-burn");
        this.BLOCK_IGNITE = this.getBoolean("block-ignite");
        this.EXPLOSIONS = this.getBoolean("explosions");
        this.ENTITY_CHANGE = this.getBoolean("entity-change");
        this.ENTITY_KILLS = this.getBoolean("entity-kills");
        this.SIGN_TEXT = this.getBoolean("sign-text");
        this.BUCKETS = this.getBoolean("buckets");
        this.LEAF_DECAY = this.getBoolean("leaf-decay");
        this.TREE_GROWTH = this.getBoolean("tree-growth");
        this.MUSHROOM_GROWTH = this.getBoolean("mushroom-growth");
        this.VINE_GROWTH = this.getBoolean("vine-growth");
        this.SCULK_SPREAD = this.getBoolean("sculk-spread");
        this.PORTALS = this.getBoolean("portals");
        this.WATER_FLOW = this.getBoolean("water-flow");
        this.LAVA_FLOW = this.getBoolean("lava-flow");
        this.LIQUID_TRACKING = this.getBoolean("liquid-tracking");
        this.ITEM_TRANSACTIONS = this.getBoolean("item-transactions");
        this.ITEM_DROPS = this.getBoolean("item-drops");
        this.ITEM_PICKUPS = this.getBoolean("item-pickups");
        this.HOPPER_TRANSACTIONS = this.getBoolean("hopper-transactions");
        this.PLAYER_INTERACTIONS = this.getBoolean("player-interactions");
        this.PLAYER_MESSAGES = this.getBoolean("player-messages");
        this.PLAYER_COMMANDS = this.getBoolean("player-commands");
        this.PLAYER_SESSIONS = this.getBoolean("player-sessions");
        this.USERNAME_CHANGES = this.getBoolean("username-changes");
        this.WORLDEDIT = this.getBoolean("worldedit");
    }

    public static void init() throws IOException {
        parseConfig(loadFiles(ConfigFile.CONFIG));
        // pass variables to ConfigFile.parseConfig(ConfigFile.loadFiles());
    }

    public static Config getGlobal() {
        return GLOBAL;
    }

    // returns a world specific config if it exists, otherwise the global config
    public static Config getConfig(final World world) {
        Config ret = CONFIG_BY_WORLD.get(world);
        if (ret == null) {
            ret = CONFIG_BY_WORLD_NAME.getOrDefault(world.getName(), GLOBAL);
            CONFIG_BY_WORLD.put(world, ret);
        }
        return ret;
    }

    public Config() {
        this.config = new LinkedHashMap<>();
    }

    public void setDefaults(final Config defaults) {
        this.defaults = defaults;
    }

    private String get(final String key, final String dfl) {
        String configured = this.config.get(key);
        if (configured == null) {
            if (dfl != null) {
                return dfl;
            }
            if (this.defaults == null) {
                configured = DEFAULT_VALUES.get(key);
            }
            else {
                configured = this.defaults.config.getOrDefault(key, DEFAULT_VALUES.get(key));
            }
        }
        return configured;
    }

    private boolean getBoolean(final String key) {
        final String configured = this.get(key, null);
        return configured != null && configured.startsWith("t");
    }

    private boolean getBoolean(final String key, final boolean dfl) {
        final String configured = this.get(key, null);
        return configured == null ? dfl : configured.startsWith("t");
    }

    private int getInt(final String key) {
        return this.getInt(key, 0);
    }

    private int getInt(final String key, final int dfl) {
        String configured = this.get(key, null);

        if (configured == null) {
            return dfl;
        }

        configured = configured.replaceAll("[^0-9]", "");

        return configured.isEmpty() ? dfl : Integer.parseInt(configured);
    }

    private String getString(final String key) {
        final String configured = this.get(key, null);
        return configured == null ? "" : configured;
    }

    public void clearConfig() {
        this.config.clear();
    }

    public void loadDefaults() {
        this.clearConfig();
        this.readValues();
    }

    public void load(final InputStream in) throws IOException {
        // if we fail reading, we will not corrupt our current config.
        final Map<String, String> newConfig = new LinkedHashMap<>(this.config.size());
        ConfigFile.load(in, newConfig, false);

        this.clearConfig();
        this.config.putAll(newConfig);

        this.readValues();
    }

    private static Map<String, byte[]> loadFiles(String fileName) throws IOException {
        final CoreProtect plugin = CoreProtect.getInstance();
        final File configFolder = plugin.getDataFolder();
        if (!configFolder.exists()) {
            configFolder.mkdirs();
        }

        final Map<String, byte[]> map = new HashMap<>();
        final File globalFile = new File(configFolder, fileName);

        if (globalFile.exists()) {
            // we always add options to the global config
            final byte[] data = Files.readAllBytes(globalFile.toPath());
            map.put("config", data);

            // can't modify GLOBAL, we're likely off-main here
            final Config temp = new Config();
            temp.load(new ByteArrayInputStream(data));
            temp.addMissingOptions(globalFile);
        }
        else {
            final Config temp = new Config();
            temp.loadDefaults();
            temp.addMissingOptions(globalFile);
        }

        for (final File worldConfigFile : configFolder.listFiles((File file) -> file.getName().endsWith(".yml"))) {
            final String name = worldConfigFile.getName();
            if (name.equals(ConfigFile.CONFIG) || name.equals(ConfigFile.LANGUAGE)) {
                continue;
            }

            map.put(name.substring(0, name.length() - ".yml".length()), Files.readAllBytes(worldConfigFile.toPath()));
        }

        return map;
    }

    // this should only be called on the main thread
    private static void parseConfig(final Map<String, byte[]> data) {
        if (!Bukkit.isPrimaryThread()) {
            // we call reloads asynchronously
            // for now this solution is good enough to ensure we only modify on the main thread
            final CompletableFuture<Void> complete = new CompletableFuture<>();

            Scheduler.runTask(CoreProtect.getInstance(), () -> {
                try {
                    parseConfig(data);
                }
                catch (final Throwable thr) {
                    if (thr instanceof ThreadDeath) {
                        throw (ThreadDeath) thr;
                    }
                    complete.completeExceptionally(thr);
                    return;
                }
                complete.complete(null);
            });

            complete.join();
            return;
        }

        CONFIG_BY_WORLD_NAME.clear();
        CONFIG_BY_WORLD.clear();

        // we need to load global first since it is used for config defaults
        final byte[] defaultData = data.get("config");
        if (defaultData != null) {
            try {
                GLOBAL.load(new ByteArrayInputStream(defaultData));
            }
            catch (final IOException ex) {
                throw new RuntimeException(ex); // shouldn't happen
            }
        }
        else {
            GLOBAL.loadDefaults();
        }

        for (final Map.Entry<String, byte[]> entry : data.entrySet()) {
            final String worldName = entry.getKey();
            if (worldName.equals("config")) {
                continue;
            }

            final byte[] fileData = entry.getValue();
            final Config config = new Config();
            config.setDefaults(GLOBAL);

            try {
                config.load(new ByteArrayInputStream(fileData));
            }
            catch (final IOException ex) {
                throw new RuntimeException(ex); // shouldn't happen
            }

            CONFIG_BY_WORLD_NAME.put(worldName, config);
        }
    }

    public void addMissingOptions(final File file) throws IOException {
        final boolean writeHeader = !file.exists() || file.length() == 0;
        try (final FileOutputStream fout = new FileOutputStream(file, true)) {
            OutputStreamWriter out = new OutputStreamWriter(new BufferedOutputStream(fout), StandardCharsets.UTF_8);
            if (writeHeader) {
                out.append(DEFAULT_FILE_HEADER);
                out.append(LINE_SEPARATOR);
            }

            for (final Map.Entry<String, String> entry : DEFAULT_VALUES.entrySet()) {
                final String key = entry.getKey();
                final String defaultValue = entry.getValue();

                final String configuredValue = this.config.get(key);

                if (configuredValue != null) {
                    continue;
                }

                final String[] header = HEADERS.get(key);

                if (header != null) {
                    out.append(LINE_SEPARATOR);
                    for (final String headerLine : header) {
                        out.append(headerLine);
                        out.append(LINE_SEPARATOR);
                    }
                }

                out.append(key);
                out.append(": ");
                out.append(defaultValue);
                out.append(LINE_SEPARATOR);
            }

            out.close();
        }
    }
}
