/*
 * JPDFThumbnailsPanel.java
 *
 * Created on September 19, 2008, 9:25 PM
 */
package ch.randelshofer.pdf;

import ch.randelshofer.pdf.plaf.PDFLookAndFeel;
import ch.randelshofer.pdf.plaf.PDFUtilities;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;

import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;

import com.sun.pdfview.*;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.font.LineMetrics;
import java.awt.geom.RoundRectangle2D;
import javax.swing.border.Border;

/**
 * JPDFThumbnailsPanel.java
 *
 * @author Werner Randelshofer, Hausmatt 10, CH-6405 Immensee
 * @version 1.0 September 19, 2008 Created.
 */
public class JPDFThumbnailsPanel extends JPanel
        implements Runnable, Scrollable, ImageObserver {

    /** The PDFFile being displayed */
    private PDFFile file;
    /** Array of images, one per page in the file */
    private Image images[];
    /** Size of the thumbnailInsets between images*/
    private Insets thumbnailInsets = new Insets(3, 3, 20, 3);
    /** 
     * Height of each line.  Thumbnails will be scaled to this height
     * (minus the thumbnailInsets).
     */
    private int lineheight = 96 + thumbnailInsets.top + thumbnailInsets.bottom;
    /**
     * Guesstimate of the width of a thumbnail that hasn't been processed
     * yet.
     */
    private int defaultWidth = (int)(96/1.41) +
            thumbnailInsets.left + thumbnailInsets.right;
    /**
     * Array of the bounds of each of the thumbnails. 
     */
    private Rectangle[] bounds;
    /** Thread that renders each thumbnail in turn */
    private Thread anim;
    /** Which thumbnail is selected, or -1 if no thumbnail selected. */
    private int showing = -1;
    /**
     * Which thumbnail needs to be drawn next, or -1 if the previously
     * needed thumbnail is being processed.
     */
    private int needdrawn = -1;
    /**
     * Whether the default width has been guesstimated for this PDFFile
     * yet.
     */
    private boolean defaultNotSet = true;
    /** The PageChangeListener that is listening for page changes */
    private PageChangeListener listener;

    public JPDFThumbnailsPanel() {
        this(null);
    }

    /**
     * Creates a new ThumbPanel based on a PDFFile.  The file may be null.
     * Automatically starts rendering thumbnails for that file.
     */
    public JPDFThumbnailsPanel(PDFFile file) {
        setPDF(file);
        PDFLookAndFeel.installColorsAndFont(this,
                "ThumbnailsPanel.background",
                "ThumbnailsPanel.foreground",
                "ThumbnailsPanel.font");
    }

    public void setPDF(PDFFile file) {
        if (this.file != null) {
            stop();
        }
        this.file = file;
        if (file != null) {
            int count = file.getNumPages();
            images = new Image[count];
            bounds = new Rectangle[count];
            setPreferredSize(new Dimension(1, 1));
            addMouseListener(new MouseAdapter() {

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

                @Override
                public void mouseReleased(MouseEvent evt) {
                    handleClick(evt.getX(), evt.getY());
                }
            });
            start();
        } else {
            images = new Image[0];
            setPreferredSize(new Dimension(1, 1));
        }
    }

    /**
     * Renders each of the pages in the PDFFile into a thumbnail.
     * Preferentially works on the needdrawn thumbnail, otherwise, go in
     * order.
     */
    public void run() {
        int workingon = 0;	// the thumbnail we'll be rendering next.

        while (anim == Thread.currentThread()) {

            if (needdrawn >= 0) {
                workingon = needdrawn;
                needdrawn = -1;
            }


            // find an unfinished page
            int loop;
            for (loop = images.length; loop > 0; loop--) {
                if (images[workingon] == null) {
                    break;
                }
                workingon++;
                if (workingon >= images.length) {
                    workingon = 0;
                }
            }
            if (loop == 0) {
                // done all pages.
                break;
            }

            // build the page
            try {
                int pagetoread = workingon + 1;
                //		int pagetoread= 1;
//		System.out.println("Read page: " + pagetoread);
                PDFPage p = file.getPage(pagetoread, true);

                int w,h;
                if (p.getRotation()==0||p.getRotation()==180) {
                 w = (int) Math.ceil((lineheight - thumbnailInsets.top - thumbnailInsets.bottom) * p.getAspectRatio());
                 h = lineheight - thumbnailInsets.top - thumbnailInsets.bottom;
                } else {
                 w = lineheight - thumbnailInsets.top - thumbnailInsets.bottom;
                 h = (int) Math.ceil((lineheight - thumbnailInsets.top - thumbnailInsets.bottom) / p.getAspectRatio());
                }
                //		if (!p.isFinished()) {
                //		    System.out.println("Page not finished!");
                //		    p.waitForFinish();
                //		}
                //		flag.clear();
                //		int pagetowrite= 0;
                int pagetowrite = workingon;

//                Image i = p.getImage(wid, (lineheight - thumbnailInsets.top - thumbnailInsets.bottom), null, this, true, true);
                Image i = p.getImage(w, h, null, this, true, true);

                // images[0] = i;
                images[pagetowrite] = i;

                //		flag.waitForFlag();
                if (defaultNotSet) {
                    defaultNotSet = false;
                    setDefaultWidth(w);
                }
                repaint();
            } catch (Exception e) {
                e.printStackTrace();

                int size = lineheight - thumbnailInsets.top - thumbnailInsets.bottom;
                images[workingon] = new BufferedImage(size, size,
                        BufferedImage.TYPE_BYTE_BINARY);
            }
        }
    }

    /**
     * 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;
    }

    /**
     * Stops the render thread.  Be sure to call this before dropping
     * a ThumbPanel.
     */
    public void stop() {
        if (anim != null) {
            Thread t = anim;
            anim = null;
            try {
                t.join();
            } catch (InterruptedException ex) {
                // continue
            }
        }
    }

    /**
     * (Re-)starts the render thread.  
     */
    public void start() {
        stop();
        anim = new Thread(this);
        anim.start();
    }

    /**
     * Sets the default width of an un-processed thumbnail.
     * @param width the width of an unknown thumbnail, in pixels.
     */
    public void setDefaultWidth(int width) {
        defaultWidth = width;
        //	setPreferredSize(new Dimension(width, lineheight));
    }

    /**
     * Handles a mouse click in the panel.  Figures out which page was
     * clicked, and calls showPage.
     * @param x the x coordinate of the mouse click
     * @param y the y coordinate of the mouse click
     */
    public void handleClick(int x, int y) {
        int linecount = -1;
        int line = y / lineheight;
        Insets insets = getInsets();
        // run through the thumbnail locations, counting new lines
        // until the appropriate line is reached.
        for (int i = 0; i < bounds.length; i++) {
            if (bounds[i] != null && bounds[i].contains(x, y)) {
                showPage(i);
                break;
            }
        }
    }

    /**
     * Sets the currently viewed page, indicates it with a highlight
     * thumbnailInsets, and makes sure the thumbnail is visible.
     */
    public void pageShown(int pagenum) {
        if (showing != pagenum) {
            // FIND THE SELECTION RECTANGLE
            //	    getViewPort.scrollRectToVisible(r);
            if (pagenum >= 0 && getParent() instanceof JViewport) {
                if (bounds[pagenum] != null) {
                    scrollRectToVisible(bounds[pagenum]);
                }
            }
            showing = pagenum;
            repaint();
        }
    }

    /**
     * Notifies the listeners that a page has been selected.  Performs
     * the notification in the AWT thread.
     * Also highlights the selected page.  Does this first so that feedback
     * is immediate.
     */
    public void showPage(int pagenum) {
        pageShown(pagenum);
        SwingUtilities.invokeLater(new GotoLater(pagenum));
    }

    /**
     * Simple runnable to tell listeners that the page has changed.
     */
    class GotoLater implements Runnable {

        int page;

        public GotoLater(int pagenum) {
            page = pagenum;
        }

        @Override
        public void run() {
            if (listener != null) {
                listener.gotoPage(page);
            }
        }
    }

    /**
     * Updates the positions of the thumbnails, and draws them to the
     * screen.
     */
    @Override
    public void paintComponent(Graphics gr) {
        if (images == null) {
            return;
        }
        Graphics2D g = (Graphics2D) gr;
        Object oldHints = PDFUtilities.beginGraphics(g);


        PDFLookAndFeel laf = PDFLookAndFeel.getInstance();
        Font font = laf.getFont("ThumbnailsPanel.font");
        Font selectionFont = laf.getFont("ThumbnailsPanel.selectionFont");
        Color selectionBackground = laf.getColor("ThumbnailsPanel.selectionBackground");
        Color selectionForeground = laf.getColor("ThumbnailsPanel.selectionForeground");
        Color backgroundColor = getBackground();
        Color foregroundColor = getForeground();
        int iconTextGap = (Integer) laf.get("ThumbnailsPanel.iconTextGap");
        g.setColor(backgroundColor);
        g.fillRect(0, 0, getWidth(), getHeight());
        Border[] thumbnailBorders = (Border[]) laf.get("ThumbnailsPanel.thumbnailBorders");
        //    Flag flag= new Flag();


        Insets insets = getInsets();
        int x = insets.left;
        int y = insets.top;
        int maxwidth = 0;
        int preferredwidth=0;
        Rectangle clip = g.getClipBounds();
        int width = getWidth();

        Insets bi = thumbnailBorders[0].getBorderInsets(this);
        for (int i = 0; i < images.length; i++) {
            // calculate the x location of the thumbnail, based on its width
            int w = defaultWidth;
            int imgW = defaultWidth - thumbnailInsets.left - thumbnailInsets.right;
            int imgH = lineheight - thumbnailInsets.top - thumbnailInsets.bottom;
            if (images[i] != null) {
                w = (int) images[i].getWidth(null) + thumbnailInsets.left + thumbnailInsets.right;
                imgW = images[i].getWidth(null);
                imgH = images[i].getHeight(null);
            }


            // need a new line?
            if (x + w > width - insets.right && x != insets.left) {
                x = insets.left;
                y += lineheight;
            }
            // if the thumbnail is visible, draw it.
            if (clip.intersects(new Rectangle(x - thumbnailInsets.left, y - thumbnailInsets.top, w + thumbnailInsets.left + thumbnailInsets.right,
                    lineheight + thumbnailInsets.top + thumbnailInsets.bottom))) {
                if (images[i] != null) {
                    // thumbnail is ready.
                    g.drawImage(images[i], x + thumbnailInsets.left, y + thumbnailInsets.top, this);
                } else {
                    // thumbnail isn't ready.  Remember that we need it...
                    if (needdrawn == -1) {
                        needdrawn = i;
                    }
                    // ... and draw a blank thumbnail.
                    g.setColor(Color.lightGray);
                    g.fillRect(x + thumbnailInsets.left, y + thumbnailInsets.top,
                            imgW,
                            imgH);
                }
                thumbnailBorders[(i == showing) ? 1 : 0].paintBorder(this, g,
                        x + thumbnailInsets.left - bi.left,
                        y + thumbnailInsets.top - bi.top,
                        imgW + bi.left + bi.right,
                        imgH + bi.top + bi.bottom);

                // Label of the thumbnail
                String label = Integer.toString(i + 1);

                // draw the selection highlight if needed.
                if (i == showing) {
                    g.setColor(selectionBackground);
                    g.drawRect(x + thumbnailInsets.left, y + thumbnailInsets.top, imgW - 1, imgH - 1);
                    g.drawRect(x + 1 + thumbnailInsets.left, y + 1 + thumbnailInsets.top, imgW - 3, imgH - 3);
                    // draw the page number
                    g.setFont(selectionFont);
                    FontMetrics fm = g.getFontMetrics();
                    LineMetrics lm = fm.getLineMetrics(label, g);

                    g.fill(new RoundRectangle2D.Double(x + thumbnailInsets.left + (imgW - fm.stringWidth(label)) / 2 - 7,
                            y + iconTextGap + thumbnailInsets.top + imgH,
                            fm.stringWidth(label) + 15,
                            fm.getAscent() + 4,
                            13, 13));

                    g.setColor(selectionForeground);
                    g.drawString(label, x + thumbnailInsets.left + (imgW - fm.stringWidth(label)) / 2, y + iconTextGap + thumbnailInsets.top + imgH + fm.getAscent());
                } else {
                    // draw the page number
                    g.setFont(font);
                    g.setColor(foregroundColor);
                    FontMetrics fm = g.getFontMetrics();
                    LineMetrics lm = fm.getLineMetrics(label, g);
                    g.drawString(label, x + thumbnailInsets.left + (imgW - fm.stringWidth(label)) / 2, y + iconTextGap + thumbnailInsets.top + imgH + fm.getAscent());
                }
            }
            // save the bounds of this thumbnail.
            bounds[i] = new Rectangle(x, y, w, lineheight);
            x += w;
            // remember the longest line
            if (x > maxwidth) {
                maxwidth = x;
            }
            if (w>preferredwidth) {
                preferredwidth=w;
            }
        }
        // if there weren't any thumbnails, make a default line width
        if (maxwidth == 0) {
            maxwidth = defaultWidth;
        }
        Dimension d = getPreferredSize();
        if (d.height != y + lineheight + insets.top + insets.bottom || d.width != preferredwidth + insets.left + insets.right) {
            setPreferredSize(new Dimension(preferredwidth + insets.left + insets.right, y + lineheight + insets.top + insets.bottom));
            revalidate();
        }
        PDFUtilities.endGraphics(g, oldHints);
    }

    /**
     * Handles notification of any image updates.  Not used any more.
     */
    @Override
    public boolean imageUpdate(Image img, int infoflags, int x, int y,
            int width, int height) {
        //	if ((infoflags & ALLBITS)!=0) {
        //	    flag.set();
        //	}
        return ((infoflags & (ALLBITS | ERROR | ABORT)) == 0);
    }

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

    public int getScrollableBlockIncrement(Rectangle visrect, int orientation,
            int direction) {
        return Math.max(lineheight, (visrect.height / lineheight) * lineheight);
    }

    public boolean getScrollableTracksViewportHeight() {
        return false;
    }

    public boolean getScrollableTracksViewportWidth() {
        return true;
    }

    public int getScrollableUnitIncrement(Rectangle visrect, int orientation,
            int direction) {
        return lineheight;
    }

    /** Repaints the thumbnail of the specified page. */
    public void repaintThumbnail(int page) {
        stop();
        images[page] = null;
        bounds[page] = null;
        needdrawn=page;
        start();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        setLayout(new java.awt.BorderLayout());
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
}
