package emu.lunarcore.data;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

import emu.lunarcore.data.config.*;

import org.reflections.Reflections;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;

import emu.lunarcore.LunarCore;
import emu.lunarcore.data.ResourceDeserializers.LunarCoreDoubleDeserializer;
import emu.lunarcore.data.ResourceDeserializers.LunarCoreHashDeserializer;
import emu.lunarcore.data.config.FloorInfo.FloorGroupSimpleInfo;
import emu.lunarcore.data.resource.ResourceBase;
import emu.lunarcore.data.resource.ResourceMap;
import emu.lunarcore.data.resource.ResourceType;
import emu.lunarcore.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import lombok.Getter;

public class ResourceLoader {
    private static boolean loaded = false;

    // Special gson factory we create for loading resources
    @Getter private static final Gson gson = new GsonBuilder()
            .registerTypeAdapter(double.class, new LunarCoreDoubleDeserializer())
            .registerTypeAdapter(long.class, new LunarCoreHashDeserializer())
            .create();

    // Load all resources
    public static void loadAll() {
        // Make sure we don't load more than once
        if (loaded) return;

        // Create data folder if it doesnt exist when starting the server
        checkDataFolder();
        // Start loading resources
        loadResources();
        // Load floor infos after resources
        loadFloorInfos();
        // Load maze abilities
        loadMazeAbilities();
        
        // Load mission infos
        loadMissionInfo();
        
        // Done
        loaded = true;
        LunarCore.getLogger().info("Resource loading complete");
    }
    
    private static void checkDataFolder() {
        File dir = new File(LunarCore.getConfig().getDataDir());
        if (!dir.exists()) {
            dir.mkdir();
        }
    }
    
    @SuppressWarnings("unchecked")
    private static <T> ResourceMap<T> getMapForExcel(Class<T> dataClass, Class<?> resourceDefinition) {
        ResourceMap<T> table = null;
        Field field = null;
        
        try {
            field = dataClass.getDeclaredField(Utils.lowerCaseFirstChar(resourceDefinition.getSimpleName()) + "Map");
            if (field == null) {
                field = dataClass.getDeclaredField(resourceDefinition.getSimpleName() + "Map");
            }
            
            field.setAccessible(true);
            table = (ResourceMap<T>) field.get(null);
            field.setAccessible(false);
        } catch (Exception e) {

        }

        return table;
    }

    private static List<Class<?>> getResourceDefClasses() {
        Reflections reflections = new Reflections(LunarCore.class.getPackage().getName());
        Set<?> classes = reflections.getSubTypesOf(ResourceBase.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;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    public static void loadResources() {
        for (Class<?> resourceDefinition : getResourceDefClasses()) {
            // Get annotation
            ResourceType type = resourceDefinition.getAnnotation(ResourceType.class);
            if (type == null) continue;

            // Get map for this excel
            ResourceMap map = ResourceLoader.getMapForExcel(type.gameDataClass(), resourceDefinition);

            try {
                int count = 0;
                for (String name : type.name()) {
                    count += loadFromResource(resourceDefinition, type, name, map);
                }
                
                LunarCore.getLogger().info("Loaded " + count + " " + resourceDefinition.getSimpleName() + "s.");
            } catch (FileNotFoundException e) {
                LunarCore.getLogger().error("Resource file not found: {}.", Arrays.toString(type.name()));
            } catch (Exception e) {
                LunarCore.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e);
            }
        }
    }

    private static <T> int loadFromResource(Class<T> c, ResourceType type, String fileName, ResourceMap<T> map) throws Exception {
        // Get file name
        String file = LunarCore.getConfig().getResourceDir() + "/ExcelOutput/" + fileName;

        // Load reader from file
        try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
            // Setup variables
            Stream<T> stream = null;

            // Determine format of json
            JsonElement json = JsonParser.parseReader(fileReader);

            if (json.isJsonArray()) {
                // Parse list
                List<T> excels = gson.fromJson(json, TypeToken.getParameterized(List.class, c).getType());
                stream = excels.stream();
            } else if (json.isJsonObject()) {
                // Check if object is map or a nested map
                boolean isMap = true;

                var it = json.getAsJsonObject().asMap().entrySet().iterator();
                if (it.hasNext()) {
                    var it2 = it.next().getValue().getAsJsonObject().asMap().entrySet().iterator();
                    String key = it2.next().getKey();
                    try {
                        Integer.parseInt(key);
                        isMap = false;
                    } catch (Exception ex) {

                    }
                }

                // Parse json
                if (isMap) {
                    // Map
                    Map<Integer, T> excels = gson.fromJson(json, TypeToken.getParameterized(Map.class, Integer.class, c).getType());
                    stream = excels.values().stream();
                } else {
                    // Nested Map
                    Map<String, Map<Integer, T>> excels = gson.fromJson(json, TypeToken.getParameterized(Map.class, String.class, TypeToken.getParameterized(Map.class, Integer.class, c).getType()).getType());
                    stream = excels.values().stream().flatMap(m -> m.values().stream());
                }
            } else {
                throw new Exception("Invalid excel file: " + fileName);
            }

            // Sanity check
            if (stream == null) return 0;
            
            // Mutable integer
            AtomicInteger count = new AtomicInteger();
            
            // Put excels into 
            stream.forEach(obj -> {
                try {
                    // Cast object to excel
                    ResourceBase excel = (ResourceBase) obj;
                    
                    // Call onLoad event
                    excel.onLoad();

                    // Add to excel map
                    if (map != null) {
                        map.add(obj);
                    }
                    
                    // Increment counter
                    count.incrementAndGet();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            
            // Return the amount of excels we loaded
            return count.get();
        }
    }

 // Might be better to cache
    private static void loadFloorInfos() {
        // Load floor infos
        LunarCore.getLogger().info("Loading floor infos... this may take a while.");
        long timestamp = System.currentTimeMillis();
        
        // Get floor directory
        File floorDir = new File(LunarCore.getConfig().getResourceDir() + "/Config/LevelOutput/RuntimeFloor/");
        boolean missingGroupInfos = false;

        if (!floorDir.exists()) {
            LunarCore.getLogger().warn("Floor infos are missing, please check your resources folder: {resources}/Config/LevelOutput/RuntimeFloor. Teleports and natural world spawns may not work!");
            return;
        }
        
        // Virtual thread list
        List<Thread> threads = new ArrayList<>();

        // Load floor infos
        for (var excel : GameData.getMapEntranceExcelMap().values()) {
            Runnable task = () -> {
                String name = "P" + excel.getPlaneID() + "_F" + excel.getFloorID();
                File file = new File(LunarCore.getConfig().getResourceDir() + "/Config/LevelOutput/RuntimeFloor/" + name + ".json");
                
                if (!file.exists()) {
                    LunarCore.getLogger().warn("Missing floor info: " + name);
                    return;
                }
                
                try (FileReader reader = new FileReader(file)) {
                    FloorInfo floor = gson.fromJson(reader, FloorInfo.class);
                    
                    synchronized (GameData.getFloorInfos()) {
                        GameData.getFloorInfos().put(name, floor);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            };
            
            // Start virtual thread
            threads.add(Thread.ofVirtual().start(task));
        }
        
        // Wait for floors to finish loading
        for (var vt : threads) {
            try {
                vt.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        
        threads.clear();

        // Load floor + group infos
        for (FloorInfo floor : GameData.getFloorInfos().values()) {
            // Get loadable groups
            var loadableGroupList = new IntArrayList();
            
            var dimension = floor.getMainDimension();
            if (dimension != null) {
                loadableGroupList = dimension.getGroupIndexList();
            }

            // Load groups
            for (int index : loadableGroupList) {
                // Get group
                FloorGroupSimpleInfo simpleGroup = floor.getSimpleGroupList().get(index);
                
                // Dont load "deprecated" groups
                if (simpleGroup.isIsDelete()) {
                    continue;
                }
                
                // Get group file from resource directory
                Runnable task = () -> {
                    File file = Paths.get(LunarCore.getConfig().getResourceDir(), simpleGroup.getGroupPath()).toFile();
                    
                    if (!file.exists()) {
                        // temp fix
                        file = Paths.get(LunarCore.getConfig().getResourceDir(), simpleGroup.getGroupPath().replace("/RuntimeGroup", "/SharedRuntimeGroup")).toFile();
                        if (!file.exists()) {
                            LunarCore.getLogger().warn("Group not found: " + simpleGroup.getGroupPath());
                            return;
                        }
                    }
        
                    try (FileReader reader = new FileReader(file)) {
                        // Parse group info
                        GroupInfo group = gson.fromJson(reader, GroupInfo.class);
                        group.setId(simpleGroup.getID());
                        
                        // Load groups into the floor info
                        synchronized (floor.getGroups()) {
                            floor.getGroups().put(simpleGroup.getID(), group);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                };
                
                // Start virtual thread
                threads.add(Thread.ofVirtual().start(task));
            }
        }
        
        // Wait for groups to finish loading
        for (var vt : threads) {
            try {
                vt.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        
        // Finish loading floors
        for (FloorInfo floor : GameData.getFloorInfos().values()) {
            // Check if we are missing group infos TODO broken
            if (floor.getGroups().size() == 0) {
                LunarCore.getLogger().warn("Floor " + floor.getFloorID() + " is missing group infos.");
                missingGroupInfos = true;
            }
            
            // Post load callback to cache floor info
            floor.onLoad();
        }
        
        // Notify the server owner if we are missing any files
        if (missingGroupInfos) {
            LunarCore.getLogger().warn("Group infos are missing, please check your resources folder: {resources}/Config/LevelOutput/SharedRuntimeGroup. Teleports, monster battles, and natural world spawns may not work!");
        }
        
        // Done
        LunarCore.getLogger().info("Loaded " + GameData.getFloorInfos().size() + " floor infos in " + (System.currentTimeMillis() - timestamp) + " ms.");
    }

    // Might be better to cache
    private static void loadMazeAbilities() {
        // Loaded configs count
        int count = 0;
        
        // Load summon unit configs
        for (var summonUnitExcel : GameData.getSummonUnitExcelMap().values()) {
            if (summonUnitExcel.isIsClient()) {
                count++;
                continue;
            }
            
            // Get file
            File file = new File(LunarCore.getConfig().getResourceDir() + "/" + summonUnitExcel.getJsonPath());
            if (!file.exists()) continue;
            
            try (FileReader reader = new FileReader(file)) {
                //LunarCore.getLogger().info("Loading summon unit config: " + file.getName());
                SummonUnitInfo info = gson.fromJson(reader, SummonUnitInfo.class);
                info.buildMazeSkillActions();
                
                summonUnitExcel.setInfo(info);
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        // Notify the server owner if we are missing any files
        /*
        if (count < GameData.getSummonUnitExcelMap().size()) {
            LunarCore.getLogger().warn("Summon unit configs are missing, please check your resources folder: {resources}/Config/ConfigSummonUnit. Character summon techniques may not work!");
        }
        */
        
        // Reset loaded count
        count = 0;

        // Load maze abilities
        for (var avatarExcel : GameData.getAvatarExcelMap().values()) {
            // Get file
            String name = avatarExcel.getNameKey();
            File file = new File(LunarCore.getConfig().getResourceDir() + "/Config/ConfigAdventureAbility/LocalPlayer/LocalPlayer_" + name + "_Ability.json");
            if (!file.exists()) {
                LunarCore.getLogger().warn("Maze ability is missing for avatar: " + name);
                continue;
            }

            try (FileReader reader = new FileReader(file)) {
                SkillAbilityInfo avatarSkills = gson.fromJson(reader, SkillAbilityInfo.class);

                if (avatarSkills.parse(avatarExcel)) {
                    count++;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        // Notify the server owner if we are missing any files
        if (count < GameData.getAvatarExcelMap().size()) {
            LunarCore.getLogger().warn("Maze abilities are missing, please check your resources folder: {resources}/Config/ConfigAdventureAbility/LocalPlayer. Character techniques may not work!");
        }

        // Done
        LunarCore.getLogger().info("Loaded " + count + " maze abilities for avatars.");
    }
    
    private static void loadMissionInfo() {
        int count = 0;
        int countTask = 0;
        for (Integer mMissionId : GameData.getMainMissionIds()) {
            File file = new File(LunarCore.getConfig().getResourceDir() + "/Config/Level/Mission/" + mMissionId + "/" + "MissionInfo_" + mMissionId + ".json");
            if (!file.exists()) continue;
            try (FileReader reader = new FileReader(file)) {
                MissionInfo info = gson.fromJson(reader, MissionInfo.class);
                for (MissionInfo.SubMissionInfo setme : info.getSubMissionList()) {
                    File taskFile = new File(LunarCore.getConfig().getResourceDir() + "/Config/Level/Mission/" + setme.getMainMissionID() + "/" + "Mission_" + setme.getId() + ".json");
                    if (!taskFile.exists()) continue;
                    try (FileReader readerTask = new FileReader(taskFile)) {
                        MissionTaskInfo infoTask = gson.fromJson(readerTask, MissionTaskInfo.class);
                        setme.setTask(infoTask);
                        countTask++;
                    } catch (Exception ex) {
                        LunarCore.getLogger().warn("Failed to load task mission info: " + taskFile.getName(), ex);
                    }
                }
                // Update GameData with the modified MissionInfo object
                GameData.getMissionInfos().put(info.getMainMissionID(), info);
                count++;
            } catch (Exception e) {
                LunarCore.getLogger().warn("Failed to load mission info: " + file.getName(), e);
            }
        }
        LunarCore.getLogger().info("Loaded " + count + " mission infos (task " + countTask + ").");    
    }
}
