/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.gui;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;

import javax.swing.event.MouseInputAdapter;

import net.sf.jaxodraw.util.JaxoColor;
import net.sf.jaxodraw.util.JaxoConstants;
import net.sf.jaxodraw.util.JaxoGeometry;
import net.sf.jaxodraw.util.JaxoUtils;


/** A zoom on the canvas.
 * @since 2.0
 */
public class JaxoZoom extends MouseInputAdapter {
    private static final boolean PRINT_ZOOM = true;

    /** A zoom factor 2. */
    public static final int ZOOM_FACTOR_X2 = 2;

    /** A zoom factor 4. */
    public static final int ZOOM_FACTOR_X4 = 4;

    /** A zoom factor 8. */
    public static final int ZOOM_FACTOR_X8 = 8;

    // 1 pixel padding
    private static final int PADDING = 1;

    private Graphics graphics;
    private final JaxoCanvasComponent theCanvas;
    private final Point zoomWindowLocation;
    private final Dimension zoomWindowSize;
    private Image background;

    // contains the same contents as 'background'
    // except for the zoom window area
    private Image foreground;
    private int zoomFactor;
    private boolean active;
    private boolean visible;
    private final Rectangle damagedBounds;

    /** Constructor.
     * @param canvas The JaxoCanvas to zoom on.
     */
    public JaxoZoom(final JaxoCanvasComponent canvas) {
        super();
        this.theCanvas = canvas;
        this.zoomFactor = 2;
        this.zoomWindowSize = new Dimension();
        this.zoomWindowLocation = new Point();
        this.damagedBounds = new Rectangle();
    }

    /** Determines the state of this zoom.
     * @return True if this zoom is currently activated.
     */
    public final boolean isActive() {
        return active;
    }

    /** Activate/Deactivate this zoom.
     * @param value True to activate this zoom.
     */
    public void setActive(final boolean value) {
        if (active != value) {
            active = value;

            if (active) {
                theCanvas.addMouseListener(this);
                theCanvas.addMouseMotionListener(this);
            } else {
                cleanup();
                theCanvas.removeMouseListener(this);
                theCanvas.removeMouseMotionListener(this);
            }
        }
    }

    /** Sets the zoom factor.
     * @param zf The zoom factor.
     */
    public final void setZoomFactor(final int zf) {
        this.zoomFactor = zf;
    }

    /** Returns the zoom factor.
     * @return The zoom factor.
     */
    public final int getZoomFactor() {
        return this.zoomFactor;
    }

    /** Sets the size of the zoom window.
     * @param width The width of the zoom window.
     * @param height The height of the zoom window.
     */
    public final void setZoomWindowSize(final int width, final int height) {
        zoomWindowSize.setSize(width, height);
    }

    /** Sets the background image for the zoom.
     * @param bg The background image.
     */
    public void setBackground(final Image bg) {
        this.background = bg;
    }

    /** Sets the graphics context for the zoom.
     * @param value The graphics context.
     */
    private void setGraphics(final Graphics value) {
        graphics = value;

        if (graphics != null) {
            final Rectangle r = theCanvas.getCanvasBounds();
            graphics.clipRect(r.x, r.y, r.width, r.height);
            graphics.translate(r.x, r.y);
        }
    }

    // mark the given area for repaint
    private void damage(final int x, final int y, final int width, final int height) {
        JaxoGeometry.add(damagedBounds, x, y, width, height);
    }

    // paint damaged area to 'graphics', then clear damaged area
    private void paintToGraphics() {
        final int x = damagedBounds.x - PADDING;
        final int y = damagedBounds.y - PADDING;

        JaxoUtils.drawImageArea(foreground, x, y,
            damagedBounds.width + (2 * PADDING),
            damagedBounds.height + (2 * PADDING), graphics);

        JaxoGeometry.clear(damagedBounds);
    }

    // paint the actual zoomed area to 'foreground'
    private void paintZoom() {
        final int width = zoomWindowSize.width;
        final int height = zoomWindowSize.height;
        final int x = zoomWindowLocation.x;
        final int y = zoomWindowLocation.y;

        final Graphics g = foreground.getGraphics();

        g.clipRect(x, y, width, height);

        if (PRINT_ZOOM) {
            final Graphics2D h = (Graphics2D) g.create();

            final Point p = theCanvas.getCanvasOrigin();
            h.translate(-p.x, -p.y);

            h.translate((1 - zoomFactor) * (x + (width / 2)),
                (1 - zoomFactor) * (y + (height / 2)));

            h.scale(zoomFactor, zoomFactor);

            theCanvas.print(h);

            h.dispose();
        } else {
            g.setColor(JaxoColor.WHITE);
            g.fillRect(x, y, width, height);

            final Point p = theCanvas.getCanvasOrigin();
            g.translate(p.x, p.y);

            g.drawImage(background, (1 - zoomFactor) * (x + (width / 2)),
                (1 - zoomFactor) * (y + (height / 2)),
                background.getWidth(null) * zoomFactor,
                background.getHeight(null) * zoomFactor, null);

            g.translate(-p.x, -p.y);
        }

        g.setColor(JaxoColor.BLACK);
        g.drawRect(x, y, width - 1, height - 1);

        g.dispose();

        damage(x, y, width, height);
    }

    /** Draws the zoom by clipping to the zoom window and scaling the
     * background image.
     */
    private void drawZoomWindow() {
        if (foreground == null) {
            createForeground();
        }

        paintZoom();

        paintToGraphics();
    }

    /** Erases the zoom window. */
    private void eraseZoomWindow() {
        final int width = zoomWindowSize.width;
        final int height = zoomWindowSize.height;
        final int x = zoomWindowLocation.x;
        final int y = zoomWindowLocation.y;

        JaxoUtils.drawImageArea(background, x, y, width, height, graphics);
    }

    /** Restore the background on 'foreground' in the area previously covered by the zoom
     * @param oldX The old x-coordinate of the zoom window.
     * @param oldY The old y-coordinate of the zoom window.
     * @param newX The new x-coordinate of the zoom window.
     * @param newY The new y-coordinate of the zoom window.
     */
    private void restoreBackground(final int oldX, final int oldY, final int newX, final int newY) {
        if (foreground == null) {
            createForeground();
        }

        final Graphics g = foreground.getGraphics();
        final int width = zoomWindowSize.width;
        final int height = zoomWindowSize.height;

        if ((newX <= oldX) && (newY <= oldY)) {
            JaxoUtils.drawImageArea(background, newX + width, oldY,
                oldX - newX, (newY + height) - oldY, g);
            JaxoUtils.drawImageArea(background, oldX, newY + height, width,
                oldY - newY, g);
        } else if ((newX > oldX) && (newY <= oldY)) {
            JaxoUtils.drawImageArea(background, oldX, oldY, newX - oldX,
                (newY + height) - oldY, g);
            JaxoUtils.drawImageArea(background, oldX, newY + height, width,
                oldY - newY, g);
        } else if ((newX > oldX) && (newY > oldY)) {
            JaxoUtils.drawImageArea(background, oldX, oldY, width,
                newY - oldY, g);
            JaxoUtils.drawImageArea(background, oldX, newY, newX - oldX,
                (oldY + height) - newY, g);
        } else { // if (newX <= oldX && newY > oldY)
            JaxoUtils.drawImageArea(background, oldX, oldY, width,
                newY - oldY, g);
            JaxoUtils.drawImageArea(background, newX + width, newY,
                oldX - newX, (oldY + height) - newY, g);
        }

        g.dispose();
    }

    /** Drags the zoom when the zoom window gets dragged.
     * @param eX The x-coordinate of the center of the zoom window
     * (where the mouse event occurred).
     * @param eY The y-coordinate of the center of the zoom window.
     */
    private void dragZoomWindow(final int eX, final int eY) {
        final int oldX = zoomWindowLocation.x;
        final int oldY = zoomWindowLocation.y;
        final int width = zoomWindowSize.width;
        final int height = zoomWindowSize.height;
        int newX = eX - (width / 2);
        int newY = eY - (height / 2);

        final Rectangle r = theCanvas.getCanvasBounds();

        // NOTE: This does not handle the (unlikely) case that
        // the canvasSize is too small for the zoom window to
        // show even one complete image
        // This may(?) also assume that width/height are even.
        if ((newX - r.x) >= (r.width - (width / 2))) {
            newX = (r.width - (width / 2) + r.x) - 1;
        } else if ((newX - r.x) < (-width / 2)) {
            newX = r.x - (width / 2);
        }

        if ((newY - r.y) >= (r.height - (height / 2))) {
            newY = (r.height - (height / 2) + r.y) - 1;
        } else if ((newY - r.y) < (-height / 2)) {
            newY = r.y - (height / 2);
        }

        restoreBackground(oldX, oldY, newX, newY);
        damage(oldX, oldY, width, height);

        zoomWindowLocation.setLocation(newX, newY);

        drawZoomWindow();
    }

    /** Returns the zoom factor size for the given mode.
     * @param mode A JaxoDraw mode as defined in {@link JaxoConstants}.
     * @return The zoom factor size, or -1, if mode does not correspond
     * to a zoom factor mode.
     */
    public static final int getZoomFactorFor(final int mode) {
        int factor = -1;
        if (mode == JaxoConstants.ZOOM_FACTOR_X2) {
            factor = ZOOM_FACTOR_X2;
        } else if (mode == JaxoConstants.ZOOM_FACTOR_X4) {
            factor = ZOOM_FACTOR_X4;
        } else if (mode == JaxoConstants.ZOOM_FACTOR_X8) {
            factor = ZOOM_FACTOR_X8;
        }
        return factor;
    }

    /**
     * Set the zoom size for a given mode.
     *
     * @param mode A JaxoDraw mode as defined in {@link JaxoConstants}.
     */
    public void setZoomFactorFor(final int mode) {
        setZoomFactor(getZoomFactorFor(mode));
    }

    private void createForeground() {
        foreground =
            ((Graphics2D) graphics).getDeviceConfiguration()
             .createCompatibleImage(background.getWidth(null),
                background.getHeight(null));

        final Graphics g = foreground.getGraphics();

        g.drawImage(background, 0, 0, null);
        g.dispose();

        JaxoGeometry.clear(damagedBounds);
    }

    /**
     * The action to be taken when the mouse is pressed on the canvas.
     *
     * @param e The corresponding mouse event.
     */
    @Override
    public final void mousePressed(final MouseEvent e) {
        if (!visible) {
            if (JaxoUtils.isButton1(e)) {
                setZoomWindowSize(160, 100);
            } else if (JaxoUtils.isButton2(e)) {
                setZoomWindowSize(240, 150);
            } else if (JaxoUtils.isButton3(e)) {
                setZoomWindowSize(320, 200);
            }

            visible = true;

            setGraphics(theCanvas.getGraphics());

            zoomWindowLocation.setLocation(e.getX()
                - (zoomWindowSize.width / 2),
                e.getY() - (zoomWindowSize.height / 2));

            drawZoomWindow();
        }
    }

    /**
     * The action to be taken when the mouse is dragged on the canvas.
     *
     * @param e The corresponding mouse event.
     */
    @Override
    public final void mouseDragged(final MouseEvent e) {
        if (visible) {
            dragZoomWindow(e.getX(), e.getY());
        }
    }

    /**
     * The action to be taken when the mouse is released on the canvas.
     *
     * @param e The corresponding mouse event.
     */
    @Override
    public final void mouseReleased(final MouseEvent e) {
        if (visible) {
            visible = false;
            eraseZoomWindow();

            cleanup();
        }
    }

    /** Cleanup Graphics, 'foreground' image, visible. */
    private void cleanup() {
        if (visible) {
            // never erased, may need repaint
            theCanvas.repaint();
        }

        visible = false;

        // cleanup if mouseReleased has been lost
        if (graphics != null) {
            graphics.dispose();
            setGraphics(null);
        }

        if (foreground != null) {
            foreground.flush();
            foreground = null;
        }
    }
}
