package com.agentdid127.resourcepack.forwards.impl.v1_21.textures;

import com.agentdid127.resourcepack.library.Converter;
import com.agentdid127.resourcepack.library.PackConverter;
import com.agentdid127.resourcepack.library.pack.Pack;
import com.agentdid127.resourcepack.library.utilities.JsonUtil;
import com.agentdid127.resourcepack.library.utilities.Logger;
import com.agentdid127.resourcepack.library.utilities.Util;
import com.google.gson.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

// https://www.minecraft.net/en-us/article/minecraft-java-edition-1-21-2
public class PostEffectShadersConverter1_21_2 extends Converter {
    private static final Map<String, String> MAPPING = new HashMap<>();

    static {
        // TODO/NOTE: There could be more?
        MAPPING.put("largeBlur", "large_blur");
        MAPPING.put("smallBlur", "small_blur");
        MAPPING.put("itemEntity", "item_entity");
    }

    public PostEffectShadersConverter1_21_2(PackConverter packConverter) {
        super(packConverter);
    }

    @Override
    public boolean shouldConvert(Gson gson, int from, int to) {
        return from <= Util.getVersionProtocol(gson, "1.21.1") && to >= Util.getVersionProtocol(gson, "1.21.2");
    }

    @Override
    public void convert(Pack pack) throws IOException {
        Path minecraftPath = pack.getWorkingPath().resolve("assets/minecraft".replace("/", File.separator));
        Path shadersPath = minecraftPath.resolve("shaders");
        Path shadersPostPath = shadersPath.resolve("post");
        if (!shadersPostPath.toFile().exists()) {
            return;
        }

        Logger.addTab();
        Path postEffectPath = minecraftPath.resolve("post_effect");
        if (postEffectPath.toFile().exists()) {
            // TODO: Should we merge directories?
            postEffectPath.toFile().delete();
        }
        Files.move(shadersPostPath, postEffectPath);

        // Update shaders
        // TODO: Determind if legacy post-effect & ignore it/delete it
        List<Path> postShaders = new ArrayList<>();
        try (Stream<Path> pathStream = Files.list(postEffectPath).filter(file -> file.toString().endsWith(".json"))) {
            pathStream.forEach(file -> {
                postShaders.add(file.getFileName());
                try {
                    JsonObject json = JsonUtil.readJson(packConverter.getGson(), file);

                    // Targets
                    if (json.has("targets") && json.get("targets").isJsonArray()) {
                        JsonObject targets = new JsonObject();
                        JsonArray targetElements = json.remove("targets").getAsJsonArray();
                        for (JsonElement element : targetElements) {
                            if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
                                String target = element.getAsString();
                                targets.add(MAPPING.getOrDefault(target, target), new JsonObject());
                            } else if (element.isJsonObject()) {
                                JsonObject object = element.getAsJsonObject();
                                if (object.has("name")) {
                                    String name = object.remove("name").getAsString();
                                    targets.add(name, object);
                                }
                            }
                        }
                        json.add("targets", targets);
                    }

                    // Passes
                    if (json.has("passes") && json.get("passes").isJsonArray()) {
                        JsonArray passes = new JsonArray();
                        JsonArray passesElements = json.remove("passes").getAsJsonArray();
                        for (JsonObject object : passesElements.asList().stream().map(JsonElement::getAsJsonObject).collect(Collectors.toList())) {
                            passes.add(this.remapPass(object));
                        }
                        json.add("passes", passes);
                    }

                    JsonUtil.writeJson(this.packConverter.getGson(), file, json);
                    Logger.debug("Saving updated post-effect shader \"" + file + "\"");
                } catch (Exception e) {
                    Logger.debug("Failed to update post-effect shader \"" + file + "\": " + e.getMessage());
                }
            });
        } catch (Exception e) {
            Logger.debug("Failed to update shader, unable to open file stream.");
        }

        Path shadersProgramPath = shadersPath.resolve("program");
        if (!shadersProgramPath.toFile().exists()) {
            return;
        }

        // TODO: Could be just all ".json" in "shaders/program"?
        postShaders.forEach(postShaderPath -> {
            Path programShaderPath = shadersProgramPath.resolve(postShaderPath);
            if (!programShaderPath.toFile().exists()) {
                return;
            }

            if (!shadersPostPath.toFile().exists()) {
                shadersPostPath.toFile().mkdirs();
            }

            try {
                Logger.debug("Moved post-effect program shader \"" + programShaderPath + "\" to post folder.");
                Files.move(programShaderPath, shadersPostPath.resolve(postShaderPath));
            } catch (IOException e) {
                Logger.debug("Failed to move post-effect program shader to post folder.");
            }
        });

        Logger.subTab();
    }

    private String fixNamespace(String name) {
        // TODO: Fix for other namespaces
        if (!name.startsWith("minecraft:")) {
            return "minecraft:" + name;
        } else {
            return name;
        }
    }

    private JsonObject remapPass(JsonObject object) {
        JsonObject pass = object.deepCopy();
        if (pass.has("name")) {
            String name = pass.remove("name").getAsString();
            pass.addProperty("program", fixNamespace("post/" + name));
        }

        if (pass.has("intarget")) {
            String inTarget = pass.remove("intarget").getAsString();
            JsonObject inputObject = new JsonObject();
            inputObject.addProperty("sampler_name", "In");
            inputObject.addProperty("target", fixNamespace(MAPPING.getOrDefault(inTarget, inTarget)));
            if (pass.has("use_linear_filter") && pass.get("use_linear_filter").isJsonPrimitive()) {
                inputObject.addProperty("bilinear", pass.remove("use_linear_filter").getAsBoolean());
            }

            JsonArray inputs = new JsonArray();
            inputs.add(inputObject);
            pass.add("inputs", inputs);
        } else if (pass.has("use_linear_filter")) {
            pass.remove("use_linear_filter"); // TODO/NOTE: ? If this is a thing, idk what to do with it
        }

        if (pass.has("auxtargets")) {
            JsonArray inputs;
            if (pass.has("inputs")) {
                inputs = pass.remove("inputs").getAsJsonArray();
            } else {
                inputs = new JsonArray();
            }

            JsonArray auxTargets = pass.remove("auxtargets").getAsJsonArray();
            for (JsonObject target : auxTargets.asList().stream().map(JsonElement::getAsJsonObject).collect(Collectors.toList())) {
                JsonObject newTarget = target.deepCopy();
                if (newTarget.has("name")) {
                    String name = newTarget.remove("name").getAsString();
                    newTarget.addProperty("sampler_name", name);
                }

                if (newTarget.has("id")) {
                    String id = newTarget.remove("id").getAsString();
                    if (id.endsWith(":depth")) {
                        newTarget.add("use_depth_buffer", new JsonPrimitive(true));
                        id = id.substring(0, id.length() - ":depth".length());
                    }
                    newTarget.addProperty("target", fixNamespace(MAPPING.getOrDefault(id, id)));
                }

                inputs.add(newTarget);
            }

            pass.add("inputs", inputs);
        }

        if (pass.has("outtarget")) {
            String outTarget = pass.remove("outtarget").getAsString();
            pass.addProperty("output", MAPPING.getOrDefault(outTarget, outTarget));
        }

        return pass;
    }
}
