import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.JPanel;

/** AnzeigePanel fuer eine 3D-Szene. Die Berechnung mit zwei Augenpunkten ermueglicht eine Rot-Cyan-Darstellung
 * des Bildes, die mit einer entsprechenden Brille einen echten rueumlichen Eindruck vermittelt.
 * ueber einen MouseLisener wird auueerdem die Drehung des Szene ermueglicht.
 */
class Board3D extends JPanel implements MouseListener, MouseMotionListener, MouseWheelListener {
    // Informationen ueber die Szene
    private ArrayList<Strecke> szene = new ArrayList<Strecke>();               // Szene, die aus einer Liste von Strecken besteht
    private ArrayList<Strecke> szene_transformiert = new ArrayList<Strecke>(); // Szene, die aus einer Liste von Strecken besteht , transformiert mit der Drehung

    private Punkt viewpoint = new Punkt(0, 0, -25);               // Ausgangspunkt fuer die Berechnung des Bildes (zwischen den beiden Augenpunkten)
    private double augenabstand = 1.1;                           // Abstand der beide Augenpunkte

    // Informationen fuer Mausbewegungen
    private MouseEvent startpunkt;                               // Mauskoordinaten, an denen die Maustaste fuer eine Rotation gedrueckt wurde
    private boolean rotate = false;                              // wird die Szene rotiert (linke Maustaste) oder verschoben (andere Maustasten)?
    private Matrix transformation = Matrix.Translation(0, 0, 0); // Transformationsmatrix, mit der die Szene multipliziert werden muss, um die Rotation mit der Maus zu ermueglichen
    private Matrix old_transformation = null;                    // Transformationsmatirx, die zu Beginn der Mausaktion gueltig war

    /** Erzeugt neues 3D-Panel mit schwarzem Hintergrund und Mauslistenern
     */
    public Board3D() {
        super();
        this.setBackground(Color.BLACK);                   // Schwarzer Hintergrund, damit die Linien besser hervortreten.

        addMouseListener(this);                            // MouseListener fuer die Bewegung der Szene
        addMouseMotionListener(this);
        addMouseWheelListener(this);
    }

    // MouseListener
    public void mousePressed(MouseEvent e) {
        startpunkt = e;                                    // Startpunkt der Mausbewegung
        old_transformation = transformation;               // und aktuelle Transformation merken

        if (e.getButton() == MouseEvent.BUTTON1) {         // Bei linker Maustaste rotieren, rechts verschieben
            rotate = true;
        } else {
            rotate = false;
        }
    }

    public void mouseReleased(MouseEvent e) {
        startpunkt = null;                                  // Mausbewegung ist zu Ende
    }

    public void mouseDragged(MouseEvent e) {
        if (startpunkt != null) {                            // Mausbewegung ist aktiv
            float d1 = e.getX() - startpunkt.getX();           // Differenzen zum Startpunkt berechnen
            float d2 = e.getY() - startpunkt.getY();

            if (rotate) {
                transformation = old_transformation.mulitpliziere(Matrix.RotationY(-d1 / 100)); // Rotationsbewegung X-Differenz => Rotation um Y-Achse bezogen auf Ursprungstransformation zu Beginn der Mausbewegung
                transformation = transformation.mulitpliziere(Matrix.RotationZ(d2 / 100));      //                   Y-Differenz => Rotation um Z-Achse
            } else {
                transformation = old_transformation.mulitpliziere(Matrix.Translation(d1 / 100.0, 0, -d2 / 100.0));  // Translationsbewegung
            }
            repaint();
        }
    }

    public void mouseWheelMoved(MouseWheelEvent e) {
        int notches = e.getWheelRotation();

        if (notches > 0) {                                                            // Skalierung durch Mausrad
            transformation = transformation.mulitpliziere(Matrix.Skalierung(1.04));
        } else {
            transformation = transformation.mulitpliziere(Matrix.Skalierung(0.98));
        }

        repaint();
    }

    // andere Mausevents werden nicht benuetigt
    public void mouseEntered(MouseEvent e) {}

    public void mouseExited(MouseEvent e) {}

    public void mouseClicked(MouseEvent e) {}

    public void mouseMoved(MouseEvent e) {}

    // MouseWheelListener
    /** Setzt den Augenabstand neu
     * @param a neuer Augenabstand
     */
    public void setAugenabstand(double a) {
        augenabstand += a;
    }

    /** Luescht die gespeicherten Strecken.
     */
    public void clearSzene() {
        szene = new ArrayList<Strecke>();
        repaint();
    }

    /** Fuegt eine Strecke der Szene hinzu
     * @param s Referenz auf die neue Strecke
     */
    public void addStrecke(Strecke s) {
        szene.add(s);
        repaint();
    }

    /** Zeichnet komplette Szene mit zwei Farben fuer linkes und rechtes Auge.
     * @param g Grafikkontext
     */
    public void paint(Graphics g) {
        super.paint(g);

        Graphics2D g2 = (Graphics2D) g;

        // Dicke Linie in cyan auswuehlen
        BasicStroke duenneLinie = new BasicStroke(1.0f);
        g2.setStroke(duenneLinie);
        g2.setColor(Color.cyan);

        // Rechten Augenpunkt bestimmen und zeichnen
        Punkt viewpoint2 = new Punkt(viewpoint.getX(), viewpoint.getY(), viewpoint.getZ());
        viewpoint2.verschiebe(new Vektor(+augenabstand/2.0, 0, 0));
        zeichneStrecken(g2, viewpoint2);

        // Nicht ganz so dicke Linie in rot (wirkt intensiver)
        duenneLinie = new BasicStroke(1.0f);
        g2.setStroke(duenneLinie);
        g2.setColor(new Color(255, 00, 00));
        g2.setXORMode(Color.black);  // Rot und Cyan sollen sich zu weiue erguenzen
        // Linken Augenpunkt bestimmen und Szene erneut zeichnen
        viewpoint2 = new Punkt(viewpoint.getX(), viewpoint.getY(), viewpoint.getZ());
        viewpoint2.verschiebe(new Vektor(-augenabstand/2, 0, 0));
        zeichneStrecken(g2, viewpoint2);
    }

    /** Zeichnet die Strecken auf einen Grafikkontext gesehen von einem bestimmten Augenpunkt
     * @param g Grafikkontext
     * @param v Augenpunkt
     */
    private void zeichneStrecken(Graphics2D g, Punkt v) {
        szene_transformiert.clear();
        for (Strecke s: szene) {
            szene_transformiert.add(new Strecke(transformation.multipliziere(s.getPunkt(0)),transformation.multipliziere(s.getPunkt(1))));
        } // end of for

        for (Strecke s: szene_transformiert) {                   // Alle Strecken zeichnen
            s.draw(g,v);
        }
    }
}
