/*
 * #%L
 * SCIFIO library for reading and converting scientific file formats.
 * %%
 * Copyright (C) 2011 - 2020 SCIFIO developers.
 * %%
 * 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 io.scif.gui;

import io.scif.BufferedImagePlane;
import io.scif.FormatException;
import io.scif.Plane;
import io.scif.Reader;
import io.scif.SCIFIOService;
import io.scif.Writer;
import io.scif.services.FormatService;
import io.scif.services.InitializeService;
import io.scif.util.FormatTools;

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
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.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.ProgressMonitor;
import javax.swing.SwingConstants;
import javax.swing.border.BevelBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.scijava.Context;
import org.scijava.io.location.FileLocation;
import org.scijava.io.location.Location;
import org.scijava.log.LogService;
import org.scijava.plugin.Parameter;
import org.scijava.service.SciJavaService;

/**
 * A basic renderer for image data.
 *
 * @author Curtis Rueden
 * @author Mark Hiner
 */
public class ImageViewer extends JFrame implements ActionListener,
	ChangeListener, KeyListener, MouseMotionListener, Runnable, WindowListener
{

	// -- Constants --

	@Parameter
	private Context context;

	@Parameter
	private LogService logService;

	@Parameter
	private FormatService formatService;

	@Parameter
	private InitializeService initializeService;

	@Parameter
	private GUIService guiService;

	private static final String TITLE = "SCIFIO Viewer";

	private static final char ANIMATION_KEY = ' ';

	// -- Fields --

	/** Current format reader. */
	private Reader myReader;

	/** Current format writer. */
	private Writer myWriter;

	private final JPanel pane;

	private final ImageIcon icon;

	private final JLabel iconLabel;

	private final JPanel sliderPanel;

	private final JSlider nSlider;

	private final JLabel probeLabel;

	private final JMenuItem fileView, fileSave;

	private String filename;

	private BufferedImage[] images;

	private boolean anim = false;

	private int fps = 10;

	private boolean canCloseReader = true;

	// -- Constructor --

	/** Constructs an image viewer. */
	public ImageViewer(final Context context) {
		super(TITLE);
		context.inject(this);
		setDefaultCloseOperation(DISPOSE_ON_CLOSE);
		addWindowListener(this);

		// content pane
		pane = new JPanel();
		pane.setLayout(new BorderLayout());
		setContentPane(pane);
		setSize(350, 350); // default size

		// navigation sliders
		sliderPanel = new JPanel();
		sliderPanel.setVisible(false);
		sliderPanel.setBorder(new EmptyBorder(5, 3, 5, 3));
		sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.Y_AXIS));
		pane.add(BorderLayout.SOUTH, sliderPanel);

		final JPanel nPanel = new JPanel();
		nPanel.setLayout(new BoxLayout(nPanel, BoxLayout.X_AXIS));
		sliderPanel.add(nPanel);
		sliderPanel.add(Box.createVerticalStrut(2));

		nSlider = new JSlider(1, 1);
		nSlider.setEnabled(false);
		nSlider.addChangeListener(this);
		nPanel.add(new JLabel("N"));
		nPanel.add(Box.createHorizontalStrut(3));
		nPanel.add(nSlider);

		final JPanel ztcPanel = new JPanel();
		ztcPanel.setLayout(new BoxLayout(ztcPanel, BoxLayout.X_AXIS));
		sliderPanel.add(ztcPanel);

		// image icon
		final BufferedImage dummy = AWTImageTools.makeImage(new byte[1][1], 1, 1,
			false);
		icon = new ImageIcon(dummy);
		iconLabel = new JLabel(icon, SwingConstants.LEFT);
		iconLabel.setVerticalAlignment(SwingConstants.TOP);
		pane.add(new JScrollPane(iconLabel));

		// cursor probe
		probeLabel = new JLabel(" ");
		probeLabel.setHorizontalAlignment(SwingConstants.CENTER);
		probeLabel.setBorder(new BevelBorder(BevelBorder.RAISED));
		pane.add(BorderLayout.NORTH, probeLabel);
		iconLabel.addMouseMotionListener(this);

		// menu bar
		final JMenuBar menubar = new JMenuBar();
		// FIXME: currently the menu bar is disabled to restrict the use of
		// ImageViewer to the Show command. We could attempt to get this
		// implementation working nicely, or just convert to an IJ2
		// implementation.
//		setJMenuBar(menubar);

		final JMenu file = new JMenu("File");
		file.setMnemonic('f');
		menubar.add(file);
		final JMenuItem fileOpen = new JMenuItem("Open...");
		fileOpen.setMnemonic('o');
		fileOpen.setActionCommand("open");
		fileOpen.addActionListener(this);
		file.add(fileOpen);
		fileSave = new JMenuItem("Save...");
		fileSave.setMnemonic('s');
		fileSave.setEnabled(false);
		fileSave.setActionCommand("save");
		fileSave.addActionListener(this);
		file.add(fileSave);
		fileView = new JMenuItem("View Metadata...");
		final JMenuItem fileExit = new JMenuItem("Exit");
		fileExit.setMnemonic('x');
		fileExit.setActionCommand("exit");
		fileExit.addActionListener(this);
		file.add(fileExit);

		final JMenu options = new JMenu("Options");
		options.setMnemonic('p');
		menubar.add(options);
		final JMenuItem optionsFPS = new JMenuItem("Frames per Second...");
		optionsFPS.setMnemonic('f');
		optionsFPS.setActionCommand("fps");
		optionsFPS.addActionListener(this);
		options.add(optionsFPS);

		final JMenu help = new JMenu("Help");
		help.setMnemonic('h');
		menubar.add(help);
		final JMenuItem helpAbout = new JMenuItem("About...");
		helpAbout.setMnemonic('a');
		helpAbout.setActionCommand("about");
		helpAbout.addActionListener(this);
		help.add(helpAbout);

		// add key listener to focusable components
		nSlider.addKeyListener(this);
	}

	/**
	 * Constructs an image viewer.
	 *
	 * @param canCloseReader whether or not the underlying reader can be closed
	 */
	public ImageViewer(final Context context, final boolean canCloseReader) {
		this(context);
		this.canCloseReader = canCloseReader;
	}

	/**
	 * Convenience overload of {@link #open(Location)} for backwards
	 * compatibility.
	 *
	 * @param id
	 */
	public void open(final String id) {
		open(new FileLocation(id));
	}

	/** Opens the given data source using the current format reader. */
	public void open(final Location id) {
		wait(true);
		try {
			canCloseReader = true;
			myReader = initializeService.initializeReader(id);
			final long planeCount = myReader.getMetadata().get(0).getPlaneCount();
			final ProgressMonitor progress = new ProgressMonitor(this, "Reading " +
				id, null, 0, 1);
			progress.setProgress(1);
			final BufferedImage[] img = new BufferedImage[(int) planeCount];
			for (long planeIndex = 0; planeIndex < planeCount; planeIndex++) {
				if (progress.isCanceled()) break;
				final Plane plane = myReader.openPlane(0, planeIndex);
				img[(int) planeIndex] = AWTImageTools.openImage(plane, myReader, 0);
			}
			progress.setProgress(2);
			setImages(myReader, img);
			myReader.close(true);
		}
		catch (final FormatException exc) {
			logService.info("", exc);
			wait(false);
			return;
		}
		catch (final IOException exc) {
			logService.info("", exc);
			wait(false);
			return;
		}
		wait(false);
	}

	/**
	 * Convenience overload of {@link #save}, saves the current image to the given
	 * local file destination.
	 *
	 * @param id
	 */
	public void save(final String id) {
		save(new FileLocation(id));
	}

	/**
	 * Saves the current images to the given destination using the current format
	 * writer.
	 */
	public void save(final Location id) {
		if (images == null) return;
		wait(true);
		try {
			myWriter.setDest(id);
			final boolean stack = myWriter.canDoStacks();
			final ProgressMonitor progress = new ProgressMonitor(this, "Saving " + id,
				null, 0, stack ? images.length : 1);
			if (stack) {
				// save entire stack
				for (int i = 0; i < images.length; i++) {
					progress.setProgress(i);
					final boolean canceled = progress.isCanceled();
					myWriter.savePlane(0, i, getPlane(images[i]));
					if (canceled) break;
				}
				progress.setProgress(images.length);
			}
			else {
				// save current image only
				myWriter.savePlane(0, 0, getPlane(getImage()));
				progress.setProgress(1);
			}
			myWriter.close();
		}
		catch (FormatException | IOException exc) {
			logService.info("", exc);
		}
		wait(false);
	}

	/** Sets the viewer to display the given images. */
	public void setImages(final BufferedImage[] img) {
		setImages(null, img);
	}

	/**
	 * Sets the viewer to display the given images, obtaining corresponding core
	 * metadata from the specified format reader.
	 */
	public void setImages(final Reader reader, final BufferedImage[] img) {
		filename = reader == null ? null : reader.getCurrentLocation().getName();
		myReader = reader;
		images = img;

		fileView.setEnabled(true);

		fileSave.setEnabled(true);
		nSlider.removeChangeListener(this);
		nSlider.setValue(1);
		nSlider.setMaximum(images.length);
		nSlider.setEnabled(images.length > 1);
		nSlider.addChangeListener(this);
		sliderPanel.setVisible(images.length > 1);

		updateLabel(-1, -1);
		sb.setLength(0);
		if (filename != null) {
			sb.append(reader.getCurrentLocation().getName());
			sb.append(" ");
		}
		final String format = reader == null ? null : reader.getFormat()
			.getFormatName();
		if (format != null) {
			sb.append("(");
			sb.append(format);
			sb.append(")");
			sb.append(" ");
		}
		if (filename != null || format != null) sb.append("- ");
		sb.append(TITLE);
		setTitle(sb.toString());
		if (images != null) icon.setImage(images[0]);
		pack();
	}

	/** Gets the currently displayed image. */
	public BufferedImage getImage() {
		final int ndx = getPlaneIndex();
		return images == null || ndx >= images.length ? null : images[ndx];
	}

	public Plane getPlane(final BufferedImage image) {
		final BufferedImagePlane plane = new BufferedImagePlane();
		plane.setData(image);
		return plane;
	}

	/** Gets the index of the currently displayed image. */
	public int getPlaneIndex() {
		return nSlider.getValue() - 1;
	}

	// -- Window API methods --
	@Override
	public void setVisible(final boolean visible) {
		super.setVisible(visible);
		// kick off animation thread
		new Thread(this).start();
	}

	// -- ActionListener API methods --

	/** Handles menu commands. */
	@Override
	public void actionPerformed(final ActionEvent e) {
		final String cmd = e.getActionCommand();
		if ("open".equals(cmd)) {
			wait(true);
			final JFileChooser chooser = guiService.buildFileChooser(formatService
				.getAllFormats());
			wait(false);
			final int rval = chooser.showOpenDialog(this);
			if (rval == JFileChooser.APPROVE_OPTION) {
				final File file = chooser.getSelectedFile();
				if (file != null) open(file.getAbsolutePath(), myReader);
			}
		}
		else if ("save".equals(cmd)) {
			wait(true);
			final JFileChooser chooser = guiService.buildFileChooser(formatService
				.getOutputFormats());
			wait(false);
			final int rval = chooser.showSaveDialog(this);
			if (rval == JFileChooser.APPROVE_OPTION) {
				if (myWriter != null) {
					try {
						myWriter.close();
					}
					catch (final IOException e1) {
						logService.error(e1);
					}
				}
				final File file = chooser.getSelectedFile();
				try {
					myWriter = initializeService.initializeWriter(myReader.getMetadata(),
						new FileLocation(file));
				}
				catch (FormatException | IOException e1) {
					logService.error(e);
				}
				if (file != null) save(file.getAbsolutePath(), myWriter);
			}
		}
		else if ("exit".equals(cmd)) dispose();
		else if ("fps".equals(cmd)) {
			// HACK - JOptionPane prevents shutdown on dispose
			setDefaultCloseOperation(EXIT_ON_CLOSE);

			final String result = JOptionPane.showInputDialog(this,
				"Animate using space bar. How many frames per second?", "" + fps);
			try {
				fps = Integer.parseInt(result);
			}
			catch (final NumberFormatException exc) {
				logService.debug("Could not parse fps " + fps, exc);
			}
		}
		else if ("about".equals(cmd)) {
			// HACK - JOptionPane prevents shutdown on dispose
			setDefaultCloseOperation(EXIT_ON_CLOSE);

			final String msg = "<html>" + "SCIFIO core for reading and " +
				"converting file formats." + "<br>Copyright (C) 2005 - 2013" +
				" Open Microscopy Environment:" + "<ul>" +
				"<li>Board of Regents of the University of Wisconsin-Madison</li>" +
				"<li>Glencoe Software, Inc.</li>" + "<li>University of Dundee</li>" +
				"</ul>" + "<br><br>See <a href=\"" +
				"http://loci.wisc.edu/software/scifio\">" +
				"http://loci.wisc.edu/software/scifio</a>" +
				"<br>for help with using SCIFIO.";
			JOptionPane.showMessageDialog(null, msg, "SCIFIO",
				JOptionPane.INFORMATION_MESSAGE);
		}
	}

	// -- ChangeListener API methods --

	/** Handles slider events. */
	@Override
	public void stateChanged(final ChangeEvent e) {
		final boolean outOfBounds = false;
		updateLabel(-1, -1);
		final BufferedImage image = outOfBounds ? null : getImage();
		if (image == null) {
			iconLabel.setIcon(null);
			iconLabel.setText("No image plane");
		}
		else {
			icon.setImage(image);
			iconLabel.setIcon(icon);
			iconLabel.setText(null);
		}
	}

	// -- KeyListener API methods --

	/** Handles key presses. */
	@Override
	public void keyPressed(final KeyEvent e) {
		if (e.getKeyChar() == ANIMATION_KEY) anim = !anim; // toggle animation
	}

	@Override
	public void keyReleased(final KeyEvent e) {}

	@Override
	public void keyTyped(final KeyEvent e) {}

	// -- MouseMotionListener API methods --

	/** Handles cursor probes. */
	@Override
	public void mouseDragged(final MouseEvent e) {
		updateLabel(e.getX(), e.getY());
	}

	/** Handles cursor probes. */
	@Override
	public void mouseMoved(final MouseEvent e) {
		updateLabel(e.getX(), e.getY());
	}

	// -- Runnable API methods --

	/** Handles animation. */
	@Override
	public void run() {
		while (isVisible()) {
			try {
				Thread.sleep(1000 / fps);
			}
			catch (final InterruptedException exc) {
				logService.debug("", exc);
			}
		}
	}

	// -- WindowListener API methods --

	@Override
	public void windowClosing(final WindowEvent e) {}

	@Override
	public void windowActivated(final WindowEvent e) {}

	@Override
	public void windowDeactivated(final WindowEvent e) {}

	@Override
	public void windowOpened(final WindowEvent e) {}

	@Override
	public void windowIconified(final WindowEvent e) {}

	@Override
	public void windowDeiconified(final WindowEvent e) {}

	@Override
	public void windowClosed(final WindowEvent e) {
		try {
			if (myWriter != null) {
				myWriter.close();
			}
			if (canCloseReader && myReader != null) {
				myReader.close();
			}
		}
		catch (final IOException io) {}
	}

	// -- Helper methods --

	private final StringBuffer sb = new StringBuffer();

	/** Updates cursor probe label. */
	protected void updateLabel(int x, int y) {
		if (images == null) return;
		final int ndx = getPlaneIndex();
		sb.setLength(0);
		if (images.length > 1) {
			sb.append("N=");
			sb.append(ndx + 1);
			sb.append("/");
			sb.append(images.length);
		}
		final BufferedImage image = images[ndx];
		final int w = image == null ? -1 : image.getWidth();
		final int h = image == null ? -1 : image.getHeight();
		if (x >= w) x = w - 1;
		if (y >= h) y = h - 1;
		if (x >= 0 && y >= 0) {
			if (images.length > 1) sb.append("; ");
			sb.append("X=");
			sb.append(x);
			if (w > 0) {
				sb.append("/");
				sb.append(w);
			}
			sb.append("; Y=");
			sb.append(y);
			if (h > 0) {
				sb.append("/");
				sb.append(h);
			}
			if (image != null) {
				final Raster r = image.getRaster();
				final double[] pix = r.getPixel(x, y, (double[]) null);
				sb.append("; value");
				sb.append(pix.length > 1 ? "s=(" : "=");
				for (int i = 0; i < pix.length; i++) {
					if (i > 0) sb.append(", ");
					sb.append(pix[i]);
				}
				if (pix.length > 1) sb.append(")");
				sb.append("; type=");
				final int pixelType = AWTImageTools.getPixelType(image);
				sb.append(FormatTools.getPixelTypeString(pixelType));
			}
		}
		sb.append(" ");
		probeLabel.setText(sb.toString());
	}

	/** Toggles wait cursor. */
	protected void wait(final boolean wait) {
		setCursor(wait ? Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) : null);
	}

	/**
	 * Opens from the given data source using the specified reader in a separate
	 * thread.
	 */
	protected void open(final String id, final Reader r) {
		new Thread("ImageViewer-Opener") {

			@Override
			public void run() {
				try {
					myReader.close();
				}
				catch (final IOException exc) {
					logService.info("", exc);
				}
				myReader = r;
				open(id);
			}
		}.start();
	}

	/**
	 * Saves to the given data destination using the specified writer in a
	 * separate thread.
	 */
	protected void save(final String id, final Writer w) {
		new Thread("ImageViewer-Saver") {

			@Override
			public void run() {
				try {
					myWriter.close();
				}
				catch (final IOException exc) {
					logService.info("", exc);
				}
				myWriter = w;
				save(id);
			}
		}.start();
	}
}