package de.duehl.swing.ui.elements.pictures;

/*
 * 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.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

/**
 * Diese Klasse stellt ein Bild dar, dass sich an die Ausmaße des Panels anpasst.
 *
 * Links zum Thema:
 *
 * https://wiki.byte-welt.net/wiki/Bilder_skalieren
 *
 * http://web.archive.org/web/20070515094604/https://today.java.net/pub/a/today/2007/04/03/
 *     perils-of-image-getscaledinstance.html
 * https://stackoverflow.com/questions/24745147/java-resize-image-without-losing-quality/
 *     24746194#24746194
 * https://stackoverflow.com/questions/3967731/
 *     how-to-improve-the-performance-of-g-drawimage-method-for-resizing-images/32278737#32278737
 *
 *
 * @version 1.01     2025-07-19
 * @author Christian Dühl
 */

public class PicturePanel extends JPanel {

    private static final long serialVersionUID = 1L;

    private static final PictureCache CACHE = new PictureCache(10_000);


    /** Das Bild das dargestellt wird. */
    private BufferedImage image;

    /** Die Breite des Bildes. */
    private int imageWidth;

    /** Die Höhe des Bildes. */
    private int imageHeight;

    /**
     * Die Hintergrundfarbe die zu sehen ist, wenn das Seitenverhältnis des Bildes nicht dem
     * Seitenverhältnis des Panels entspricht.
     */
    private Color backgroundColor;

    /** Gibt an, ob das Bild größer dargestellt werden darf, als es ist. */
    private boolean allowBiggerPicture;

    private int horizontalAlignment;

    /** Gibt an, ob fehlende Bilder erlaubt sind. Anderenfalls wird eine Ausnahme geworfen. */
    private boolean missingPicturesAllowed;

    /**
     * Gibt an, ob die eingelesenen Bilder im Cache gespeichert werden.
     *
     * Vorteil: Ein neues Betrachten des Bildes geht schneller.
     *
     * Nachteil: Ändert sich das Bild zwischendurch, sieht man dann noch das alte Bild. Außerdem
     * könnte beim Betrachten sehr vieler Bilder der Speicher recht voll werden.
     */
    private boolean cacheImages;

    /** Konstruktor. */
    public PicturePanel() {
        setAllowBiggerPicture(true);
        setHorizontalCentered();
        missingPicturesAllowed = false;
    }

    /**
     * Konstruktor.
     *
     * @param pictureFilename
     *            Der Dateiname des anzuzeigenden Bildes mit Pfad.
     */
    public PicturePanel(String pictureFilename) {
        this();
        loadPicture(pictureFilename);
    }

    /** Legt fest, ob das Bild größer dargestellt werden darf, als es ist. */
    public void setAllowBiggerPicture(boolean allowBiggerPicture) {
        this.allowBiggerPicture = allowBiggerPicture;
    }

    /**
     * Setter für die Hintergrundfarbe die zu sehen ist, wenn das Seitenverhältnis des Bildes nicht
     * dem Seitenverhältnis des Panels entspricht. Der Wert null steht dafür, dass keine
     * Hintergrundfarbe eingezeichnet wird.
     */
    public void setBackgroundColor(Color backgroundColor) {
        this.backgroundColor = backgroundColor;
    }

    /** Setzt das Bild horizontal mittig. */
    public void setHorizontalCentered() {
        horizontalAlignment = SwingConstants.CENTER;
    }

    /** Setzt das Bild horizontal an den linken Rand. */
    public void setHorizontalLeft() {
        horizontalAlignment = SwingConstants.LEFT;
    }

    /** Setzt das Bild horizontal an den rechten Rand. */
    public void setHorizontalRight() {
        horizontalAlignment = SwingConstants.RIGHT;
    }

    /** Legt fest, dass fehlende Bilder erlaubt sind. */
    public void allowMissingPictures() {
        missingPicturesAllowed = true;
    }

    /**
     * Legt fest, dass die eingelesenen Bilder im Cache gespeichert werden.
     *
     * Vorteile:
     *  - Ein neues Betrachten des Bildes geht schneller.
     *
     * Nachteile:
     *  - Ändert sich das Bild zwischendurch, sieht man dann noch das alte Bild.
     *  - Außerdem könnte beim Betrachten sehr vieler Bilder der Speicher recht voll werden.
     *
     * Um den zweiten nachteiligen Punkt zu vermeiden, wird dem Cache eine Obergrenze von 10.000
     * Bildern gesetzt.
     */
    public void usePictureCache() {
        cacheImages = true;
    }

    /**
     * Lädt das Bild aus der angegebenen Datei.
     *
     * @param pictureFilename
     *            Der Dateiname des anzuzeigenden Bildes mit Pfad.
     */
    public void loadPicture(String pictureFilename) {
        if (cacheImages && CACHE.isFilenameKnown(pictureFilename)) {
            loadPictureFromCache(pictureFilename);
        }
        else {
            reallyLoadPicture(pictureFilename);
            if (cacheImages && image != null) {
                CACHE.store(pictureFilename, image);
            }
        }
    }

    private void loadPictureFromCache(String pictureFilename) {
        image = CACHE.load(pictureFilename);
        imageWidth = image.getWidth();
        imageHeight = image.getHeight();
    }

    private void reallyLoadPicture(String pictureFilename) {
        try {
            tryToLoadPicture(pictureFilename);
        }
        catch (Exception exception) {
            handleLoadException(pictureFilename, exception);
        }
    }

    private void tryToLoadPicture(String pictureFilename) throws IOException {
        image = ImageIO.read(new File(pictureFilename));
        imageWidth = image.getWidth();
        imageHeight = image.getHeight();
    }

    private void handleLoadException(String pictureFilename, Exception exception) {
        image = null;
        imageWidth = -1;
        imageHeight = -1;
        if (!missingPicturesAllowed) {
            throw new RuntimeException("Fehler beim Einlesen des Bildes '" + pictureFilename
                    + "': " + exception.getMessage());
        }
    }

    /** Setzt das Bild direkt als anderweitig beschafftes BufferedImage. */
    public void setPictureDirectly(BufferedImage image) {
        this.image = image;
        if (image == null) {
            imageWidth = -1;
            imageHeight = -1;
            if (!missingPicturesAllowed) {
                throw new RuntimeException("Es wurde ein null-Bildes direkt gesetzt.");
            }
        }
        else {
            imageWidth = image.getWidth();
            imageHeight = image.getHeight();
        }
    }

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

        if (null != backgroundColor) {
            drawBackgroundColor(graphics2);
        }

        if (null != image) {
            setRenderingHints(graphics2);
            drawImage(graphics2);
        }

        graphics2.dispose();
    }

    private void drawBackgroundColor(Graphics2D graphics2) {
        graphics2.setColor(backgroundColor);
        graphics2.fillRect(0, 0, getWidth() - 1, getHeight() - 1);
    }

    /**
     * Hier wird gesagt, dass bilineare Interpolation verwendet werden soll, was zwar etwas
     * langsamer ist, aber für höhere Bildqualität sorgt.
     *
     * Hätte man statt VALUE_INTERPOLATION_BILINEAR den Wert
     * VALUE_INTERPOLATION_NEAREST_NEIGHBOR angegeben, wäre die Bildqualität geringer, aber das
     * Zeichnen etwas schneller.
     */
    private void setRenderingHints(Graphics2D graphics2) {
        graphics2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    }

    /** Zeichnet das Bild skaliert so, dass es auf das Panel passt und nicht verzerrt wird. */
    private void drawImage(Graphics2D graphics2) {
        int panelWidth = getWidth();
        int panelHeight = getHeight();

        double factorWidth = computeFactor(imageWidth, panelWidth);
        double factorHeight = computeFactor(imageHeight, panelHeight);

        double factor;
        if (factorWidth < factorHeight) {
            factor = factorWidth;
        }
        else {
            factor = factorHeight;
        }

        int width = (int) (imageWidth * factor);
        int height = (int) (imageHeight * factor);

        int x;
        int y;
        if (width < panelWidth) {
            if (horizontalAlignment == SwingConstants.CENTER) {
                x = (panelWidth - width) / 2;
            }
            else if (horizontalAlignment == SwingConstants.LEFT) {
                x = 0;
            }
            else if (horizontalAlignment == SwingConstants.RIGHT) {
                x = panelWidth - width;
            }
            else {
                throw new RuntimeException("Unbekannte horizontalAlignment: '"
                        + horizontalAlignment + "'");
            }
            y = 0;
        }
        else if (height < panelHeight) {
            x = 0;
            y = (panelHeight - height) / 2;
        }
        else  {
            x = 0;
            y = 0;
        }

        graphics2.drawImage(image, x, y, width, height, null);
    }

    private double computeFactor(int original, int screen) {
        if (original <= screen && !allowBiggerPicture) {
            return 1.0;
        }
        else {
            return ((double) screen) / ((double) original);
        }
    }

    /** Getter für die Breite des Bildes. */
    public int getImageWidth() {
        return imageWidth;
    }

    /** Getter für die Höhe des Bildes. */
    public int getImageHeight() {
        return imageHeight;
    }

    /**
     * Erzeugt einen MouseWheelListener, welcher den reactor informiert, und hinterlegt ihn.
     *
     * @param reactor
     *            Die Komponente die auf das Scrollen reagiert.
     */
    public void createAndAddMouseWheelListener(MouseWheelReactor reactor) {
        addMouseWheelListener(PicturePanelHelper.createMouseWheelListener(reactor));
    }

}
