package org.mutterer.imagej1;

import ij.IJ;
import ij.ImagePlus;
import ij.Macro;
import ij.WindowManager;
import ij.gui.ImageCanvas;
import ij.gui.ImageWindow;
import ij.gui.Line;
import ij.gui.OvalRoi;
import ij.gui.Overlay;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.gui.TextRoi;
import ij.plugin.PlugIn;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.image.ColorModel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

public class Asteroids_ implements PlugIn {

    private static final String OVERLAY_TYPE_PROPERTY = "overlayType";
    private static final String TYPE_0 = "0";
    private static final String TYPE_1 = "1";
    private static final String TYPE_2 = "2";
    private static final String TYPE_4 = "4";
    private static final String TYPE_0_KIND_PROPERTY = "type0Kind";
    private static final String TYPE_0_KIND_GOOD = "good";
    private static final String TYPE_0_KIND_BAD = "bad";
    private static final String TYPE_0_KIND_BLUE = "blue";
    private static final String SETUP_GAME_MACRO_RESOURCE_PATH = "/setup_game.ijm";
    private static final String TYPE_4_PANEL_FONT_RESOURCE_NAME = "Commodore Pixelized v1.2.ttf";
    private static final String TYPE_4_PANEL_FONT_RESOURCE_PATH = "/" + TYPE_4_PANEL_FONT_RESOURCE_NAME;
    private static final double TYPE_1_LENGTH = 30.0;
    private static final double TYPE_1_BASE_WIDTH = 20.0;
    private static final double TYPE_1_INITIAL_ANGLE = -Math.PI / 2.0;
    private static final double TYPE_1_ROTATION_STEP = Math.PI / 15.0;
    private static final double TYPE_1_THRUST_STEP = 0.7;
    private static final double TYPE_1_INITIAL_THRUST = 0.4;
    private static final double TYPE_2_LENGTH = 12.0;
    private static final double TYPE_2_SPEED_PER_FRAME = 10.0;
    private static final int TYPE_0_FLASH_FRAMES = 3;
    private static final String START_INSTRUCTIONS = "** ASTEROIDS GAME **\n\n"
            + "Shoot green asteroids\n\n"
            + "Move ship:\nLEFT/RIGHT: rotate\nUP: thrust\n"
            + "SPACE: Fire\n\n"
            + "Press P to start or pause";
    private static final String PAUSED_TEXT = "PAUSED\nPress P to resume";
    private static volatile Thread animationThread;
    private static volatile boolean animationRunning;
    private static volatile boolean gamePaused;
    private static volatile boolean gameOver;
    private static volatile String pauseOverlayText = PAUSED_TEXT;
    private static volatile int ships = 3;
    private static volatile int score = 0;
    private static volatile int goodType0Remaining = 0;
    private static final int TARGET_FPS = 30;
    private static volatile ImageWindow boundWindow;
    private static volatile Component boundCanvas;
    private static volatile KeyEventDispatcher boundKeyDispatcher;
    private static volatile Overlay activeAnimatedOverlay;
    private static volatile ImagePlus activeImage;
    private static volatile int type1OverlayIndex = -1;
    private static volatile double type1CenterX;
    private static volatile double type1CenterY;
    private static volatile double type1DirectionAngle = TYPE_1_INITIAL_ANGLE;
    private static volatile double type1VelocityX;
    private static volatile double type1VelocityY;
    private static volatile List<LaserItem> activeType2Lasers = new ArrayList<>();
    private static volatile int lutFlashFramesRemaining = 0;
    private static volatile ColorModel previousLutColorModel;
    private static volatile boolean debugLoggingEnabled;
    private static final Object overlayLock = new Object();
    private static final Random RNG = new Random();
    private static final Font TYPE_4_PANEL_BASE_FONT = loadType4PanelBaseFont();

    @Override
    public void run(String arg) {
        String macroOptions = Macro.getOptions();
        debugLoggingEnabled = isDebugEnabled(arg) || isDebugEnabled(macroOptions);
        debug_log("run arg='" + String.valueOf(arg) + "' macroOptions='" + String.valueOf(macroOptions) + "'");

        if (WindowManager.getImageCount() < 1.0)
            runSetupGameMacroFromResources();

        ImagePlus imp = WindowManager.getCurrentImage();

        RNG.setSeed(42L);

        activateFrontmostWindow(imp);

        Overlay sourceOverlay = imp.getOverlay();
        if (sourceOverlay == null || sourceOverlay.size() == 0) {
            Overlay demoOverlay = new Overlay();
            int width = imp.getWidth();
            int height = imp.getHeight();
            for (int i = 0; i < 10; i++) {
                int diameter = 10 + RNG.nextInt(41);
                int maxX = Math.max(0, width - diameter);
                int maxY = Math.max(0, height - diameter);
                int x = RNG.nextInt(maxX + 1);
                int y = RNG.nextInt(maxY + 1);

                OvalRoi circle = new OvalRoi(x, y, diameter, diameter);
                circle.setStrokeColor(Color.YELLOW);
                circle.setStrokeWidth(1.0);
                circle.setName("item-" + (i + 1));
                circle.setProperty(OVERLAY_TYPE_PROPERTY, TYPE_0);
                demoOverlay.add(circle);
            }

            imp.setOverlay(demoOverlay);
            sourceOverlay = demoOverlay;
        }

        initializeOverlayTypes(sourceOverlay);
        addOrUpdateType1Triangle(sourceOverlay, imp);

        bindKeyControls(imp);
        startAnimation(imp, sourceOverlay);
    }

    private static boolean isDebugEnabled(String arg) {
        if (arg == null) {
            return false;
        }

        String normalized = arg.trim().toLowerCase();
        if (normalized.isEmpty()) {
            return false;
        }

        if ("debug".equals(normalized)) {
            return true;
        }

        // Support macro/plugin arg styles like "debug=true", "mode=debug", or token lists.
        if (normalized.contains("debug=true") || normalized.contains("debug=1")
                || normalized.contains("debug=yes") || normalized.contains("debug=on")) {
            return true;
        }

        String[] tokens = normalized.split("[\\s,;]+");
        for (String token : tokens) {
            if ("debug".equals(token)) {
                return true;
            }
        }

        return false;
    }

    private void runSetupGameMacroFromResources() {
        String codeSourcePath="";
        try {
            java.net.URL location = Asteroids_.class.getProtectionDomain().getCodeSource().getLocation();
            codeSourcePath = new java.io.File(location.toURI()).getAbsolutePath();
            debug_log("Class code source path: " + codeSourcePath);
        } catch (Exception e) {
            debug_log("Could not resolve class code source filesystem path: " + e.getMessage());
        }
        debug_log("Resolved /setup_game.ijm URL: " + Asteroids_.class.getResource(SETUP_GAME_MACRO_RESOURCE_PATH));

        try (InputStream in = Asteroids_.class.getResourceAsStream(SETUP_GAME_MACRO_RESOURCE_PATH)) {
            if (in == null) {
                debug_log("Missing macro resource: /setup_game.ijm");
                return;
            }
            IJ.runMacro(readUtf8(in),codeSourcePath);
            
        } catch (IOException e) {
            debug_log("Failed to run setup_game.ijm: " + e.getMessage());
        }
    }

    private static void debug_log(String message) {
        if (debugLoggingEnabled) {
            IJ.log(message);
        }
    }

    private String readUtf8(InputStream in) throws IOException {
        StringBuilder builder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line).append('\n');
            }
        }
        return builder.toString();
    }

    private void activateFrontmostWindow(ImagePlus imp) {
        ImageWindow window = imp.getWindow();
        if (window == null) {
            return;
        }

        EventQueue.invokeLater(() -> {
            window.toFront();
            window.requestFocus();
            ImageCanvas canvas = window.getCanvas();
            if (canvas != null) {
                canvas.requestFocusInWindow();
            }
        });
    }

    private void startAnimation(ImagePlus imp, Overlay sourceOverlay) {
        stopAnimation();

        final int imageWidth = imp.getWidth();
        final int imageHeight = imp.getHeight();
        final double centerX = imageWidth / 2.0;
        final double centerY = imageHeight / 2.0;

        final Overlay animatedOverlay = new Overlay();
        final List<AnimatedItem> items = new ArrayList<>();
        final List<LaserItem> lasers = new ArrayList<>();
        int localType1Index = -1;

        for (int i = 0; i < sourceOverlay.size(); i++) {
            Roi original = sourceOverlay.get(i);
            if (original == null) {
                continue;
            }

            Roi copy = (Roi) original.clone();
            String type = getOverlayType(copy);
            Rectangle bounds = copy.getBounds();

            if (TYPE_0.equals(type)) {
                double roiCenterX = bounds.x + bounds.width / 2.0;
                double roiCenterY = bounds.y + bounds.height / 2.0;
                double dirX = roiCenterX - centerX;
                double dirY = roiCenterY - centerY;
                double norm = Math.sqrt(dirX * dirX + dirY * dirY);

                if (norm < 1e-6) {
                    double angle = (2.0 * Math.PI * i) / Math.max(1, sourceOverlay.size());
                    dirX = Math.cos(angle);
                    dirY = Math.sin(angle);
                } else {
                    dirX /= norm;
                    dirY /= norm;
                }

                double speedPxPerSec = 20.0 + (i % 7) * 4.0;
                double vx = dirX * (speedPxPerSec / TARGET_FPS);
                double vy = dirY * (speedPxPerSec / TARGET_FPS);

                items.add(new AnimatedItem(animatedOverlay.size(), copy, bounds.x, bounds.y, bounds.width,
                        bounds.height, vx, vy));
            } else if (TYPE_1.equals(type)) {
                localType1Index = animatedOverlay.size();
            }
            animatedOverlay.add(copy);
        }

        fitType4TextToImageWidth(animatedOverlay, imageWidth);
        centerType4OverlayItems(animatedOverlay, imageWidth, imageHeight);

        if (items.isEmpty() && localType1Index < 0) {
            IJ.showStatus("No animatable overlay items available.");
            return;
        }

        activeAnimatedOverlay = animatedOverlay;
        activeImage = imp;
        type1OverlayIndex = localType1Index;
        activeType2Lasers = lasers;
        ships = 3;
        score = 0;
        goodType0Remaining = countGoodType0(items);
        gamePaused = true;
        gameOver = false;
        pauseOverlayText = START_INSTRUCTIONS;
        updateSliceMetadata(imp);

        animationRunning = true;
        animationThread = new Thread(() -> {
            final long frameMillis = 1000L / TARGET_FPS;
            IJ.showStatus("Paused: " + imp.getTitle() + " (press P to start)");

            while (animationRunning && !Thread.currentThread().isInterrupted()) {
                if (imp.getWindow() == null) {
                    break;
                }

                long frameStart = System.currentTimeMillis();

                synchronized (overlayLock) {
                    int panelIndex = findOverlayIndexByType(animatedOverlay, TYPE_4);
                    if (gamePaused) {
                        if (panelIndex < 0) {
                            animatedOverlay.add(buildOverlayPanel(pauseOverlayText, Color.YELLOW));
                        }
                    } else {
                        if (panelIndex >= 0) {
                            removeOverlayAt(panelIndex, items, lasers, animatedOverlay);
                        }

                        for (int i = items.size() - 1; i >= 0; i--) {
                            AnimatedItem item = items.get(i);
                            if (item.flashFramesRemaining > 0) {
                                item.flashFramesRemaining--;
                                if (item.flashFramesRemaining == 0) {
                                    removeAnimatedItemAt(i, items, lasers, animatedOverlay);
                                }
                                continue;
                            }

                            item.x += item.vx;
                            item.y += item.vy;

                            if (item.x > imageWidth) {
                                item.x = -item.width;
                            } else if (item.x + item.width < 0) {
                                item.x = imageWidth;
                            }

                            if (item.y > imageHeight) {
                                item.y = -item.height;
                            } else if (item.y + item.height < 0) {
                                item.y = imageHeight;
                            }

                            item.roi.setLocation((int) Math.round(item.x), (int) Math.round(item.y));
                        }

                        if (type1OverlayIndex >= 0 && type1OverlayIndex < animatedOverlay.size()) {
                            type1CenterX += type1VelocityX;
                            type1CenterY += type1VelocityY;

                            double forwardExtent = (2.0 * TYPE_1_LENGTH) / 3.0;
                            double backwardExtent = TYPE_1_LENGTH / 3.0;
                            double verticalExtent = TYPE_1_BASE_WIDTH / 2.0;
                            double halfExtent = Math.max(forwardExtent, Math.max(backwardExtent, verticalExtent));

                            if (type1CenterX > imageWidth + halfExtent) {
                                type1CenterX = -halfExtent;
                            } else if (type1CenterX < -halfExtent) {
                                type1CenterX = imageWidth + halfExtent;
                            }

                            if (type1CenterY > imageHeight + halfExtent) {
                                type1CenterY = -halfExtent;
                            } else if (type1CenterY < -halfExtent) {
                                type1CenterY = imageHeight + halfExtent;
                            }

                            Roi movedTriangle = createType1Triangle(type1CenterX, type1CenterY, type1DirectionAngle);
                            animatedOverlay.set(movedTriangle, type1OverlayIndex);
                        }

                        for (int i = lasers.size() - 1; i >= 0; i--) {
                            LaserItem laser = lasers.get(i);
                            laser.x1 += laser.vx;
                            laser.y1 += laser.vy;
                            laser.x2 += laser.vx;
                            laser.y2 += laser.vy;

                            int hitItemIndex = findType0HitAt(laser.x2, laser.y2, items);
                            if (hitItemIndex >= 0) {
                                handleType0Hit(hitItemIndex, items, imp);
                                flashType0Item(hitItemIndex, items);
                                removeLaserAt(i, lasers, items, animatedOverlay);
                                continue;
                            }

                            boolean outOfBounds = laser.x2 < 0 || laser.x2 >= imageWidth || laser.y2 < 0
                                    || laser.y2 >= imageHeight;
                            if (outOfBounds) {
                                removeLaserAt(i, lasers, items, animatedOverlay);
                                continue;
                            }

                            Roi movedLaser = createType2LaserRoi(laser.x1, laser.y1, laser.x2, laser.y2);
                            animatedOverlay.set(movedLaser, laser.overlayIndex);
                        }
                    }

                    updateLutFlashState(imp);
                    fitType4TextToImageWidth(animatedOverlay, imageWidth);
                    centerType4OverlayItems(animatedOverlay, imageWidth, imageHeight);
                }

                EventQueue.invokeLater(() -> {
                    if (imp.getWindow() != null) {
                        imp.setOverlay(animatedOverlay);
                        imp.updateAndDraw();
                    }
                });

                long elapsed = System.currentTimeMillis() - frameStart;
                long sleepMillis = frameMillis - elapsed;
                if (sleepMillis > 0) {
                    try {
                        Thread.sleep(sleepMillis);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }

            animationRunning = false;
            activeAnimatedOverlay = null;
            activeImage = null;
            type1OverlayIndex = -1;
            type1VelocityX = 0.0;
            type1VelocityY = 0.0;
            gamePaused = false;
            gameOver = false;
            pauseOverlayText = PAUSED_TEXT;
            goodType0Remaining = 0;
            restorePreviousLutIfNeeded(imp);
            activeType2Lasers = new ArrayList<>();

            EventQueue.invokeLater(() -> {
                if (imp.getWindow() != null) {
                    imp.setOverlay(new Overlay());
                    imp.updateAndDraw();
                }
            });

            IJ.showStatus("Animation stopped on: " + imp.getTitle());
        }, "OverlayAnimator");

        animationThread.setDaemon(true);
        animationThread.start();
    }

    private void stopAnimation() {
        animationRunning = false;
        gamePaused = false;
        gameOver = false;
        pauseOverlayText = PAUSED_TEXT;
        goodType0Remaining = 0;
        Thread runningThread = animationThread;
        if (runningThread != null && runningThread.isAlive()) {
            runningThread.interrupt();
        }
        animationThread = null;
    }

    private void bindKeyControls(ImagePlus imp) {
        unbindKeyControls();

        ImageWindow window = imp.getWindow();
        if (window == null) {
            return;
        }

        Map<Integer, KeyBinding> keyActions = createKeyActions(imp);

        KeyEventDispatcher dispatcher = e -> {
            if (e.getID() != KeyEvent.KEY_PRESSED) {
                return false;
            }
            if (boundWindow == null || !boundWindow.isFocused()) {
                return false;
            }

            KeyBinding binding = keyActions.get(e.getKeyCode());
            if (binding == null) {
                return false;
            }

            binding.action.run();
            if (binding.consumeEvent) {
                e.consume();
                return true;
            }
            return false;
        };

        KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(dispatcher);

        ImageCanvas canvas = window.getCanvas();
        if (canvas != null) {
            canvas.setFocusable(true);
            canvas.requestFocusInWindow();
            boundCanvas = canvas;
        }

        boundWindow = window;
        boundKeyDispatcher = dispatcher;
    }

    private Map<Integer, KeyBinding> createKeyActions(ImagePlus imp) {
        Map<Integer, KeyBinding> keyActions = new HashMap<>();
        keyActions.put(KeyEvent.VK_ESCAPE, new KeyBinding(() -> {
            stopAnimation();
            IJ.showStatus("Animation stopped by ESC: " + imp.getTitle());
        }, true));
        keyActions.put(KeyEvent.VK_LEFT, new KeyBinding(() -> rotateType1(-TYPE_1_ROTATION_STEP), true));
        keyActions.put(KeyEvent.VK_RIGHT, new KeyBinding(() -> rotateType1(TYPE_1_ROTATION_STEP), true));
        keyActions.put(KeyEvent.VK_UP, new KeyBinding(this::applyType1Thrust, true));
        keyActions.put(KeyEvent.VK_SPACE, new KeyBinding(this::fireType2Laser, true));
        keyActions.put(KeyEvent.VK_P, new KeyBinding(() -> togglePause(imp), true));
        keyActions.put(KeyEvent.VK_DOWN, new KeyBinding(() -> {
        }, true));
        return keyActions;
    }

    private void togglePause(ImagePlus imp) {
        synchronized (overlayLock) {
            if (gameOver) {
                IJ.showStatus("Game over: " + imp.getTitle() + " (press ESC)");
                return;
            }
            gamePaused = !gamePaused;
            if (gamePaused) {
                pauseOverlayText = PAUSED_TEXT;
            }
        }
        IJ.showStatus((gamePaused ? "Paused: " : "Resumed: ") + imp.getTitle());
    }

    private void unbindKeyControls() {
        KeyEventDispatcher dispatcher = boundKeyDispatcher;
        if (dispatcher != null) {
            KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(dispatcher);
        }

        Component canvas = boundCanvas;
        if (canvas != null) {
            canvas.setFocusable(true);
        }

        boundWindow = null;
        boundCanvas = null;
        boundKeyDispatcher = null;
    }

    private void initializeOverlayTypes(Overlay overlay) {
        List<Roi> type0Rois = new ArrayList<>();
        for (int i = 0; i < overlay.size(); i++) {
            Roi roi = overlay.get(i);
            if (roi != null) {
                if (roi.getProperty(OVERLAY_TYPE_PROPERTY) == null) {
                    roi.setProperty(OVERLAY_TYPE_PROPERTY, TYPE_0);
                }
                if (!TYPE_0.equals(getOverlayType(roi))) {
                    continue;
                }
                type0Rois.add(roi);
            }
        }

        if (type0Rois.isEmpty()) {
            return;
        }

        boolean[] isBad = new boolean[type0Rois.size()];
        int badCount = 0;
        for (int i = 0; i < type0Rois.size(); i++) {
            if (RNG.nextDouble() < 0.1) {
                isBad[i] = true;
                badCount++;
            }
        }

        int minBad = Math.min(6, type0Rois.size());
        while (badCount < minBad) {
            int idx = RNG.nextInt(type0Rois.size());
            if (!isBad[idx]) {
                isBad[idx] = true;
                badCount++;
            }
        }

        for (int i = 0; i < type0Rois.size(); i++) {
            Roi roi = type0Rois.get(i);
            if (isBad[i]) {
                roi.setFillColor(new Color(255, 0, 0, 220));
                roi.setStrokeColor(Color.RED);
                roi.setProperty(TYPE_0_KIND_PROPERTY, TYPE_0_KIND_BAD);
            } else {
                roi.setFillColor(new Color(0, 255, 0, 220));
                roi.setStrokeColor(Color.GREEN);
                roi.setProperty(TYPE_0_KIND_PROPERTY, TYPE_0_KIND_GOOD);
            }
            roi.setStrokeWidth(2.0);
        }
    }

    private String getOverlayType(Roi roi) {
        String type = roi.getProperty(OVERLAY_TYPE_PROPERTY);
        return type == null ? TYPE_0 : type;
    }

    private void addOrUpdateType1Triangle(Overlay overlay, ImagePlus imp) {
        type1CenterX = imp.getWidth() / 2.0;
        type1CenterY = imp.getHeight() / 2.0;
        type1DirectionAngle = TYPE_1_INITIAL_ANGLE;
        type1VelocityX = Math.cos(type1DirectionAngle) * TYPE_1_INITIAL_THRUST;
        type1VelocityY = Math.sin(type1DirectionAngle) * TYPE_1_INITIAL_THRUST;

        Roi triangle = createType1Triangle(type1CenterX, type1CenterY, type1DirectionAngle);

        for (int i = 0; i < overlay.size(); i++) {
            Roi existing = overlay.get(i);
            if (existing != null && TYPE_1.equals(getOverlayType(existing))) {
                overlay.set(triangle, i);
                return;
            }
        }

        overlay.add(triangle);
    }

    private Roi createType1Triangle(double centerX, double centerY, double directionAngle) {
        double tipFromCenter = (2.0 * TYPE_1_LENGTH) / 3.0;
        double baseFromCenter = TYPE_1_LENGTH / 3.0;
        double halfBase = TYPE_1_BASE_WIDTH / 2.0;

        double[] localX = { tipFromCenter, -baseFromCenter, -baseFromCenter };
        double[] localY = { 0.0, -halfBase, halfBase };

        int[] x = new int[3];
        int[] y = new int[3];
        double cos = Math.cos(directionAngle);
        double sin = Math.sin(directionAngle);

        for (int i = 0; i < 3; i++) {
            double rx = localX[i] * cos - localY[i] * sin;
            double ry = localX[i] * sin + localY[i] * cos;
            x[i] = (int) Math.round(centerX + rx);
            y[i] = (int) Math.round(centerY + ry);
        }

        PolygonRoi triangle = new PolygonRoi(x, y, 3, Roi.POLYGON);

        triangle.setFillColor(new Color(255, 105, 0, 80));
        triangle.setStrokeColor(new Color(255, 105, 0));
        triangle.setStrokeWidth(2.0);
        triangle.setName("type-1-triangle");
        triangle.setProperty(OVERLAY_TYPE_PROPERTY, TYPE_1);
        return triangle;
    }

    private void rotateType1(double deltaAngle) {
        Overlay overlay = activeAnimatedOverlay;
        ImagePlus imp = activeImage;
        int triangleIndex = type1OverlayIndex;

        if (gamePaused || overlay == null || imp == null || triangleIndex < 0 || triangleIndex >= overlay.size()) {
            return;
        }

        type1DirectionAngle += deltaAngle;
        Roi updatedTriangle = createType1Triangle(type1CenterX, type1CenterY, type1DirectionAngle);
        overlay.set(updatedTriangle, triangleIndex);

        EventQueue.invokeLater(() -> {
            if (imp.getWindow() != null) {
                imp.setOverlay(overlay);
                imp.updateAndDraw();
            }
        });
    }

    private void applyType1Thrust() {
        if (gamePaused || type1OverlayIndex < 0 || activeImage == null || activeAnimatedOverlay == null) {
            return;
        }

        type1VelocityX += Math.cos(type1DirectionAngle) * TYPE_1_THRUST_STEP;
        type1VelocityY += Math.sin(type1DirectionAngle) * TYPE_1_THRUST_STEP;
    }

    private void fireType2Laser() {
        Overlay overlay = activeAnimatedOverlay;
        ImagePlus imp = activeImage;
        List<LaserItem> lasers = activeType2Lasers;

        if (gamePaused || overlay == null || imp == null || type1OverlayIndex < 0 || lasers == null) {
            return;
        }

        double dirX = Math.cos(type1DirectionAngle);
        double dirY = Math.sin(type1DirectionAngle);
        double tipFromCenter = (2.0 * TYPE_1_LENGTH) / 3.0;

        double startX = type1CenterX + dirX * tipFromCenter;
        double startY = type1CenterY + dirY * tipFromCenter;
        double endX = startX + dirX * TYPE_2_LENGTH;
        double endY = startY + dirY * TYPE_2_LENGTH;

        synchronized (overlayLock) {
            Roi laserRoi = createType2LaserRoi(startX, startY, endX, endY);
            overlay.add(laserRoi);
            int overlayIndex = overlay.size() - 1;

            double vx = dirX * TYPE_2_SPEED_PER_FRAME;
            double vy = dirY * TYPE_2_SPEED_PER_FRAME;
            lasers.add(new LaserItem(overlayIndex, startX, startY, endX, endY, vx, vy));
        }
    }

    private Roi createType2LaserRoi(double x1, double y1, double x2, double y2) {
        Line laser = new Line(x1, y1, x2, y2);
        laser.setStrokeColor(Color.RED);
        laser.setStrokeWidth(2.0);
        laser.setProperty(OVERLAY_TYPE_PROPERTY, TYPE_2);
        return laser;
    }

    private TextRoi buildOverlayPanel(String text, Color color) {
        TextRoi panel = new TextRoi(0, 0, text, TYPE_4_PANEL_BASE_FONT.deriveFont(22f));
        panel.setStrokeColor(color);
        panel.setFillColor(new Color(80, 80, 80, 170));
        panel.setProperty(OVERLAY_TYPE_PROPERTY, TYPE_4);
        panel.setName("type-4-panel");
        return panel;
    }

    private static Font loadType4PanelBaseFont() {
        final String resourcePath = TYPE_4_PANEL_FONT_RESOURCE_PATH;
        debug_log("[Asteroids_Game] Attempting to load TYPE_4 panel font from resource: " + resourcePath);

        java.net.URL viaClass = Asteroids_.class.getResource(resourcePath);
        java.net.URL viaClassLoader = Asteroids_.class.getClassLoader()
            .getResource(TYPE_4_PANEL_FONT_RESOURCE_NAME);
        debug_log("[Asteroids_Game] getResource('" + resourcePath + "') => " + String.valueOf(viaClass));
        debug_log("[Asteroids_Game] ClassLoader.getResource('" + TYPE_4_PANEL_FONT_RESOURCE_NAME + "') => "
            + String.valueOf(viaClassLoader));

        try (InputStream in = Asteroids_.class.getResourceAsStream(resourcePath)) {
            if (in == null) {
                debug_log("[Asteroids_Game] Font stream is null for " + resourcePath
                        + ". Falling back to SansSerif bold 22.");
                return new Font("SansSerif", Font.BOLD, 22);
            }

            Font loaded = Font.createFont(Font.TRUETYPE_FONT, in);
            debug_log("[Asteroids_Game] Loaded font successfully. Family='" + loaded.getFamily() + "' Name='"
                    + loaded.getFontName() + "'.");
            return loaded.deriveFont(Font.PLAIN, 22f);
        } catch (FontFormatException e) {
            debug_log("[Asteroids_Game] Font format error while loading " + resourcePath + ": " + e);
            for (StackTraceElement ste : e.getStackTrace()) {
                debug_log("[Asteroids_Game]   at " + ste.toString());
            }
            return new Font("SansSerif", Font.BOLD, 22);
        } catch (IOException e) {
            debug_log("[Asteroids_Game] IO error while loading " + resourcePath + ": " + e);
            for (StackTraceElement ste : e.getStackTrace()) {
                debug_log("[Asteroids_Game]   at " + ste.toString());
            }
            return new Font("SansSerif", Font.BOLD, 22);
        }
    }

    private void centerType4OverlayItems(Overlay overlay, int imageWidth, int imageHeight) {
        for (int i = 0; i < overlay.size(); i++) {
            Roi roi = overlay.get(i);
            if (roi == null || !TYPE_4.equals(getOverlayType(roi))) {
                continue;
            }

            Rectangle bounds = roi.getBounds();
            int centeredX = Math.max(0, (imageWidth - bounds.width) / 2);
            int centeredY = Math.max(0, (imageHeight - bounds.height) / 2);
            roi.setLocation(centeredX, centeredY);
        }
    }

    private void fitType4TextToImageWidth(Overlay overlay, int imageWidth) {
        int maxWidth = Math.max(40, imageWidth - 20);
        for (int i = 0; i < overlay.size(); i++) {
            Roi roi = overlay.get(i);
            if (!(roi instanceof TextRoi) || !TYPE_4.equals(getOverlayType(roi))) {
                continue;
            }

            TextRoi textRoi = (TextRoi) roi;
            String text = textRoi.getText();
            Font originalFont = textRoi.getCurrentFont();
            int fittedSize = originalFont.getSize();

            while (fittedSize > 10) {
                TextRoi candidate = new TextRoi(0, 0, text, originalFont.deriveFont((float) fittedSize));
                if (candidate.getBounds().width <= maxWidth) {
                    break;
                }
                fittedSize--;
            }

            if (fittedSize != originalFont.getSize()) {
                TextRoi fitted = new TextRoi(roi.getBounds().x, roi.getBounds().y, text,
                        originalFont.deriveFont((float) fittedSize));
                fitted.setStrokeColor(roi.getStrokeColor());
                fitted.setFillColor(roi.getFillColor());
                fitted.setProperty(OVERLAY_TYPE_PROPERTY, TYPE_4);
                fitted.setName(roi.getName());
                overlay.set(fitted, i);
            }
        }
    }

    private int findOverlayIndexByType(Overlay overlay, String type) {
        for (int i = overlay.size() - 1; i >= 0; i--) {
            Roi roi = overlay.get(i);
            if (roi != null && type.equals(getOverlayType(roi))) {
                return i;
            }
        }
        return -1;
    }

    private int findType0HitAt(double x, double y, List<AnimatedItem> items) {
        int px = (int) Math.round(x);
        int py = (int) Math.round(y);
        for (int i = 0; i < items.size(); i++) {
            AnimatedItem item = items.get(i);
            if (item.flashFramesRemaining > 0) {
                continue;
            }
            if (item.roi.contains(px, py)) {
                return i;
            }
        }
        return -1;
    }

    private void handleType0Hit(int itemIndex, List<AnimatedItem> items, ImagePlus imp) {
        if (itemIndex < 0 || itemIndex >= items.size()) {
            return;
        }

        AnimatedItem item = items.get(itemIndex);
        String kind = item.roi.getProperty(TYPE_0_KIND_PROPERTY);
        if (TYPE_0_KIND_GOOD.equals(kind)) {
            score += 10;
            goodType0Remaining = Math.max(0, goodType0Remaining - 1);
            if (goodType0Remaining == 0) {
                promoteRemainingBadToBlue(items);
            }
        } else if (TYPE_0_KIND_BAD.equals(kind)) {
            ships = Math.max(0, ships - 1);
            triggerBadHitLutFlash(imp);
            if (ships == 0) {
                gameOver = true;
                gamePaused = true;
                pauseOverlayText = "GAME OVER\nPress ESC to exit";
            }
        } else if (TYPE_0_KIND_BLUE.equals(kind)) {
            score += 100;
            int activeCount = countActiveType0Targets(items);
            if (activeCount == 1) {
                gameOver = true;
                gamePaused = true;
                pauseOverlayText = "YOU WIN\nPress ESC to exit";
            }
        }

        updateSliceMetadata(imp);
    }

    private void triggerBadHitLutFlash(ImagePlus imp) {
        if (imp == null || imp.getProcessor() == null) {
            return;
        }

        if (lutFlashFramesRemaining <= 0) {
            previousLutColorModel = imp.getProcessor().getColorModel();
            IJ.run(imp, "Red Hot", "");
        }
        lutFlashFramesRemaining = TYPE_0_FLASH_FRAMES * 2;
    }

    private void updateLutFlashState(ImagePlus imp) {
        if (lutFlashFramesRemaining <= 0) {
            return;
        }

        lutFlashFramesRemaining--;
        if (lutFlashFramesRemaining == 0) {
            restorePreviousLutIfNeeded(imp);
        }
    }

    private void restorePreviousLutIfNeeded(ImagePlus imp) {
        if (imp == null || imp.getProcessor() == null) {
            lutFlashFramesRemaining = 0;
            previousLutColorModel = null;
            return;
        }

        if (previousLutColorModel != null) {
            imp.getProcessor().setColorModel(previousLutColorModel);
            imp.updateAndDraw();
        }

        lutFlashFramesRemaining = 0;
        previousLutColorModel = null;
    }

    private int countGoodType0(List<AnimatedItem> items) {
        int count = 0;
        for (AnimatedItem item : items) {
            if (TYPE_0_KIND_GOOD.equals(item.roi.getProperty(TYPE_0_KIND_PROPERTY))) {
                count++;
            }
        }
        return count;
    }

    private void promoteRemainingBadToBlue(List<AnimatedItem> items) {
        for (AnimatedItem item : items) {
            if (item.flashFramesRemaining > 0) {
                continue;
            }
            if (TYPE_0_KIND_BAD.equals(item.roi.getProperty(TYPE_0_KIND_PROPERTY))) {
                item.roi.setProperty(TYPE_0_KIND_PROPERTY, TYPE_0_KIND_BLUE);
                item.roi.setStrokeColor(new Color(40, 120, 255));
                item.roi.setFillColor(new Color(40, 120, 255, 220));
            }
        }
    }

    private int countActiveType0Targets(List<AnimatedItem> items) {
        int count = 0;
        for (AnimatedItem item : items) {
            if (item.flashFramesRemaining == 0) {
                count++;
            }
        }
        return count;
    }

    private void updateSliceMetadata(ImagePlus imp) {
        if (imp == null || imp.getStack() == null) {
            return;
        }
        String metadata = "Score: " + score + " | Ships: " + ships;
        imp.getStack().setSliceLabel(metadata, imp.getCurrentSlice());
        imp.repaintWindow();
    }

    private void flashType0Item(int itemIndex, List<AnimatedItem> items) {
        if (itemIndex < 0 || itemIndex >= items.size()) {
            return;
        }
        AnimatedItem item = items.get(itemIndex);
        item.roi.setStrokeColor(Color.WHITE);
        item.roi.setFillColor(new Color(255, 255, 255, 220));
        item.flashFramesRemaining = TYPE_0_FLASH_FRAMES;
    }

    private void removeAnimatedItemAt(int itemIndex, List<AnimatedItem> items, List<LaserItem> lasers,
            Overlay overlay) {
        AnimatedItem removed = items.remove(itemIndex);
        removeOverlayAt(removed.overlayIndex, items, lasers, overlay);
    }

    private void removeLaserAt(int laserListIndex, List<LaserItem> lasers, List<AnimatedItem> items, Overlay overlay) {
        LaserItem removed = lasers.remove(laserListIndex);
        removeOverlayAt(removed.overlayIndex, items, lasers, overlay);
    }

    private void removeOverlayAt(int removedOverlayIndex, List<AnimatedItem> items, List<LaserItem> lasers,
            Overlay overlay) {
        if (removedOverlayIndex >= 0 && removedOverlayIndex < overlay.size()) {
            overlay.remove(removedOverlayIndex);
        }

        if (type1OverlayIndex > removedOverlayIndex) {
            type1OverlayIndex--;
        }

        for (AnimatedItem item : items) {
            if (item.overlayIndex > removedOverlayIndex) {
                item.overlayIndex--;
            }
        }

        for (LaserItem laser : lasers) {
            if (laser.overlayIndex > removedOverlayIndex) {
                laser.overlayIndex--;
            }
        }
    }

    private static class KeyBinding {
        private final Runnable action;
        private final boolean consumeEvent;

        private KeyBinding(Runnable action, boolean consumeEvent) {
            this.action = action;
            this.consumeEvent = consumeEvent;
        }
    }

    private static class AnimatedItem {
        private int overlayIndex;
        private final Roi roi;
        private final int width;
        private final int height;
        private final double vx;
        private final double vy;
        private double x;
        private double y;
        private int flashFramesRemaining;

        private AnimatedItem(int overlayIndex, Roi roi, double startX, double startY, int width, int height, double vx,
                double vy) {
            this.overlayIndex = overlayIndex;
            this.roi = roi;
            this.x = startX;
            this.y = startY;
            this.width = width;
            this.height = height;
            this.vx = vx;
            this.vy = vy;
            this.flashFramesRemaining = 0;
        }
    }

    private static class LaserItem {
        private int overlayIndex;
        private double x1;
        private double y1;
        private double x2;
        private double y2;
        private final double vx;
        private final double vy;

        private LaserItem(int overlayIndex, double x1, double y1, double x2, double y2, double vx, double vy) {
            this.overlayIndex = overlayIndex;
            this.x1 = x1;
            this.y1 = y1;
            this.x2 = x2;
            this.y2 = y2;
            this.vx = vx;
            this.vy = vy;
        }
    }
}
