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

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JPanel;

import de.duehl.vocabulary.japanese.logic.symbol.kanji.strokes.own.data.KanjiStrokePath;
import de.duehl.vocabulary.japanese.logic.symbol.kanji.strokes.path.PathParser;
import de.duehl.vocabulary.japanese.ui.dialog.kanji.stroke.partial.PartialPath;

/**
 * Diese Klasse stellt die Pfade mit den Strichen zu einem Kanji dar, wobei man von außen
 * festlegen kann, wie weit sie bereits gezeichnet sein sollen.
 *
 * Die Pfade wurden ehemals einmalig aus den SVG-Dateien aus dem Kanji-VG Projekt eingelesen und
 * dann in eine Datei in einem eigenen Format geschrieben. Aus dieser stammen die hier
 * dargestellten Pfade.
 *
 * Die Klasse hat eine Methode, mit der man setzen kann, bis zu welchem Strich dargestellt wird.
 * Es wird dann dargestellt:
 *     - Alles vor der aktuellen Strichnummer schwarz
 *     - die aktuelle Strichnummer rot
 *     - die Strichnummern danach blass grau
 * Wird eine Zahl übergeben, die größer als die Anzahl der Striche ist, wird alles schwarz
 * dargestellt.
 *
 *
 * Zu Kanji-VG siehe:
 *
 * https://github.com/KanjiVG/kanjivg
 *
 * KanjiVG is copyright Ulrich Apel and released under the Creative Commons Attribution-Share
 * Alike 3.0 licence: http://creativecommons.org/licenses/by-sa/3.0/
 *
 * The project's documentation is at https://kanjivg.tagaini.net/.
 *
 * @version 1.01     2026-08-22
 * @author Christian Dühl
 */

public class KanjiStrokePanel extends JPanel {

    private static final long serialVersionUID = 1L;

    private static final double KANJIVG_SIZE = 109.0;


    /** Die darzustellenden Striche eines Kanji. */
    private final List<KanjiStrokePath> kanjiStrokePathes;

    /** Die Liste der 2D-Pathes, die aus den Striche des Kanji erzeugt wurden. */
    private List<Path2D.Double> pathes2D;

    /** Die PartialPathes für die Animation des aktuell gezeichneten Strichs. */
    private List<PartialPath> partialPathes;

    /** Die Nummer des Strichs, bis zum dem das Kanji dargestellt werden soll. */
    private int actualStrokeNumber;

    /** Die Breite des Rahmens um das eigentliche Kanji. */
    private int borderWidth;

    /** Der Fortschritt beim Zeichnen des aktuellen Strichs: 0.0 bis 1.0. */
    private double actualStrokeProgress;

    /** Das Graphics2D-Objekt mit dem gezeichnet wird. */
    private Graphics2D g2D;

    /** Die Länge des kürzesten dargestellten Striches. */
    private double minStrokeLength;

    /** Der Median der Längen der dargestellten Striche. */
    private double medianStrokeLength;

    /** Die Länge des längsten dargestellten Striches. */
    private double maxStrokeLength;

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

        actualStrokeNumber = 0;
        actualStrokeProgress = 0.0;
        borderWidth = 10;
        parsePathes();
    }

    private void parsePathes() {
        pathes2D = new ArrayList<>();
        partialPathes = new ArrayList<>();
        List<Double> lengthes = new ArrayList<>();

        for (KanjiStrokePath kanjiStrokePath : kanjiStrokePathes) {
            String path = kanjiStrokePath.getPath();

            PathParser parser = new PathParser(path);
            parser.parse();

            Path2D.Double swingPath = parser.getSwingPath();
            pathes2D.add(swingPath);

            PartialPath partialPath = new PartialPath(swingPath);
            partialPathes.add(partialPath);
            double length = partialPath.getTotalLength();
            lengthes.add(length);
        }

        int n = lengthes.size();
        if (n > 1) {
            Collections.sort(lengthes);
            minStrokeLength = lengthes.get(0);
            medianStrokeLength = lengthes.get(n / 2);
            maxStrokeLength = lengthes.get(n - 1);
        }
        else if (n == 1) {
            minStrokeLength = lengthes.get(0);
            medianStrokeLength = lengthes.get(0);
            maxStrokeLength = lengthes.get(0);
        }
        else {
            throw new RuntimeException("Die Stroke-Pfade des Kanji haben die Anzahl 0.");
        }
    }

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

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

    /**
     * Setzt den Fortschritt beim Zeichnen des aktuellen Strichs.
     *
     * @param actualStrokeProgress
     *            Der Fortschritt gibt an, wie weit der aktuelle Strich gezeichnet wird, z.B.:
     *            0.0 = Beginn des Strichs,
     *            0.5 = halber Strich und
     *            1.0 = vollständiger Strich.
     */
    public void setActualStrokeProgress(double actualStrokeProgress) {
        if (actualStrokeProgress < 0.0 || actualStrokeProgress > 1.0) {
            throw new IllegalArgumentException(
                    "Der Strichfortschritt muss zwischen 0.0 und 1.0 liegen: "
                    + actualStrokeProgress);
        }

        this.actualStrokeProgress = actualStrokeProgress;
        paintAgain();
    }

    /** Zeichnet den Inhalt neu. */
    private void paintAgain() {
        repaint();
        revalidate();
        validate();
    }

    /**
     * Gibt die Länge des aktuell dargestellten Striches zurück.
     *
     * Im Normalfall ist actualStrokeNumber - 1 der richtige index. Falls aber noch keine Strich
     * gezeichnet würden, dann wäre actualStrokeIndex = -1 und damit würde man versuchen auf diesen
     * Index zuzugreifen. Daher verhindere ich den Wert -1. Das kam nur vor, als im Konstruktor von
     * KanjiStrokeWithControlsPanel noch calculateActualStrokeDelay() aufgerufen wurde. Aber
     * sicherheitshalber lasse ich auch diese Korrektur hier bestehen.
     *
     * Am Ende der Animation sind wir bei Anzahl PFade + 1. Daher muss ich auch diesen Wert
     * abfangen.
     */
    public double getLengthOfActualStrokeForAnimation() {
        int actualStrokeIndex = actualStrokeNumber - 1;

        if (actualStrokeIndex < 0) {
            actualStrokeIndex = 0;
        }
        else if (actualStrokeIndex > partialPathes.size() - 1) {
            actualStrokeIndex = partialPathes.size() - 1;
        }

        return partialPathes.get(actualStrokeIndex).getTotalLength();
    }

    /** Getter für die Länge des kürzesten dargestellten Striches. */
    public double getLengthOfShortestStroke() {
        return minStrokeLength;
    }

    /** Getter für den Median der Längen der dargestellten Striche. */
    public double getMedianLengthOfStrokes() {
        return medianStrokeLength;
    }

    /** Getter für die Länge des längsten dargestellten Striches. */
    public double getLengthOfLongestStroke() {
        return maxStrokeLength;
    }

    /** Hier werden die Striche auf das Panel gezeichnet. */
    @Override
    public void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);

        createG2D(graphics);

        try {
            setRenderingHints();
            setStroke();
            paintStrokes();
        }
        finally {
            g2D.dispose();
        }
    }

    private void createG2D(Graphics graphics) {
        g2D = (Graphics2D) graphics.create();
    }

    private void setRenderingHints() {
        g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    }

    private void setStroke() {
        g2D.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    }

    /**
     * Zeichnet die Striche ein.
     *
     * Anfangs bin ich die einfach der Reihenfolge nach durchgegangen, habe die ersten schwarz, den
     * aktuelle rot und die anderen grau gezeichnet, aber leider sieht das bei Kreuzungen nicht so
     * schön aus, weil dann ein grauer Strich über den schwarzen oder roten gezeichnet wird. Daher
     * zeichne ich nun erst die grauen Striche, falls es welche zu zeichnen gibt, und dann die
     * anderen. Zum Beispiel fiel das beim Kanji 澪 auf.
     */
    private void paintStrokes() {
        prepareTransformation();

        paintNotYetPaintedStrokes();
        paintAlreadyPaintedStrokes();
        paintActualStroke();
    }

    /**
     * Transformiert die Darstellung von den 109 x 109 Pixeln bei KanjiVG auf die
     * Darstellungsfläche. Dabei werden praktischer Weise Dinge wie die Strichstärke mit skaliert.
     */
    private void prepareTransformation() {
        int usableWidth = getWidth() - 2 * borderWidth;
        int usableHeight = getHeight() - 2 * borderWidth;

        double scale = Math.min(usableWidth, usableHeight) / KANJIVG_SIZE;

        double kanjiWidth = KANJIVG_SIZE * scale;
        double kanjiHeight = KANJIVG_SIZE * scale;

        double offsetX = borderWidth + (usableWidth - kanjiWidth) / 2;
        double offsetY = borderWidth + (usableHeight - kanjiHeight) / 2;

        g2D.translate(offsetX, offsetY);
        g2D.scale(scale, scale);
    }

    /**
     * Zeichnet die zukünftigen Striche in hellgrau (diese beginnen bei actualStrokeIndex und nicht
     * bei actualStrokeIndex + 1, damit der hintere noch nicht gezeichnete Teil des gerade in der
     * Darstellung befindlichen Striches zu sehen ist.
     *
     * Falls aber noch keine Strich gezeichnet würden, dann wäre actualStrokeIndex -1 und damit
     * würde man versuchen, in der Methode paintStroke() auf diesen Index zuzugreifen. Daher
     * berechne ich den ersten Index vorher:
     */
    private void paintNotYetPaintedStrokes() {
        int actualStrokeIndex = actualStrokeNumber - 1;

        int firstStrokeIndex = Math.max(actualStrokeIndex, 0);
        for (int strokeIndex = firstStrokeIndex; strokeIndex < kanjiStrokePathes.size();
                ++strokeIndex) {
            paintStroke(strokeIndex, Color.LIGHT_GRAY);
        }
    }

    /** Zeichnet die bereits zuvor gezeichneten Striche in schwarz: */
    private void paintAlreadyPaintedStrokes() {
        int actualStrokeIndex = actualStrokeNumber - 1;

        for (int strokeIndex = 0; strokeIndex < actualStrokeIndex; ++strokeIndex) {
            paintStroke(strokeIndex, Color.BLACK);
        }
    }

    /**
     * Zeichnet den aktuellen Strich in rot, dieser wird langsam Stück für Stück animiert.
     *
     * Bei einem vollständig gezeichneten Strich verwenden wir bewusst wieder den
     * ursprünglichen Path2D und nicht die approximierte PartialPath-Geometrie:
     */
    private void paintActualStroke() {
        int actualStrokeIndex = actualStrokeNumber - 1;

        if (actualStrokeIndex >= 0 && actualStrokeIndex < kanjiStrokePathes.size()) {
            if (actualStrokeProgress >= 1.0) {
                paintStroke(actualStrokeIndex, Color.RED);
            }
            else {
                paintPartialStroke(actualStrokeIndex, Color.RED, actualStrokeProgress);
            }
        }
    }

    private void paintStroke(int strokeIndex, Color color) {
        KanjiStrokePath kanjiStrokePath = kanjiStrokePathes.get(strokeIndex);
        checkStrokeNumbersAreTheSame(strokeIndex, kanjiStrokePath);
        Path2D.Double swingPath = pathes2D.get(strokeIndex);
        paintPath(color, swingPath);
    }

    private void paintPartialStroke(int strokeIndex, Color color, double progress) {
        KanjiStrokePath kanjiStrokePath = kanjiStrokePathes.get(strokeIndex);
        checkStrokeNumbersAreTheSame(strokeIndex, kanjiStrokePath);
        PartialPath partialPath = partialPathes.get(strokeIndex);
        Path2D.Double swingPath = partialPath.createPath(progress);
        paintPath(color, swingPath);
    }

    private void checkStrokeNumbersAreTheSame(int strokeIndex, KanjiStrokePath kanjiStrokePath) {
        int strokeNumber = strokeIndex + 1;
        if (strokeNumber != kanjiStrokePath.getStrokeNumber()) {
            throw new RuntimeException("Die Strichnummern weichen voneinander ab.\n"
                    + "\t" + "strokeNumber                     : " + strokeNumber + "\n"
                    + "\t" + "kanjiStrokePath.getStrokeNumber(): "
                            + kanjiStrokePath.getStrokeNumber() + "\n"
                    + "\t" + "kanjiStrokePath                  : " + kanjiStrokePath + "\n"
                    );
        }
    }

    private void paintPath(Color color, Path2D.Double swingPath) {
        g2D.setColor(color);
        g2D.draw(swingPath);
    }

}
