/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2023  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.features.script.lua.libs;

import com.google.gson.*;
import de.steamwar.bausystem.region.Region;
import de.steamwar.core.Core;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.api.Disable;
import de.steamwar.linkage.api.Enable;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.TwoArgFunction;
import org.luaj.vm2.lib.VarArgFunction;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Linked
public class StorageLib implements LuaLib, Enable, Disable {

    private final Gson gson = new Gson();
    private final File storageDirectory = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "script_storage");

    private static final HashMap<String, LuaValue> GLOBAL_STORAGE = new HashMap<>();
    private static final HashMap<UUID, HashMap<String, LuaValue>> PLAYER_STORAGE = new HashMap<>();
    private static final HashMap<Region, HashMap<String, LuaValue>> REGION_STORAGE = new HashMap<>();

    @Override
    public void enable() {
        if (Core.getVersion() <= 15) return;
        if (!storageDirectory.exists()) storageDirectory.mkdirs();

        try {
            JsonObject jsonObject = JsonParser.parseReader(new FileReader(new File(storageDirectory, "global.json"))).getAsJsonObject();
            jsonObject.keySet().forEach(key -> {
                GLOBAL_STORAGE.put(key, fromJson(jsonObject.get(key)));
            });
        } catch (Exception e) {}

        File regionStorageDirectory = new File(storageDirectory, "region");
        regionStorageDirectory.mkdirs();
        for (File regionStorage : regionStorageDirectory.listFiles()) {
            try {
                JsonObject jsonObject = JsonParser.parseReader(new FileReader(regionStorage)).getAsJsonObject();
                HashMap<String, LuaValue> map = new HashMap<>();
                jsonObject.keySet().forEach(key -> {
                    map.put(key, fromJson(jsonObject.get(key)));
                });
                Region region = Region.getREGION_MAP().get(regionStorage.getName().substring(0, regionStorage.getName().length() - ".json".length()));
                REGION_STORAGE.put(region, map);
            } catch (Exception e) {}
        }

        File playerStorageDirectory = new File(storageDirectory, "player");
        playerStorageDirectory.mkdirs();
        for (File playerStorage : playerStorageDirectory.listFiles()) {
            try {
                JsonObject jsonObject = JsonParser.parseReader(new FileReader(playerStorage)).getAsJsonObject();
                HashMap<String, LuaValue> map = new HashMap<>();
                jsonObject.keySet().forEach(key -> {
                    map.put(key, fromJson(jsonObject.get(key)));
                });
                SteamwarUser steamwarUser = SteamwarUser.get(Integer.parseInt(playerStorage.getName().substring(0, playerStorage.getName().length() - ".json".length())));
                PLAYER_STORAGE.put(steamwarUser.getUUID(), map);
            } catch (Exception e) {}
        }
    }

    private LuaValue fromJson(JsonElement jsonElement) {
        if (jsonElement.isJsonNull()) {
            return LuaValue.NIL;
        }
        if (jsonElement.isJsonPrimitive()) {
            JsonPrimitive jsonPrimitive = jsonElement.getAsJsonPrimitive();
            if (jsonPrimitive.isBoolean()) {
                return LuaValue.valueOf(jsonPrimitive.getAsBoolean());
            }
            if (jsonPrimitive.isNumber()) {
                try {
                    return LuaValue.valueOf(jsonPrimitive.getAsInt());
                } catch (NumberFormatException e) {}
                try {
                    return LuaValue.valueOf(jsonPrimitive.getAsDouble());
                } catch (NumberFormatException e) {}
            }
            if (jsonPrimitive.isString()) {
                return LuaValue.valueOf(jsonPrimitive.getAsString());
            }
            return null;
        }
        if (jsonElement.isJsonObject()) {
            JsonObject jsonObject = jsonElement.getAsJsonObject();
            LuaTable luaTable = new LuaTable();
            jsonObject.keySet().forEach(string -> {
                LuaValue value = fromJson(jsonObject.get(string));
                if (value == null) return;
                luaTable.set(string, value);
            });
            return luaTable;
        }
        return null;
    }

    @Override
    public void disable() {
        if (Core.getVersion() <= 15) return;
        if (!storageDirectory.exists()) storageDirectory.mkdirs();
        try {
            FileWriter fileWriter = new FileWriter(new File(storageDirectory, "global.json"));
            gson.toJson(toJson(GLOBAL_STORAGE), fileWriter);
            fileWriter.close();
        } catch (IOException e) {}

        File regionStorageDirectory = new File(storageDirectory, "region");
        regionStorageDirectory.mkdirs();
        for (Map.Entry<Region, HashMap<String, LuaValue>> entry : REGION_STORAGE.entrySet()) {
            try {
                FileWriter fileWriter = new FileWriter(new File(regionStorageDirectory, entry.getKey().getName() + ".json"));
                gson.toJson(toJson(entry.getValue()), fileWriter);
                fileWriter.close();
            } catch (IOException e) {}
        }

        File playerStorageDirectory = new File(storageDirectory, "player");
        playerStorageDirectory.mkdirs();
        for (Map.Entry<UUID, HashMap<String, LuaValue>> entry : PLAYER_STORAGE.entrySet()) {
            try {
                FileWriter fileWriter = new FileWriter(new File(playerStorageDirectory, SteamwarUser.get(entry.getKey()).getId() + ".json"));
                gson.toJson(toJson(entry.getValue()), fileWriter);
                fileWriter.close();
            } catch (IOException e) {}
        }
    }

    private JsonObject toJson(HashMap<String, LuaValue> valueMap) {
        JsonObject jsonObject = new JsonObject();
        valueMap.forEach((string, luaValue) -> {
            JsonElement value = toJson(luaValue);
            if (value == null) return;
            jsonObject.add(string, value);
        });
        return jsonObject;
    }

    private JsonElement toJson(LuaValue luaValue) {
        if (luaValue.isnil()) {
            return JsonNull.INSTANCE;
        }
        try {
            return new JsonPrimitive(luaValue.checkboolean());
        } catch (Exception e) {}
        try {
            return new JsonPrimitive(luaValue.checkint());
        } catch (Exception e) {}
        try {
            return new JsonPrimitive(luaValue.checkdouble());
        } catch (Exception e) {}

        if (luaValue.isstring()) {
            return new JsonPrimitive(luaValue.tojstring());
        }
        if (luaValue.istable()) {
            LuaTable luaTable = luaValue.checktable();
            JsonObject jsonObject = new JsonObject();
            for (LuaValue key : luaTable.keys()) {
                JsonElement value = toJson(luaTable.get(key));
                if (value == null) continue;
                try {
                    jsonObject.add(key.checkjstring(), value);
                } catch (Exception e) {}
            }
            return jsonObject;
        }
        return null;
    }

    @Override
    public String name() {
        return "storage";
    }

    @Override
    public LuaTable get(Player player) {
        LuaTable storageLib = new LuaTable();

        LuaTable global = new LuaTable();
        global.set("get", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return GLOBAL_STORAGE.getOrDefault(arg.checkjstring(), NIL);
            }
        });
        global.set("set", new TwoArgFunction() {
            @Override
            public LuaValue call(LuaValue arg1, LuaValue arg2) {
                return GLOBAL_STORAGE.put(arg1.checkjstring(), arg2);
            }
        });
        global.set("has", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return valueOf(GLOBAL_STORAGE.containsKey(arg.checkjstring()));
            }
        });
        global.set("remove", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return GLOBAL_STORAGE.remove(arg.checkjstring());
            }
        });
        global.set("accessor", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                String key = arg.checkjstring();
                return new VarArgFunction() {
                    @Override
                    public Varargs invoke(Varargs args) {
                        if (args.narg() == 0) {
                            return GLOBAL_STORAGE.getOrDefault(key, NIL);
                        } else {
                            GLOBAL_STORAGE.put(key, args.arg(1));
                            return NIL;
                        }
                    }
                };
            };
        });
        storageLib.set("global", global);

        LuaTable playerStorage = new LuaTable();
        HashMap<String, LuaValue> playerStorageMap = PLAYER_STORAGE.computeIfAbsent(player.getUniqueId(), k -> new HashMap<>());
        playerStorage.set("get", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return playerStorageMap.getOrDefault(arg.checkjstring(), NIL);
            }
        });
        playerStorage.set("set", new TwoArgFunction() {
            @Override
            public LuaValue call(LuaValue arg1, LuaValue arg2) {
                return playerStorageMap.put(arg1.checkjstring(), arg2);
            }
        });
        playerStorage.set("has", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return valueOf(playerStorageMap.containsKey(arg.checkjstring()));
            }
        });
        playerStorage.set("remove", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                return playerStorageMap.remove(arg.checkjstring());
            }
        });
        playerStorage.set("accessor", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                String key = arg.checkjstring();
                return new VarArgFunction() {
                    @Override
                    public Varargs invoke(Varargs args) {
                        if (args.narg() == 0) {
                            return playerStorageMap.getOrDefault(key, NIL);
                        } else {
                            playerStorageMap.put(key, args.arg(1));
                            return NIL;
                        }
                    }
                };
            };
        });
        storageLib.set("player", playerStorage);

        LuaTable regionStorage = new LuaTable();
        regionStorage.set("get", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                HashMap<String, LuaValue> regionStorageMap = REGION_STORAGE.computeIfAbsent(Region.getRegion(player.getLocation()), k -> new HashMap<>());
                return regionStorageMap.getOrDefault(arg.checkjstring(), NIL);
            }
        });
        regionStorage.set("set", new TwoArgFunction() {
            @Override
            public LuaValue call(LuaValue arg1, LuaValue arg2) {
                HashMap<String, LuaValue> regionStorageMap = REGION_STORAGE.computeIfAbsent(Region.getRegion(player.getLocation()), k -> new HashMap<>());
                return regionStorageMap.put(arg1.checkjstring(), arg2);
            }
        });
        regionStorage.set("has", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                HashMap<String, LuaValue> regionStorageMap = REGION_STORAGE.computeIfAbsent(Region.getRegion(player.getLocation()), k -> new HashMap<>());
                return valueOf(regionStorageMap.containsKey(arg.checkjstring()));
            }
        });
        regionStorage.set("remove", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                HashMap<String, LuaValue> regionStorageMap = REGION_STORAGE.computeIfAbsent(Region.getRegion(player.getLocation()), k -> new HashMap<>());
                return regionStorageMap.remove(arg.checkjstring());
            }
        });
        regionStorage.set("accessor", new OneArgFunction() {
            @Override
            public LuaValue call(LuaValue arg) {
                HashMap<String, LuaValue> regionStorageMap = REGION_STORAGE.computeIfAbsent(Region.getRegion(player.getLocation()), k -> new HashMap<>());
                String key = arg.checkjstring();
                return new VarArgFunction() {
                    @Override
                    public Varargs invoke(Varargs args) {
                        if (args.narg() == 0) {
                            return regionStorageMap.getOrDefault(key, NIL);
                        } else {
                            regionStorageMap.put(key, args.arg(1));
                            return NIL;
                        }
                    }
                };
            };
        });
        storageLib.set("region", regionStorage);

        return storageLib;
    }

    public static void removePlayer(Player player) {
        PLAYER_STORAGE.remove(player);
    }
}
