package de.duehl.vocabulary.japanese.ui.dialog.kanji.stroke;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.List;

import javax.swing.Action;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.swing.ui.GuiTools;
import de.duehl.swing.ui.key.BindKeysOnRootPane;
import de.duehl.swing.ui.key.KeyDefinition;

import de.duehl.vocabulary.japanese.logic.symbol.kanji.strokes.own.data.KanjiStrokePath;

/**
 * Diese Klasse stellt die Pfade mit den Strichen zu einem Kanji zusammen mit Steuerelementen dar,
 * mit denen man die Darstellung manuell weiterschalten, automatisch animieren oder das Endergebnis
 * darstellen kann.
 *
 * Außerdem kann man festlegen, wie breit der äußere freie Rahmen um das Kanji sein soll.
 *
 * Die Strichnummer wird über einen JSlider gesteuert. Die Vor-/Zurück-Schaltflächen sowie die
 * automatische Animation verändern ebenfalls nur den Slider.
 *
 * @version 1.01     2026-08-22
 * @author Christian Dühl
 */

public class KanjiStrokeWithControlsPanel {

    /** Die minimale Zeit zwischen zwei Animationsschritten in Millisekunden. */
    private static final int ANIMATION_MIN_DELAY = 10;

    /** Die initiale Zeit zwischen zwei Animationsschritten in Millisekunden. */
    private static final int ANIMATION_DELAY = 400;

    /** Die maximale Zeit zwischen zwei Animationsschritten in Millisekunden. */
    private static final int ANIMATION_MAX_DELAY = 2000;

    /** Die Anzahl der gezeichneten Teilstriche. */
    private static final int NUMBER_OF_PART_STEPS = 30;

    /** Der Anteil zwischen 0.0 und 1.0 pro Animationsschritt der aktuellen Strichs. */
    private static final double ACTUAL_STROKE_ANIMATION_STEP_SIZE =
            1.0D / (NUMBER_OF_PART_STEPS + 1); // + 1 weil es vorher bei 0 beginnt.


    /** Die darzustellenden Striche eines Kanji. */
    private final KanjiStrokePanel kanjiStrokePanel;

    /** Die Anzahl der Striche, die dargestellt werden. */
    private final int strokeCount;

    /** Das Panel auf dem das Panel mit den Strichen und die Steuerelemente dargestellt werden. */
    private final JPanel panel;

    /** Der Slider zur Auswahl des aktuellen Strichs. */
    private final JSlider strokeSlider;

    /** Der Timer für die Pause zwischen den Strichen bei der automatischen Animation. */
    private final Timer animationPauseTimer;

    /** Der Timer für die automatische Animation des aktuellen Strichs. */
    private final Timer actualStrokeAnimationTimer;

    /** Der Slider zur Auswahl des Geschwindigkeit der Animation. */
    private final JSlider animationDelaySlider;

    /** Der Button um ganz an den Anfang zu springen. */
    private final JButton toStartButton;

    /** Der Button für einen Strich zurück. */
    private final JButton previousButton;

    /** Der Button für einen Strich vor. */
    private final JButton nextButton;

    /** Der Button um ganz ans Ende zu springen. */
    private final JButton toEndButton;

    /** Der Button zum Starten/Fortsetzen der Animation. */
    private final JButton playButton;

    /** Der Button zum Anhalten der Animation. */
    private final JButton stopButton;

    /** Das Eingabefeld für die Breite des äußeren Randes. */
    private final JSpinner borderWidthSpinner;

    /** Fortschritt des aktuell animierten Strichs von 0.0 bis 1.0. */
    private double actualStrokeAnimationProgress;

    /**
     * Konstruktor.
     *
     * @param kanjiStrokePathes
     *            Die darzustellenden Striche eines Kanji.
     */
    public KanjiStrokeWithControlsPanel(List<KanjiStrokePath> kanjiStrokePathes) {
        kanjiStrokePanel = new KanjiStrokePanel(kanjiStrokePathes);

        strokeCount = kanjiStrokePathes.size();

        panel = new JPanel();

        toStartButton = createButton("|◀", e -> noStroke());
        previousButton = createButton("<", e -> previousStroke());
        nextButton = createButton(">", e -> nextStroke());
        toEndButton = createButton("▶|", e -> allStrokesPainted());
        playButton = createButton("▶", e -> startAnimation());
        stopButton = createButton("■", e -> stopAnimation());

        strokeSlider = new JSlider(0, strokeCount + 1, 0); // +1 für letzter Strich schwarz
        strokeSlider.addChangeListener(e -> strokeSliderChanged());

        borderWidthSpinner = new JSpinner(
                new SpinnerNumberModel(10, 0, 100, 1));
        borderWidthSpinner.addChangeListener(e -> borderWidthChanged());
        borderWidthSpinner.setFocusable(false);

        animationPauseTimer = new Timer(ANIMATION_DELAY, e -> animationPauseStep());

        actualStrokeAnimationTimer = new Timer(0, e -> actualStrokeAnimationStep());
                                               // der Delay wird später gesetzt
        animationDelaySlider = new JSlider(ANIMATION_MIN_DELAY, ANIMATION_MAX_DELAY, ANIMATION_DELAY);
        animationDelaySlider.addChangeListener(e -> animationDelaySliderChanged());

        actualStrokeAnimationProgress = 0.0;

        initPanel();
        updateButtonStates();
    }

    private JButton createButton(String text, ActionListener listener) {
        JButton button = new JButton(text);
        button.addActionListener(listener);
        return button;
    }

    /** Geht ganz an den Anfang. */
    private void noStroke() {
        strokeSlider.setValue(0);
        setStrokeToFullyPainted();
        updateButtonStates();
    }


    /** Geht einen Strich zurück. */
    private void previousStroke() {
        if (strokeSlider.getValue() > strokeSlider.getMinimum()) {
            strokeSlider.setValue(strokeSlider.getValue() - 1);
            setStrokeToFullyPainted();
        }
    }

    /** Einen Strich vor. */
    private void nextStroke() {
        if (strokeSlider.getValue() < strokeSlider.getMaximum()) {
            strokeSlider.setValue(strokeSlider.getValue() + 1);
            setStrokeToFullyPainted();
        }
    }

    /** Geht ganz ans Ende. */
    private void allStrokesPainted() {
        strokeSlider.setValue(strokeCount + 1); // +1 für letzter Strich schwarz
        setStrokeToFullyPainted();
    }

    private void setStrokeToFullyPainted() {
        actualStrokeAnimationTimer.stop();
        animationPauseTimer.stop();
        actualStrokeAnimationProgress = 1.0;
        kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);
    }

    /** Startet bzw. setzt die automatische Animation fort. */
    private void startAnimation() {
        int strokeNumber = strokeSlider.getValue();
        if (strokeNumber >= strokeCount) {
            strokeSlider.setValue(0);
        }
        else if (strokeNumber > 0) {
            actualStrokeAnimationProgress = 1.0;
            kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);
        }
        else {
            actualStrokeAnimationProgress = 0.0;
        }

        kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);

        //actualStrokeAnimationTimer.stop();
        animationPauseTimer.start();
        updateButtonStates();
    }

    /** Hält die automatische Animation an. */
    private void stopAnimation() {
        animationPauseTimer.stop();
        actualStrokeAnimationTimer.stop();
        actualStrokeAnimationProgress = 1.0;
        kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);
        updateButtonStates();
    }

    /** Führt einen Schritt der automatischen Animation aus. */
    private void animationPauseStep() {
        animationPauseTimer.stop();

        int strokeNumber = strokeSlider.getValue();

        if (strokeNumber >= strokeCount + 1) { // +1 für letzter Strich schwarz
            allStrokesPainted();
        }
        else {
            strokeSlider.setValue(strokeNumber + 1);
            actualStrokeAnimationProgress = 0.0;
            kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);
            actualStrokeAnimationTimer.setDelay(calculateActualStrokeDelay());
            actualStrokeAnimationTimer.start();
        }

        updateButtonStates();
    }

    private void actualStrokeAnimationStep() {
        actualStrokeAnimationProgress += ACTUAL_STROKE_ANIMATION_STEP_SIZE;

        if (actualStrokeAnimationProgress > 1.0D) {
            actualStrokeAnimationProgress = 1.0D;
            actualStrokeAnimationTimer.stop();

            // Strich ist vollständig gezeichnet:
            kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);

            int strokeNumber = strokeSlider.getValue();
            if (strokeNumber < strokeCount + 1) { // +1 für letzter Strich schwarz
                // Jetzt erst die Pause bis zum nächsten Strich.
                animationPauseTimer.restart();
            }
            else {
                animationPauseTimer.stop();
                updateButtonStates();
            }
        }
        else {
            kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);
        }
    }

    private static final int DELAY_ADD = 15;

    private int calculateActualStrokeDelay() {
        /*
         * Zuerst berechnen wir den Delay für die Darstellung des aktuellen Strokes über die Länge
         * der Pause und die Anzahl der darzustellenden Schritte:
         */
        //int baseDelay = animationPauseTimer.getDelay() / NUMBER_OF_PART_STEPS;
        int baseDelay = (animationPauseTimer.getDelay() - DELAY_ADD) / NUMBER_OF_PART_STEPS;
        // Das machte bei testausgaben nichtmal einen Unterschied.

        /*
         * Die Zeichendauer wird proportional zur Länge des Striches angepasst. Als Referenz dient
         * die mediane Strichlänge.
         */
        double length = kanjiStrokePanel.getLengthOfActualStrokeForAnimation();
        double median = kanjiStrokePanel.getMedianLengthOfStrokes();

        double factor = length / median;
        factor = 1.0 + 2.5 * (factor - 1.0); // dehnt den Faktor von 1 ausgehend

        /*
         * Extrem kurze oder lange Striche dürfen die Animation nicht völlig aus dem Takt bringen.
         */
        //factor = Math.max(0.5, Math.min(2.0, factor));
        factor = Math.max(0.0, Math.min(5.0, factor));

        return DELAY_ADD + (int) (baseDelay * factor);
    }

    /** Aktualisiert die Zustände der Buttons. */
    private void updateButtonStates() {
        int value = strokeSlider.getValue();

        previousButton.setEnabled(value > strokeSlider.getMinimum());
        nextButton.setEnabled(value < strokeSlider.getMaximum());

        boolean animationRunning = isAnimationRunning();
        playButton.setEnabled(!animationRunning);
        stopButton.setEnabled(animationRunning);
    }

    /** Reaktion auf Änderungen des Sliders. */
    private void strokeSliderChanged() {
        int strokeNumber = strokeSlider.getValue();

        actualStrokeAnimationProgress = 1.0;
        kanjiStrokePanel.setActualStrokeProgress(actualStrokeAnimationProgress);

        kanjiStrokePanel.setActualStrokeNumber(strokeNumber);

        updateButtonStates();
    }

    /** Reaktion auf Änderungen des Sliders für die Verzögerung bei der Animation. */
    private void animationDelaySliderChanged() {
        int delay = animationDelaySlider.getValue();
        animationPauseTimer.setDelay(delay);
        actualStrokeAnimationTimer.setDelay(calculateActualStrokeDelay());
    }

    /** Reaktion auf Änderungen der Rahmenbreite. */
    private void borderWidthChanged() {
        int borderWidth = (Integer) borderWidthSpinner.getValue();

        kanjiStrokePanel.setBorderWidth(borderWidth);
        kanjiStrokePanel.repaint();
    }

    private void initPanel() {
        panel.setLayout(new BorderLayout());

        panel.add(kanjiStrokePanel, BorderLayout.CENTER);
        panel.add(createControlPart(), BorderLayout.SOUTH);
    }

    private Component createControlPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(createUpperControls(), BorderLayout.NORTH);
        panel.add(createMiddleControls(), BorderLayout.CENTER);
        panel.add(createLowerControls(), BorderLayout.SOUTH);

        return panel;
    }

    private Component createUpperControls() {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());

        panel.add(toStartButton);
        panel.add(previousButton);
        panel.add(strokeSlider);
        panel.add(nextButton);
        panel.add(toEndButton);

        return panel;
    }

    private Component createMiddleControls() {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());

        panel.add(playButton);
        panel.add(stopButton);

        panel.add(new JLabel("  Verzögerung:"));
        panel.add(animationDelaySlider);

        return panel;
    }

    private Component createLowerControls() {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout());

        panel.add(new JLabel("Rahmen:"));
        panel.add(borderWidthSpinner);

        return panel;
    }

    /**
     * Legt die Tastaturkürzel zum Durchgehen der Strichfolge mit Page-Up und Page-Down sowie zum
     * Starten und Stoppen der Animation fest.
     *
     * Da der Panel erst dann über eine RootPane verfügt, wenn er in einen Frame oder Dialog
     * eingefügt wird, muss man diese Methode von außen aufrufen.
     */
    public void keyBindings() {
        keybindingsForManualSteppingThroughTheStrokes();
        keybindingsForStartAndStopAnimation();
        requestFocusInRightElement();
    }

    /** Legt die Tastaturkürzel zum Durchgehen der Strichfolge mit Page-Up und Page-Down fest. */
    private void keybindingsForManualSteppingThroughTheStrokes() {
        BindKeysOnRootPane bindPageUp = new BindKeysOnRootPane(panel.getRootPane());
        bindPageUp.setKeyBindingPageUp(() -> previousStroke());

        BindKeysOnRootPane bindPageDown = new BindKeysOnRootPane(panel.getRootPane());
        bindPageDown.setKeyBindingPageDown(() -> nextStroke());

        KeyDefinition keyDefinitionPageUp = new KeyDefinition(KeyEvent.VK_PAGE_UP, 0);
        KeyDefinition keyDefinitionPageDown = new KeyDefinition(KeyEvent.VK_PAGE_DOWN, 0);
        GuiTools.disableKeys(
                CollectionsHelper.buildListFrom(keyDefinitionPageUp, keyDefinitionPageDown),
                strokeSlider.getInputMap(),
                animationDelaySlider.getInputMap());
    }

    /** Legt die Tastaturkürzel zum Starten und Stoppen der Animation fest. */
    private void keybindingsForStartAndStopAnimation() {
        KeyStroke keyStrokeSpace = KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0);
        String actionMapKeySpace = "Space";
        Action actionSpace = BindKeysOnRootPane.runnableToAction(() -> startOrStopAnimation());

        JRootPane rootPane = panel.getRootPane();
        InputMap inputMap = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        inputMap.put(keyStrokeSpace, actionMapKeySpace);
        rootPane.getActionMap().put(actionMapKeySpace, actionSpace);
    }

    private void requestFocusInRightElement() {
        SwingUtilities.invokeLater(() -> strokeSlider.requestFocusInWindow());
    }

    private void startOrStopAnimation() {
        if (isAnimationRunning()) {
            stopAnimation();
        }
        else {
            startAnimation();
        }
    }

    /** Setter für die Breite des Rahmens um das eigentliche Kanji. */
    public void setBorderWidth(int borderWidth) {
        kanjiStrokePanel.setBorderWidth(borderWidth);
        borderWidthSpinner.setValue(borderWidth);
    }

    /** Das Panel auf dem das Panel mit den Strichen und die Steuerelemente dargestellt werden. */
    public JPanel getPanel() {
        return panel;
    }

    /** Gibt zurück, ob gerade animiert wird. */
    public boolean isAnimationRunning() {
        return animationPauseTimer.isRunning()
                || actualStrokeAnimationTimer.isRunning();
    }

    // Wird nur für die Testklasse benötigt:

    /** Setter für die Nummer des Strichs, bis zum dem das Kanji dargestellt werden soll. */
    public void setActualStrokeNumber(int actualStrokeNumber) {
        kanjiStrokePanel.setActualStrokeNumber(actualStrokeNumber);
        strokeSlider.setValue(actualStrokeNumber);

        updateButtonStates();
    }

    /** Setzt die gewünschte Größe des Panels. */
    public void setPreferredSize(Dimension dimension) {
        panel.setPreferredSize(dimension);
    }

    // Brauchen wir das? Aufgerufen wird es nicht:

    /** Setzt die Darstellung auf den ersten Strich zurück und beendet den Timer. */
    public void reset() {
        animationPauseTimer.stop();
        actualStrokeAnimationTimer.stop();
        strokeSlider.setValue(0);
        updateButtonStates();
    }

    /** Gibt den aktuellen Strich zurück. */
    public int getActualStrokeNumber() {
        return strokeSlider.getValue();
    }

}
