/*
 * #%L
 * BSD implementations of Bio-Formats readers and writers
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package loci.formats.gui;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;

import loci.formats.FormatException;
import loci.formats.FormatTools;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * PreviewPane is a panel for use as a JFileChooser accessory, displaying
 * a thumbnail for the selected image, loaded in a separate thread.
 */
public class PreviewPane extends JPanel
  implements PropertyChangeListener, Runnable
{

  // -- Constants --

  private static final Logger LOGGER =
    LoggerFactory.getLogger(PreviewPane.class);

  // -- Fields --

  /** Reader for use when loading thumbnails. */
  protected BufferedImageReader reader;

  /** Current ID to load. */
  protected String loadId;

  /** Last ID loaded. */
  protected String lastId;

  /** Thumbnail loading thread. */
  protected Thread loader;

  /** Flag indicating whether loader thread should keep running. */
  protected boolean loaderAlive;

  /** Method for syncing the view to the model. */
  protected Runnable refresher;

  // -- Fields - view --

  /** Labels containing thumbnail and dimensional information. */
  protected JLabel iconLabel, formatLabel, resLabel, zctLabel, typeLabel;

  // -- Fields - model --

  protected ImageIcon icon;
  protected String iconText, formatText, resText, zctText, typeText;
  protected String iconTip, formatTip, resTip, zctTip, typeTip;

  // -- Constructor --

  /** Constructs a preview pane for the given file chooser. */
  public PreviewPane(JFileChooser jc) {
    super();

    reader = new BufferedImageReader();
    reader.setNormalized(true);

    // create view
    setBorder(new EmptyBorder(0, 10, 0, 10));
    setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
    iconLabel = new JLabel();
    iconLabel.setMinimumSize(new java.awt.Dimension(128, -1));
    iconLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
    add(iconLabel);
    add(Box.createVerticalStrut(7));
    formatLabel = new JLabel();
    formatLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
    add(formatLabel);
    add(Box.createVerticalStrut(5));
    resLabel = new JLabel();
    resLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
    add(resLabel);
    zctLabel = new JLabel();
    zctLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
    add(zctLabel);
    typeLabel = new JLabel();
    typeLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT);
    add(typeLabel);

    // smaller font for most labels
    Font font = formatLabel.getFont();
    font = font.deriveFont(font.getSize2D() - 3);
    formatLabel.setFont(font);
    resLabel.setFont(font);
    zctLabel.setFont(font);
    typeLabel.setFont(font);

    // populate model
    icon = null;
    iconText = formatText = resText = zctText = typeText = "";
    iconTip = formatTip = resTip = zctTip = typeTip = null;

    if (jc != null) {
      jc.setAccessory(this);
      jc.addPropertyChangeListener(this);

      refresher = new Runnable() {
        @Override
        public void run() {
          iconLabel.setIcon(icon);
          iconLabel.setText(iconText);
          iconLabel.setToolTipText(iconTip);
          formatLabel.setText(formatText);
          formatLabel.setToolTipText(formatTip);
          resLabel.setText(resText);
          resLabel.setToolTipText(resTip);
          zctLabel.setText(zctText);
          zctLabel.setToolTipText(zctTip);
          typeLabel.setText(typeText);
          typeLabel.setToolTipText(typeTip);
        }
      };

      // start separate loader thread
      loaderAlive = true;
      loader = new Thread(this, "Preview");
      loader.start();
    }
  }

  // -- Component API methods --

  /* @see java.awt.Component#getPreferredSize() */
  @Override
  public Dimension getPreferredSize() {
    Dimension prefSize = super.getPreferredSize();
    return new Dimension(148, prefSize.height);
  }

  // -- PropertyChangeListener API methods --

  /**
   * Property change event, to listen for when a new
   * file is selected, or the file chooser closes.
   */
  @Override
  public void propertyChange(PropertyChangeEvent e) {
    String prop = e.getPropertyName();
    if (prop.equals("JFileChooserDialogIsClosingProperty")) {
      // notify loader thread that it should stop
      loaderAlive = false;
    }

    if (!prop.equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) return;

    File f = (File) e.getNewValue();
    if (f != null && (f.isDirectory() || !f.exists())) f = null;

    loadId = f == null ? null : f.getAbsolutePath();
  }

  // -- Runnable API methods --

  /** Thumbnail loading routine. */
  @Override
  public void run() {
    while (loaderAlive) {
      try { Thread.sleep(100); }
      catch (InterruptedException exc) { LOGGER.info("", exc); }

      try { // catch-all for unanticipated exceptions
        final String id = loadId;
        if ((id == null && lastId == null) || (id != null && id.equals(lastId))) {
          continue;
        }
        if (id != null && lastId != null) {
          String[] files = reader.getUsedFiles();
          boolean found = false;
          for (int i=0; i<files.length; i++) {
            if (id.equals(files[i])) {
              found = true;
              lastId = id;
              break;
            }
          }
          if (found) continue;
        }
        lastId = id;

        icon = null;
        iconText = id == null ? "" : "Reading...";
        formatText = resText = zctText = typeText = "";
        iconTip = id;
        formatTip = resTip = zctTip = typeTip = "";

        if (id == null) {
          SwingUtilities.invokeLater(refresher);
          continue;
        }

        try { reader.setId(id); }
        catch (FormatException exc) {
          LOGGER.debug("Failed to initialize {}", id, exc);
          boolean badFormat =
            exc.getMessage().startsWith("Unknown file format");
          iconText = "Unsupported " + (badFormat ? "format" : "file");
          formatText = resText = "";
          SwingUtilities.invokeLater(refresher);
          lastId = null;
          continue;
        }
        catch (IOException exc) {
          LOGGER.debug("Failed to initialize {}", id, exc);
          iconText = "Unsupported file";
          formatText = resText = "";
          SwingUtilities.invokeLater(refresher);
          lastId = null;
          continue;
        }
        if (!id.equals(loadId)) {
          SwingUtilities.invokeLater(refresher);
          continue;
        }

        icon = new ImageIcon(makeImage("Loading..."));
        iconText = "";
        String format = reader.getFormat();
        formatText = format;
        formatTip = format;
        resText = reader.getSizeX() + " x " + reader.getSizeY();
        zctText = reader.getSizeZ() + "Z x " +
          reader.getSizeT() + "T x " + reader.getSizeC() + "C";
        typeText = reader.getRGBChannelCount() + " x " +
          FormatTools.getPixelTypeString(reader.getPixelType());
        SwingUtilities.invokeLater(refresher);

        // open middle image thumbnail
        int z = reader.getSizeZ() / 2;
        int t = reader.getSizeT() / 2;
        int ndx = reader.getIndex(z, 0, t);
        BufferedImage thumb = null;
        try { thumb = reader.openThumbImage(ndx); }
        catch (FormatException exc) {
          LOGGER.debug("Failed to read thumbnail #{} from {}",
            new Object[] {ndx, id}, exc);
        }
        catch (IOException exc) {
          LOGGER.debug("Failed to read thumbnail #{} from {}",
            new Object[] {ndx, id}, exc);
        }
        icon = new ImageIcon(thumb == null ? makeImage("Failed") : thumb);
        iconText = "";

        SwingUtilities.invokeLater(refresher);
      }
      catch (Exception exc) {
        LOGGER.info("", exc);
        icon = null;
        iconText = "Thumbnail failure";
        formatText = resText = zctText = typeText = "";
        iconTip = loadId;
        formatTip = resTip = zctTip = typeTip = "";
        SwingUtilities.invokeLater(refresher);
      }
    }
  }

  // -- PreviewPane API methods --

  /** Closes the underlying image reader. */
  public void close() throws IOException {
    if (reader != null) {
      reader.close();
    }
  }

  // -- Helper methods --

  /**
   * Creates a blank image with the given message painted on top (e.g.,
   * a loading or error message), matching the size of the active reader's
   * thumbnails.
   */
  private BufferedImage makeImage(String message) {
    int w = reader.getThumbSizeX(), h = reader.getThumbSizeY();
    if (w < 128) w = 128;
    if (h < 32) h = 32;
    BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
    Graphics2D g = image.createGraphics();
    Rectangle2D.Float r = (Rectangle2D.Float)
      g.getFont().getStringBounds(message, g.getFontRenderContext());
    g.drawString(message, (w - r.width) / 2, (h - r.height) / 2 + r.height);
    g.dispose();
    return image;
  }

}
