package emu.grasscutter.data;

import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.data.binout.AbilityModifier.AbilityConfigData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierActionType;
import emu.grasscutter.data.common.PointData;
import emu.grasscutter.data.common.ScenePointConfig;
import emu.grasscutter.game.managers.blossom.BlossomConfig;
import emu.grasscutter.game.quest.QuestEncryptionKey;
import emu.grasscutter.game.world.SpawnDataEntry;
import emu.grasscutter.game.world.SpawnDataEntry.GridBlockId;
import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry;
import emu.grasscutter.scripts.SceneIndexManager;
import emu.grasscutter.utils.JsonUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import lombok.val;

import org.reflections.Reflections;

import java.io.*;
import java.nio.file.Files;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static emu.grasscutter.utils.FileUtils.getDataPath;
import static emu.grasscutter.utils.FileUtils.getResourcePath;
import static emu.grasscutter.utils.Language.translate;

public class ResourceLoader {

    private static final List<String> loadedResources = new ArrayList<>();

    public static List<Class<?>> getResourceDefClasses() {
        Reflections reflections = new Reflections(ResourceLoader.class.getPackage().getName());
        Set<?> classes = reflections.getSubTypesOf(GameResource.class);

        List<Class<?>> classList = new ArrayList<>(classes.size());
        classes.forEach(o -> {
            Class<?> c = (Class<?>) o;
            if (c.getAnnotation(ResourceType.class) != null) {
                classList.add(c);
            }
        });

        classList.sort((a, b) -> b.getAnnotation(ResourceType.class).loadPriority().value() - a.getAnnotation(ResourceType.class).loadPriority().value());

        return classList;
    }

    private static boolean loadedAll = false;
    public static void loadAll() {
        if (loadedAll) return;
        Grasscutter.getLogger().info(translate("messages.status.resources.loading"));

        // Load ability lists
        loadAbilityEmbryos();
        loadOpenConfig();
        loadAbilityModifiers();
        // Load resources
        loadResources(true);
        // Process into depots
        GameDepot.load();
        // Load spawn data and quests
        loadGadgetConfigData();
        loadSpawnData();
        loadQuests();
        loadScriptSceneData();
        // Load scene points - must be done AFTER resources are loaded
        loadScenePoints();
        // Load default home layout
        loadHomeworldDefaultSaveData();
        loadNpcBornData();
        loadBlossomResources();

        Grasscutter.getLogger().info(translate("messages.status.resources.finish"));
        loadedAll = true;
    }

    public static void loadResources() {
        loadResources(false);
    }

    public static void loadResources(boolean doReload) {
        for (Class<?> resourceDefinition : getResourceDefClasses()) {
            ResourceType type = resourceDefinition.getAnnotation(ResourceType.class);

            if (type == null) {
                continue;
            }

            @SuppressWarnings("rawtypes")
            Int2ObjectMap map = GameData.getMapByResourceDef(resourceDefinition);

            if (map == null) {
                continue;
            }

            try {
                loadFromResource(resourceDefinition, type, map, doReload);
            } catch (Exception e) {
                Grasscutter.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e.getLocalizedMessage());
            }
        }
    }

    @SuppressWarnings("rawtypes")
    protected static void loadFromResource(Class<?> c, ResourceType type, Int2ObjectMap map, boolean doReload) throws Exception {
        if (!loadedResources.contains(c.getSimpleName()) || doReload) {
            for (String name : type.name()) {
                loadFromResource(c, name, map);
            }
            loadedResources.add(c.getSimpleName());
            Grasscutter.getLogger().debug("Loaded " + map.size() + " " + c.getSimpleName() + "s.");
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    protected static <T> void loadFromResource(Class<T> c, String fileName, Int2ObjectMap map) throws Exception {
        List<T> list = JsonUtils.loadToList(getResourcePath("ExcelBinOutput/" + fileName), c);

        for (T o : list) {
            GameResource res = (GameResource) o;
            res.onLoad();
            map.put(res.getId(), res);
        }
    }

    private static void loadScenePoints() {
        val pattern = Pattern.compile("scene([0-9]+)_point\\.json");
        try {
            Files.newDirectoryStream(getResourcePath("BinOutput/Scene/Point"), "scene*_point.json").forEach(path -> {
                val matcher = pattern.matcher(path.getFileName().toString());
                if (!matcher.find()) return;
                int sceneId = Integer.parseInt(matcher.group(1));
                ScenePointConfig config;

                try {
                    config = JsonUtils.loadToClass(path, ScenePointConfig.class);
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }

                if (config.points == null) return;

                List<Integer> scenePoints = new ArrayList<>();
                for (Map.Entry<String, JsonElement> entry : config.points.entrySet()) {
                    String key = entry.getKey();
                    String name = sceneId + "_" + key;
                    int id = Integer.parseInt(key);
                    PointData pointData = JsonUtils.decode(entry.getValue(), PointData.class);
                    pointData.setId(id);

                    GameData.getScenePointIdList().add(id);
                    GameData.getScenePointEntries().put(name, new ScenePointEntry(name, pointData));
                    scenePoints.add(id);

                    pointData.updateDailyDungeon();
                }
                GameData.getScenePointsPerScene().put(sceneId, scenePoints);
            });
        } catch (IOException e) {
            Grasscutter.getLogger().error("Scene point files cannot be found, you cannot use teleport waypoints!");
            return;
        }
    }

    private static void loadAbilityEmbryos() {
        List<AbilityEmbryoEntry> embryoList = null;

        // Read from cached file if exists
        try {
            embryoList = JsonUtils.loadToList(getDataPath("AbilityEmbryos.json"), AbilityEmbryoEntry.class);
        } catch (Exception ignored) {}

        if (embryoList == null) {
            // Load from BinOutput
            val pattern = Pattern.compile("ConfigAvatar_(.+?)\\.json");

            val l = new ArrayList<AbilityEmbryoEntry>();
            try {
                Files.newDirectoryStream(getResourcePath("BinOutput/Avatar/"), "ConfigAvatar_*.json").forEach(path -> {
                    val matcher = pattern.matcher(path.getFileName().toString());
                    if (!matcher.find()) return;
                    String avatarName = matcher.group(1);
                    AvatarConfig config;

                    try {
                        config = JsonUtils.loadToClass(path, AvatarConfig.class);
                    } catch (Exception e) {
                        Grasscutter.getLogger().error("Error loading player ability embryos:", e);
                        return;
                    }

                    if (config.abilities == null) return;

                    int s = config.abilities.size();
                    AbilityEmbryoEntry al = new AbilityEmbryoEntry(avatarName, config.abilities.stream().map(Object::toString).toArray(size -> new String[s]));
                    l.add(al);
                });
            } catch (IOException e) {
                Grasscutter.getLogger().error("Error loading ability embryos: no files found");
                return;
            }

            embryoList = l;

            try {
                GameDepot.setPlayerAbilities(JsonUtils.loadToMap(getResourcePath("BinOutput/AbilityGroup/AbilityGroup_Other_PlayerElementAbility.json"), String.class, AvatarConfig.class));
            } catch (IOException e) {
                Grasscutter.getLogger().error("Error loading player abilities:", e);
            }
        }

        if (embryoList == null || embryoList.isEmpty()) {
            Grasscutter.getLogger().error("No embryos loaded!");
            return;
        }

        for (AbilityEmbryoEntry entry : embryoList) {
            GameData.getAbilityEmbryoInfo().put(entry.getName(), entry);
        }
    }

    private static void loadAbilityModifiers() {
        // Load from BinOutput
        try {
            Files.newDirectoryStream(getResourcePath("BinOutput/Ability/Temp/AvatarAbilities/")).forEach(path -> {
                List<AbilityConfigData> abilityConfigList;

                try {
                    abilityConfigList = JsonUtils.loadToList(path, AbilityConfigData.class);
                } catch (IOException e) {
                    Grasscutter.getLogger().error("Error loading ability modifiers from path " + path.toString() + ": ", e);
                    return;
                }

                abilityConfigList.forEach(data -> {
                    if (data.Default.modifiers == null || data.Default.modifiers.size() == 0) {
                        return;
                    }

                    String name = data.Default.abilityName;
                    AbilityModifierEntry modifierEntry = new AbilityModifierEntry(name);
                    data.Default.modifiers.forEach((key, modifier) -> {
                        Stream.ofNullable(modifier.onAdded)
                            .flatMap(Stream::of)
                            .filter(action -> action.$type.contains("HealHP"))
                            .forEach(action -> {
                                action.type = AbilityModifierActionType.HealHP;
                                modifierEntry.getOnAdded().add(action);
                            });
                        Stream.ofNullable(modifier.onThinkInterval)
                            .flatMap(Stream::of)
                            .filter(action -> action.$type.contains("HealHP"))
                            .forEach(action -> {
                                action.type = AbilityModifierActionType.HealHP;
                                modifierEntry.getOnThinkInterval().add(action);
                            });
                        Stream.ofNullable(modifier.onRemoved)
                            .flatMap(Stream::of)
                            .filter(action -> action.$type.contains("HealHP"))
                            .forEach(action -> {
                                action.type = AbilityModifierActionType.HealHP;
                                modifierEntry.getOnRemoved().add(action);
                            });
                    });

                    GameData.getAbilityModifiers().put(name, modifierEntry);
                });
            });
        } catch (IOException e) {
            Grasscutter.getLogger().error("Error loading ability modifiers: ", e);
            return;
        }
    }

    private static void loadSpawnData() {
        String[] spawnDataNames = {"Spawns.json", "GadgetSpawns.json"};
        ArrayList<SpawnGroupEntry> spawnEntryMap = new ArrayList<>();

        for (String name : spawnDataNames) {
            // Load spawn entries from file
            try (InputStreamReader reader = DataLoader.loadReader(name)) {
                // Add spawns to group if it already exists in our spawn group map
                spawnEntryMap.addAll(JsonUtils.loadToList(reader, SpawnGroupEntry.class));
            } catch (Exception ignored) {}
        }

        if (spawnEntryMap.isEmpty()) {
            Grasscutter.getLogger().error("No spawn data loaded!");
            return;
        }

        HashMap<GridBlockId, ArrayList<SpawnDataEntry>> areaSort = new HashMap<>();
        //key = sceneId,x,z , value = ArrayList<SpawnDataEntry>
        for (SpawnGroupEntry entry : spawnEntryMap) {
            entry.getSpawns().forEach(
                s -> {
                    s.setGroup(entry);
                    GridBlockId point = s.getBlockId();
                    if (!areaSort.containsKey(point)) {
                        areaSort.put(point, new ArrayList<>());
                    }
                    areaSort.get(point).add(s);
                }
            );
        }
        GameDepot.addSpawnListById(areaSort);
    }

    private static void loadOpenConfig() {
        // Read from cached file if exists
        List<OpenConfigEntry> list = null;

        try {
            list = JsonUtils.loadToList(getDataPath("OpenConfig.json"), OpenConfigEntry.class);
        } catch (Exception ignored) {}

        if (list == null) {
            Map<String, OpenConfigEntry> map = new TreeMap<>();
            String[] folderNames = {"BinOutput/Talent/EquipTalents/", "BinOutput/Talent/AvatarTalents/"};

            for (String folderName : folderNames) {
                try {
                    Files.newDirectoryStream(getResourcePath(folderName), "*.json").forEach(path -> {
                        try {
                            JsonUtils.loadToMap(path, String.class, OpenConfigData[].class)
                                .forEach((name, data) -> map.put(name, new OpenConfigEntry(name, data)));
                        } catch (Exception e) {
                            e.printStackTrace();
                            return;
                        }
                    });
                } catch (IOException e) {
                    Grasscutter.getLogger().error("Error loading open config: no files found in " + folderName);
                    return;
                }
            }

            list = new ArrayList<>(map.values());
        }

        if (list == null || list.isEmpty()) {
            Grasscutter.getLogger().error("No openconfig entries loaded!");
            return;
        }

        for (OpenConfigEntry entry : list) {
            GameData.getOpenConfigEntries().put(entry.getName(), entry);
        }
    }

    private static void loadQuests() {
        try {
            Files.list(getResourcePath("BinOutput/Quest/")).forEach(path -> {
                try {
                    val mainQuest = JsonUtils.loadToClass(path, MainQuestData.class);
                    GameData.getMainQuestDataMap().put(mainQuest.getId(), mainQuest);
                } catch (IOException e) {

                }
            });
        } catch (IOException e) {
            Grasscutter.getLogger().error("Quest data missing");
            return;
        }

        try {
            val questEncryptionMap = GameData.getMainQuestEncryptionMap();
            String path = "QuestEncryptionKeys.json";
            try {
                JsonUtils.loadToList(getResourcePath(path), QuestEncryptionKey.class).forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
            } catch (IOException | NullPointerException ignored) {}
            try {
                DataLoader.loadList(path, QuestEncryptionKey.class).forEach(key -> questEncryptionMap.put(key.getMainQuestId(), key));
            } catch (IOException | NullPointerException ignored) {}
            Grasscutter.getLogger().debug("Loaded {} quest keys.", questEncryptionMap.size());
        } catch (Exception e) {
            Grasscutter.getLogger().error("Unable to load quest keys.", e);
        }

        Grasscutter.getLogger().debug("Loaded " + GameData.getMainQuestDataMap().size() + " MainQuestDatas.");
    }

    public static void loadScriptSceneData() {
        try {
            Files.list(getResourcePath("ScriptSceneData/")).forEach(path -> {
                try {
                    GameData.getScriptSceneDataMap().put(path.getFileName().toString(), JsonUtils.loadToClass(path, ScriptSceneData.class));
                } catch (IOException e) {
                    e.printStackTrace();
                    return;
                }
            });
            Grasscutter.getLogger().debug("Loaded " + GameData.getScriptSceneDataMap().size() + " ScriptSceneDatas.");
        } catch (IOException e) {
            Grasscutter.getLogger().debug("ScriptSceneData folder missing or empty.");
            return;
        }
    }

    private static void loadHomeworldDefaultSaveData() {
        val pattern = Pattern.compile("scene([0-9]+)_home_config\\.json");
        try {
            Files.newDirectoryStream(getResourcePath("BinOutput/HomeworldDefaultSave"), "scene*_home_config.json").forEach(path -> {
                val matcher = pattern.matcher(path.getFileName().toString());
                if (!matcher.find()) return;

                try {
                    val sceneId = Integer.parseInt(matcher.group(1));
                    val data = JsonUtils.loadToClass(path, HomeworldDefaultSaveData.class);
                    GameData.getHomeworldDefaultSaveData().put(sceneId, data);
                } catch (Exception ignored) {}
            });
            Grasscutter.getLogger().debug("Loaded " + GameData.getHomeworldDefaultSaveData().size() + " HomeworldDefaultSaveDatas.");
        } catch (IOException e) {
            Grasscutter.getLogger().error("Failed to load HomeworldDefaultSave folder.");
        }
    }

    private static void loadNpcBornData() {
        try {
            Files.newDirectoryStream(getResourcePath("BinOutput/Scene/SceneNpcBorn/"), "*.json").forEach(path -> {
                try {
                    val data = JsonUtils.loadToClass(path, SceneNpcBornData.class);
                    if (data.getBornPosList() == null || data.getBornPosList().size() == 0) {
                        return;
                    }

                    data.setIndex(SceneIndexManager.buildIndex(3, data.getBornPosList(), item -> item.getPos().toPoint()));
                    GameData.getSceneNpcBornData().put(data.getSceneId(), data);
                } catch (IOException ignored) {}
            });
            Grasscutter.getLogger().debug("Loaded " + GameData.getSceneNpcBornData().size() + " SceneNpcBornDatas.");
        } catch (IOException e) {
            Grasscutter.getLogger().error("Failed to load SceneNpcBorn folder.");
        }
    }

    private static void loadGadgetConfigData() {
        try {
            Files.newDirectoryStream(getResourcePath("BinOutput/Gadget/"), "*.json").forEach(path -> {
                try {
                    GameData.getGadgetConfigData().putAll(JsonUtils.loadToMap(path, String.class, ConfigGadget.class));
                } catch (Exception e) {
                    Grasscutter.getLogger().error("failed to load ConfigGadget entries for " + path.toString(), e);
                    return;
                }
            });

            Grasscutter.getLogger().debug("Loaded {} ConfigGadget entries.", GameData.getGadgetConfigData().size());
        } catch (IOException e) {
            Grasscutter.getLogger().error("Failed to load ConfigGadget folder.");
        }
    }

    private static void loadBlossomResources() {
        try {
            GameDepot.setBlossomConfig(DataLoader.loadClass("BlossomConfig.json", BlossomConfig.class));
            Grasscutter.getLogger().debug("Loaded BlossomConfig.");
        } catch (IOException e) {
            Grasscutter.getLogger().warn("Failed to load BlossomConfig.");
        }
    }

    // BinOutput configs

    public static class AvatarConfig {
        @SerializedName(value="abilities", alternate={"targetAbilities"})
        public ArrayList<AvatarConfigAbility> abilities;
    }

    public static class AvatarConfigAbility {
        public String abilityName;
        public String toString() {
            return abilityName;
        }
    }

    private static class OpenConfig {
        public OpenConfigData[] data;
    }

    public static class OpenConfigData {
        public String $type;
        public String abilityName;

        @SerializedName(value="talentIndex", alternate={"OJOFFKLNAHN"})
        public int talentIndex;

        @SerializedName(value="skillID", alternate={"overtime"})
        public int skillID;

        @SerializedName(value="pointDelta", alternate={"IGEBKIHPOIF"})
        public int pointDelta;
    }
}