package com.spaceshipproject;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Container;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;

import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Capture / restauration / nettoyage / generation de schematiques compactes (palette + RLE)
 * stockees en JSON pour le plugin SpaceShipProject.
 *
 * Format JSON :
 * {
 *   "size":[sx,sy,sz],
 *   "spawn":[ox,oy,oz],
 *   "palette":["minecraft:air","minecraft:smooth_quartz", ...],
 *   "blocks":[[paletteIndex, runLength], ...],
 *   "containers":[
 *      {"x":1,"y":2,"z":3,"items":[{"slot":0,"data":"base64..."}, ...]}
 *   ]
 * }
 */
public final class SchematicHelper {

    private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
    private static final String AIR = "minecraft:air";

    private SchematicHelper() {}

    /* ============================ CAPTURE ============================ */

    /**
     * Capture la region [origin, origin+size) du monde dans une representation JSON compacte.
     * Conserve le spawn relatif (pour ne pas le perdre lors d'un re-load).
     */
    public static String captureRegion(World world,
                                       int originX, int originY, int originZ,
                                       int sizeX, int sizeY, int sizeZ,
                                       int spawnX, int spawnY, int spawnZ) {
        Map<String, Integer> paletteIndex = new LinkedHashMap<>();
        List<String> palette = new ArrayList<>();
        List<int[]> blocks = new ArrayList<>();
        JsonArray containers = new JsonArray();

        int currentIdx = -1;
        int run = 0;

        for (int y = 0; y < sizeY; y++) {
            for (int z = 0; z < sizeZ; z++) {
                for (int x = 0; x < sizeX; x++) {
                    Block b = world.getBlockAt(originX + x, originY + y, originZ + z);
                    String state = b.getBlockData().getAsString();
                    Integer idx = paletteIndex.get(state);
                    if (idx == null) {
                        idx = palette.size();
                        palette.add(state);
                        paletteIndex.put(state, idx);
                    }
                    if (idx == currentIdx) {
                        run++;
                    } else {
                        if (currentIdx != -1) blocks.add(new int[]{currentIdx, run});
                        currentIdx = idx;
                        run = 1;
                    }

                    // Capture du contenu des conteneurs (coffres, barils, fournaises, etc.)
                    BlockState bs = b.getState(false);
                    if (bs instanceof Container container) {
                        JsonObject entry = serializeContainer(container, x, y, z);
                        if (entry != null) containers.add(entry);
                    }
                }
            }
        }
        if (run > 0) blocks.add(new int[]{currentIdx, run});

        JsonObject root = new JsonObject();
        JsonArray sizeArr = new JsonArray();
        sizeArr.add(sizeX); sizeArr.add(sizeY); sizeArr.add(sizeZ);
        root.add("size", sizeArr);

        JsonArray spawnArr = new JsonArray();
        spawnArr.add(spawnX); spawnArr.add(spawnY); spawnArr.add(spawnZ);
        root.add("spawn", spawnArr);

        JsonArray paletteArr = new JsonArray();
        for (String p : palette) paletteArr.add(p);
        root.add("palette", paletteArr);

        JsonArray blocksArr = new JsonArray();
        for (int[] run2 : blocks) {
            JsonArray ja = new JsonArray();
            ja.add(run2[0]);
            ja.add(run2[1]);
            blocksArr.add(ja);
        }
        root.add("blocks", blocksArr);

        if (containers.size() > 0) root.add("containers", containers);

        return GSON.toJson(root);
    }

    private static JsonObject serializeContainer(Container container, int x, int y, int z) {
        Inventory inv = container.getSnapshotInventory();
        if (inv == null) return null;
        JsonArray items = new JsonArray();
        for (int slot = 0; slot < inv.getSize(); slot++) {
            ItemStack stack = inv.getItem(slot);
            if (stack == null || stack.getType().isAir()) continue;
            JsonObject e = new JsonObject();
            e.addProperty("slot", slot);
            e.addProperty("data", Base64.getEncoder().encodeToString(stack.serializeAsBytes()));
            items.add(e);
        }
        if (items.size() == 0) return null;
        JsonObject obj = new JsonObject();
        obj.addProperty("x", x);
        obj.addProperty("y", y);
        obj.addProperty("z", z);
        obj.add("items", items);
        return obj;
    }

    /* ============================ RESTORE ============================ */

    /**
     * Restaure une schematique JSON dans [origin, origin+size).
     * Les blocs "minecraft:air" sont remplaces par de l'air (efface ce qui etait la).
     */
    public static void restoreRegion(String json, World world,
                                     int originX, int originY, int originZ) {
        JsonObject root = JsonParser.parseString(json).getAsJsonObject();
        JsonArray sizeArr = root.getAsJsonArray("size");
        int sizeX = sizeArr.get(0).getAsInt();
        int sizeY = sizeArr.get(1).getAsInt();
        int sizeZ = sizeArr.get(2).getAsInt();

        JsonArray paletteArr = root.getAsJsonArray("palette");
        BlockData[] palette = new BlockData[paletteArr.size()];
        for (int i = 0; i < paletteArr.size(); i++) {
            String state = paletteArr.get(i).getAsString();
            try {
                palette[i] = Bukkit.createBlockData(state);
            } catch (IllegalArgumentException ex) {
                palette[i] = Bukkit.createBlockData(Material.AIR);
            }
        }

        JsonArray blocksArr = root.getAsJsonArray("blocks");
        int cursor = 0; // index dans l'ordre Y -> Z -> X
        int total = sizeX * sizeY * sizeZ;
        for (int i = 0; i < blocksArr.size() && cursor < total; i++) {
            JsonArray pair = blocksArr.get(i).getAsJsonArray();
            int idx = pair.get(0).getAsInt();
            int len = pair.get(1).getAsInt();
            BlockData data = palette[idx];
            for (int j = 0; j < len && cursor < total; j++, cursor++) {
                int x = cursor % sizeX;
                int z = (cursor / sizeX) % sizeZ;
                int y = cursor / (sizeX * sizeZ);
                Block b = world.getBlockAt(originX + x, originY + y, originZ + z);
                b.setBlockData(data, false);
            }
        }

        // Restauration du contenu des conteneurs (apres avoir pose les blocs).
        JsonElement contEl = root.get("containers");
        if (contEl != null && contEl.isJsonArray()) {
            for (JsonElement el : contEl.getAsJsonArray()) {
                JsonObject obj = el.getAsJsonObject();
                int x = obj.get("x").getAsInt();
                int y = obj.get("y").getAsInt();
                int z = obj.get("z").getAsInt();
                Block b = world.getBlockAt(originX + x, originY + y, originZ + z);
                BlockState bs = b.getState(false);
                if (!(bs instanceof Container container)) continue;
                Inventory inv = container.getInventory();
                if (inv == null) continue;
                JsonArray items = obj.getAsJsonArray("items");
                for (JsonElement itemEl : items) {
                    JsonObject itemObj = itemEl.getAsJsonObject();
                    int slot = itemObj.get("slot").getAsInt();
                    String b64 = itemObj.get("data").getAsString();
                    try {
                        byte[] bytes = Base64.getDecoder().decode(b64);
                        ItemStack stack = ItemStack.deserializeBytes(bytes);
                        if (slot >= 0 && slot < inv.getSize()) inv.setItem(slot, stack);
                    } catch (IllegalArgumentException ignored) { /* item invalide */ }
                }
            }
        }
    }

    public static int[] readSize(String json) {
        JsonObject root = JsonParser.parseString(json).getAsJsonObject();
        JsonArray sz = root.getAsJsonArray("size");
        return new int[]{sz.get(0).getAsInt(), sz.get(1).getAsInt(), sz.get(2).getAsInt()};
    }

    public static int[] readSpawn(String json) {
        JsonObject root = JsonParser.parseString(json).getAsJsonObject();
        JsonArray sp = root.getAsJsonArray("spawn");
        return new int[]{sp.get(0).getAsInt(), sp.get(1).getAsInt(), sp.get(2).getAsInt()};
    }

    /* ============================ EXPANSION ============================ */

    /**
     * Etend la schematique a la nouvelle taille (les dimensions doivent etre >= a l'ancienne).
     * Les blocs ajoutes sont de l'air. Le spawn est conserve.
     * Si {@code markCornerMaterial} != null, place ce materiau aux 8 coins de la nouvelle boite
     * pour signaler visuellement les nouvelles limites.
     */
    public static String expandSchematic(String oldJson, int newX, int newY, int newZ,
                                          Material markCornerMaterial) {
        JsonObject root = JsonParser.parseString(oldJson).getAsJsonObject();
        JsonArray oldSize = root.getAsJsonArray("size");
        int ox = oldSize.get(0).getAsInt();
        int oy = oldSize.get(1).getAsInt();
        int oz = oldSize.get(2).getAsInt();
        if (newX < ox || newY < oy || newZ < oz) {
            throw new IllegalArgumentException("Les nouvelles dimensions doivent etre >= aux anciennes.");
        }

        JsonArray paletteArr = root.getAsJsonArray("palette");
        List<String> palette = new ArrayList<>(paletteArr.size());
        for (JsonElement e : paletteArr) palette.add(e.getAsString());
        if (palette.isEmpty() || !palette.get(0).equals(AIR)) {
            // S'assurer que l'air est connu : si absent, l'ajouter en fin de palette.
            int airIdx = palette.indexOf(AIR);
            if (airIdx < 0) palette.add(AIR);
        }
        int airIdx = palette.indexOf(AIR);
        if (airIdx < 0) airIdx = palette.size() - 1;

        // Decoder l'ancien RLE en un tableau d'indices [oy*oz*ox].
        int[] oldFlat = decodeBlocks(root.getAsJsonArray("blocks"), ox * oy * oz);

        int totalNew = newX * newY * newZ;
        int[] newFlat = new int[totalNew];
        for (int i = 0; i < totalNew; i++) newFlat[i] = airIdx;
        // Copier dans le nouveau plan (ordre Y -> Z -> X).
        for (int y = 0; y < oy; y++) {
            for (int z = 0; z < oz; z++) {
                for (int x = 0; x < ox; x++) {
                    int oldIdx = y * (oz * ox) + z * ox + x;
                    int newIdx = y * (newZ * newX) + z * newX + x;
                    newFlat[newIdx] = oldFlat[oldIdx];
                }
            }
        }

        // Optionnellement marquer les 8 coins.
        if (markCornerMaterial != null) {
            String state = markCornerMaterial.createBlockData().getAsString();
            int markIdx = palette.indexOf(state);
            if (markIdx < 0) { palette.add(state); markIdx = palette.size() - 1; }
            int[][] corners = {
                    {0, 0, 0},
                    {newX - 1, 0, 0},
                    {0, 0, newZ - 1},
                    {newX - 1, 0, newZ - 1},
                    {0, newY - 1, 0},
                    {newX - 1, newY - 1, 0},
                    {0, newY - 1, newZ - 1},
                    {newX - 1, newY - 1, newZ - 1}
            };
            for (int[] c : corners) {
                int idx = c[1] * (newZ * newX) + c[2] * newX + c[0];
                newFlat[idx] = markIdx;
            }
        }

        // Re-encoder en RLE.
        List<int[]> runs = new ArrayList<>();
        int currentIdx = -1, run = 0;
        for (int v : newFlat) {
            if (v == currentIdx) { run++; }
            else {
                if (currentIdx != -1) runs.add(new int[]{currentIdx, run});
                currentIdx = v; run = 1;
            }
        }
        if (run > 0) runs.add(new int[]{currentIdx, run});

        JsonObject out = new JsonObject();
        JsonArray sizeArr = new JsonArray(); sizeArr.add(newX); sizeArr.add(newY); sizeArr.add(newZ);
        out.add("size", sizeArr);
        out.add("spawn", root.getAsJsonArray("spawn")); // spawn conserve
        JsonArray pal = new JsonArray();
        for (String p : palette) pal.add(p);
        out.add("palette", pal);
        JsonArray blocks = new JsonArray();
        for (int[] r : runs) { JsonArray ja = new JsonArray(); ja.add(r[0]); ja.add(r[1]); blocks.add(ja); }
        out.add("blocks", blocks);
        // Conserve les containers (positions invariantes car ancres au coin 0,0,0).
        if (root.has("containers")) out.add("containers", root.get("containers"));
        return GSON.toJson(out);
    }

    /** Decode le RLE en tableau d'indices pleins. */
    private static int[] decodeBlocks(JsonArray blocksArr, int total) {
        int[] out = new int[total];
        int cursor = 0;
        for (JsonElement el : blocksArr) {
            JsonArray pair = el.getAsJsonArray();
            int idx = pair.get(0).getAsInt();
            int len = pair.get(1).getAsInt();
            for (int j = 0; j < len && cursor < total; j++, cursor++) out[cursor] = idx;
        }
        return out;
    }

    /* ============================ UTILITAIRES ============================ */

    /** Vide la region (place de l'air partout). */
    public static void clearRegion(World world,
                                   int originX, int originY, int originZ,
                                   int sizeX, int sizeY, int sizeZ) {
        BlockData air = Bukkit.createBlockData(Material.AIR);
        for (int y = 0; y < sizeY; y++) {
            for (int z = 0; z < sizeZ; z++) {
                for (int x = 0; x < sizeX; x++) {
                    Block b = world.getBlockAt(originX + x, originY + y, originZ + z);
                    if (b.getType() != Material.AIR) b.setBlockData(air, false);
                }
            }
        }
    }

    /** True si la zone contient au moins un bloc non-air. */
    public static boolean hasCollision(World world,
                                       int originX, int originY, int originZ,
                                       int sizeX, int sizeY, int sizeZ) {
        for (int y = 0; y < sizeY; y++) {
            for (int z = 0; z < sizeZ; z++) {
                for (int x = 0; x < sizeX; x++) {
                    Block b = world.getBlockAt(originX + x, originY + y, originZ + z);
                    if (b.getType() != Material.AIR) return true;
                }
            }
        }
        return false;
    }

    /* ============================ DEFAULT SCHEMATIC ============================ */

    /**
     * Genere une schematique par defaut : sol plein, mur d'un bloc avec piliers aux coins,
     * et bordure de verre tout autour au niveau y=1. Le reste est de l'air.
     * Le spawn par defaut est centre, sur le sol +1.
     */
    public static String buildDefaultSchematic(int sizeX, int sizeY, int sizeZ,
                                               Material platform, Material edge, Material glass) {
        String platformState = platform.createBlockData().getAsString();
        String edgeState     = edge.createBlockData().getAsString();
        String glassState    = glass.createBlockData().getAsString();

        Map<String, Integer> paletteIndex = new LinkedHashMap<>();
        List<String> palette = new ArrayList<>();
        // 0 = air toujours
        paletteIndex.put(AIR, 0); palette.add(AIR);

        int pIdx = paletteIndex.computeIfAbsent(platformState, s -> { palette.add(s); return palette.size() - 1; });
        int eIdx = paletteIndex.computeIfAbsent(edgeState,     s -> { palette.add(s); return palette.size() - 1; });
        int gIdx = paletteIndex.computeIfAbsent(glassState,    s -> { palette.add(s); return palette.size() - 1; });

        List<int[]> blocks = new ArrayList<>();
        int currentIdx = -1, run = 0;

        for (int y = 0; y < sizeY; y++) {
            for (int z = 0; z < sizeZ; z++) {
                for (int x = 0; x < sizeX; x++) {
                    int idx;
                    boolean isEdgeXZ = (x == 0 || x == sizeX - 1) && (z == 0 || z == sizeZ - 1);
                    boolean onBorder = (x == 0 || x == sizeX - 1 || z == 0 || z == sizeZ - 1);
                    if (y == 0) {
                        idx = pIdx;
                    } else if (y == 1 && onBorder) {
                        idx = isEdgeXZ ? eIdx : gIdx;
                    } else if (y == sizeY - 1 && isEdgeXZ) {
                        idx = eIdx;
                    } else {
                        idx = 0;
                    }
                    if (idx == currentIdx) {
                        run++;
                    } else {
                        if (currentIdx != -1) blocks.add(new int[]{currentIdx, run});
                        currentIdx = idx;
                        run = 1;
                    }
                }
            }
        }
        if (run > 0) blocks.add(new int[]{currentIdx, run});

        JsonObject root = new JsonObject();
        JsonArray sizeArr = new JsonArray();
        sizeArr.add(sizeX); sizeArr.add(sizeY); sizeArr.add(sizeZ);
        root.add("size", sizeArr);

        JsonArray spawnArr = new JsonArray();
        spawnArr.add(sizeX / 2); spawnArr.add(1); spawnArr.add(sizeZ / 2);
        root.add("spawn", spawnArr);

        JsonArray paletteArr = new JsonArray();
        for (String p : palette) paletteArr.add(p);
        root.add("palette", paletteArr);

        JsonArray blocksArr = new JsonArray();
        for (int[] run2 : blocks) {
            JsonArray ja = new JsonArray();
            ja.add(run2[0]);
            ja.add(run2[1]);
            blocksArr.add(ja);
        }
        root.add("blocks", blocksArr);

        return GSON.toJson(root);
    }
}
