/*
 *   This file is a part of the SteamWar software.
 *
 *   Copyright (C) 2025 SteamWar.de-Serverteam
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package de.steamwar.lobby.map;

import de.steamwar.lobby.LobbySystem;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.ItemFrame;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.MapCanvas;
import org.bukkit.map.MapPalette;
import org.bukkit.map.MapRenderer;
import org.bukkit.map.MapView;
import org.bukkit.util.Vector;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.*;

public class CustomMap implements Listener {

    public static void init() {
    }

    private static final CustomMap LEFT = new CustomMap(new File(System.getProperty("user.home") + "/lobbyBanner/left.png"),
            new Vector(2346, 48, 1297), new Vector(2345, 48, 1297), new Vector(2344, 48, 1297), new Vector(2343, 48, 1297), new Vector(2342, 48, 1297), new Vector(2341, 48, 1297), new Vector(2340, 48, 1297),
            new Vector(2346, 47, 1297), new Vector(2345, 47, 1297), new Vector(2344, 47, 1297), new Vector(2343, 47, 1297), new Vector(2342, 47, 1297), new Vector(2341, 47, 1297), new Vector(2340, 47, 1297),
            new Vector(2346, 46, 1297), new Vector(2345, 46, 1297), new Vector(2344, 46, 1297), new Vector(2343, 46, 1297), new Vector(2342, 46, 1297), new Vector(2341, 46, 1297), new Vector(2340, 46, 1297),
            new Vector(2346, 45, 1297), new Vector(2345, 45, 1297), new Vector(2344, 45, 1297), new Vector(2343, 45, 1297), new Vector(2342, 45, 1297), new Vector(2341, 45, 1297), new Vector(2340, 45, 1297)
    );

    private static final CustomMap RIGHT = new CustomMap(new File(System.getProperty("user.home") + "/lobbyBanner/right.png"),
            new Vector(2330, 48, 1297), new Vector(2329, 48, 1297), new Vector(2328, 48, 1297), new Vector(2327, 48, 1297), new Vector(2326, 48, 1297), new Vector(2325, 48, 1297), new Vector(2324, 48, 1297),
            new Vector(2330, 47, 1297), new Vector(2329, 47, 1297), new Vector(2328, 47, 1297), new Vector(2327, 47, 1297), new Vector(2326, 47, 1297), new Vector(2325, 47, 1297), new Vector(2324, 47, 1297),
            new Vector(2330, 46, 1297), new Vector(2329, 46, 1297), new Vector(2328, 46, 1297), new Vector(2327, 46, 1297), new Vector(2326, 46, 1297), new Vector(2325, 46, 1297), new Vector(2324, 46, 1297),
            new Vector(2330, 45, 1297), new Vector(2329, 45, 1297), new Vector(2328, 45, 1297), new Vector(2327, 45, 1297), new Vector(2326, 45, 1297), new Vector(2325, 45, 1297), new Vector(2324, 45, 1297)
    );

    private File mapFile;
    private Map<Vector, Integer> itemFrameIndex = new HashMap<>();
    private ItemFrame[] itemFrames;
    private long lastModified = Long.MAX_VALUE;

    public CustomMap(File mapFile, Vector... itemFrames) {
        this.mapFile = mapFile;
        this.itemFrames = new ItemFrame[itemFrames.length];
        for (int i = 0; i < itemFrames.length; i++) {
            itemFrameIndex.put(itemFrames[i], i);
        }

        Bukkit.getScheduler().runTaskTimer(LobbySystem.getInstance(), () -> {
            long modified = mapFile.lastModified();
            if (modified > lastModified) {
                lastModified = modified;
                System.out.println("Updating Banner: " + mapFile.getName());
                Bukkit.getScheduler().runTaskAsynchronously(LobbySystem.getInstance(), () -> {
                    try {
                        run();
                    } catch (IOException e) {
                        // Ignore
                    }
                });
            }
        }, 200L, 200L);
        Bukkit.getPluginManager().registerEvents(this, LobbySystem.getInstance());
    }

    @EventHandler
    public void onChunkLoad(ChunkLoadEvent event) {
        for (Entity entity : event.getChunk().getEntities()) {
            if (!(entity instanceof ItemFrame)) continue;
            ItemFrame itemFrame = (ItemFrame) entity;
            Vector vector = itemFrame.getLocation().getBlock().getLocation().toVector();
            if (itemFrameIndex.containsKey(vector)) {
                if (itemFrames[itemFrameIndex.get(vector)] != null) continue;
                itemFrames[itemFrameIndex.get(vector)] = itemFrame;
                lastModified = 0;

                ItemStack itemStack = new ItemStack(Material.FILLED_MAP, 1);
                MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
                mapMeta.setMapView(Bukkit.createMap(itemFrame.getWorld()));
                itemStack.setItemMeta(mapMeta);
                itemFrame.setItem(itemStack);

                MapView mapView = ((MapMeta) itemFrame.getItem().getItemMeta()).getMapView();
                new ArrayList<>(mapView.getRenderers()).forEach(mapView::removeRenderer);
                mapView.addRenderer(new MapRenderer() {
                    @Override
                    public void render(MapView map, MapCanvas canvas, Player player) {
                        for (int x = 0; x < 128; x++) {
                            for (int y = 0; y < 128; y++) {
                                canvas.setPixel(x, y, (byte) 32);
                            }
                        }
                    }
                });
            }
        }
    }

    private void run() throws IOException {
        BufferedImage bufferedImage = ImageIO.read(mapFile);

        Set<Integer>[] patches = new Set[256];
        for (int patch = 0; patch < patches.length; patch++) {
            patches[patch] = new HashSet<>();
        }

        for (int y = 0; y < bufferedImage.getHeight(); y++) {
            for (int x = 0; x < bufferedImage.getWidth(); x++) {
                Color color = new Color(bufferedImage.getRGB(x, y));
                double red = color.getRed() / 255.0;
                double green = color.getGreen() / 255.0;
                double blue = color.getBlue() / 255.0;
                double luminance = Math.sqrt(0.299 * red * red + 0.587 * green * green + 0.114 * blue * blue);
                luminance *= 255;
                patches[(int) luminance].add(x << 16 | y);
            }
        }

        for (int patch = 0; patch < patches.length; patch++) {
            Set<Integer> points = patches[patch];
            if (points.isEmpty()) continue;
            BufferedImage patchImage = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
            points.forEach(point -> {
                int x = point >>> 16;
                int y = point & 0xFFFF;
                patchImage.setRGB(x, y, bufferedImage.getRGB(x, y));
            });
            floodFill(patchImage);
            dither(patchImage);
            points.forEach(point -> {
                int x = point >>> 16;
                int y = point & 0xFFFF;
                bufferedImage.setRGB(x, y, patchImage.getRGB(x, y));
            });
        }

        for (int y = 0; y < bufferedImage.getHeight(); y += 128) {
            for (int x = 0; x < bufferedImage.getWidth(); x += 128) {
                ItemFrame itemFrame = itemFrames[y / 128 * 7 + x / 128];
                if (itemFrame == null) continue;
                int finalX = x;
                int finalY = y;
                Bukkit.getScheduler().runTaskLater(LobbySystem.getInstance(), () -> {
                    ItemStack itemStack = itemFrame.getItem();
                    MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
                    MapView mapView = mapMeta.getMapView();
                    new ArrayList<>(mapView.getRenderers()).forEach(mapView::removeRenderer);
                    mapView.addRenderer(new MapRenderer() {
                        @Override
                        public void render(MapView map, MapCanvas canvas, Player player) {
                            for (int dy = 0; dy < 128; dy++) {
                                for (int dx = 0; dx < 128; dx++) {
                                    int ax = dx + finalX;
                                    int ay = dy + finalY;
                                    Color color = new Color(bufferedImage.getRGB(ax, ay));
                                    canvas.setPixel(dx, dy, ColorInit.getColorByte(color.getRed(), color.getGreen(), color.getBlue()));
                                }
                            }
                        }
                    });

                    mapMeta.setMapView(mapView);
                    itemStack.setItemMeta(mapMeta);
                    itemFrame.setItem(itemStack);
                }, 1);
            }
        }
    }

    private static void floodFill(BufferedImage bufferedImage) {
        WritableRaster alpha = bufferedImage.getAlphaRaster();
        int[] data = alpha.getPixels(0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), new int[bufferedImage.getWidth() * bufferedImage.getHeight()]);
        for (int i = 0; i < 5; i++) {
            Set<Integer> changes = new HashSet<>();
            for (int y = 0; y < bufferedImage.getHeight(); y++) {
                for (int x = 0; x < bufferedImage.getWidth(); x++) {
                    if (data[y * bufferedImage.getWidth() + x] == 0) continue;
                    int color = bufferedImage.getRGB(x, y);
                    if (x > 0 && data[y * bufferedImage.getWidth() + x - 1] == 0) {
                        bufferedImage.setRGB(x - 1, y, color);
                        changes.add((x - 1) << 16 | y);
                    }
                    if (x < bufferedImage.getWidth() - 1 && data[y * bufferedImage.getWidth() + x + 1] == 0) {
                        bufferedImage.setRGB(x + 1, y, color);
                        changes.add((x + 1) << 16 | y);
                    }
                    if (y > 0 && data[(y - 1) * bufferedImage.getWidth() + x] == 0) {
                        bufferedImage.setRGB(x, y - 1, color);
                        changes.add(x << 16 | (y - 1));
                    }
                    if (y < bufferedImage.getHeight() - 1 && data[(y + 1) * bufferedImage.getWidth() + x] == 0) {
                        bufferedImage.setRGB(x, y + 1, color);
                        changes.add(x << 16 | (y + 1));
                    }
                }
            }
            if (changes.isEmpty()) return;
            changes.forEach(point -> {
                int x = point >>> 16;
                int y = point & 0xFFFF;
                data[y * bufferedImage.getWidth() + x] = 255;
            });
        }
    }

    private static BufferedImage dither(BufferedImage image) {
        final double multiplier1 = 7 / 48.0;
        final double multiplier2 = 3 / 48.0;
        final double multiplier3 = 5 / 48.0;
        final double multiplier4 = 1 / 48.0;
        WritableRaster alphaRaster = image.getAlphaRaster();
        WritableRaster raster = image.getRaster();
        int numBands = raster.getNumBands();
        int[] pixels = raster.getPixels(0, 0, image.getWidth(), image.getHeight(), new int[image.getWidth() * image.getHeight() * numBands]);

        int width = image.getWidth();
        int height = image.getHeight();

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (alphaRaster.getPixel(x, y, new int[1])[0] == 0) continue;

                int red = pixels[(y * width + x) * numBands];
                int i2 = (y * width + x) * numBands + 1;
                int green = pixels[i2];
                int i3 = (y * width + x) * numBands + 2;
                int blue = pixels[i3];
                Color nearest = MapPalette.getColor(ColorInit.getColorByte(red, green, blue));

                pixels[(y * width + x) * numBands] = nearest.getRed();
                pixels[i2] = nearest.getGreen();
                pixels[i3] = nearest.getBlue();

                int quantErrorRed = red - nearest.getRed();
                int quantErrorGreen = green - nearest.getGreen();
                int quantErrorBlue = blue - nearest.getBlue();

                int mr1 = (int) (quantErrorRed * multiplier1);
                int mg1 = (int) (quantErrorGreen * multiplier1);
                int mb1 = (int) (quantErrorBlue * multiplier1);

                int mr2 = (int) (quantErrorRed * multiplier2);
                int mg2 = (int) (quantErrorGreen * multiplier2);
                int mb2 = (int) (quantErrorBlue * multiplier2);

                int mr3 = (int) (quantErrorRed * multiplier3);
                int mg3 = (int) (quantErrorGreen * multiplier3);
                int mb3 = (int) (quantErrorBlue * multiplier3);

                int mr4 = (int) (quantErrorRed * multiplier4);
                int mg4 = (int) (quantErrorGreen * multiplier4);
                int mb4 = (int) (quantErrorBlue * multiplier4);

                if (x < width - 1) {
                    int i = (y * width + x + 1) * numBands;
                    pixels[i] = clamp(pixels[i] + mr1);
                    pixels[i + 1] = clamp(pixels[i + 1] + mg1);
                    pixels[i + 2] = clamp(pixels[i + 2] + mb1);
                }

                if (x < width - 2) {
                    int i = (y * width + x + 2) * numBands;
                    pixels[i] = clamp(pixels[i] + mr3);
                    pixels[i + 1] = clamp(pixels[i + 1] + mg3);
                    pixels[i + 2] = clamp(pixels[i + 2] + mb3);
                }

                if (y < height - 1) {
                    if (x > 1) {
                        int i = ((y + 1) * width + x - 2) * numBands;
                        pixels[i] = clamp(pixels[i] + mr2);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg2);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb2);
                    }
                    if (x > 0) {
                        int i = ((y + 1) * width + x - 1) * numBands;
                        pixels[i] = clamp(pixels[i] + mr3);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg3);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb3);
                    }
                    if (x < width - 1) {
                        int i = ((y + 1) * width + x + 1) * numBands;
                        pixels[i] = clamp(pixels[i] + mr3);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg3);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb3);
                    }
                    if (x < width - 2) {
                        int i = ((y + 1) * width + x + 2) * numBands;
                        pixels[i] = clamp(pixels[i] + mr2);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg2);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb2);
                    }
                    int i = (y * width + x) * numBands;
                    pixels[i] = clamp(pixels[i] + mr1);
                    pixels[i + 1] = clamp(pixels[i + 1] + mg1);
                    pixels[i + 2] = clamp(pixels[i + 2] + mb1);
                }

                if (y < height - 2) {
                    if (x > 1) {
                        int i = ((y + 2) * width + x - 2) * numBands;
                        pixels[i] = clamp(pixels[i] + mr4);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg4);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb4);
                    }
                    if (x > 0) {
                        int i = ((y + 2) * width + x - 1) * numBands;
                        pixels[i] = clamp(pixels[i] + mr2);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg2);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb2);
                    }
                    if (x < width - 1) {
                        int i = ((y + 2) * width + x + 1) * numBands;
                        pixels[i] = clamp(pixels[i] + mr2);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg2);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb2);
                    }
                    if (x < width - 2) {
                        int i = ((y + 2) * width + x + 2) * numBands;
                        pixels[i] = clamp(pixels[i] + mr4);
                        pixels[i + 1] = clamp(pixels[i + 1] + mg4);
                        pixels[i + 2] = clamp(pixels[i + 2] + mb4);
                    }
                    int i = (y * width + x) * numBands;
                    pixels[i] = clamp(pixels[i] + mr3);
                    pixels[i + 1] = clamp(pixels[i + 1] + mg3);
                    pixels[i + 2] = clamp(pixels[i + 2] + mb3);
                }
            }
        }
        raster.setPixels(0, 0, width, height, pixels);
        return image;
    }

    private static int clamp(int value) {
        return Math.max(0, Math.min(value, 255));
    }
}
