package de.peeeq.wurstio.intermediateLang.interpreter;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.peeeq.wurstscript.WLogger;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * Loads WurstStdlib2 object-editing wrapper class mappings from the bundled
 * {@code stdlib-obj-mappings.json} resource (generated by {@code generate-obj-mappings.ts}).
 *
 * <p>Maps base object IDs to stdlib wrapper class names and their named field accessor
 * methods, enabling the compiler to emit human-readable wrapper code instead of raw
 * {@code createObjectDefinition()} calls when exporting object data.
 *
 * <p>Run {@code deno run --allow-read --allow-write generate-obj-mappings.ts} from the
 * project root to regenerate the JSON after stdlib changes.
 */
public final class StdlibObjectMappings {

    private StdlibObjectMappings() {}

    /**
     * Info about a wrapper method that sets a specific object field.
     *
     * @param methodName       the Wurst method name, e.g. {@code "setCooldown"}
     * @param hasLevel         true if the method signature is {@code setXxx(int level, T value)}
     * @param isBoolField      true if the Wurst method accepts a {@code bool} (stored as int 0/1)
     * @param storageValueType the underlying {@code def.set*} value type, e.g. {@code "String"}
     * @param parameterType    the public setter value parameter type, e.g. {@code "ArmorType"}
     */
    public record FieldMethodInfo(String methodName, boolean hasLevel, boolean isBoolField,
                                  String storageValueType, String parameterType) {}

    /**
     * Maps base ability ID (4-char, e.g. {@code "Aslo"}) to the stdlib wrapper class name
     * (e.g. {@code "AbilityDefinitionSlow"}).
     */
    public static final Map<String, String> ABILITY_CLASS_BY_BASE_ID;

    /**
     * Maps base ability ID → field key ({@code "fieldId:dataPtr"}) → {@link FieldMethodInfo}.
     * {@code dataPtr} is {@code -1} for non-level fields.
     */
    public static final Map<String, Map<String, FieldMethodInfo>> ABILITY_FIELD_METHODS;

    /** Field methods for {@code UnitDefinition} (w3u, non-building non-hero). */
    public static final Map<String, FieldMethodInfo> UNIT_FIELD_METHODS;

    /** Field methods for {@code BuildingDefinition} (w3u buildings). */
    public static final Map<String, FieldMethodInfo> BUILDING_FIELD_METHODS;

    /** Field methods for {@code HeroDefinition} (w3u heroes). */
    public static final Map<String, FieldMethodInfo> HERO_FIELD_METHODS;

    /** Base IDs of stock WC3 buildings (from unitbalance.slk isbldg column). */
    public static final Set<String> BUILDING_BASE_IDS;

    /** Base IDs of stock WC3 heroes (from unitbalance.slk Primary column). */
    public static final Set<String> HERO_BASE_IDS;

    /** Field methods for {@code BuffDefinition} (w3h). */
    public static final Map<String, FieldMethodInfo> BUFF_FIELD_METHODS;

    /** Field methods for {@code ItemDefinition} (w3t). */
    public static final Map<String, FieldMethodInfo> ITEM_FIELD_METHODS;

    /** Field methods for {@code DestructableDefinition} (w3b). */
    public static final Map<String, FieldMethodInfo> DESTRUCTABLE_FIELD_METHODS;

    /** Field methods for {@code DoodadDefinition} (w3d). */
    public static final Map<String, FieldMethodInfo> DOODAD_FIELD_METHODS;

    // -------------------------------------------------------------------------
    // Loading
    // -------------------------------------------------------------------------

    static {
        Map<String, String> abilityClasses = Collections.emptyMap();
        Map<String, Map<String, FieldMethodInfo>> abilityMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> unitMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> buildingMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> heroMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> buffMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> itemMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> destructableMethods = Collections.emptyMap();
        Map<String, FieldMethodInfo> doodadMethods = Collections.emptyMap();
        Set<String> buildingBaseIds = Collections.emptySet();
        Set<String> heroBaseIds = Collections.emptySet();

        try (InputStream is = StdlibObjectMappings.class
                .getClassLoader()
                .getResourceAsStream("stdlib-obj-mappings.json")) {

            if (is == null) {
                WLogger.warning("stdlib-obj-mappings.json not found on classpath; "
                    + "object editing output will use raw format only.");
            } else {
                JsonObject root = JsonParser.parseReader(
                    new InputStreamReader(is, StandardCharsets.UTF_8)).getAsJsonObject();

                abilityClasses = parseStringMap(root.getAsJsonObject("abilityClassByBaseId"));

                // classParents and classOwnFields are used to resolve inheritance at load time
                Map<String, String> classParents =
                    parseStringMap(root.getAsJsonObject("classParents"));
                Map<String, Map<String, FieldMethodInfo>> classOwnFields =
                    parseClassOwnFields(root.getAsJsonObject("classOwnFields"));

                abilityMethods = resolveAbilityMethods(abilityClasses, classParents, classOwnFields);
                unitMethods = parseFlatFieldMethods(root.getAsJsonObject("unitFieldMethods"));
                buildingMethods = parseFlatFieldMethods(root.getAsJsonObject("buildingFieldMethods"));
                heroMethods = parseFlatFieldMethods(root.getAsJsonObject("heroFieldMethods"));
                buffMethods = parseFlatFieldMethods(root.getAsJsonObject("buffFieldMethods"));
                itemMethods = parseFlatFieldMethods(root.getAsJsonObject("itemFieldMethods"));
                destructableMethods = parseFlatFieldMethods(root.getAsJsonObject("destructableFieldMethods"));
                doodadMethods = parseFlatFieldMethods(root.getAsJsonObject("doodadFieldMethods"));
                buildingBaseIds = parseStringSet(root.getAsJsonArray("buildingBaseIds"));
                heroBaseIds = parseStringSet(root.getAsJsonArray("heroBaseIds"));
            }
        } catch (Exception e) {
            WLogger.warning("Failed to load stdlib-obj-mappings.json: " + e.getMessage());
        }

        ABILITY_CLASS_BY_BASE_ID = Collections.unmodifiableMap(abilityClasses);
        ABILITY_FIELD_METHODS = Collections.unmodifiableMap(abilityMethods);
        UNIT_FIELD_METHODS = Collections.unmodifiableMap(unitMethods);
        BUILDING_FIELD_METHODS = Collections.unmodifiableMap(buildingMethods);
        HERO_FIELD_METHODS = Collections.unmodifiableMap(heroMethods);
        BUFF_FIELD_METHODS = Collections.unmodifiableMap(buffMethods);
        ITEM_FIELD_METHODS = Collections.unmodifiableMap(itemMethods);
        DESTRUCTABLE_FIELD_METHODS = Collections.unmodifiableMap(destructableMethods);
        DOODAD_FIELD_METHODS = Collections.unmodifiableMap(doodadMethods);
        BUILDING_BASE_IDS = Collections.unmodifiableSet(buildingBaseIds);
        HERO_BASE_IDS = Collections.unmodifiableSet(heroBaseIds);
    }

    // -------------------------------------------------------------------------
    // JSON parsing helpers
    // -------------------------------------------------------------------------

    private static Map<String, String> parseStringMap(JsonObject obj) {
        if (obj == null) return Collections.emptyMap();
        Map<String, String> result = new LinkedHashMap<>(obj.size() * 2);
        for (Map.Entry<String, JsonElement> e : obj.entrySet()) {
            result.put(e.getKey(), e.getValue().getAsString());
        }
        return result;
    }

    private static Set<String> parseStringSet(JsonArray arr) {
        if (arr == null) return Collections.emptySet();
        Set<String> result = new HashSet<>(arr.size() * 2);
        for (JsonElement e : arr) {
            result.add(e.getAsString());
        }
        return result;
    }

    /** Parses {@code classOwnFields}: className -> { fieldKey -> FieldMethodInfo } */
    private static Map<String, Map<String, FieldMethodInfo>> parseClassOwnFields(JsonObject obj) {
        if (obj == null) return Collections.emptyMap();
        Map<String, Map<String, FieldMethodInfo>> result = new HashMap<>(obj.size() * 2);
        for (Map.Entry<String, JsonElement> e : obj.entrySet()) {
            result.put(e.getKey(), parseFlatFieldMethods(e.getValue().getAsJsonObject()));
        }
        return result;
    }

    /**
     * Parses a flat field-methods object:
     * {@code { "fieldKey": ["methodName", hasLevel, isBool], ... }}
     */
    private static Map<String, FieldMethodInfo> parseFlatFieldMethods(JsonObject obj) {
        if (obj == null) return Collections.emptyMap();
        Map<String, FieldMethodInfo> result = new LinkedHashMap<>(obj.size() * 2);
        for (Map.Entry<String, JsonElement> e : obj.entrySet()) {
            var arr = e.getValue().getAsJsonArray();
            String storageValueType = arr.size() > 3 ? arr.get(3).getAsString() : "";
            String parameterType = arr.size() > 4 ? arr.get(4).getAsString() : "";
            result.put(e.getKey(), new FieldMethodInfo(
                arr.get(0).getAsString(),
                arr.get(1).getAsBoolean(),
                arr.get(2).getAsBoolean(),
                storageValueType,
                parameterType
            ));
        }
        return result;
    }

    // -------------------------------------------------------------------------
    // Inheritance resolution
    // -------------------------------------------------------------------------

    /**
     * For each ability base ID, builds the complete field method map by walking the
     * class hierarchy from the mapped wrapper class up to the root, merging own fields.
     * Results are cached so each class is resolved at most once.
     */
    private static Map<String, Map<String, FieldMethodInfo>> resolveAbilityMethods(
            Map<String, String> abilityClassByBaseId,
            Map<String, String> classParents,
            Map<String, Map<String, FieldMethodInfo>> classOwnFields) {

        Map<String, Map<String, FieldMethodInfo>> cache = new HashMap<>();
        Map<String, Map<String, FieldMethodInfo>> result =
            new HashMap<>(abilityClassByBaseId.size() * 2);

        for (Map.Entry<String, String> e : abilityClassByBaseId.entrySet()) {
            result.put(e.getKey(),
                resolveForClass(e.getValue(), classParents, classOwnFields, cache));
        }
        return result;
    }

    private static Map<String, FieldMethodInfo> resolveForClass(
            String className,
            Map<String, String> classParents,
            Map<String, Map<String, FieldMethodInfo>> classOwnFields,
            Map<String, Map<String, FieldMethodInfo>> cache) {

        if (cache.containsKey(className)) return cache.get(className);

        Map<String, FieldMethodInfo> result = new LinkedHashMap<>();
        // Own fields take priority over parent fields
        result.putAll(classOwnFields.getOrDefault(className, Collections.emptyMap()));

        String parent = classParents.get(className);
        if (parent != null) {
            resolveForClass(parent, classParents, classOwnFields, cache)
                .forEach(result::putIfAbsent);
        }

        Map<String, FieldMethodInfo> unmodifiable = Collections.unmodifiableMap(result);
        cache.put(className, unmodifiable);
        return unmodifiable;
    }
}
