package p.studio.workspaces.assets.metadata;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import p.packer.messages.assets.AssetFamilyCatalog;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public final class AssetStudioMetadataService {
    public static final String STUDIO_ASSET_METADATA_FILE = "studio.asset.json";
    public static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
    public static final String GLYPH_MATERIAL_SUPPORT_FILE = "glyph-material.json";

    private final ObjectMapper mapper = new ObjectMapper();

    public AssetStudioMetadataSnapshot read(Path assetRoot, AssetFamilyCatalog assetFamily) {
        final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
        return new AssetStudioMetadataSnapshot(
                readGlyphSpecialization(normalizedAssetRoot, assetFamily),
                readSceneBankMetadata(normalizedAssetRoot, assetFamily),
                readGlyphMaterialMetadata(normalizedAssetRoot, assetFamily));
    }

    public void writeSceneBankMetadata(Path assetRoot, AssetStudioSceneBankMetadata metadata) throws IOException {
        final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
        final AssetStudioSceneBankMetadata normalized = Objects.requireNonNull(metadata, "metadata");
        final var root = mapper.createObjectNode();
        root.put("schema_version", 1);
        root.put("map_width", normalized.mapWidth());
        root.put("map_height", normalized.mapHeight());
        root.put("layer_count", normalized.layerCount());
        final ArrayNode layers = root.putArray("layers");
        for (AssetStudioSceneLayerBinding binding : normalized.layerBindings()) {
            final var layer = layers.addObject();
            layer.put("index", binding.index());
            layer.put("enabled", binding.enabled());
            layer.put("name", binding.layerName());
            layer.put("width", binding.width());
            layer.put("height", binding.height());
            layer.put("tilemap", binding.tilemap());
            layer.put("tileset_asset_root", binding.tilesetAssetRoot());
            layer.put("parallax_factor_x", binding.parallaxFactorX());
            layer.put("parallax_factor_y", binding.parallaxFactorY());
            layer.put("default_palette_id", binding.defaultPaletteId());
            if (binding.tiles().stream().anyMatch(AssetStudioSceneTile::active)) {
                final ArrayNode tiles = layer.putArray("tiles");
                for (AssetStudioSceneTile tile : binding.tiles()) {
                    final var tileNode = tiles.addObject();
                    tileNode.put("active", tile.active());
                    tileNode.put("glyph_id", tile.glyphId());
                    tileNode.put("palette_id", tile.paletteId());
                    tileNode.put("flip_x", tile.flipX());
                    tileNode.put("flip_y", tile.flipY());
                }
            }
        }
        mapper.writerWithDefaultPrettyPrinter().writeValue(
                normalizedAssetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
                root);
    }

    public void writeGlyphSpecialization(Path assetRoot, AssetStudioGlyphSpecialization specialization) throws IOException {
        final Path metadataPath = Objects.requireNonNull(assetRoot, "assetRoot")
                .toAbsolutePath()
                .normalize()
                .resolve(STUDIO_ASSET_METADATA_FILE);
        final AssetStudioGlyphSpecialization normalized = Objects.requireNonNullElse(
                specialization,
                AssetStudioGlyphSpecialization.NONE);
        if (normalized == AssetStudioGlyphSpecialization.NONE) {
            Files.deleteIfExists(metadataPath);
            return;
        }
        final var root = mapper.createObjectNode();
        root.put("schema_version", 1);
        root.put("glyph_bank_specialization", normalized.manifestValue());
        mapper.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), root);
    }

    public void writeGlyphMaterialMetadata(Path assetRoot, AssetStudioGlyphMaterialMetadata metadata) throws IOException {
        final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
        final AssetStudioGlyphMaterialMetadata normalized = Objects.requireNonNull(metadata, "metadata");
        final var root = mapper.createObjectNode();
        root.put("schema_version", 1);
        final ArrayNode schemas = root.putArray("schemas");
        for (AssetStudioGlyphMaterialSchema schema : normalized.schemas()) {
            final var schemaNode = schemas.addObject();
            schemaNode.put("schema_id", schema.schemaId());
            schemaNode.put("status", schema.status().manifestValue());
            schemaNode.put("version", schema.version());
            final var materialIndicesNode = schemaNode.putObject("material_indices");
            schema.materialIndices().entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .forEach(entry -> materialIndicesNode.put(String.valueOf(entry.getKey()), entry.getValue()));
            final ArrayNode bindingsNode = schemaNode.putArray("artifact_bindings");
            for (AssetStudioGlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
                final var bindingNode = bindingsNode.addObject();
                bindingNode.put("artifact_id", binding.artifactId());
                bindingNode.put("fingerprint", binding.fingerprint());
            }
        }
        final ArrayNode palettes = root.putArray("palettes");
        for (AssetStudioGlyphMaterialPalette palette : normalized.palettes()) {
            final var paletteNode = palettes.addObject();
            paletteNode.put("palette_id", palette.paletteId());
            if (palette.displayName() != null) {
                paletteNode.put("name", palette.displayName());
            }
            paletteNode.put("schema_id", palette.schemaId());
            paletteNode.put("version", palette.version());
            final ArrayNode colorsNode = paletteNode.putArray("colors");
            palette.colors().forEach(colorsNode::add);
        }
        mapper.writerWithDefaultPrettyPrinter().writeValue(
                normalizedAssetRoot.resolve(GLYPH_MATERIAL_SUPPORT_FILE).toFile(),
                root);
    }

    private AssetStudioGlyphSpecialization readGlyphSpecialization(Path assetRoot, AssetFamilyCatalog assetFamily) {
        if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
            return AssetStudioGlyphSpecialization.NONE;
        }
        final Path metadataPath = assetRoot.resolve(STUDIO_ASSET_METADATA_FILE);
        if (!Files.isRegularFile(metadataPath)) {
            return AssetStudioGlyphSpecialization.NONE;
        }
        try {
            final JsonNode root = mapper.readTree(metadataPath.toFile());
            if (root == null || !root.isObject()) {
                return AssetStudioGlyphSpecialization.NONE;
            }
            return AssetStudioGlyphSpecialization.fromManifestValue(root.path("glyph_bank_specialization").asText(null));
        } catch (IOException ignored) {
            return AssetStudioGlyphSpecialization.NONE;
        }
    }

    private AssetStudioSceneBankMetadata readSceneBankMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
        if (assetFamily != AssetFamilyCatalog.SCENE_BANK) {
            return null;
        }
        final Path supportFile = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
        if (!Files.isRegularFile(supportFile)) {
            return defaultSceneBankMetadata(supportFile);
        }
        try {
            final JsonNode root = mapper.readTree(supportFile.toFile());
            if (root == null || !root.isObject()) {
                return null;
            }
            final int mapWidth = root.path("map_width").asInt(0);
            final int mapHeight = root.path("map_height").asInt(0);
            final int layerCount = root.path("layer_count").asInt(0);
            if (mapWidth <= 0 || mapHeight <= 0 || layerCount != 4) {
                return null;
            }
            if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != layerCount) {
                return null;
            }
            final List<AssetStudioSceneLayerBinding> bindings = new ArrayList<>();
            final Set<Integer> indexes = new HashSet<>();
            for (JsonNode layerNode : layersNode) {
                final int index = layerNode.path("index").asInt(0);
                final boolean enabled = layerNode.path("enabled").asBoolean(false);
                final String layerName = layerNode.path("name").asText("").trim();
                final int width = layerNode.path("width").asInt(mapWidth);
                final int height = layerNode.path("height").asInt(mapHeight);
                final String tilemap = layerNode.path("tilemap").asText("").trim();
                final String tilesetAssetRoot = layerNode.path("tileset_asset_root").asText("").trim();
                final float parallaxFactorX = (float) layerNode.path("parallax_factor_x").asDouble(1.0d);
                final float parallaxFactorY = (float) layerNode.path("parallax_factor_y").asDouble(1.0d);
                final int defaultPaletteId = layerNode.path("default_palette_id").asInt(0);
                if (index < 1 || index > layerCount || layerName.isBlank() || width <= 0 || height <= 0 || tilemap.isBlank() || !indexes.add(index)) {
                    return null;
                }
                final List<AssetStudioSceneTile> tiles = readSceneTiles(layerNode.path("tiles"), width, height, defaultPaletteId);
                if (tiles == null) {
                    return null;
                }
                bindings.add(new AssetStudioSceneLayerBinding(
                        enabled,
                        index,
                        layerName,
                        width,
                        height,
                        tilemap,
                        tilesetAssetRoot,
                        parallaxFactorX,
                        parallaxFactorY,
                        defaultPaletteId,
                        tiles));
            }
            bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
            return new AssetStudioSceneBankMetadata(mapWidth, mapHeight, layerCount, bindings, supportFile);
        } catch (IOException ignored) {
            return null;
        }
    }

    private AssetStudioGlyphMaterialMetadata readGlyphMaterialMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
        if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
            return null;
        }
        final Path supportFile = assetRoot.resolve(GLYPH_MATERIAL_SUPPORT_FILE);
        if (!Files.isRegularFile(supportFile)) {
            return null;
        }
        try {
            final JsonNode root = mapper.readTree(supportFile.toFile());
            if (root == null || !root.isObject()) {
                return null;
            }
            if (!(root.path("schemas") instanceof ArrayNode schemasNode) || !(root.path("palettes") instanceof ArrayNode palettesNode)) {
                return null;
            }
            final List<AssetStudioGlyphMaterialSchema> schemas = new ArrayList<>();
            final Set<String> schemaIds = new HashSet<>();
            for (JsonNode schemaNode : schemasNode) {
                final String schemaId = schemaNode.path("schema_id").asText("").trim();
                final AssetStudioGlyphMaterialSchemaStatus status = AssetStudioGlyphMaterialSchemaStatus.fromManifestValue(
                        schemaNode.path("status").asText(null));
                final int version = schemaNode.path("version").asInt(0);
                if (schemaId.isBlank() || status == null || version <= 0 || !schemaIds.add(schemaId)) {
                    return null;
                }
                final JsonNode materialIndicesNode = schemaNode.path("material_indices");
                if (!materialIndicesNode.isObject()) {
                    return null;
                }
                final Map<Integer, String> materialIndices = new java.util.TreeMap<>();
                materialIndicesNode.fields().forEachRemaining(entry -> {
                    try {
                        final int index = Integer.parseInt(entry.getKey());
                        final String color = normalizeHtmlColor(entry.getValue().asText(null));
                        if (index >= 1 && index <= 15 && color != null) {
                            materialIndices.put(index, color);
                        }
                    } catch (NumberFormatException ignored) {
                        // handled below through validation
                    }
                });
                if (materialIndices.isEmpty()
                        || materialIndices.size() != materialIndicesNode.size()
                        || !denseIndices(materialIndices.keySet())) {
                    return null;
                }
                if (!(schemaNode.path("artifact_bindings") instanceof ArrayNode bindingsNode) || bindingsNode.isEmpty()) {
                    return null;
                }
                final List<AssetStudioGlyphMaterialArtifactBinding> bindings = new ArrayList<>();
                final Set<String> artifactIds = new HashSet<>();
                for (JsonNode bindingNode : bindingsNode) {
                    final String artifactId = bindingNode.path("artifact_id").asText("").trim();
                    final String fingerprint = bindingNode.path("fingerprint").asText("").trim();
                    if (artifactId.isBlank() || fingerprint.isBlank() || !artifactIds.add(artifactId)) {
                        return null;
                    }
                    bindings.add(new AssetStudioGlyphMaterialArtifactBinding(artifactId, fingerprint));
                }
                schemas.add(new AssetStudioGlyphMaterialSchema(schemaId, status, version, materialIndices, bindings));
            }
            final List<AssetStudioGlyphMaterialPalette> palettes = new ArrayList<>();
            final Set<String> paletteIds = new HashSet<>();
            for (JsonNode paletteNode : palettesNode) {
                final String paletteId = paletteNode.path("palette_id").asText("").trim();
                final String displayName = paletteNode.path("name").asText(null);
                final String schemaId = paletteNode.path("schema_id").asText("").trim();
                final int version = paletteNode.path("version").asInt(0);
                if (paletteId.isBlank() || schemaId.isBlank() || version <= 0 || !paletteIds.add(paletteId) || !schemaIds.contains(schemaId)) {
                    return null;
                }
                if (!(paletteNode.path("colors") instanceof ArrayNode colorsNode)) {
                    return null;
                }
                final List<String> colors = new ArrayList<>();
                for (JsonNode colorNode : colorsNode) {
                    final String color = normalizeHtmlColor(colorNode.asText(null));
                    if (color == null) {
                        return null;
                    }
                    colors.add(color);
                }
                palettes.add(new AssetStudioGlyphMaterialPalette(paletteId, displayName, schemaId, version, colors));
            }
            return new AssetStudioGlyphMaterialMetadata(schemas, palettes, supportFile);
        } catch (IOException ignored) {
            return null;
        }
    }

    private boolean denseIndices(Set<Integer> indices) {
        if (indices.isEmpty()) {
            return false;
        }
        int expected = 1;
        for (Integer index : new java.util.TreeSet<>(indices)) {
            if (index == null || index != expected) {
                return false;
            }
            expected += 1;
        }
        return true;
    }

    private String normalizeHtmlColor(String value) {
        if (value == null) {
            return null;
        }
        final String normalized = value.trim().toUpperCase();
        return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
    }

    private AssetStudioSceneBankMetadata defaultSceneBankMetadata(Path supportFile) {
        return new AssetStudioSceneBankMetadata(
                16,
                16,
                4,
                List.of(
                        new AssetStudioSceneLayerBinding(false, 1, "Layer 1", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0),
                        new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0),
                        new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0),
                        new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0)),
                supportFile);
    }

    private List<AssetStudioSceneTile> readSceneTiles(JsonNode tilesNode, int width, int height, int defaultPaletteId) {
        final int tileCount = width * height;
        if (!(tilesNode instanceof ArrayNode arrayNode)) {
            final List<AssetStudioSceneTile> defaults = new ArrayList<>(tileCount);
            for (int index = 0; index < tileCount; index += 1) {
                defaults.add(AssetStudioSceneTile.inactive(defaultPaletteId));
            }
            return List.copyOf(defaults);
        }
        if (arrayNode.size() != tileCount) {
            return null;
        }
        final List<AssetStudioSceneTile> tiles = new ArrayList<>(tileCount);
        for (JsonNode tileNode : arrayNode) {
            final boolean active = tileNode.path("active").asBoolean(false);
            final int glyphId = tileNode.path("glyph_id").asInt(0);
            final int paletteId = tileNode.path("palette_id").asInt(defaultPaletteId);
            final boolean flipX = tileNode.path("flip_x").asBoolean(false);
            final boolean flipY = tileNode.path("flip_y").asBoolean(false);
            if (glyphId < 0 || paletteId < 0) {
                return null;
            }
            tiles.add(new AssetStudioSceneTile(active, glyphId, paletteId, flipX, flipY));
        }
        return List.copyOf(tiles);
    }
}
