package de.duehl.swing.ui.buttons.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.BorderFactory;
import javax.swing.JButton;
import javax.swing.border.Border;

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

import static de.duehl.swing.ui.buttons.ButtonHelper.BUTTON_DIMENSION;
import static de.duehl.swing.ui.buttons.ButtonHelper.BUTTON_MOUSE_LISTENER;

/**
 * Diese Klasse stellt die Basisklasse für die selbstgezeichneten Buttons dar.
 *
 * @version 1.01     2025-07-23
 * @author Christian Dühl
 */

public abstract class PaintedButton extends JButton {

    /**
     * Diese Klasse hält die Werte, die gebraucht werden, um eine nicht-flache Darstellung zu
     * erzeugen.
     */
    public class UnFlatValues {

        private final boolean contentAreaFilled;
        private final Border border;
        private final boolean borderPainted;

        public UnFlatValues(boolean contentAreaFilled, Border border, boolean borderPainted) {
            this.contentAreaFilled = contentAreaFilled;
            this.border = border;
            this.borderPainted = borderPainted;
        }

        public boolean isContentAreaFilled() {
            return contentAreaFilled;
        }

        public Border getBorder() {
            return border;
        }

        public boolean isBorderPainted() {
            return borderPainted;
        }

    }

    private static final long serialVersionUID = -1L;

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

    /**
     * Gibt an, ob der Schalter in einer horizontalen oder vertikalen Anordnung angezeigt werden
     * soll.
     */
    private boolean horizontal;

    /** 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;

    /** Werte, um die nicht-flache Darstellung herzustellen. */
    private final UnFlatValues unFlatValues;

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

    /** Gibt an, ob die Farbe bei MouseOver geändert werden soll. */
    private boolean changeColorOnMouseOver;

    /** Gibt an, ob der Button bei MouseOver etwas verschoben dargestellt werden. */
    private boolean shiftOnMouseOver;

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

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

        this.symbolColor = symbolColor;
        this.backgroundColor = null;
        this.horizontal = true;
        this.forceSquare = false;

        changeColorOnMouseOver = true;
        shiftOnMouseOver = true;

        //setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
        setPreferredSize(BUTTON_DIMENSION);
        setFocusable(false);

        unFlatValues = new UnFlatValues(isContentAreaFilled(), getBorder(), isBorderPainted());
        showFlat();

        addMouseListener(BUTTON_MOUSE_LISTENER);
        setRolloverEnabled(true);
    }

    /**
     * Entfernt den standardmäßig hinzugefügten MouseListener, der einen Rahmen anzeigt, wenn die
     * Maus über dem Button ist.
     */
    public void removeStandardMouseListener() {
        removeMouseListener(BUTTON_MOUSE_LISTENER);
    }

    /** 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();
    }

    /** Legt fest, dass die Farbe bei MouseOver nicht geändert werden soll. */
    public final void ignoreMouseOver() {
        changeColorOnMouseOver = false;
    }

    /** Gibt an, ob die Farbe bei MouseOver geändert werden soll. */
    public final boolean isChangeColorOnMouseOver() {
        return changeColorOnMouseOver;
    }

    /** Gibt an, ob der Button bei MouseOver etwas verschoben dargestellt werden. */
    public boolean isShiftOnMouseOver() {
        return shiftOnMouseOver;
    }

    /** Legt fest, dass der Button bei MouseOver nicht etwas verschoben dargestellt werden. */
    public void noShiftOnMouseOver() {
        shiftOnMouseOver = false;
    }

    /** Stellt auf flache Darstellung um (Standard). */
    public void showFlat() {
        setContentAreaFilled(false);
        setBorder(BorderFactory.createEtchedBorder());
        setBorderPainted(false);
    }

    /** Stellt auf nicht-flache Darstellung um. */
    public void showNonFlat() {
        setContentAreaFilled(unFlatValues.isContentAreaFilled());
        setBorder(unFlatValues.getBorder());
        setBorderPainted(unFlatValues.isBorderPainted());
    }

    /**
     * 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;
    }

    /**
     * Gibt an, ob der Schalter in einer horizontalen oder vertikalen Anordnung angezeigt werden
     * soll.
     */
    public boolean isHorizontal() {
        return horizontal;
    }

    /**
     * Legt fest, ob der Schalter in einer horizontalen oder vertikalen Anordnung angezeigt werden
     * soll.
     */
    public void setHorizontal(boolean horizontal) {
        this.horizontal = horizontal;
    }

    /**
     * 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();

        shiftIfButtoninIsPressed();
        setStroke();

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

        graphics2.dispose();
    }

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

    /** Für gedrückte Buttons wird die Grafik sacht verschoben. */
    private void shiftIfButtoninIsPressed() {
        if (shiftOnMouseOver && getModel().isPressed()) {
            graphics2.translate(1, 1);
        }
    }

    /** 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) {
            if (horizontal) {
                paintHorizontalSymbolSquare();
            }
            else {
                paintVerticalSymbolSquare();
            }
        }
        else {
            if (horizontal) {
                paintHorizontalSymbolRectangle();
            }
            else {
                paintVerticalSymbolRectangle();
            }
        }
    }

//    /**
//     * Farbwahl abhängig davon, ob die Maus über dem Button ist.
//     *
//     * Default: Schwarz normalerweise, wenn die Maus darüber kommt, Rot.
//     */
//    protected void setSymbolColor() {
//        //graphics2.setColor(Color.BLACK);
//        Color normalColor = getForeground();
//        graphics2.setColor(normalColor);
//        if (getModel().isRollover()) {
//            //graphics2.setColor(Color.RED);
//            graphics2.setColor(ButtonHelper.antiColor(normalColor));
//        }
//    }

    /**
     * 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 void setSymbolColorInGraphics() {
        graphics2.setColor(symbolColor);
        if (changeColorOnMouseOver && getModel().isRollover()) {
            graphics2.setColor(ButtonHelper.antiColor(symbolColor));
        }
    }

    /** Zeichnet das Symbol in horizontaler und rechteckiger Anordnung. */
    abstract protected void paintHorizontalSymbolRectangle();

    /** Zeichnet das Symbol in vertikaler und rechteckiger Anordnung. */
    abstract protected void paintVerticalSymbolRectangle();

    /** Zeichnet das Symbol in horizontaler und quadratischer Anordnung. */
    abstract protected void paintHorizontalSymbolSquare();

    /** Zeichnet das Symbol in vertikaler und quadratischer Anordnung. */
    abstract protected void paintVerticalSymbolSquare();

    /**
     * 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());
    }

}