/*
 * $Id: PagePanel.java,v 1.2 2007/12/20 18:33:33 rbair Exp $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ch.randelshofer.pdf;

import com.sun.pdfview.annotation.LinkAnnotation;
import org.monte.media.image.SubpixAA;
import ch.randelshofer.pdf.plaf.PDFLookAndFeel;
import com.sun.pdfview.*;
import com.sun.pdfview.action.GoToAction;
import com.sun.pdfview.action.PDFAction;
import com.sun.pdfview.action.UriAction;
import com.sun.pdfview.annotation.PDFAnnotation;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;

/**
 * A Swing-based panel that displays a single PDF page.  
 * <p>
 * @version 2.2.1 2010-03-25 Werner Randelshofer paintComponents crashed when
 * the Graphics object returned null for the rendering hints map.
 * <br>2.2 2010-01-29 Werner Randelshofer Enable/Disable subpixel antialiasing.
 * <br>2.1 2009-11-28 Werner Randelshofer Added support for subpixel antialiasing.
 * <br>2.0 2008-09-18 Werner Randelshofer Reworked for use in an applet.
 */
public class JPDFPagePanel extends JPanel implements Scrollable {

    /**
     * Name of the "preferredZoomFactor" property.
     */
    public final static String PREFERRED_ZOOM_FACTOR_PROPERTY = "preferredZoomFactor";
    /**
     * Name of the "zoomFactor" property.
     */
    public final static String ZOOM_FACTOR_PROPERTY = "zoomFactor";
    /**
     * Name of the "page" property.
     */
    public final static String PAGE_PROPERTY = "page";
    /** Holds the location and dimensions of the page in view coordinates.
     */
    private Rectangle renderedViewRect;
    /** Holds the location and dimensions of the page for which an renderedImage was
     * rendered in view coordinates. */
    private Rectangle renderedRect;
    /** Holds the location and dimensions of the page for which an renderedImage was
     * rendered in page coordinates. */
    private Rectangle2D.Float renderedClip;
    /** Holds the renderedImage into which the page is rendered for the renderedRect. */
    private BufferedImage renderedImage;
    /** Holds the PDF Page which is displayed by this panel. */
    private PDFPage page;
    /**
     * The zoom factor which was used to determine the preferred size of
     * the panel.
     */
    private double preferredZoomFactor = 1;
    /**
     * The zoom factor which was used to convert the page from page coordinates
     * into view coordinates.
     */
    private double zoomFactor = 1;
    /** a flag indicating whether the current page is done or not. */
    private Flag flag = new Flag();
    /** Whether we currently render with subpixel anti-aliasing. */
    private boolean isSubpixAAOn = false;
    /** Super-sampling used for subpixel anti-aliasing.  */
    private int superX = 3, superY = 1;
    /** antialiasing method being used. */
    private Object aaMethod;
    /** subpixel antialiasing engine. */
    private SubpixAA subpixAAEngine;
    /** Wether subpixel antialiasing is desired.
     *
     */
    private boolean isSubpixAAEnabled = false;
    private PageChangeListener listener;
    private OpenURIListener uriListener;

    @Override
    public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    @Override
    public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        return 10;
    }

    @Override
    public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
    if (orientation == SwingConstants.VERTICAL) {
                return visibleRect.height;
            }
            else {
                return visibleRect.width;
            }
    }

    @Override
    public boolean getScrollableTracksViewportWidth() {
        return false;
    }

    @Override
    public boolean getScrollableTracksViewportHeight() {
       return false;
    }

    private class EventHandler implements MouseListener, MouseMotionListener, ImageObserver {

        @Override
        public void mouseClicked(MouseEvent e) {
      if (page==null)return;      
            LinkAnnotation clickedLink = getLinkAt(e.getPoint());
            if (clickedLink != null) {
                PDFAction action=clickedLink.getAction();
                if (action instanceof GoToAction) {
                    if (listener != null) {
                        listener.gotoPage( ((GoToAction)action).getDestination());
                    }
                   
                }else
                if (action instanceof UriAction) {
                    if (uriListener != null) {
                        try {
                            uriListener.openURI(new URI(((UriAction)action).getUri()),e.isControlDown()||e.isAltDown()?"_blank":"_self");
                        } catch (URISyntaxException ex) {
                           ex.printStackTrace();
                        }
                    }
                   
                }
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            requestFocus();
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mouseDragged(MouseEvent e) {
        }

        @Override
        public void mouseMoved(MouseEvent e) {
//System.out.println("JPDFPagePanel "+e.getX()+","+e.getY()+" =>"+viewToPage(e.getPoint())+" =>"+pageToView(viewToPage(e.getPoint()))+" z:"+zoomFactor)            ;
      if (page==null)return;      
            LinkAnnotation hoverLink = getLinkAt(e.getPoint());
            setCursor(hoverLink == null ? Cursor.getDefaultCursor() : Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        }

        public boolean imageUpdate(Image img, int infoflags, int x, int y,
                int width, int height) {
            if ((infoflags & (SOMEBITS | ALLBITS)) != 0) {
                if (isSubpixAAOn) {
                    repaint(x / superX + renderedRect.x, y / superY + renderedRect.y, width / superX, height / superY);
                } else {
                    repaint(x + renderedRect.x, y + renderedRect.y, width, height);
                }
            }
            if ((infoflags & (ALLBITS | ERROR | ABORT)) != 0) {
                flag.set();
                return false;
            } else {
                return true;
            }
        }
    }
    private EventHandler eventHandler;
    private Border pageBorder;

    /**
     * Create a new PagePanel, with a default size of 800 by 600 pixels.
     */
    public JPDFPagePanel() {
        setFocusable(true);
        eventHandler = new EventHandler();
        addMouseListener(eventHandler);
        addMouseMotionListener(eventHandler);

        PDFLookAndFeel plaf = PDFLookAndFeel.getInstance();

        setBorder(plaf.getBorder("PagePanel.border"));

        pageBorder = plaf.getBorder("Page.border");
    }
    

    protected void stopRendering() {
        if (page != null && renderedImage != null) {
            page.stop(renderedRect.width, renderedRect.height, renderedClip);
            renderedImage = null;
            flag.set();
        }
    }

    public void setPage(PDFPage newValue) {
        PDFPage oldValue = page;
        stopRendering();
        page = newValue;
        firePropertyChange(PAGE_PROPERTY, oldValue, newValue);
        updatePreferredSize();
        revalidate();
        repaint();
        SwingUtilities.invokeLater(new Runnable() {@Override
public void run() {
            Point p=getMousePosition();
            if (p!=null) {
            LinkAnnotation hoverLink = getLinkAt(p);
            setCursor(hoverLink == null ? Cursor.getDefaultCursor() : Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        }
        }});
    }

    /**
     * Gets the page currently being displayed
     */
    public PDFPage getPage() {
        return page;
    }

    public void setSubpixAAEnabled(boolean newValue) {
        if (newValue != isSubpixAAEnabled) {
            isSubpixAAEnabled = newValue;
            if (renderedImage != null) {
                renderedImage.flush();
                renderedImage = null;
            }
            repaint();
        }
    }

    public boolean isSubpixAAEnabled() {
        return isSubpixAAEnabled;
    }

    /**
     * Set up the graphics transform to match the clip region
     * to the image size.
     */
    private void setupRendering(Graphics2D g) {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
                RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g.setRenderingHint(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);
        /*
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_OFF);
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
        g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,
        RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
        g.setRenderingHint(RenderingHints.KEY_RENDERING,
        RenderingHints.VALUE_RENDER_SPEED);
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
        RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
         */
    }

    /**
     * Draw the renderedImage.
     */
    @Override
    public void paintComponent(Graphics gr) {
        if (page == null) {
            return;
        }
        Graphics2D g = (Graphics2D) gr;
        setupRendering(g);

        Dimension size = getSize();
        Insets insets = getInsets();

        if (renderedViewRect != null) {
            g.setColor(Color.WHITE);
            g.fill(renderedViewRect);

            Insets bi = pageBorder.getBorderInsets(this);
            pageBorder.paintBorder(this, g, renderedViewRect.x - bi.left,
                    renderedViewRect.y - bi.top,
                    renderedViewRect.width + bi.left + bi.right,
                    renderedViewRect.height + bi.top + bi.bottom);
        }

        double oldValue = zoomFactor;
        zoomFactor = Math.min((size.width - insets.left - insets.right) / page.getWidth(),
                (size.height - insets.top - insets.bottom) / page.getHeight());
        firePropertyChange(ZOOM_FACTOR_PROPERTY, oldValue, zoomFactor);

        Rectangle pageRect = new Rectangle(
                insets.left + ((size.width - insets.left - insets.right) - (int) (page.getWidth() * zoomFactor)) / 2,
                insets.top + ((size.height - insets.top - insets.bottom) - (int) (page.getHeight() * zoomFactor)) / 2,
                (int) (page.getWidth() * zoomFactor),
                (int) (page.getHeight() * zoomFactor));
        if (renderedImage != null) {
            // Render the image
            if (isSubpixAAOn) {
                if (subpixAAEngine == null) {
                    subpixAAEngine = new SubpixAA();
                }
                subpixAAEngine.drawAA(g, renderedImage, //
                        renderedRect.x, renderedRect.y, renderedRect.width, renderedRect.height,//
                        aaMethod,
                        eventHandler);
                //ClearType.drawClearType(g, renderedImage, renderedRect.x, renderedRect.y, superY, eventHandler);
                //g.drawImage(renderedImage, renderedRect.x, renderedRect.y, renderedImage.getWidth(null)/superX,renderedImage.getHeight(null)/superY,eventHandler);
            } else {
                g.drawImage(renderedImage, renderedRect.x, renderedRect.y, eventHandler);
            }

            // Check if the rendered image still fully covers the visible area
            Rectangle visibleArea = new Rectangle(renderedViewRect);
            if (getParent() instanceof JViewport) {
                JViewport viewport = (JViewport) getParent();
                Rectangle.intersect(viewport.getViewRect(), visibleArea, visibleArea);
            }
            if (!renderedRect.contains(visibleArea)
                    || !renderedViewRect.equals(pageRect)) {
                stopRendering();
            }
        }

        if (renderedImage == null && page != null) {
            aaMethod = g.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);
            Map<RenderingHints.Key, Object> map = (Map<RenderingHints.Key, Object>) Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints");
            aaMethod = (map == null) ? null : map.get(RenderingHints.KEY_TEXT_ANTIALIASING);

            isSubpixAAOn = isSubpixAAEnabled && aaMethod != RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
            superX = SubpixAA.getSourceDimension(1, 1, aaMethod).width;
            superY = SubpixAA.getSourceDimension(1, 1, aaMethod).height;

            flag.clear();

            renderedViewRect = pageRect;

            renderedRect = new Rectangle(renderedViewRect);

            // Avoid rendering an image, which is larger than the
            // screen bounds
            Rectangle screenBounds = getGraphicsConfiguration().getBounds();
            if (screenBounds.width < renderedRect.width || screenBounds.height < renderedRect.height) {
                if (getParent() instanceof JViewport) {
                    JViewport viewport = (JViewport) getParent();
                    Rectangle.intersect(viewport.getViewRect(), renderedRect, renderedRect);
                }
            }

            /*renderedClip = new Rectangle2D.Float(
            (float) ((renderedRect.x - renderedPageRect.x) / zoomFactor),
            (float) ((renderedRect.y - renderedPageRect.y) / zoomFactor),
            (float) ((renderedRect.width) / zoomFactor),
            (float) ((renderedRect.height) / zoomFactor));
             */
            renderedClip = new Rectangle2D.Float(
                    (float) ((renderedRect.x - renderedViewRect.x) / zoomFactor),
                    (float) ((renderedRect.y - renderedViewRect.y) / zoomFactor),
                    (float) (Math.min(renderedRect.width, renderedViewRect.width) / zoomFactor),
                    (float) (Math.min(renderedRect.height, renderedViewRect.height) / zoomFactor));
            renderedClip.y = page.getHeight() - renderedClip.y - renderedClip.height;

            Rectangle2D bbx = page.getBBox();
            renderedClip.x += bbx.getX();
            renderedClip.y += bbx.getY();

            int rotation = page.getRotation();
            if (rotation == 90 || rotation == 270) {
                renderedClip = new Rectangle2D.Float(renderedClip.y, renderedClip.x, renderedClip.height, renderedClip.width);
            }
//System.out.println("JPDFPagePanel rendering aaEna:"+isSubpixAAEnabled+" aaOn:"+isSubpixAAOn+" method:"+aaMethod+" "+renderedRect+" super:"+superX+","+superY);
            if (isSubpixAAOn) {
                renderedImage = (BufferedImage) page.getImage(renderedRect.width * superX, renderedRect.height * superY, renderedClip, eventHandler);

            } else {
                renderedImage = (BufferedImage) page.getImage(renderedRect.width, renderedRect.height, renderedClip, eventHandler);
            }
            repaint();
        }
    }

    /**
     * Gets the preferred zoom factor from device space to page space.
     */
    public double getPreferredZoomFactor() {
        return preferredZoomFactor;
    }

    public void setPreferredZoomFactor(double newValue) {
        if (newValue != preferredZoomFactor) {
            stopRendering();
            double oldValue = preferredZoomFactor;
            preferredZoomFactor = newValue;
            firePropertyChange(PREFERRED_ZOOM_FACTOR_PROPERTY, oldValue, newValue);
            updatePreferredSize();
            revalidate();
        }
    }

    /**
     * Gets the current zoom factor from device space to page space.
     */
    public double getZoomFactor() {
        return zoomFactor;
    }

    /**
     * Waits until the page is either complete or had an error.
     */
    public void waitForCurrentPage() {
        flag.waitForFlag();
    }

    public Rectangle getViewRect() {
        return renderedRect;
    }

    public Rectangle2D.Float getClipRect() {
        return renderedClip;
    }

    public void updatePreferredSize() {
        Insets insets = getInsets();
        Dimension preferredSize = new Dimension(insets.left + insets.right, insets.top + insets.bottom);

        if (page != null) {
            preferredSize.width += (int) Math.ceil(page.getWidth() * preferredZoomFactor);
            preferredSize.height += (int) Math.ceil(page.getHeight() * preferredZoomFactor);
        }
        setPreferredSize(preferredSize);
    }

    /** Converts a point in view coordinates to page coordinates. */
    public Point2D.Float viewToPage(Point p) {
        //p.x+renderedRect.x;
        switch (page.getRotation()) {
            case 0:
            default:
                return new Point2D.Float(//
                        (float) ((p.x - renderedViewRect.x) / zoomFactor),//
                        page.getHeight() - (float) ((p.y - renderedViewRect.y) / zoomFactor));
            case 90:
                return new Point2D.Float(//
                        (float) ((p.y - renderedViewRect.x) / zoomFactor),//
                        (float) ((p.x - renderedViewRect.y) / zoomFactor)//
                        );
            case 180:
                return new Point2D.Float(//
                        page.getWidth() - (float) ((p.x - renderedViewRect.x) / zoomFactor),//
                        page.getHeight() - (float) ((p.y - renderedViewRect.y) / zoomFactor));
            case 270:
                return new Point2D.Float(//
                        page.getHeight() - (float) ((p.y - renderedViewRect.x) / zoomFactor),//
                        page.getWidth() - (float) ((p.x - renderedViewRect.y) / zoomFactor)//
                        );
        }

    }
    /** Converts a point in view coordinates to page coordinates. */
    public Point pageToView(Point2D.Float p) {
        //p.x+renderedRect.x;
        switch (page.getRotation()) {
            case 0:
            default:
                return new Point(//
                        (int) (p.x * zoomFactor + renderedViewRect.x) ,//
                        (int) (((page.getHeight()-p.y)) * zoomFactor + renderedViewRect.y));
            case 90:
                return new Point(//
                        (int) ((p.y*zoomFactor + renderedViewRect.x) ),//
                        (int) ((p.x*zoomFactor + renderedViewRect.y) )//
                        );
            case 180:
                return new Point(//
                        (int) (((page.getWidth()-p.x)*zoomFactor + renderedViewRect.x) ),//
                        (int) (((page.getHeight()-p.y)*zoomFactor + renderedViewRect.y) ));
            case 270:
                return new Point(//
                         (int) (((page.getHeight()-p.y)*zoomFactor + renderedViewRect.x) ),//
                         (int) (((page.getWidth()-p.x)*zoomFactor + renderedViewRect.y) )//
                        );
        }

    }

    public LinkAnnotation getLinkAt(Point point) {
        Point2D.Float p = viewToPage(point);

        List<PDFAnnotation> links = page.getAnnots(PDFAnnotation.LINK_ANNOTATION);
        LinkAnnotation linkAtPoint = null;
        if (links != null) {
            for (PDFAnnotation l : links) {
                if (l.getRect().contains(p)) {
                    if (uriListener!=null/*||!(l.getAction()instanceof UriAction)*/
                            ||  (l instanceof LinkAnnotation) 
                            ) {
                        
                    linkAtPoint = (LinkAnnotation) l;
                    }
                    break;
                }
            }
        }
        return linkAtPoint;
    }
   /**
     * Adds a PageChangeListener to receive notification of page clicks.
     */
    public void addPageChangeListener(PageChangeListener pl) {
        // [[MW: should be an array list instead of only one]]
        listener = pl;
    }

    /**
     * Removes a PageChangeListener from the notification list.
     */
    public void removePageChangeListener(PageChangeListener pl) {
        // [[MW: should be an array list instead of only one]]
        listener = null;
    }
   /**
     * Adds a PageChangeListener to receive notification of page clicks.
     */
    public void addOpenURIListener(OpenURIListener pl) {
        // [[MW: should be an array list instead of only one]]
        uriListener = pl;
    }

    /**
     * Removes a PageChangeListener from the notification list.
     */
    public void removeOpenURIListener(OpenURIListener pl) {
        // [[MW: should be an array list instead of only one]]
        uriListener = null;
    }
    
    public void scrollPageRectToVisible(Rectangle2D.Float r) {
        Point p1=pageToView(new Point2D.Float((float)r.getX(),(float)r.getY()));
        Point p2=pageToView(new Point2D.Float((float)(r.getX()+r.getWidth()),(float)(r.getY()+r.getHeight())));
        scrollRectToVisible(new Rectangle(p1.x,p1.y,p2.x-p1.x+1,p2.y-p1.y+1));
    }
}
