/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.canvas;

import icy.gui.main.MainFrame;
import icy.gui.viewer.Viewer;
import icy.main.Icy;
import icy.painter.Overlay;
import icy.sequence.DimensionId;
import icy.sequence.Sequence;
import icy.type.rectangle.Rectangle5D;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;

import javax.swing.JComponent;

/**
 * @author Stephane
 */
public abstract class IcyCanvas2D extends IcyCanvas
{
    /**
     * 
     */
    private static final long serialVersionUID = 743937493919099495L;

    /** mouse position (image coordinate space) */
    protected Point2D.Double mouseImagePos;

    // image coordinate to canvas coordinate transform
    protected final AffineTransform transform;
    // canvas coordinate to image coordinate transform
    protected AffineTransform inverseTransform;
    protected boolean transformChanged;

    public IcyCanvas2D(Viewer viewer)
    {
        super(viewer);

        // default for 2D canvas
        posX = -1;
        posY = -1;
        posZ = 0;
        posT = 0;

        // initial mouse position
        mouseImagePos = new Point2D.Double();
        transform = new AffineTransform();
        inverseTransform = new AffineTransform();
        transformChanged = false;

        // adjust LUT alpha level for 2D view
        lut.setAlphaToOpaque();
    }

    @Override
    public void setPositionZ(int z)
    {
        // position -1 not supported for Z dimension on this canvas
        if (z != -1)
            super.setPositionZ(z);
    }

    @Override
    public void setPositionT(int t)
    {
        // position -1 not supported for T dimension on this canvas
        if (t != -1)
            super.setPositionT(t);
    }

    @Override
    public double getMouseImagePosX()
    {
        // can be called before constructor ended
        if (mouseImagePos == null)
            return 0d;

        return mouseImagePos.x;

    }

    @Override
    public double getMouseImagePosY()
    {
        // can be called before constructor ended
        if (mouseImagePos == null)
            return 0d;

        return mouseImagePos.y;
    }

    /**
     * Return mouse image position
     */
    public Point2D.Double getMouseImagePos()
    {
        return (Point2D.Double) mouseImagePos.clone();
    }

    public void setMouseImagePos(double x, double y)
    {
        if ((mouseImagePos.x != x) || (mouseImagePos.y != y))
        {
            mouseImagePos.x = x;
            mouseImagePos.y = y;

            // direct update of mouse canvas position
            mousePos = imageToCanvas(mouseImagePos);
            // notify change
            mouseImagePositionChanged(DimensionId.NULL);
        }
    }

    /**
     * Set mouse image position
     */
    public void setMouseImagePos(Point2D.Double point)
    {
        setMouseImagePos(point.x, point.y);
    }

    @Override
    public boolean setMousePos(int x, int y)
    {
        final boolean result = super.setMousePos(x, y);

        if (result)
        {
            if (mouseImagePos == null)
                mouseImagePos = new Point2D.Double();

            final Point2D newPos = canvasToImage(mousePos);
            final double newX = newPos.getX();
            final double newY = newPos.getY();
            boolean changed = false;

            // need to check against NaN is conversion is not supported
            if (!Double.isNaN(newX) && (newX != mouseImagePos.x))
            {
                mouseImagePos.x = newX;
                changed = true;
            }
            // need to check against NaN is conversion is not supported
            if (!Double.isNaN(newY) && (newY != mouseImagePos.y))
            {
                mouseImagePos.y = newY;
                changed = true;
            }

            // notify change
            if (changed)
                mouseImagePositionChanged(DimensionId.NULL);
        }

        return result;
    }

    /**
     * @deprecated Use {@link #setMousePos(int, int)} instead
     */
    @Deprecated
    public void setMouseCanvasPos(int x, int y)
    {
        setMousePos(x, y);
    }

    /**
     * @deprecated Use {@link #setMousePos(Point)} instead.
     */
    @Deprecated
    public void setMouseCanvasPos(Point point)
    {
        setMousePos(point);
    }

    @Override
    protected void setMouseImagePosXInternal(double value)
    {
        mouseImagePos.x = value;

        // direct update of mouse canvas position
        mousePos = imageToCanvas(mouseImagePos);

        super.setMouseImagePosXInternal(value);
    }

    @Override
    protected void setMouseImagePosYInternal(double value)
    {
        mouseImagePos.y = value;

        // direct update of mouse canvas position
        mousePos = imageToCanvas(mouseImagePos);

        super.setMouseImagePosYInternal(value);
    }

    /**
     * Convert specified canvas delta to image delta.<br>
     */
    protected Point2D.Double canvasToImageDelta(int x, int y, double scaleX, double scaleY, double rot)
    {
        // get cos and sin
        final double cos = Math.cos(-rot);
        final double sin = Math.sin(-rot);

        // apply rotation
        final double resX = (x * cos) - (y * sin);
        final double resY = (x * sin) + (y * cos);

        // and scale
        return new Point2D.Double(resX / scaleX, resY / scaleY);
    }

    /**
     * Convert specified canvas delta point to image delta point
     */
    public Point2D.Double canvasToImageDelta(int x, int y)
    {
        return canvasToImageDelta(x, y, getScaleX(), getScaleY(), getRotationZ());
    }

    /**
     * Convert specified canvas delta point to image delta point
     */
    public Point2D.Double canvasToImageDelta(Point point)
    {
        return canvasToImageDelta(point.x, point.y);
    }

    /**
     * Convert specified canvas delta point to image delta point.
     * The conversion is affected by zoom ratio but with the specified logarithm factor.
     */
    public Point2D.Double canvasToImageLogDelta(int x, int y, double logFactor)
    {
        final double sx = getScaleX() / Math.pow(10, Math.log10(getScaleX()) / logFactor);
        final double sy = getScaleY() / Math.pow(10, Math.log10(getScaleY()) / logFactor);

        return canvasToImageDelta(x, y, sx, sy, getRotationZ());
    }

    /**
     * Convert specified canvas delta point to image delta point.
     * The conversion is affected by zoom ratio but with the specified logarithm factor.
     */
    public Point2D.Double canvasToImageLogDelta(int x, int y)
    {
        return canvasToImageLogDelta(x, y, 5d);
    }

    /**
     * Convert specified canvas point to image point.<br>
     * By default we consider the rotation applied relatively to canvas center.<br>
     * Override this method if you want different transformation type.
     */
    protected Point2D.Double canvasToImage(int x, int y, int offsetX, int offsetY, double scaleX, double scaleY,
            double rot)
    {
        // get canvas center
        final double canvasCenterX = getCanvasSizeX() / 2;
        final double canvasCenterY = getCanvasSizeY() / 2;

        // center to canvas for rotation
        final double dx = x - canvasCenterX;
        final double dy = y - canvasCenterY;

        // get cos and sin
        final double cos = Math.cos(-rot);
        final double sin = Math.sin(-rot);

        // apply rotation
        double resX = (dx * cos) - (dy * sin);
        double resY = (dx * sin) + (dy * cos);

        // translate back to position
        resX += canvasCenterX;
        resY += canvasCenterY;

        // basic transform to image coordinates
        resX = ((resX - offsetX) / scaleX);
        resY = ((resY - offsetY) / scaleY);

        return new Point2D.Double(resX, resY);
    }

    /**
     * Convert specified canvas point to image point
     */
    public Point2D.Double canvasToImage(int x, int y)
    {
        final Point2D.Double result = new Point2D.Double(0d, 0d);

        // we can directly use the transform object here
        getInverseTransform().transform(new Point2D.Double(x, y), result);

        return result;

        // return canvasToImage(x, y, getOffsetX(), getOffsetY(), getScaleX(), getScaleY(),
        // getRotationZ());
    }

    /**
     * Convert specified canvas point to image point
     */
    public Point2D.Double canvasToImage(Point point)
    {
        return canvasToImage(point.x, point.y);
    }

    /**
     * Convert specified canvas rectangle to image rectangle
     */
    public Rectangle2D.Double canvasToImage(int x, int y, int w, int h)
    {
        // convert each rectangle point
        final Point2D.Double pt1 = canvasToImage(x, y);
        final Point2D.Double pt2 = canvasToImage(x + w, y);
        final Point2D.Double pt3 = canvasToImage(x + w, y + h);
        final Point2D.Double pt4 = canvasToImage(x, y + h);

        // get minimum and maximum X / Y
        final double minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
        final double maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
        final double minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
        final double maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));

        // return transformed rectangle
        return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
    }

    /**
     * Convert specified canvas rectangle to image rectangle
     */
    public Rectangle2D.Double canvasToImage(Rectangle rect)
    {
        return canvasToImage(rect.x, rect.y, rect.width, rect.height);
    }

    /**
     * Convert specified image delta to canvas delta.<br>
     */
    protected Point imageToCanvasDelta(double x, double y, double scaleX, double scaleY, double rot)
    {
        // apply scale
        final double dx = x * scaleX;
        final double dy = y * scaleY;

        // get cos and sin
        final double cos = Math.cos(rot);
        final double sin = Math.sin(rot);

        // apply rotation
        final double resX = (dx * cos) - (dy * sin);
        final double resY = (dx * sin) + (dy * cos);

        return new Point((int) Math.round(resX), (int) Math.round(resY));
    }

    /**
     * Convert specified image delta point to canvas delta point
     */
    public Point imageToCanvasDelta(double x, double y)
    {
        return imageToCanvasDelta(x, y, getScaleX(), getScaleY(), getRotationZ());
    }

    /**
     * Convert specified image delta point to canvas delta point
     */
    public Point imageToCanvasDelta(Point2D.Double point)
    {
        return imageToCanvasDelta(point.x, point.y);
    }

    /**
     * Convert specified image point to canvas point.<br>
     * By default we consider the rotation applied relatively to image center.<br>
     * Override this method if you want different transformation type.
     */
    protected Point imageToCanvas(double x, double y, int offsetX, int offsetY, double scaleX, double scaleY, double rot)
    {
        // get canvas center
        final double canvasCenterX = getCanvasSizeX() / 2;
        final double canvasCenterY = getCanvasSizeY() / 2;

        // basic transform to canvas coordinates and canvas centering
        final double dx = ((x * scaleX) + offsetX) - canvasCenterX;
        final double dy = ((y * scaleY) + offsetY) - canvasCenterY;

        // get cos and sin
        final double cos = Math.cos(rot);
        final double sin = Math.sin(rot);

        // apply rotation
        double resX = (dx * cos) - (dy * sin);
        double resY = (dx * sin) + (dy * cos);

        // translate back to position
        resX += canvasCenterX;
        resY += canvasCenterY;

        return new Point((int) Math.round(resX), (int) Math.round(resY));
    }

    /**
     * Convert specified image point to canvas point
     */
    public Point imageToCanvas(double x, double y)
    {
        final Point result = new Point();

        // we can directly use the transform object here
        getTransform().transform(new Point2D.Double(x, y), result);

        return result;

        // return imageToCanvas(x, y, getOffsetX(), getOffsetY(), getScaleX(), getScaleY(),
        // getRotationZ());
    }

    /**
     * Convert specified image point to canvas point
     */
    public Point imageToCanvas(Point2D.Double point)
    {
        return imageToCanvas(point.x, point.y);
    }

    /**
     * Convert specified image rectangle to canvas rectangle
     */
    public Rectangle imageToCanvas(double x, double y, double w, double h)
    {
        // convert each rectangle point
        final Point pt1 = imageToCanvas(x, y);
        final Point pt2 = imageToCanvas(x + w, y);
        final Point pt3 = imageToCanvas(x + w, y + h);
        final Point pt4 = imageToCanvas(x, y + h);

        // get minimum and maximum X / Y
        final int minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
        final int maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
        final int minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
        final int maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));

        // return transformed rectangle
        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
    }

    /**
     * Convert specified image rectangle to canvas rectangle
     */
    public Rectangle imageToCanvas(Rectangle2D.Double rect)
    {
        return imageToCanvas(rect.x, rect.y, rect.width, rect.height);
    }

    /**
     * Get 2D view size in canvas pixel coordinate
     * 
     * @return a Dimension which represents the visible size.
     */
    public Dimension getCanvasSize()
    {
        return new Dimension(getCanvasSizeX(), getCanvasSizeY());
    }

    /**
     * Get 2D image size
     */
    public Dimension getImageSize()
    {
        return new Dimension(getImageSizeX(), getImageSizeY());
    }

    /**
     * Get 2D image size in canvas pixel coordinate
     */
    public Dimension getImageCanvasSize()
    {
        final double imageSizeX = getImageSizeX();
        final double imageSizeY = getImageSizeY();
        final double scaleX = getScaleX();
        final double scaleY = getScaleY();
        final double rot = getRotationZ();

        // convert image rectangle
        final Point pt1 = imageToCanvas(0d, 0d, 0, 0, scaleX, scaleY, rot);
        final Point pt2 = imageToCanvas(imageSizeX, 0d, 0, 0, scaleX, scaleY, rot);
        final Point pt3 = imageToCanvas(0d, imageSizeY, 0, 0, scaleX, scaleY, rot);
        final Point pt4 = imageToCanvas(imageSizeX, imageSizeY, 0, 0, scaleX, scaleY, rot);

        final int minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
        final int maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
        final int minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
        final int maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));

        return new Dimension(maxX - minX, maxY - minY);
    }

    /**
     * Get 2D canvas visible rectangle (canvas coordinate).
     */
    public Rectangle getCanvasVisibleRect()
    {
        // try to return view component visible rectangle by default
        final Component comp = getViewComponent();
        if (comp instanceof JComponent)
            return ((JComponent) comp).getVisibleRect();

        // just return the canvas component visible rectangle
        return getVisibleRect();
    }

    /**
     * Get 2D image visible rectangle (image coordinate).<br>
     * Prefer the {@link Graphics#getClipBounds()} method for paint operation as the image visible
     * rectangle may return wrong information sometime (when using the {@link #getRenderedImage(int, int, int, boolean)}
     * method for instance).
     */
    public Rectangle2D getImageVisibleRect()
    {
        return canvasToImage(getCanvasVisibleRect());
    }
    
    /**
     * Adjust view position and possibly scaling factor to ensure the specified region become visible.<br>
     * It's up to the Canvas implementation to decide how to make the region visible.
     * 
     * @param region
     *        the region we want to see
     */
    public void centerOn(Rectangle region)
    {
        // override it in Canvas implementation
    }

    /**
     * Center image on specified image position in canvas
     */
    public void centerOnImage(double x, double y)
    {
        // get point on canvas
        final Point pt = imageToCanvas(x, y);
        final int canvasCenterX = getCanvasSizeX() / 2;
        final int canvasCenterY = getCanvasSizeY() / 2;

        final Point2D.Double newTrans = canvasToImageDelta(canvasCenterX - pt.x, canvasCenterY - pt.y, 1d, 1d,
                getRotationZ());

        setOffsetX(getOffsetX() + (int) Math.round(newTrans.x));
        setOffsetY(getOffsetY() + (int) Math.round(newTrans.y));
    }

    /**
     * Center image on specified image position in canvas
     */
    public void centerOnImage(Point2D.Double pt)
    {
        centerOnImage(pt.x, pt.y);
    }

    /**
     * Center image in canvas
     */
    public void centerImage()
    {
        centerOnImage(getImageSizeX() / 2, getImageSizeY() / 2);
    }

    /**
     * get scale X and scale Y so image fit in canvas view dimension
     */
    protected Point2D.Double getFitImageToCanvasScale()
    {
        final double imageSizeX = getImageSizeX();
        final double imageSizeY = getImageSizeY();

        if ((imageSizeX > 0d) && (imageSizeY > 0d))
        {
            final double rot = getRotationZ();

            // convert image rectangle
            final Point pt1 = imageToCanvas(0d, 0d, 0, 0, 1d, 1d, rot);
            final Point pt2 = imageToCanvas(imageSizeX, 0d, 0, 0, 1d, 1d, rot);
            final Point pt3 = imageToCanvas(0d, imageSizeY, 0, 0, 1d, 1d, rot);
            final Point pt4 = imageToCanvas(imageSizeX, imageSizeY, 0, 0, 1d, 1d, rot);

            final int minX = Math.min(pt1.x, Math.min(pt2.x, Math.min(pt3.x, pt4.x)));
            final int maxX = Math.max(pt1.x, Math.max(pt2.x, Math.max(pt3.x, pt4.x)));
            final int minY = Math.min(pt1.y, Math.min(pt2.y, Math.min(pt3.y, pt4.y)));
            final int maxY = Math.max(pt1.y, Math.max(pt2.y, Math.max(pt3.y, pt4.y)));

            // get image dimension transformed by rotation
            final double sx = (double) getCanvasSizeX() / (double) (maxX - minX);
            final double sy = (double) getCanvasSizeY() / (double) (maxY - minY);

            return new Point2D.Double(sx, sy);
        }

        return null;
    }

    /**
     * Change scale so image fit in canvas view dimension
     */
    public void fitImageToCanvas()
    {
        final Point2D.Double s = getFitImageToCanvasScale();

        if (s != null)
        {
            final double scale = Math.min(s.x, s.y);

            setScaleX(scale);
            setScaleY(scale);
        }
    }

    /**
     * Change canvas size (so viewer size) to get it fit with image dimension if possible
     */
    public void fitCanvasToImage()
    {
        final MainFrame mainFrame = Icy.getMainInterface().getMainFrame();
        final Dimension imageCanvasSize = getImageCanvasSize();

        if ((imageCanvasSize.width > 0) && (imageCanvasSize.height > 0) && (mainFrame != null))
        {
            final Dimension maxDim = mainFrame.getDesktopSize();
            final Dimension adjImgCnvSize = canvasToViewer(imageCanvasSize);

            // fit in available space --> resize viewer
            viewer.setSize(Math.min(adjImgCnvSize.width, maxDim.width), Math.min(adjImgCnvSize.height, maxDim.height));
        }
    }

    /**
     * Convert canvas dimension to viewer dimension
     */
    public Dimension canvasToViewer(Dimension dim)
    {
        final Dimension canvasViewSize = getCanvasSize();
        final Dimension viewerSize = viewer.getSize();
        final Dimension result = new Dimension(dim);

        result.width -= canvasViewSize.width;
        result.width += viewerSize.width;
        result.height -= canvasViewSize.height;
        result.height += viewerSize.height;

        return result;
    }

    /**
     * Convert viewer dimension to canvas dimension
     */
    public Dimension viewerToCanvas(Dimension dim)
    {
        final Dimension canvasViewSize = getCanvasSize();
        final Dimension viewerSize = viewer.getSize();
        final Dimension result = new Dimension(dim);

        result.width -= viewerSize.width;
        result.width += canvasViewSize.width;
        result.height -= viewerSize.height;
        result.height += canvasViewSize.height;

        return result;
    }

    /**
     * Update internal {@link AffineTransform} object.
     */
    protected void updateTransform()
    {
        final int canvasCenterX = getCanvasSizeX() / 2;
        final int canvasCenterY = getCanvasSizeY() / 2;

        // rotation is centered to canvas
        transform.setToTranslation(canvasCenterX, canvasCenterY);
        transform.rotate(getRotationZ());
        transform.translate(-canvasCenterX, -canvasCenterY);

        transform.translate(getOffsetX(), getOffsetY());
        transform.scale(getScaleX(), getScaleY());

        transformChanged = true;
    }

    /**
     * Return the 2D {@link AffineTransform} object which convert from image coordinate to canvas
     * coordinate.<br>
     * {@link Overlay} should directly use the transform information from the {@link Graphics2D} object provided in
     * their {@link Overlay#paint(Graphics2D, Sequence, IcyCanvas)} method.
     */
    public AffineTransform getTransform()
    {
        return transform;
    }

    /**
     * Return the 2D {@link AffineTransform} object which convert from canvas coordinate to image
     * coordinate.<br>
     * {@link Overlay} should directly use the transform information from the {@link Graphics2D} object provided in
     * their {@link Overlay#paint(Graphics2D, Sequence, IcyCanvas)} method.
     */
    public AffineTransform getInverseTransform()
    {
        if (transformChanged)
        {
            try
            {
                inverseTransform = transform.createInverse();
            }
            catch (NoninvertibleTransformException e)
            {
                inverseTransform = new AffineTransform();
            }

            transformChanged = false;
        }

        return inverseTransform;
    }

    @Override
    public void changed(IcyCanvasEvent event)
    {
        super.changed(event);

        switch (event.getType())
        {
            case OFFSET_CHANGED:
            case ROTATION_CHANGED:
            case SCALE_CHANGED:
                updateTransform();
                break;
        }
    }
}