package p.packer.repositories;

import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;

import javax.imageio.ImageIO;
import java.io.ByteArrayInputStream;
import java.nio.file.Path;
import java.util.*;

public class PackerGlyphBankWalker extends PackerAbstractBankWalker<PackerGlyphBankRequirements> {
    private static final int MAX_COLORS_PER_PALETTE = 15;
    private static final int COLOR_KEY_RGB = 0x00FF00FF;
    private static final Set<String> SUPPORTED_MIME_TYPES = Set.of(
            "image/png"
    );

    public PackerGlyphBankWalker(ObjectMapper mapper) {
        super(mapper);
    }

    @Override
    protected Set<String> getSupportedMimeTypes() {
        return SUPPORTED_MIME_TYPES;
    }

    @Override
    protected boolean canReuseMetadata(
            final PackerFileProbe fileProbe,
            final PackerFileCacheEntry cachedEntry) {
        if (!super.canReuseMetadata(fileProbe, cachedEntry)) {
            return false;
        }
        return hasReusableTileMetadata(cachedEntry.metadata());
    }

    @Override
    protected PackerProbeResult processFileProbe(
            final PackerFileProbe fileProbe,
            final Path assetRoot,
            final PackerGlyphBankRequirements requirements) {
        final List<PackerDiagnostic> diagnostics = new ArrayList<>();

        final var image = readImage(fileProbe, diagnostics);
        if (image == null) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Unable to decode tile image content: " + fileProbe.path().getFileName(),
                    fileProbe.path(),
                    true
            ));
            return new PackerProbeResult(fileProbe, Map.of(), diagnostics);
        }

        final var width = image.getWidth();
        final var height = image.getHeight();
        if (width != requirements.tileSize() || height != requirements.tileSize()) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Invalid glyph dimensions for " + fileProbe.path().getFileName()
                            + ": expected " + requirements.tileSize() + "x" + requirements.tileSize()
                            + " but got " + width + "x" + height,
                    fileProbe.path(),
                    true
            ));
        }

        final Map<Integer, Integer> paletteIndexByRgb = new LinkedHashMap<>();
        final byte[] paletteIndices = new byte[image.getWidth() * image.getHeight()];
        boolean partialAlphaFound = false;
        boolean exceededPaletteLimit = false;

        int offset = 0;
        for (var y = 0; y < image.getHeight(); y++) {
            for (var x = 0; x < image.getWidth(); x++) {
                final int argb = image.getRGB(x, y);
                final int alpha = (argb >>> 24) & 0xFF;
                final int rgb = argb & 0x00FFFFFF;

                if (alpha == 0 || rgb == COLOR_KEY_RGB) {
                    paletteIndices[offset++] = 0;
                    continue;
                }

                if (alpha < 0xFF) {
                    partialAlphaFound = true;
                }

                Integer paletteIndex = paletteIndexByRgb.get(rgb);
                if (paletteIndex == null) {
                    if (paletteIndexByRgb.size() >= MAX_COLORS_PER_PALETTE) {
                        exceededPaletteLimit = true;
                        paletteIndices[offset++] = 0;
                        continue;
                    }
                    paletteIndex = paletteIndexByRgb.size() + 1;
                    paletteIndexByRgb.put(rgb, paletteIndex);
                }

                paletteIndices[offset++] = paletteIndex.byteValue();
            }
        }

        if (partialAlphaFound) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.WARNING,
                    PackerDiagnosticCategory.HYGIENE,
                    "Tile contains partial alpha; flattening to RGB and ignoring alpha channel",
                    fileProbe.path(),
                    false
            ));
        }

        if (exceededPaletteLimit) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Tile image exceeds color limit for " + fileProbe.path().getFileName()
                            + ": expected at most " + MAX_COLORS_PER_PALETTE + " colors for indices 1..15",
                    fileProbe.path(),
                    true
            ));
        }

        if (diagnostics.stream().anyMatch(PackerDiagnostic::blocking)) {
            return new PackerProbeResult(fileProbe, Map.of(), diagnostics);
        }

        final var tile = new PackerTileIndexedV1(image.getWidth(), image.getHeight(), paletteIndices);
        final var palette = buildPalette(paletteIndexByRgb);
        // Disabled for now: derived tile/palette JSON artifacts are not emitted during normal walks.
        // If we want filesystem-side debug artifacts again, uncomment the call below.
        // serializeProbeArtifacts(fileProbe, assetRoot, tile, palette, diagnostics);
        return new PackerProbeResult(fileProbe, Map.of("tile", tile, "palette", palette), diagnostics);
    }

    private boolean hasReusableTileMetadata(final Map<String, Object> metadata) {
        final Object tileValue = metadata.get("tile");
        if (!(tileValue instanceof Map<?, ?> tile)) {
            return false;
        }
        final Object paletteValue = metadata.get("palette");
        if (!(paletteValue instanceof Map<?, ?> palette)) {
            return false;
        }
        final Object width = tile.get("width");
        final Object height = tile.get("height");
        final Object indices = tile.get("paletteIndices");
        final Object colors = palette.get("originalArgb8888");
        return width instanceof Number
                && height instanceof Number
                && indices instanceof List<?>
                && colors instanceof List<?>;
    }

    /*
    private void serializeProbeArtifacts(
            final PackerFileProbe fileProbe,
            final Path assetRoot,
            final PackerTileIndexedV1 tile,
            final PackerPaletteV1 palette,
            final List<PackerDiagnostic> diagnostics) {
        final Path basePath = baseOutputPath(assetRoot, fileProbe.path());
        final Path tileOutputPath = Path.of(basePath + ".tile.json");
        final Path paletteOutputPath = Path.of(basePath + ".palette.json");

        try {
            Files.createDirectories(Objects.requireNonNull(tileOutputPath.getParent()));
            mapper.writerWithDefaultPrettyPrinter().writeValue(tileOutputPath.toFile(), toTilePayload(tile));
            mapper.writerWithDefaultPrettyPrinter().writeValue(paletteOutputPath.toFile(), toPalettePayload(palette));
        } catch (IOException exception) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Unable to serialize tile/palette artifacts for " + fileProbe.path().getFileName()
                            + ": " + exception.getMessage(),
                    fileProbe.path(),
                    true
            ));
        }
    }

    private Path baseOutputPath(final Path assetRoot, final Path sourcePath) {
        final Path assetsRoot = assetsRoot(assetRoot);
        final Path relativeSourcePath = assetsRoot.relativize(sourcePath.toAbsolutePath().normalize());
        final String fileName = sourcePath.getFileName().toString();
        final int extensionStart = fileName.lastIndexOf('.');
        final String outputBaseName = extensionStart > 0 ? fileName.substring(0, extensionStart) : fileName;
        final Path relativeParent = relativeSourcePath.getParent();
        final Path outputRoot = assetsRoot.resolve(".prometeu");
        if (relativeParent == null) {
            return outputRoot.resolve(outputBaseName);
        }
        return outputRoot.resolve(relativeParent).resolve(outputBaseName);
    }

    private Path assetsRoot(final Path assetRoot) {
        Path current = assetRoot.toAbsolutePath().normalize();
        while (current != null && current.getFileName() != null) {
            if ("assets".equals(current.getFileName().toString())) {
                return current;
            }
            current = current.getParent();
        }
        throw new IllegalStateException("Unable to resolve assets root for tile artifact output: " + assetRoot);
    }
    */

    private Map<String, Object> toTilePayload(final PackerTileIndexedV1 tile) {
        final List<Integer> indices = new ArrayList<>(tile.paletteIndices().length);
        for (final byte value : tile.paletteIndices()) {
            indices.add(Byte.toUnsignedInt(value));
        }

        return Map.of(
                "width", tile.width(),
                "height", tile.height(),
                "paletteIndices", indices
        );
    }

    private Map<String, Object> toPalettePayload(final PackerPaletteV1 palette) {
        final List<Integer> convertedRgb565 = new ArrayList<>(palette.convertedRgb565().length);
        for (final short value : palette.convertedRgb565()) {
            convertedRgb565.add(Short.toUnsignedInt(value));
        }

        return Map.of(
                "originalArgb8888", palette.originalArgb8888(),
                "convertedRgb565", convertedRgb565
        );
    }

    private PackerPaletteV1 buildPalette(final Map<Integer, Integer> paletteIndexByRgb) {
        final int[] originalArgb8888 = new int[paletteIndexByRgb.size()];
        final short[] convertedRgb565 = new short[paletteIndexByRgb.size()];
        for (final var entry : paletteIndexByRgb.entrySet()) {
            final int rgb = entry.getKey();
            final int index = entry.getValue() - 1;
            final int argb = 0xFF000000 | rgb;
            originalArgb8888[index] = argb;
            convertedRgb565[index] = convertToRgb565(argb);
        }
        return new PackerPaletteV1(originalArgb8888, convertedRgb565);
    }

    private short convertToRgb565(final int argb) {
        final int red = (argb >> 16) & 0xFF;
        final int green = (argb >> 8) & 0xFF;
        final int blue = argb & 0xFF;

        final int r5 = (red >> 3) & 0x1F;
        final int g6 = (green >> 2) & 0x3F;
        final int b5 = (blue >> 3) & 0x1F;
        return (short) ((r5 << 11) | (g6 << 5) | b5);
    }

    private java.awt.image.BufferedImage readImage(
            final PackerFileProbe fileProbe,
            final List<PackerDiagnostic> diagnostics) {
        try {
            return ImageIO.read(new ByteArrayInputStream(fileProbe.content()));
        } catch (Exception exception) {
            diagnostics.add(new PackerDiagnostic(
                    PackerDiagnosticSeverity.ERROR,
                    PackerDiagnosticCategory.STRUCTURAL,
                    "Unable to decode tile image content: " + exception.getMessage(),
                    fileProbe.path(),
                    true
            ));
            return null;
        }
    }
}
