/******************************************************************************
 ** $Id: BalkenVerlauf.java 794 2015-10-13 20:54:59Z wmh $
 ** Diese Datei ist Bestandteil der Java-Quelltexte des Würfelspiels JaFuffy.
 ** Lauffähig ab Java 6.
 ******************************************************************************
 ** Copyright (C) Wolfgang Hauck <wolfgang.hauck@3kelvin.de>
 ******************************************************************************
 ** This program is free software; you can redistribute it and/or modify
 ** it under the terms of the GNU General Public License as published by
 ** the Free Software Foundation; either version 2 of the License, or
 ** (at your option) any later version.
 **
 ** This program is distributed in the hope that it will be useful,
 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 ** GNU General Public License for more details.
 **
 ** You should have received a copy of the GNU General Public License
 ** along with this program; if not, write to the Free Software
 ** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 ******************************************************************************
 ** Die aktuellste Version von JaFuffy findet sich im Internet unter
 ** <http://jafuffy.3kelvin.de>.
 **
 ** Kommentare, Fehler oder Erweiterungswünsche bitte per E-Mail senden an
 ** <jafuffy@3kelvin.de>.
 ******************************************************************************/
package jafuffy.bedienung;

import jafuffy.logik.Spieler;
import jafuffy.logik.Statistik;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;

import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;

/** Anzeige des Turnierverlaufs, nach jeder Runde aktualisiert. */
@SuppressWarnings("serial")
class BalkenVerlauf extends JPanel {

    // (min.) Größe des Zeichenfeldes & Beschriftungsgröße
    private static final int BREITE = 400;
    private static final int HOEHE = 250;
    private static final int RAND = 20;
    private static final int BESCHRIFTUNG = 20;
    // Konstanten für Balkengrafik (Abstände)
    private static final double LUECKE_HOR = 50;
    private static final double LUECKE_VERT = 15;
    private static final double BALKENBREITE = 75;
    private static final double BALKENTIEFE = 15;
    // Neigung für Perspektive
    private static final double STEIGUNG = 0.5;
    // Hilfslinie
    private static final float[] DASH = { 16 };
    // Farbe
    private static final Color WAND_RECHTS = new Color(240, 240, 240);
    private static final Color WAND_UNTEN = new Color(235, 235, 235);
    private static final Color WAND_HINTEN = new Color(245, 245, 245);
    private static final Color[] BALKEN_UNTEN = new Color[Spieler.SPIELER];
    private static final Color[] BALKEN_OBEN = new Color[Spieler.SPIELER];
    // Farbverlauf unten -> oben
    static {
        for (int i = 0; i < Spieler.SPIELER; i++) {
            BALKEN_UNTEN[i] = Report.FARBEN[i].darker();
            BALKEN_OBEN[i] = Report.FARBEN[i].brighter();
        }
    }

    // Zoomfunktionen
    private final JPopupMenu popup = new JPopupMenu();
    private final JMenuItem kleiner = new JMenuItem("Verkleinern");
    private final JMenuItem groesser = new JMenuItem("Vergrößern");
    private final JMenuItem normal = new JMenuItem("Standard");
    private final JMenuItem aktuell = new JMenuItem("Aktuell");

    // statistische Daten & Spielverlauf
    private final Statistik statistik;
    private ArrayList<Integer>[] verlauf;
    // Anzahl der Spiele (tatsächlich gespielt bzw. Vorgabe pro Turnier)
    private int anzahl;
    // max. Höhe für Balkengrafik (über Null)
    private double maxHoehe;
    // Höhe der hinteren Koordinatenwand
    private double wandhoehe;
    // Transformation zum Zeichnen des Balkendiagramms
    private AffineTransform trafo;
    private double tx;
    private double ty;

    // für Aktualisierung (Ausschnittsverschiebung)
    private class Aktualisierung implements Runnable {
        @Override
        public void run() {
            aktualisieren();
            repaint();
        }
    }

    private final Aktualisierung aktualisierung = new Aktualisierung();

    // Zoom-Faktor
    private int faktor = 1;
    // Feldgröße anpassen und zum aktuellen Stand springen?
    private boolean anpassen;

    /**
     * Konstruktor.
     *
     * @param statistik
     */
    BalkenVerlauf(Statistik statistik) {

        setBackground(Color.white);
        setPreferredSize(new Dimension(BREITE, HOEHE));

        this.statistik = statistik;

        setToolTipText("<html><p>Aufschlüsselung nach den einzelnen Spielen.</p>"
                + "<p>Zoomfunktionen auf rechter Maustaste.</p>"
                + "<p>Ziehen (\"Dragging\") verfügbar.</p></html>");

        // kleiner/größer/normal
        ActionListener zoom = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                zoomen(e);
            }
        };
        kleiner.setEnabled(false);
        kleiner.addActionListener(zoom);
        groesser.addActionListener(zoom);
        normal.setEnabled(false);
        normal.addActionListener(zoom);
        aktuell.addActionListener(zoom);
        aktuell.setToolTipText("Letzten Stand anzeigen");
        // Popup-Menü
        popup.add(kleiner);
        popup.add(groesser);
        popup.addSeparator();
        popup.add(normal);
        popup.addSeparator();
        popup.add(aktuell);
        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                maybeShowPopup(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                maybeShowPopup(e);
            }

            private void maybeShowPopup(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    kleiner.setEnabled(faktor > 1);
                    normal.setEnabled(faktor > 1);
                    popup.show(e.getComponent(), e.getX(), e.getY());
                }
            }
        });

    }

    /**
     * Look&Feel korrekt ändern (auch Kontextmenü).
     */
    @Override
    public void updateUI() {
        super.updateUI();
        if (popup != null) {
            SwingUtilities.updateComponentTreeUI(popup);
        }
    }

    @Override
    public void setVisible(boolean sichtbar) {
        if (sichtbar && anpassen) {
            aktualisieren();
        }
    }

    /** Neues Turnier gestartet oder Turnier fortgesetzt. */
    void neuesTurnier() {
        verlauf = statistik.verlauf();
        wandhoehe = Math.ceil((statistik.mittelwert() + statistik.abweichung()) / 100.) * 100.;
        maxHoehe = wandhoehe;
        anzahl = statistik.anzahl();
        if (anzahl == 0) {
            anzahl = verlauf[0].size();
        }
        anpassen = true;
        faktor();
        repaint();
        SwingUtilities.invokeLater(aktualisierung);
    }

    /** Neuer Spielstand. */
    void neuerStand() {
        if (anpassen) {
            aktualisieren();
        }
    }

    /** Spiel beendet. */
    void spielende() {
        anpassen = true;
        if (statistik.anzahl() == 0) {
            anzahl++;
            faktor();
            repaint();
            SwingUtilities.invokeLater(aktualisierung);
        }
    }

    /**
     * Trafo berechnen, Balkendiagramm neu zeichnen.
     *
     * @param g
     */
    @Override
    protected void paintComponent(Graphics g) {

        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g;

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);

        // Trafo und Koordinatenwände
        koordinatenwaende(g2, true);
        // Balkengrafik
        balkengrafik(g2);

    }

    /**
     * Erstellt Transformation und zeichnet hintere, seitliche und untere Koordinatenwände.
     *
     * @param g2
     *            Grafikumfeld
     * @param zeichnen
     *            Wirklich zeichnen
     */
    private void koordinatenwaende(Graphics2D g2, boolean zeichnen) {

        super.paintComponent(g2);
        g2.setFont(new Font("Dialog", Font.PLAIN, BESCHRIFTUNG));

        FontMetrics fm = g2.getFontMetrics();
        double beschriftungslaenge =
                BESCHRIFTUNG / 2 + fm.stringWidth(String.valueOf(statistik.mittelwert()));
        double nutzbreite = getWidth() - beschriftungslaenge - 2 * RAND;
        double nutzhoehe = getHeight() - 2 * RAND;
        double grundbreite =
                statistik.spieler().size() * (BALKENBREITE + LUECKE_HOR) - LUECKE_HOR;
        double grundhoehe = -y(anzahl);
        double breite = grundbreite + grundhoehe / STEIGUNG;
        double hoehe = grundhoehe + maxHoehe;
        double skalierung;

        // merke Ursprung
        trafo = g2.getTransform();
        tx = trafo.getTranslateX();
        ty = trafo.getTranslateY();

        // affine Transformation für die Umrechnung aus abstrakten
        // 2D-Koordinaten in Bildschirmdarstellung. Fallunterscheidung
        // langes/hohes Grafikfeld.
        if (hoehe / breite <= nutzhoehe / nutzbreite) {
            skalierung = nutzbreite / breite;
            g2.translate(grundbreite * skalierung + RAND, (nutzhoehe - hoehe * skalierung) / 2
                    + maxHoehe * skalierung + RAND);
        } else {
            skalierung = nutzhoehe / hoehe;
            g2.translate((nutzbreite - breite * skalierung) / 2 + grundbreite * skalierung
                    + RAND, maxHoehe * skalierung + RAND);
        }
        g2.scale(skalierung, skalierung);

        // Beschriftung
        if (zeichnen) {
            for (int punkte = 0; punkte <= wandhoehe; punkte += 100) {
                g2.drawString(String.valueOf(punkte), (float) (BESCHRIFTUNG / 2 + grundhoehe
                        / STEIGUNG), (float) (grundhoehe - punkte - BESCHRIFTUNG / 4));
            }
        }

        // endgültige Transformation auf Bildschirm
        g2.scale(-1, -1);
        trafo = g2.getTransform();

        // Nur Trafo verlangt?
        if (!zeichnen) {
            return;
        }

        // Grafikgrößen
        Rectangle2D rechteck;
        BasicStroke dick = new BasicStroke((float) (2 / skalierung));
        BasicStroke normal = new BasicStroke((float) (1 / skalierung));
        BasicStroke gestrichelt =
                new BasicStroke((float) (1 / skalierung), BasicStroke.CAP_BUTT,
                        BasicStroke.JOIN_BEVEL, 1, DASH, 0);

        // hintere Wand
        rechteck = new Rectangle2D.Double(0, 0, grundbreite, wandhoehe);
        g2.setTransform(trafo);
        g2.setPaint(WAND_HINTEN);
        g2.fill(rechteck);
        g2.setPaint(Color.black);
        g2.setStroke(dick);
        g2.draw(rechteck);
        g2.setStroke(normal);
        for (double y = 100; y < wandhoehe; y += 100) {
            g2.draw(new Line2D.Double(0, y, grundbreite, y));
        }
        g2.setStroke(gestrichelt);
        for (double y = 50; y < wandhoehe; y += 100) {
            g2.draw(new Line2D.Double(0, y, grundbreite, y));
        }

        // rechte Wand
        rechteck =
                new Rectangle2D.Double(-grundhoehe / STEIGUNG, 0, grundhoehe / STEIGUNG,
                        wandhoehe);
        g2.setTransform(trafo);
        g2.shear(0, STEIGUNG);
        g2.setPaint(WAND_RECHTS);
        g2.fill(rechteck);
        g2.setPaint(Color.black);
        g2.setStroke(dick);
        g2.draw(rechteck);
        g2.setStroke(normal);
        for (double y = 100; y < wandhoehe; y += 100) {
            g2.draw(new Line2D.Double(-grundhoehe / STEIGUNG, y, 0, y));
        }
        g2.setStroke(gestrichelt);
        for (double y = 50; y < wandhoehe; y += 100) {
            g2.draw(new Line2D.Double(-grundhoehe / STEIGUNG, y, 0, y));
        }

        // Boden
        rechteck = new Rectangle2D.Double(0, -grundhoehe, grundbreite, grundhoehe);
        g2.setTransform(trafo);
        g2.shear(1 / STEIGUNG, 0);
        g2.setPaint(WAND_UNTEN);
        g2.fill(rechteck);
        g2.setPaint(Color.black);
        g2.setStroke(dick);
        g2.draw(rechteck);

        // Linienstil zurücksetzen auf mittel
        g2.setStroke(new BasicStroke((float) (1.5 / skalierung)));

    }

    /**
     * Zeichnet Balkengrafik
     *
     * @param g2
     * @param g2
     *            Grafikumfeld
     */
    private void balkengrafik(Graphics2D g2) {

        int punkte;
        Rectangle2D rechteck;
        GradientPaint farbverlauf;
        double x;
        double y;

        for (int s = verlauf.length - 1; s >= 0; s--) {
            for (int i = 0; i < verlauf[s].size(); i++) {
                punkte = verlauf[s].get(i).intValue();
                x = x(s, i) - BALKENTIEFE / STEIGUNG; // vorne links unten
                y = y(i) - BALKENTIEFE; // vorne links unten
                if (punkte > 0) {
                    if (y(i) + punkte > maxHoehe) {
                        maxHoehe = y(i) + punkte;
                    }
                    // vorne
                    rechteck = new Rectangle2D.Double(x, y, BALKENBREITE, punkte);
                    farbverlauf =
                            new GradientPaint(0, (float) y, BALKEN_UNTEN[s], 0,
                                    (float) (y + wandhoehe), BALKEN_OBEN[s]);
                    g2.setPaint(farbverlauf);
                    g2.setTransform(trafo);
                    g2.fill(rechteck);
                    g2.setPaint(Color.black);
                    g2.draw(rechteck);
                    // seitlich
                    rechteck = new Rectangle2D.Double(0, 0, BALKENTIEFE / STEIGUNG, punkte);
                    farbverlauf =
                            new GradientPaint(0, 0, BALKEN_UNTEN[s], 0, (float) wandhoehe,
                                    BALKEN_OBEN[s]);
                    g2.setTransform(trafo);
                    g2.translate(x + BALKENBREITE, y);
                    g2.shear(0, STEIGUNG);
                    g2.setPaint(farbverlauf);
                    g2.fill(rechteck);
                    g2.setPaint(Color.black);
                    g2.draw(rechteck);
                    // oben
                    rechteck = new Rectangle2D.Double(0, 0, BALKENBREITE, BALKENTIEFE);
                    g2.setPaint(deckelfarbe(s, punkte));
                    g2.setTransform(trafo);
                    g2.translate(x, y + punkte);
                    g2.shear(1 / STEIGUNG, 0);
                    g2.fill(rechteck);
                    g2.setPaint(Color.black);
                    g2.draw(rechteck);
                }
            }
        }

    }

    /**
     * @param s
     *            Nummer des Spielers
     * @param i
     *            Nummer des Spiels im Turnier
     * @return x-Wert des rechten hinteren Balkenfußpunktes
     */
    private double x(int s, int i) {
        return (BALKENBREITE + LUECKE_HOR) * (statistik.spieler().size() - s - 1) + y(i)
                / STEIGUNG;
    }

    /**
     * @param i
     *            Nummer des Spiels im Turnier
     * @return y-Wert des rechten hinteren Balkenfußpunktes
     */
    private double y(int i) {
        return -(BALKENTIEFE + LUECKE_VERT) * i;
    }

    /**
     * @param s
     *            Nummer des Spielers
     * @param punkte
     *            Balkenhöhe
     */
    private Color deckelfarbe(int s, int punkte) {

        int r1 = BALKEN_UNTEN[s].getRed();
        int g1 = BALKEN_UNTEN[s].getGreen();
        int b1 = BALKEN_UNTEN[s].getBlue();
        int r2 = BALKEN_OBEN[s].getRed();
        int g2 = BALKEN_OBEN[s].getGreen();
        int b2 = BALKEN_OBEN[s].getBlue();

        if (punkte < wandhoehe) {
            return new Color(r1 + (r2 - r1) * punkte / (int) wandhoehe, g1 + (g2 - g1) * punkte
                    / (int) wandhoehe, b1 + (b2 - b1) * punkte / (int) wandhoehe);
        } else {
            return BALKEN_OBEN[s];
        }

    }

    /**
     * Zeichenfeldgroesse ändern oder zum aktuellen Stand vorrücken
     *
     * @param e
     */
    private void zoomen(ActionEvent e) {

        String ac = e.getActionCommand();
        Rectangle r = ((JViewport) getParent()).getViewRect();
        int u = r.x + r.width / 2; // Ausschnittmittelpunkt
        int v = r.y + r.height / 2; // Ausschnittmittelpunkt

        if (ac.equals("Vergrößern")) {
            u *= 2;
            v *= 2;
            faktor *= 2;
            zoomen();
            scrollRectToVisible(new Rectangle(u - r.width / 2, v - r.height / 2, r.width,
                    r.height));
            revalidate();
        } else if (ac.equals("Verkleinern") && faktor > 1) {
            u /= 2;
            v /= 2;
            faktor /= 2;
            zoomen();
            scrollRectToVisible(new Rectangle(u - r.width / 2, v - r.height / 2, r.width,
                    r.height));
            revalidate();
        } else if (ac.equals("Standard")) {
            faktor = 1;
            zoomen();
            revalidate();
        } else if (ac.equals("Aktuell")) {
            SwingUtilities.invokeLater(aktualisierung);
        }

    }

    /** Heuristisch ermittelter Vergrößerungsfaktor, falls nötig zoomen */
    private void faktor() {
        int faktor =
                (int) Math.round(Math.pow(2.,
                        Math.ceil(Math.log(Math.ceil(anzahl / 10.)) / Math.log(2.))));
        if (faktor != this.faktor) {
            this.faktor = faktor;
            zoomen();
        }
    }

    /**
     * Zoomen (Angabe der bevorzugten Größe, Neuzeichnen mittels anderer Methoden)
     */
    private void zoomen() {
        setPreferredSize(new Dimension(faktor * BREITE, faktor * HOEHE));
    }

    /** Auschnitt zum aktuellen Spiel verschieben (Balkengrundfläche sichtbar) */
    private void aktualisieren() {

        if (isShowing()) {
            anpassen = false;
            double[] src = new double[4];
            double[] dest = new double[4];
            int i;
            i = verlauf[0].size() - 1;
            src[0] = x(0, i) + BALKENBREITE;
            src[1] = y(i);
            src[2] = x(verlauf.length - 1, i) - BALKENTIEFE / STEIGUNG;
            src[3] = y(i) - BALKENTIEFE;
            koordinatenwaende((Graphics2D) getGraphics(), false);
            trafo.transform(src, 0, dest, 0, 2);
            scrollRectToVisible(new Rectangle((int) (dest[0] - tx) - 4,
                    (int) (dest[1] - ty) - 4, (int) (dest[2] - dest[0]) + 8,
                    (int) (dest[3] - dest[1]) + 8));
        }
    }

}
