/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.apps.svgbrowser;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;

import javax.swing.JDialog;
import javax.swing.event.MouseInputAdapter;

import org.apache.batik.bridge.ViewBox;
import org.apache.batik.gvt.CanvasGraphicsNode;
import org.apache.batik.gvt.CompositeGraphicsNode;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.swing.gvt.GVTTreeRendererAdapter;
import org.apache.batik.swing.gvt.GVTTreeRendererEvent;
import org.apache.batik.swing.gvt.JGVTComponent;
import org.apache.batik.swing.gvt.Overlay;
import org.apache.batik.swing.svg.SVGDocumentLoaderAdapter;
import org.apache.batik.swing.svg.SVGDocumentLoaderEvent;
import org.apache.batik.util.resources.ResourceManager;
import org.apache.batik.util.SVGConstants;

import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGSVGElement;

/**
 * This class represents a Dialog that displays a Thumbnail of the current SVG
 * document.
 *
 * @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
 * @version $Id$
 */
public class ThumbnailDialog extends JDialog {

    /**
     * The resource file name
     */
    protected static final String RESOURCES =
        "org.apache.batik.apps.svgbrowser.resources.ThumbnailDialog";

    /**
     * The resource bundle
     */
    protected static ResourceBundle bundle;

    /**
     * The resource manager
     */
    protected static ResourceManager resources;

    static {
        bundle = ResourceBundle.getBundle(RESOURCES, Locale.getDefault());
        resources = new ResourceManager(bundle);
    }

    /** The canvas that owns the SVG document to display. */
    protected JSVGCanvas svgCanvas;

    /** The canvas that displays the thumbnail. */
    protected JGVTComponent svgThumbnailCanvas;

    /** A flag bit that indicates a document has been loaded. */
    protected boolean documentChanged;

    /** The overlay used to display the area of interest. */
    protected AreaOfInterestOverlay overlay;

    /** The overlay used to display the area of interest. */
    protected AreaOfInterestListener aoiListener;

    protected boolean interactionEnabled = true;

    /**
     * Constructs a new <code>ThumbnailDialog</code> for the specified canvas.
     *
     * @param owner the owner frame
     * @param svgCanvas the canvas that owns the SVG document to display
     */
    public ThumbnailDialog(Frame owner, JSVGCanvas svgCanvas) {
        super(owner, resources.getString("Dialog.title"));

        addWindowListener(new ThumbnailListener());

        // register listeners to maintain consistency
        this.svgCanvas = svgCanvas;
        svgCanvas.addGVTTreeRendererListener(new ThumbnailGVTListener());
        svgCanvas.addSVGDocumentLoaderListener(new ThumbnailDocumentListener());
        svgCanvas.addComponentListener(new ThumbnailCanvasComponentListener());

        // create the thumbnail
        svgThumbnailCanvas = new JGVTComponent();
        overlay = new AreaOfInterestOverlay();
        svgThumbnailCanvas.getOverlays().add(overlay);
        svgThumbnailCanvas.setPreferredSize(new Dimension(150, 150));
        svgThumbnailCanvas.addComponentListener(new ThumbnailComponentListener());
        aoiListener = new AreaOfInterestListener();
        svgThumbnailCanvas.addMouseListener(aoiListener);
        svgThumbnailCanvas.addMouseMotionListener(aoiListener);
        getContentPane().add(svgThumbnailCanvas, BorderLayout.CENTER);
    }

    public void setInteractionEnabled(boolean b) {
        if (b == interactionEnabled) return;
        interactionEnabled = b;
        if (b) {
            svgThumbnailCanvas.addMouseListener      (aoiListener);
            svgThumbnailCanvas.addMouseMotionListener(aoiListener);
        } else {
            svgThumbnailCanvas.removeMouseListener      (aoiListener);
            svgThumbnailCanvas.removeMouseMotionListener(aoiListener);
        }
    }

    public boolean getInteractionEnabled() {
        return interactionEnabled;
    }

    /**
     * Updates the thumbnail component.
     */
    protected void updateThumbnailGraphicsNode() {
        svgThumbnailCanvas.setGraphicsNode(svgCanvas.getGraphicsNode());
        updateThumbnailRenderingTransform();
    }

    protected CanvasGraphicsNode getCanvasGraphicsNode(GraphicsNode gn) {
        if (!(gn instanceof CompositeGraphicsNode))
            return null;
        CompositeGraphicsNode cgn = (CompositeGraphicsNode)gn;
        List children = cgn.getChildren();
        if (children.size() == 0)
            return null;
        gn = (GraphicsNode)cgn.getChildren().get(0);
        if (!(gn instanceof CanvasGraphicsNode))
            return null;
        return (CanvasGraphicsNode)gn;
    }

    /**
     * Updates the thumbnail component rendering transform.
     */
    protected void updateThumbnailRenderingTransform() {
        SVGDocument svgDocument = svgCanvas.getSVGDocument();
        if (svgDocument != null) {
            SVGSVGElement elt = svgDocument.getRootElement();
            Dimension dim = svgThumbnailCanvas.getSize();

            // XXX Update this to use the animated values of 'viewBox'
            //     and 'preserveAspectRatio'.
            String viewBox = elt.getAttributeNS
                (null, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE);

            AffineTransform Tx;
            if (viewBox.length() != 0) {
                String aspectRatio = elt.getAttributeNS
                    (null, SVGConstants.SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE);
                Tx = ViewBox.getPreserveAspectRatioTransform
                    (elt, viewBox, aspectRatio, dim.width, dim.height, null);
            } else {
                // no viewBox has been specified, create a scale transform
                Dimension2D docSize = svgCanvas.getSVGDocumentSize();
                double sx = dim.width / docSize.getWidth();
                double sy = dim.height / docSize.getHeight();
                double s = Math.min(sx, sy);
                Tx = AffineTransform.getScaleInstance(s, s);
            }

            GraphicsNode gn = svgCanvas.getGraphicsNode();
            CanvasGraphicsNode cgn = getCanvasGraphicsNode(gn);
            if (cgn != null) {
                AffineTransform vTx = cgn.getViewingTransform();
                if ((vTx != null) && !vTx.isIdentity()) {
                    try {
                        AffineTransform invVTx = vTx.createInverse();
                        Tx.concatenate(invVTx);
                    } catch (NoninvertibleTransformException nite) {
                        /* nothing */
                    }
                }
            }

            svgThumbnailCanvas.setRenderingTransform(Tx);
            overlay.synchronizeAreaOfInterest();
        }
    }

    /**
     * Used to determine whether or not the GVT tree of the thumbnail has to be
     * updated.
     */
    protected class ThumbnailDocumentListener extends SVGDocumentLoaderAdapter {

        public void documentLoadingStarted(SVGDocumentLoaderEvent e) {
            documentChanged = true;
        }
    }

    /**
     * Used to perform a translation using the area of interest.
     */
    protected class AreaOfInterestListener extends MouseInputAdapter {

        protected int sx, sy;
        protected boolean in;

        public void mousePressed(MouseEvent evt) {
            sx = evt.getX();
            sy = evt.getY();
            in = overlay.contains(sx, sy);
            overlay.setPaintingTransform(new AffineTransform());
        }

        public void mouseDragged(MouseEvent evt) {
            if (in) {
                int dx = evt.getX() - sx;
                int dy = evt.getY() - sy;
                overlay.setPaintingTransform
                    (AffineTransform.getTranslateInstance(dx, dy));
                svgThumbnailCanvas.repaint();
            }
        }

        public void mouseReleased(MouseEvent evt) {
            if (in) {
                in = false;

                int dx = evt.getX() - sx;
                int dy = evt.getY() - sy;
                AffineTransform at = overlay.getOverlayTransform();
                Point2D pt0 = new Point2D.Float(0, 0);
                Point2D pt = new Point2D.Float(dx, dy);

                try {
                    at.inverseTransform(pt0, pt0);
                    at.inverseTransform(pt, pt);
                    double tx = pt0.getX() - pt.getX();
                    double ty = pt0.getY() - pt.getY();
                    at = svgCanvas.getRenderingTransform();
                    at.preConcatenate
                        (AffineTransform.getTranslateInstance(tx, ty));
                    svgCanvas.setRenderingTransform(at);
                } catch (NoninvertibleTransformException ex) { }
            }
        }
    }

    /**
     * Used to update the overlay and/or the GVT tree of the thumbnail.
     */
    protected class ThumbnailGVTListener extends GVTTreeRendererAdapter {

        public void gvtRenderingCompleted(GVTTreeRendererEvent e) {
            if (documentChanged) {
                updateThumbnailGraphicsNode();
                documentChanged = false;
            } else {
                overlay.synchronizeAreaOfInterest();
                svgThumbnailCanvas.repaint();
            }
        }

        public void gvtRenderingCancelled(GVTTreeRendererEvent e) {
            if (documentChanged) {
                svgThumbnailCanvas.setGraphicsNode(null);
                svgThumbnailCanvas.setRenderingTransform(new AffineTransform());
            }
        }

        public void gvtRenderingFailed(GVTTreeRendererEvent e) {
            if (documentChanged) {
                svgThumbnailCanvas.setGraphicsNode(null);
                svgThumbnailCanvas.setRenderingTransform(new AffineTransform());
            }
        }
    }

    /**
     * Used the first time the thumbnail dialog is shown to make visible the
     * current GVT tree being displayed by the original SVG component.
     */
    protected class ThumbnailListener extends WindowAdapter {

        public void windowOpened(WindowEvent evt) {
            updateThumbnailGraphicsNode();
        }
    }

    /**
     * Used to allow the SVG document being displayed by the thumbnail to be
     * resized properly.
     */
    protected class ThumbnailComponentListener extends ComponentAdapter {

        public void componentResized(ComponentEvent e) {
            updateThumbnailRenderingTransform();
        }
    }

    /**
     * Used to allow the SVG document being displayed by the thumbnail to be
     * resized properly when parent resizes.
     */
    protected class ThumbnailCanvasComponentListener extends ComponentAdapter {

        public void componentResized(ComponentEvent e) {
            updateThumbnailRenderingTransform();
        }
    }

    /**
     * An overlay that represents the current area of interest.
     */
    protected class AreaOfInterestOverlay implements Overlay {

        protected Shape s;
        protected AffineTransform at;
        protected AffineTransform paintingTransform = new AffineTransform();

        public boolean contains(int x, int y) {
            return (s != null) ? s.contains(x, y) : false;
        }

        public AffineTransform getOverlayTransform() {
            return at;
        }

        public void setPaintingTransform(AffineTransform rt) {
            this.paintingTransform = rt;
        }

        public AffineTransform getPaintingTransform() {
            return paintingTransform;
        }

        public void synchronizeAreaOfInterest() {
            paintingTransform = new AffineTransform();
            Dimension dim = svgCanvas.getSize();
            s = new Rectangle2D.Float(0, 0, dim.width, dim.height);
            try {
                at = svgCanvas.getRenderingTransform().createInverse();
                at.preConcatenate(svgThumbnailCanvas.getRenderingTransform());
                s = at.createTransformedShape(s);
            } catch (NoninvertibleTransformException ex) {
                dim = svgThumbnailCanvas.getSize();
                s = new Rectangle2D.Float(0, 0, dim.width, dim.height);
            }
        }

        public void paint(Graphics g) {
            if (s != null) {
                Graphics2D g2d = (Graphics2D)g;
                g2d.transform(paintingTransform);
                g2d.setColor(new Color(255, 255, 255, 128));
                g2d.fill(s);
                g2d.setColor(Color.black);
                g2d.setStroke(new BasicStroke());
                g2d.draw(s);
            }
        }
    }
}
