package de.renew.minimap.component;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JViewport;

import bibliothek.gui.dock.common.event.CFocusListener;
import bibliothek.gui.dock.common.intern.CDockable;

import CH.ifa.draw.standard.StandardDrawingView;
import CH.ifa.draw.standard.StandardDrawingViewContainer;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.Painter;


/**
 * A MiniMapPanel shows a small image version of the current drawing in focus.
 * Also the currently clipped viewport is illustrated on the small image version.
 * The image in the MiniMapPanel updates automatically when:
 * - The drawing in focus is manipulated,
 * - a different drawing is focused.
 * The viewport updates automatically when:
 * - A mouse click is made within the MiniMapPanel,
 * - the mouse is dragged within the MiniMapPanel.
 * <p>
 * The MiniMapPanel automatically rescales it's shown graphic to fit it's containing frame.
 * <p>
 * This class received a major rework on 28-09-2018 by Martin Wincierz
 *
 * @author Christian Roeder
 * @author Martin Wincierz
 */
public class MiniMapPanel extends JPanel
    implements ComponentListener, MouseListener, CFocusListener, MouseMotionListener, Painter
{
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(MiniMapPanel.class);

    /**
     * The drawing view to be miniMapped.
     */
    private StandardDrawingView _drawingView;
    /**
     * The viewport of the miniMapped drawing
     */
    private JViewport _viewport;

    /**
     * Instantiates a new mini map panel.
     */
    public MiniMapPanel() {
        addMouseListener(this);
        addMouseMotionListener(this);
        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
        repaint();
    }

    /**
     * Invoked when the component has been made invisible.
     *
     * @param e the event to be processed
     */
    @Override
    public void componentHidden(ComponentEvent e) {}

    /**
     * Invoked when the component's position changes.
     *
     * @param e the event to be processed
     */
    @Override
    public void componentMoved(ComponentEvent e) {
        this.repaint();
    }

    /**
     * Invoked when the component's size changes.
     *
     * @param e the event to be processed
     */
    @Override
    public void componentResized(ComponentEvent e) {
        this.repaint();
    }

    /**
     * Invoked when the component has been made visible.
     *
     * @param e the event to be processed
     */
    @Override
    public void componentShown(ComponentEvent e) {}

    /**
     * When the mouse was clicked inside the miniMapPanel, the user wants to move the
     * clipping viewportFrame to another position.
     *
     * @param e MouseEvent
     */
    @Override
    public void mouseClicked(MouseEvent e) {
        if (_drawingView == null) {
            return;
        }

        int x;
        int y;
        Point p;
        AffineTransform at;

        // First we account for the rescaling of the minimap due to fitting the frame
        double normalizeFactor = calculateNormalizeFactor();
        at = AffineTransform.getScaleInstance(normalizeFactor, normalizeFactor);
        p = new Point(e.getX(), e.getY());

        try {
            at.inverseTransform(p, p);
        } catch (NoninvertibleTransformException e1) {
            // We already know this is an invertible transformation, so this clause should never be reached
        }

        // Now we account for the zooming factor of the drawing, if any
        at = _drawingView.getAffineTransform();
        if (at != null) {
            at = (AffineTransform) at.clone();
            x = p.x;
            y = p.y;
            p = new Point(x, y);
            at.transform(p, p);

        }

        // We want the point to be in the middle of the viewport
        x = p.x;
        y = p.y;

        x = p.x - _viewport.getWidth() / 2;
        y = p.y - _viewport.getHeight() / 2;
        _drawingView.requestFocusInWindow();

        // Update the drawingView bounds to show the correct clipping of the drawing.
        _drawingView
            .setBounds(-x, -y, _drawingView.getBounds().width, _drawingView.getBounds().height);

        e.consume();
    }

    /**
     * Invoked when a mouse button has been pressed on a component.
     *
     * @param e the event to be processed
     */
    @Override
    public void mousePressed(MouseEvent e) {}

    /**
     * Invoked when a mouse button has been released on a component.
     *
     * @param e the event to be processed
     */
    @Override
    public void mouseReleased(MouseEvent e) {}

    /**
     * Invoked when the mouse enters a component.
     *
     * @param e the event to be processed
     */
    @Override
    public void mouseEntered(MouseEvent e) {}

    /**
     * Invoked when the mouse exits a component.
     *
     * @param e the event to be processed
     */
    @Override
    public void mouseExited(MouseEvent e) {}


    /**
     * Paint this component by drawing the image and drawing the viewportFrame.
     *
     * @param g the graphics
     */
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        if (_drawingView == null) {
            return;
        }

        // Scale the graphics so it always fits the containing window.
        Graphics2D g2 = (Graphics2D) g;
        double normalizeFactor = calculateNormalizeFactor();
        g2.scale(normalizeFactor, normalizeFactor);

        // Draw the contents of the focused drawing
        _drawingView.drawBackground(g);
        _drawingView.drawDrawing(g);

        // Calculate the viewport size and position
        int x = _viewport.getX() - _drawingView.getX();
        int width = _viewport.getWidth();
        int y = _viewport.getY() - _drawingView.getY();
        int height = _viewport.getHeight();

        // If the drawing is zoomed, we need to recalculate the viewport size
        AffineTransform at = _drawingView.getAffineTransform();

        if (at != null) {
            try {
                Point p;
                p = new Point(x, y);
                at.inverseTransform(p, p);
                x = p.x;
                y = p.y;

                p = new Point(width, height);
                at.inverseTransform(p, p);
                width = p.x;
                height = p.y;

            } catch (NoninvertibleTransformException e) {
                System.err.println(
                    "DrawingView for drawing " + _drawingView.drawing().getName()
                        + " had an irreversible affine transformation.");
                System.err.println(
                    "The viewport indicator of the MiniMap was not correctly reshaped, otherwise everything should be fine.");
                e.printStackTrace();
            }
        }

        g.setColor(Color.red);
        g.drawRect(x, y, width, height);
        Color color = new Color(55, 55, 55, 10);
        g.setColor(color);
        g.fillRect(x, y, width, height);

    }

    /*
     * Calculate the factor by which the current DrawingView's graphics have to be scaled to still fit into the panel.
     */
    private double calculateNormalizeFactor() {
        if (_drawingView == null) {
            return 1.0;
        }


        int drawingWidth = _drawingView.getWidth();
        double panelWidth = (double) getWidth();


        int drawingHeight = _drawingView.getHeight();
        double panelHeight = (double) getHeight();

        // If the drawing is zoomed the View has to be scaled differently
        AffineTransform at = _drawingView.getAffineTransform();
        if (at != null) {
            try {
                Point p = new Point(drawingWidth, drawingHeight);
                at.inverseTransform(p, p);
                drawingWidth = p.x;
                drawingHeight = p.y;

            } catch (NoninvertibleTransformException e) {
                LOGGER.error(
                    "DrawingView for drawing " + _drawingView.drawing().getName()
                        + " had an irreversible affine transformation.");
                LOGGER.error(
                    "The drawing of the MiniMap was not correctly reshaped, otherwise everything should be fine.");
                e.printStackTrace();
            }
        }

        double normalizeWidth = panelWidth / (double) drawingWidth;
        double normalizeHeight = panelHeight / (double) drawingHeight;

        return Math.min(normalizeHeight, normalizeWidth);
    }

    /**
     * Called when a {@link bibliothek.gui.dock.common.intern.CDockable} gains focus.
     * Changes the displayed mini map, if a new drawing ({@link CH.ifa.draw.standard.StandardDrawingViewContainer})
     * was selected.
     *
     * @param cDockable object that gained focus
     */
    @Override
    public void focusGained(CDockable cDockable) {
        if (cDockable instanceof StandardDrawingViewContainer) {
            StandardDrawingView view = ((StandardDrawingViewContainer) cDockable).getView();
            if (!(view == _drawingView)) {
                if (_drawingView != null) {
                    _drawingView.removeForeground(this);
                }

                _drawingView = view;
                _drawingView.addForeground(this);

                _viewport =
                    ((StandardDrawingViewContainer) cDockable).getScrollPane().getViewport();

                this.repaint();
            }
        }
    }

    /**
     * Called when focus is lost.
     *
     * @param cDockable object that lost focus
     */
    @Override
    public void focusLost(CDockable cDockable) {}

    /**
     * When the mouse is dragged inside the miniMapPanel, the user wants to move the
     * clipping viewportFrame to another position.
     *
     * @param e MouseEvent
     */
    @Override
    public void mouseDragged(MouseEvent e) {
        mouseClicked(e);

    }

    /**
     * Invoked when the mouse cursor has been moved onto a component
     * but no buttons have been pushed.
     *
     * @param e the event to be processed
     */
    @Override
    public void mouseMoved(MouseEvent e) {}

    /**
     * Draws into the given DrawingView.
     *
     * @param g     the graphics to be drawn
     * @param view  the view
     */
    @Override
    public void draw(Graphics g, DrawingView view) {
        this.repaint();
    }
}