package de.duehl.swing.ui.panel.painted;

/*
 * Copyright 2025 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.util.Arrays;
import java.util.List;

import javax.swing.JPanel;

import de.duehl.swing.ui.geometry.PixelLine;
import de.duehl.swing.ui.geometry.PixelPoint;

/**
 * Diese Klasse stellt die Basisklasse für die selbstgezeichneten Panel dar.
 *
 * @version 1.01     2025-11-22
 * @author Christian Dühl
 */

public abstract class PaintedPanel extends JPanel {

    private static final long serialVersionUID = -1L;

    /** Die Hintergrundfarbe des Panels. */
    private Color backgroundColor;

    /** Der Grafikkontext in dem gezeichnet werden soll. */
    protected Graphics2D graphics2;

    /**
     * Gibt an, ob ein Quadrat erzwungen wird. Ansonsten wird mit einem Rechteck der ganze Platz
     * ausgefüllt.
     */
    private boolean forceSquare;

    /** Farbe des Symbols. */
    private Color symbolColor;

    /** Konstruktor mit schwarzer Symbol-Farbe. */
    public PaintedPanel() {
        this(Color.BLACK);
    }

    /**
     * Konstruktor.
     *
     * @param symbolColor
     *            Die Farbe des Symbols.
     */
    public PaintedPanel(Color symbolColor) {
        super();

        this.symbolColor = symbolColor;
        this.backgroundColor = null;
        this.forceSquare = false;
    }

    /** Getter für die Farbe des Symbols. */
    public final Color getSymbolColor() {
        return symbolColor;
    }

    /** Setter für die Farbe des Symbols. */
    public final void setSymbolColor(Color symbolColor) {
        this.symbolColor = symbolColor;
        repaint();
    }

    /**
     * Getter für die Hintergrundfarbe des Buttons. Der Wert null steht dafür, dass keine
     * Hintergrundfarbe eingezeichnet wird.
     */
    public Color getBackgroundColor() {
        return backgroundColor;
    }

    /**
     * Setter für die Hintergrundfarbe des Buttons. Der Wert null steht dafür, dass keine
     * Hintergrundfarbe eingezeichnet wird.
     */
    public void setBackgroundColor(Color backgroundColor) {
        this.backgroundColor = backgroundColor;
        repaint();
    }

    /**
     * Gibt an, ob ein Quadrat erzwungen wird. Ansonsten wird mit einem Rechteck der ganze Platz
     * ausgefüllt.
     */
    public boolean isForceSquare() {
        return forceSquare;
    }

    /**
     * Legt fest, ob ein Quadrat erzwungen wird. Ansonsten wird mit einem Rechteck der ganze Platz
     * ausgefüllt.
     */
    public void setForceSquare(boolean forceSquare) {
        this.forceSquare = forceSquare;
    }

    /**
     * Legt fest, dass ein Quadrat erzwungen wird. Ansonsten wird mit einem Rechteck der ganze
     * Platz ausgefüllt.
     */
    public void forceSquare() {
        setForceSquare(true);
    }

    /** Zeichnet den Button. */
    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        graphics2 = (Graphics2D) graphics.create();

        setStroke();

        paintBackground();
        setSymbolColorInGraphics();
        paintSymbol();

        graphics2.dispose();
    }

    /** Setzt die Strichstärke. */
    protected void setStroke() {
        graphics2.setStroke(new BasicStroke(2));
    }

    /** Falls gewünscht die Hintergrundfarbe einzeichnen. */
    private void paintBackground() {
        if (null != backgroundColor) {
            graphics2.setColor(backgroundColor);
            graphics2.fillRect(0, 0, getWidth() - 1, getHeight() - 1);
        }
    }

    /** Zeichnet das Symbol. */
    private void paintSymbol() {
        if (forceSquare) {
            paintSymbolSquare();
        }
        else {
            paintSymbolRectangle();
        }
    }

    /**
     * Farbwahl abhängig davon, ob die Maus über dem Button ist.
     *
     * Die gewählte Farbe normalerweise, wenn die Maus darüber kommt, die
     * Komplementärfarbe.
     */
    protected final void setSymbolColorInGraphics() {
        graphics2.setColor(symbolColor);
    }

    /** Zeichnet das Symbol in rechteckiger Anordnung. */
    abstract protected void paintSymbolRectangle();

    /** Zeichnet das Symbol in quadratischer Anordnung. */
    abstract protected void paintSymbolSquare();

    /**
     * Zeichnet ein gefülltes Rechteck von einem Punkt nach rechts und unten Abhängig von Breite
     * und Höhe.
     *
     * Achtung, falls die Ecken vertauscht sind, wird nichts gezeichnet.
     *
     * @param x
     *            X-Koordinate der linken obere Ecke.
     * @param y
     *            Y-Koordinate der linken obere Ecke.
     * @param width
     *            Breite.
     * @param height
     *            Höhe.
     */
    protected final void paintPointLengthRectangle(int x, int y, int width, int height) {
        graphics2.fillRect(x, y, width, height);
    }

    /**
     * Zeichnet ein gefülltes Rechteck sinnvoll zwischen zwei Punkten, statt von einem Punkt mit
     * Breit und Höhe.
     *
     * Achtung, falls die Ecken vertauscht sind, wird nichts gezeichnet.
     *
     * @param leftUpperPixel
     *            Linke obere Ecke.
     * @param rightLowerPixel
     *            Rechte untere Ecke.
     */
    protected final void paintTwoPointRectangle(PixelPoint leftUpperPixel,
            PixelPoint rightLowerPixel) {
        paintTwoPointRectangle(leftUpperPixel.getX(), leftUpperPixel.getY(),
                rightLowerPixel.getX(), rightLowerPixel.getY());
    }

    /**
     * Zeichnet ein gefülltes Rechteck sinnvoll zwischen zwei Punkten, statt von einem Punkt mit
     * Breit und Höhe.
     *
     * Achtung, falls die Ecken vertauscht sind, wird nichts gezeichnet.
     *
     * @param leftUpperPixel
     *            Linke obere Ecke.
     * @param rightLowerPixel
     *            Rechte untere Ecke.
     */
    protected final void paintUnfilledRectangle(PixelPoint leftUpperPixel,
            PixelPoint rightLowerPixel) {
        graphics2.drawRect(
                leftUpperPixel.getX(),
                leftUpperPixel.getY(),
                rightLowerPixel.getX() - leftUpperPixel.getX(),
                rightLowerPixel.getY() - leftUpperPixel.getY());
    }

    /**
     * Zeichnet ein gefülltes Rechteck sinnvoll zwischen zwei Punkten, statt von einem Punkt mit
     * Breit und Höhe.
     *
     * Achtung, falls die Ecken vertauscht sind, wird nichts gezeichnet.
     *
     * @param leftUpperX
     *            X-Koordinate der linken obere Ecke.
     * @param leftUpperY
     *            Y-Koordinate der linken obere Ecke.
     * @param rightLowerX
     *            X-Koordinate der rechten untere Ecke.
     * @param rightLowerY
     *            Y-Koordinate der rechten untere Ecke.
     */
    protected final void paintTwoPointRectangle(int leftUpperX, int leftUpperY, int rightLowerX,
            int rightLowerY) {
        if (leftUpperX > rightLowerX
                || leftUpperY > rightLowerY) {
            return;
        }
        int x = leftUpperX;
        int y = leftUpperY;
        int width = rightLowerX - leftUpperX;
        int height = rightLowerY - leftUpperY;
        graphics2.fillRect(x, y, width, height);
    }

    /**
     * Zeichnet einen gefüllten Kreis sinnvoll mit einem Radius um einen Mittelpunkt.
     *
     * @param center
     *            Mittelpunkt des Kreises.
     * @param radius
     *            Radius des Kreises.
     */
    protected final void paintCircle(PixelPoint center, int radius) {
        paintCircle(center.getX(),  center.getY(), radius);
    }

    /**
     * Zeichnet einen gefüllten Kreis sinnvoll mit einem Radius um einen Mittelpunkt.
     *
     * @param centerX
     *            X-Koordinate des Mittelpunktes.
     * @param centerY
     *            Y-Koordinate des Mittelpunktes.
     * @param radius
     *            Radius des Kreises.
     */
    protected final void paintCircle(int centerX, int centerY, int radius) {
        int x = centerX - radius;
        int y = centerY - radius;
        int width = 2 * radius;
        int height = 2 * radius;
        paintOval(x, y, width, height);
    }

    /**
     * Zeichnet einen nicht gefüllten Kreis sinnvoll mit einem Radius um einen Mittelpunkt.
     *
     * @param center
     *            Mittelpunkt des Kreises.
     * @param radius
     *            Radius des Kreises.
     */
    protected final void paintHollowCircle(PixelPoint center, int radius) {
        paintHollowCircle(center.getX(),  center.getY(), radius);
    }

    /**
     * Zeichnet einen nicht gefüllten Kreis sinnvoll mit einem Radius um einen Mittelpunkt.
     *
     * @param centerX
     *            X-Koordinate des Mittelpunktes.
     * @param centerY
     *            Y-Koordinate des Mittelpunktes.
     * @param radius
     *            Radius des Kreises.
     */
    protected final void paintHollowCircle(int centerX, int centerY, int radius) {
        int x = centerX - radius;
        int y = centerY - radius;
        int width = 2 * radius;
        int height = 2 * radius;
        paintHollowOval(x, y, width, height);
    }

    /**
     * Zeichnet ein Oval ein.
     *
     * @param x
     *            Die x-Koordinate des Mittelpunktes.
     * @param y
     *            Die x-Koordinate des Mittelpunktes.
     * @param width
     *            Die Breite des Ovals.
     * @param height
     *            Die Höhe des Ovals.
     */
    protected final void paintOval(int x, int y, int width, int height) {
        graphics2.fillOval(x, y, width, height);
    }

    /**
     * Zeichnet ein Oval ein.
     *
     * @param x
     *            Die x-Koordinate des Mittelpunktes.
     * @param y
     *            Die x-Koordinate des Mittelpunktes.
     * @param width
     *            Die Breite des Ovals.
     * @param height
     *            Die Höhe des Ovals.
     */
    protected final void paintHollowOval(int x, int y, int width, int height) {
        graphics2.drawOval(x, y, width, height);
    }

    /**
     * Erzeugt ein Polygon aus zwei Arrays mit den Koordinaten und deren Anzahl.
     *
     * @param xPoints
     *            Array mit den x-Koordinaten der Punkte.
     * @param yPoints
     *            Array mit den y-Koordinaten der Punkte.
     * @param nPoints
     *            Anzahl der Koordinaten in beiden Arrays.
     */
    protected final Polygon createPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        return new Polygon(xPoints, yPoints, nPoints);
    }

    /**
     * Erzeugt ein Polygon aus zwei Arrays mit den Koordinaten.
     *
     * @param xPoints
     *            Array mit den x-Koordinaten der Punkte.
     * @param yPoints
     *            Array mit den y-Koordinaten der Punkte.
     * @throws RuntimeException
     *             Falls xPoints und yPoints eine unterschiedliche Länge haben.
     */
    protected final Polygon createPolygon(int[] xPoints, int[] yPoints) {
        int size = xPoints.length;
        if (size != yPoints.length) {
            throw new RuntimeException("xpoints und ypoints haben unterschiedliche Längen!"
                    + "\n\txPoints: " + size + "\n\tyPoints: " + yPoints.length);
        }
        return createPolygon(xPoints, yPoints, size);
    }

    /**
     * Erzeugt ein Polygon.
     *
     * @param points
     *            Liste der zu zeichnenden Punkte.
     */
    protected final Polygon createPolygon(List<PixelPoint> points) {
        int size = points.size();
        int[] xPoints = new int[size];
        int[] yPoints = new int[size];

        int index = 0;
        for (PixelPoint point : points) {
            xPoints[index] = point.getX();
            yPoints[index] = point.getY();
            ++index;
        }
        return createPolygon(xPoints, yPoints, size);
    }

    /**
     * Erzeugt ein Polygon.
     *
     * @param points
     *            Array der zu zeichnenden Punkte.
     */
    protected final Polygon createPolygon(PixelPoint ... points) {
        return createPolygon(Arrays.asList(points));
    }

    /**
     * Zeichnet ein Polygon ein.
     *
     * @param polygon
     *            Das zu zeichnende Polygon.
     */
    protected final void paintPolygon(Polygon polygon) {
        graphics2.fillPolygon(polygon);
    }

    /**
     * Zeichnet ein Polygon ein.
     *
     * @param xPoints
     *            Array mit den x-Koordinaten der Punkte.
     * @param yPoints
     *            Array mit den y-Koordinaten der Punkte.
     * @param nPoints
     *            Anzahl der Koordinaten in beiden Arrays.
     */
    protected final void paintPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        paintPolygon(createPolygon(xPoints, yPoints, nPoints));
    }

    /**
     * Zeichnet ein Polygon ein.
     *
     * @param xPoints
     *            Array mit den x-Koordinaten der Punkte.
     * @param yPoints
     *            Array mit den y-Koordinaten der Punkte.
     * @throws RuntimeException
     *             falls xPoints und yPoints eine unterschiedliche Länge haben.
     */
    protected final void paintPolygon(int[] xPoints, int[] yPoints) {
        paintPolygon(createPolygon(xPoints, yPoints));
    }

    /**
     * Zeichnet ein Polygon ein.
     *
     * @param points
     *            Liste der zu zeichnenden Punkte.
     */
    protected final void paintPolygon(List<PixelPoint> points) {
        paintPolygon(createPolygon(points));
    }

    /**
     * Zeichnet ein Polygon ein.
     *
     * @param points
     *            Array der zu zeichnenden Punkte.
     */
    protected final void paintPolygon(PixelPoint ... points) {
        paintPolygon(Arrays.asList(points));
    }

    /**
     * Zeichnet ein nicht ausgefülltes Polygon ein.
     *
     * @param polygon
     *            Das zu zeichnende Polygon.
     */
    protected final void paintHollowPolygon(Polygon polygon) {
        graphics2.drawPolygon(polygon);
    }

    /**
     * Zeichnet ein nicht ausgefülltes Polygon ein.
     *
     * @param xPoints
     *            Array mit den x-Koordinaten der Punkte.
     * @param yPoints
     *            Array mit den y-Koordinaten der Punkte.
     * @param nPoints
     *            Anzahl der Koordinaten in beiden Arrays.
     */
    protected final void paintHollowPolygon(int[] xPoints, int[] yPoints, int nPoints) {
        paintHollowPolygon(createPolygon(xPoints, yPoints, nPoints));
    }

    /**
     * Zeichnet ein nicht ausgefülltes Polygon ein.
     *
     * @param xPoints
     *            Array mit den x-Koordinaten der Punkte.
     * @param yPoints
     *            Array mit den y-Koordinaten der Punkte.
     * @throws RuntimeException
     *             falls xPoints und yPoints eine unterschiedliche Länge haben.
     */
    protected final void paintHollowPolygon(int[] xPoints, int[] yPoints) {
        paintHollowPolygon(createPolygon(xPoints, yPoints));
    }

    /**
     * Zeichnet ein nicht ausgefülltes Polygon ein.
     *
     * @param points
     *            Liste der zu zeichnenden Punkte.
     */
    protected final void paintHollowPolygon(List<PixelPoint> points) {
        paintHollowPolygon(createPolygon(points));
    }

    /**
     * Zeichnet ein nicht ausgefülltes Polygon ein.
     *
     * @param points
     *            Array der zu zeichnenden Punkte.
     */
    protected final void paintHollowPolygon(PixelPoint ... points) {
        paintHollowPolygon(Arrays.asList(points));
    }

    /**
     * Zeichnet eine Linie ein.
     *
     * @param start
     *            Der Startpunkt der Linie.
     * @param end
     *            Der Endpunkt der Linie.
     */
    protected final void paintLine(PixelPoint start, PixelPoint end) {
        graphics2.drawLine(
                start.getX(),
                start.getY(),
                end.getX(),
                end.getY());
    }

    /**
     * Zeichnet eine Linie ein.
     *
     * @param line
     *            Die Linie.
     */
    protected final void paintLine(PixelLine line) {
        paintLine(line.getStart(), line.getEnd());
    }

}