package p.studio.workspaces.assets.details.palette;

import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import p.studio.Container;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
    private static final double PREVIEW_SIZE = 192.0d;
    private static final double PREVIEW_FRAME_SIZE = PREVIEW_SIZE + 16.0d;

    private final Label selectorLabel = new Label(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR));
    private final ComboBox<AssetWorkspaceBankCompositionFile> tileSelector = new ComboBox<>();
    private final Canvas tileCanvas = new Canvas(PREVIEW_SIZE, PREVIEW_SIZE);
    private final Label emptyLabel = new Label(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_PREVIEW_EMPTY));
    private final Label paletteLabel = new Label(Container.i18n().text(I18n.ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE));
    private final HBox paletteStrip = new HBox(0);

    private Consumer<AssetWorkspaceBankCompositionFile> onPreviewTileChanged = ignored -> { };
    private boolean suppressSelectorEvents;

    public AssetDetailsPaletteOverhaulingPreviewPane() {
        getStyleClass().add("assets-details-palette-preview");
        setSpacing(10);
        setAlignment(Pos.TOP_CENTER);
        setMinWidth(PREVIEW_FRAME_SIZE);
        setPrefWidth(PREVIEW_FRAME_SIZE);
        setMaxWidth(PREVIEW_FRAME_SIZE);

        selectorLabel.getStyleClass().add("assets-details-palette-preview-label");
        paletteLabel.getStyleClass().add("assets-details-palette-preview-label");
        selectorLabel.setMaxWidth(Double.MAX_VALUE);
        selectorLabel.setAlignment(Pos.CENTER_LEFT);
        paletteLabel.setMaxWidth(Double.MAX_VALUE);
        paletteLabel.setAlignment(Pos.CENTER);

        tileSelector.setMinWidth(PREVIEW_FRAME_SIZE);
        tileSelector.setPrefWidth(PREVIEW_FRAME_SIZE);
        tileSelector.setMaxWidth(PREVIEW_FRAME_SIZE);
        tileSelector.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
            @Override
            protected void updateItem(AssetWorkspaceBankCompositionFile item, boolean empty) {
                super.updateItem(item, empty);
                setText(empty || item == null ? null : item.displayName());
            }
        });
        tileSelector.setButtonCell(new javafx.scene.control.ListCell<>() {
            @Override
            protected void updateItem(AssetWorkspaceBankCompositionFile item, boolean empty) {
                super.updateItem(item, empty);
                setText(empty || item == null ? null : item.displayName());
            }
        });
        tileSelector.valueProperty().addListener((ignored, oldValue, newValue) -> {
            if (!suppressSelectorEvents && newValue != null) {
                onPreviewTileChanged.accept(newValue);
            }
        });

        final VBox previewSurface = new VBox(emptyLabel, tileCanvas);
        previewSurface.getStyleClass().add("assets-details-palette-preview-surface");
        previewSurface.setAlignment(Pos.CENTER);
        previewSurface.setMinWidth(PREVIEW_FRAME_SIZE);
        previewSurface.setPrefWidth(PREVIEW_FRAME_SIZE);
        previewSurface.setMaxWidth(PREVIEW_FRAME_SIZE);
        previewSurface.setMinHeight(PREVIEW_FRAME_SIZE);
        previewSurface.setPrefHeight(PREVIEW_FRAME_SIZE);
        previewSurface.setMaxHeight(PREVIEW_FRAME_SIZE);
        VBox.setVgrow(previewSurface, Priority.ALWAYS);
        emptyLabel.getStyleClass().add("assets-details-section-message");

        paletteStrip.getStyleClass().add("assets-details-palette-preview-strip");
        paletteStrip.setAlignment(Pos.CENTER);
        paletteStrip.setMinWidth(PREVIEW_FRAME_SIZE);
        paletteStrip.setPrefWidth(PREVIEW_FRAME_SIZE);
        paletteStrip.setMaxWidth(PREVIEW_FRAME_SIZE);
        for (int index = 0; index < 15; index++) {
            final Region swatch = new Region();
            swatch.getStyleClass().add("assets-details-palette-preview-swatch");
            swatch.setMinSize(16.0d, 26.0d);
            swatch.setPrefSize(16.0d, 26.0d);
            swatch.setMaxSize(16.0d, 26.0d);
            paletteStrip.getChildren().add(swatch);
        }

        getChildren().addAll(selectorLabel, tileSelector, previewSurface, paletteLabel, paletteStrip);
    }

    public void setOnPreviewTileChanged(Consumer<AssetWorkspaceBankCompositionFile> onPreviewTileChanged) {
        this.onPreviewTileChanged = Objects.requireNonNull(onPreviewTileChanged, "onPreviewTileChanged");
    }

    public void render(
            List<AssetWorkspaceBankCompositionFile> previewFiles,
            AssetWorkspaceBankCompositionFile previewTileFile,
            AssetWorkspaceBankCompositionFile previewPaletteFile,
            int renderableFileCount) {
        suppressSelectorEvents = true;
        tileSelector.setItems(FXCollections.observableArrayList(previewFiles));
        tileSelector.setValue(previewTileFile);
        suppressSelectorEvents = false;

        final boolean renderable = previewTileFile != null && previewPaletteFile != null;
        tileCanvas.setVisible(renderable);
        tileCanvas.setManaged(renderable);
        emptyLabel.setVisible(!renderable);
        emptyLabel.setManaged(!renderable);
        updatePaletteStrip(previewPaletteFile);
        if (renderable) {
            renderTile(tileCanvas.getGraphicsContext2D(), previewTileFile, previewPaletteFile);
        } else {
            clearCanvas(tileCanvas.getGraphicsContext2D());
        }
    }

    private void updatePaletteStrip(AssetWorkspaceBankCompositionFile previewPaletteFile) {
        final List<Integer> colors = previewPaletteFile == null ? List.of() : paletteDisplayColors(previewPaletteFile.metadata());
        for (int index = 0; index < paletteStrip.getChildren().size(); index++) {
            final Region swatch = (Region) paletteStrip.getChildren().get(index);
            final int argb = colorAt(colors, index);
            swatch.setStyle("-fx-background-color: " + cssColor(argb) + ";");
        }
    }

    private void renderTile(GraphicsContext graphics, AssetWorkspaceBankCompositionFile tileFile, AssetWorkspaceBankCompositionFile paletteFile) {
        clearCanvas(graphics);
        final Map<String, Object> tile = nestedMap(tileFile.metadata(), "tile");
        if (tile == null) {
            return;
        }
        final Integer width = asInteger(tile.get("width"));
        final Integer height = asInteger(tile.get("height"));
        final List<Integer> indices = paletteIndices(tile.get("paletteIndices"));
        final List<Integer> colors = paletteDisplayColors(paletteFile.metadata());
        final List<Integer> sourceColors = tileDisplayColors(tileFile.metadata());
        final Map<String, Integer> schemaIndicesByColor = schemaIndicesByColor(paletteFile.metadata());
        if (width == null || height == null || width <= 0 || height <= 0 || indices.isEmpty()) {
            return;
        }

        final double pixelWidth = PREVIEW_SIZE / width;
        final double pixelHeight = PREVIEW_SIZE / height;
        for (int index = 0; index < indices.size(); index++) {
            final int paletteIndex = schemaPaletteIndex(indices.get(index), sourceColors, schemaIndicesByColor);
            final int x = index % width;
            final int y = index / width;
            graphics.setFill(toFxColor(paletteIndexToArgb(colors, paletteIndex)));
            graphics.fillRect(x * pixelWidth, y * pixelHeight, pixelWidth, pixelHeight);
        }
    }

    private void clearCanvas(GraphicsContext graphics) {
        final double cell = 12.0d;
        for (int y = 0; y < PREVIEW_SIZE / cell; y++) {
            for (int x = 0; x < PREVIEW_SIZE / cell; x++) {
                final boolean even = ((x + y) % 2) == 0;
                graphics.setFill(even
                        ? javafx.scene.paint.Color.rgb(20, 28, 38)
                        : javafx.scene.paint.Color.rgb(28, 36, 48));
                graphics.fillRect(x * cell, y * cell, cell, cell);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
        final Object value = metadata.get(key);
        if (value instanceof Map<?, ?> nested) {
            return (Map<String, Object>) nested;
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private List<Integer> paletteIndices(Object value) {
        if (value instanceof List<?> list) {
            return (List<Integer>) list;
        }
        return List.of();
    }

    @SuppressWarnings("unchecked")
    private List<Integer> paletteDisplayColors(Map<String, Object> metadata) {
        final Map<String, Object> palette = nestedMap(metadata, "palette");
        if (palette == null) {
            return List.of();
        }
        final Object converted = palette.get("convertedRgb565");
        if (converted instanceof List<?> list) {
            return ((List<Integer>) list).stream()
                    .map(this::rgb565ToArgb8888)
                    .toList();
        }
        return List.of();
    }

    @SuppressWarnings("unchecked")
    private List<Integer> tileDisplayColors(Map<String, Object> metadata) {
        final Map<String, Object> palette = nestedMap(metadata, "palette");
        if (palette == null) {
            return List.of();
        }
        final Object original = palette.get("originalArgb8888");
        if (original instanceof List<?> list) {
            return (List<Integer>) list;
        }
        return List.of();
    }

    private Map<String, Integer> schemaIndicesByColor(Map<String, Object> metadata) {
        final Object materialIndices = metadata.get("schemaMaterialIndices");
        if (!(materialIndices instanceof Map<?, ?> map)) {
            return Map.of();
        }
        final Map<String, Integer> indicesByColor = new java.util.LinkedHashMap<>();
        for (Map.Entry<?, ?> entry : map.entrySet()) {
            final Integer index = asInteger(entry.getKey());
            if (index == null || index <= 0 || !(entry.getValue() instanceof String color) || color.isBlank()) {
                continue;
            }
            indicesByColor.put(color.trim().toUpperCase(), index);
        }
        return Map.copyOf(indicesByColor);
    }

    private Integer asInteger(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        return null;
    }

    private int paletteIndexToArgb(List<Integer> colors, int paletteIndex) {
        if (paletteIndex <= 0) {
            return 0x00000000;
        }
        return colorAt(colors, paletteIndex - 1);
    }

    private int schemaPaletteIndex(int tilePaletteIndex, List<Integer> sourceColors, Map<String, Integer> schemaIndicesByColor) {
        if (tilePaletteIndex <= 0 || sourceColors.isEmpty() || schemaIndicesByColor.isEmpty()) {
            return tilePaletteIndex;
        }
        final int sourceColorIndex = tilePaletteIndex - 1;
        if (sourceColorIndex < 0 || sourceColorIndex >= sourceColors.size()) {
            return 0;
        }
        return schemaIndicesByColor.getOrDefault(cssColor(sourceColors.get(sourceColorIndex)), 0);
    }

    private int colorAt(List<Integer> colors, int index) {
        if (index < 0 || index >= colors.size()) {
            return 0x00000000;
        }
        final Integer color = colors.get(index);
        return color == null ? 0x00000000 : color;
    }

    private String cssColor(int argb) {
        final int alpha = (argb >>> 24) & 0xFF;
        if (alpha == 0) {
            return "transparent";
        }
        final int red = (argb >>> 16) & 0xFF;
        final int green = (argb >>> 8) & 0xFF;
        final int blue = argb & 0xFF;
        return String.format("#%02X%02X%02X", red, green, blue);
    }

    private javafx.scene.paint.Color toFxColor(int argb) {
        final int alpha = (argb >>> 24) & 0xFF;
        if (alpha == 0) {
            return javafx.scene.paint.Color.TRANSPARENT;
        }
        final int red = (argb >>> 16) & 0xFF;
        final int green = (argb >>> 8) & 0xFF;
        final int blue = argb & 0xFF;
        return javafx.scene.paint.Color.rgb(red, green, blue, alpha / 255.0d);
    }

    private int rgb565ToArgb8888(Integer rgb565) {
        if (rgb565 == null) {
            return 0x00000000;
        }
        final int value = rgb565 & 0xFFFF;
        final int red = ((value >> 11) & 0x1F) * 255 / 31;
        final int green = ((value >> 5) & 0x3F) * 255 / 63;
        final int blue = (value & 0x1F) * 255 / 31;
        return 0xFF000000 | (red << 16) | (green << 8) | blue;
    }
}
