package ij.plugin.tool;
import ij.*;
import ij.plugin.frame.PlugInFrame;
import ij.process.*;
import ij.measure.*;
import ij.plugin.filter.Analyzer;
import ij.gui.*;
import ij.util.Tools;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.awt.geom.*;

/**
 * This plugin continuously displays the pixel values of the cursor and
 * its surroundings. It is usefule for examining how a filter changes the 
 * data (also during preview).
 *
 * If the Pixel Inspector Window is in the foreground, "c" with any modifier
 * keys (CTRL-C etc) copies the current data into the clipboard (tab-delimited).
 * The arrow keys nudge the position.
 *
 * Preferences (Press the Prefs button at top left):
 *
 * Radius determines the size of the window, 3x3 for radius=1, etc.
 * The Pixel Inspector window must be closed and opened to get the new
 * size.
 * Readout for grayscale 8&16 bit images can be raw, calibrated or
 * hexadecimal.
 * Readout for RGB images can ge R,G,B triples, gray value or hexadecimal.
 * For copying the data to clipboard, it can be selected whether the position
 * (x,y) is not not written, written in the first line or in the same way
 * as the header lines of the Pixel Inspector panel.
 *
 * Limitations and known problems:
 *
 * x and y coordinates are always uncalibrated pixel numbers.
 *
 * Some image operations do not update the display.
 *
 * Michael Schmid
 * Version 2007-Dec-06 - bugs fixed:
 *		did not always follow cursor
 *		nudge could make the display hang
 *		pixel value calibration was sometimes ignored
 * Version 2007-Dec-14 - supports exponential format for large/small data values
 */
public class  PixelInspectionTool extends PlugInTool {
	PixelInspector pi;

	public void mousePressed(ImagePlus imp, MouseEvent e) {
		drawOutline(imp, e);
	}

	public void mouseDragged(ImagePlus imp, MouseEvent e) {
		drawOutline(imp, e);
	}

	public void showOptionsDialog() {
		if (pi!=null) pi.showDialog();		
	}

	void drawOutline(ImagePlus imp, MouseEvent e) {
		ImageCanvas ic = imp.getCanvas();
		int x = ic.offScreenX(e.getX());
		int y = ic.offScreenY(e.getY());
		int radius = PixelInspector.radius;
		int size = radius*2+1;
		Overlay overlay = imp.getOverlay();
		if (overlay==null)
			overlay = new Overlay();
		Roi roi = null;
		int index = PixelInspector.getIndex(overlay, PixelInspector.TITLE);
		if (index>=0) {
			roi = overlay.get(index);
			Rectangle r = roi.getBounds();
			if (r.width!=size || r.height!=size) {
				overlay.remove(index);
				roi = null;
			}
			if (roi!=null)
				roi.setLocation(x-radius, y-radius);
		}
		if (roi==null) {
			roi = new Roi(x-radius, y-radius, size, size);
			roi.setName(PixelInspector.TITLE);
			roi.setStrokeColor(Color.red);
			overlay.add(roi);
		}
		imp.setOverlay(overlay);
		if (pi==null) {
			if (PixelInspector.instance!=null)
				PixelInspector.instance.close();
			pi = new PixelInspector(imp, this);
		}
		pi.update(imp, PixelInspector.POSITION_UPDATE, x, y);
	}

	public String getToolName() {
		return "Pixel Inspection Tool";
	}

	public String getToolIcon() {
		return "Cb00T3b09PT8b09xC037L2e0cL0c02L0220L20d0Pd0f2fcde2e0BccP125665210";
	}

}


class PixelInspector extends PlugInFrame
		implements ImageListener, KeyListener, MouseListener, Runnable {
	//ImageListener: listens to changes of image data
	//KeyListener: for fix/unfix key
	//MouseListener: for "Prefs" label
	//Runnable: for background thread

	/* Preferences and related */
	static final String PREFS_KEY="pixelinspector."; //key in IJ_Prefs.txt
	static int radius = (int)Prefs.get(PREFS_KEY+"radius", 3);
	private static final String LOC_KEY = "inspector.loc";
	final static int MAX_RADIUS = 10;//the largest radius possible (ImageJ can hang if too large)
	int grayDisplayType = 0;		//how to display 8-bit&16-bit grayscale pixels
	final static String[] GRAY_DISPLAY_TYPES = {"Raw","Calibrated","Hex"};
	final static int GRAY_RAW = 0, GRAY_CAL = 1, GRAY_HEX = 2;
	int rgbDisplayType = 0;			//how to display rgb pixels
	final static String[] RGB_DISPLAY_TYPES = {"R,G,B","Gray Value","Hex"};
	final static int RGB_RGB = 0, RGB_GRAY = 1, RGB_HEX = 2;
	int copyType = 0;				//what to copy to the clipboard
	final static String[] COPY_TYPES = {"Data Only","x y and Data","Header and Data"};
	final static int COPY_DATA = 0, COPY_XY = 1, COPY_HEADER = 2;
	int colorNumber = 0;			//color of the position marker in fixed mode
	final static String[] COLOR_STRINGS = {"red","orange","yellow","green","cyan","blue","magenta",};
	final static Color[] COLORS = {Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA};
	int fixKey = '!';				//the key (keycode+0x10000 or char) for fixing/unfixing the position
	final static int KEYCODE_OFFSET = 0x10000;	//we add this to keycodes to separate them from key characters
	/* current status */
	private int x0,y0;				//the current position
	int nextUpdate;					//type of next update
	final static int POSITION_UPDATE = 1, FULL_UPDATE = 2;
	static final String TITLE = "Pixel Inspector";
	static PixelInspector instance;
	PixelInspectionTool tool;

	ImageJ ij;
	ImagePlus imp;					//the ImagePlus that we listen to
	int id;					        //the image ID
	int bitDepth;                 //the image bit depth
	int digits;						//decimal fraction digits to display
	boolean expMode;				//whether to display the data in exp format
	ImageCanvas canvas;				//the canvas of imp
	Thread bgThread;				//thread for output (in the background)
	Label[] labels;					//the display fields
	//Label prefsLabel = new Label("Prefs\u2026");
	Label prefsLabel = new Label("Prefs");
	

	/* Initialization, preparing the window (panel) **/
	public PixelInspector(ImagePlus imp, PixelInspectionTool tool) {
		super("Pixel Values");
		instance = this;
		this.imp = imp;
		this.tool = tool;
		ij = IJ.getInstance();
		if (ij == null) return;		//it won't work with the ImageJ applet
		if (imp==null) {
			IJ.noImage(); return;
		}
		id = imp.getID();
		bitDepth = imp.getBitDepth();
		//setTitle("Pixels of "+imp.getTitle());
		WindowManager.addWindow(this);
		//readPreferences();
		prefsLabel.addMouseListener(this);
		addKeyListener(this);
		init();
		Point loc = Prefs.getLocation(PREFS_KEY+"loc");
		if (loc!=null)
			setLocation(loc);
		else
			GUI.centerOnImageJScreen(this);
		setResizable(false);
		show();
		toFront();
		addImageListeners();
											//thread for output in the background
		bgThread = new Thread(this, "Pixel Inspector");
		bgThread.start();
		bgThread.setPriority(Math.max(bgThread.getPriority()-3, Thread.MIN_PRIORITY));
		update(FULL_UPDATE);				//the first data display
	}

	private void init() {
		removeAll();
		int size = 2*radius+2;			   //number of columns and rows
		labels = new Label[size*size];
		for (int i=1; i<labels.length; i++) //make the labels (display fields)
			labels[i] = new Label();
		initializeLabels();					//fill the labels with spaceholders
		setLayout(new GridLayout(size, size, 0, 0));
		for (int row=0,p=0; row<size; row++) {
			for (int col=0; col<size; col++,p++) {
				if (row == 0 && col == 0)
					add(prefsLabel);
				else
					add(labels[p]);
			}
		}
		GUI.scale(this);
		pack();
	}

	public void close() {
		super.close();			   //also does WindowManager.removeWindow(this);
		Prefs.saveLocation(PREFS_KEY+"loc", getLocation());
		removeImageListeners();
		 synchronized(this) {				 //terminate the background thread
			bgThread.interrupt();
		}
		instance = null;
		tool.pi = null;
		removeOutline();
	}

	private void removeOutline() {
		Overlay overlay = imp.getOverlay();
		if (overlay==null) return;
		int index = getIndex(overlay, TITLE);
		if (index>=0) {
			overlay.remove(index);
			imp.setOverlay(overlay);
		}
	}

	private void addImageListeners() {
		imp.addImageListener(this);
		ImageWindow win = imp.getWindow();
		if (win == null) close();
		canvas = win.getCanvas();
		canvas.addKeyListener(this);
	}

	private void removeImageListeners() {
		imp.removeImageListener(this);
		canvas.removeKeyListener(this);
	}

	//ImageListener
	public void imageUpdated(ImagePlus imp) { update(FULL_UPDATE); }
	public void imageOpened(ImagePlus imp) {}
	public void imageClosed(ImagePlus imp) {}

	//KeyListener
	public void keyPressed(KeyEvent e) {
		boolean thisPanel = e.getSource() instanceof PixelInspector;
		if (thisPanel && e.getKeyCode()==KeyEvent.VK_C) { 
			copyToClipboard();
			return;
		}
		if (e.getKeyCode()==KeyEvent.VK_UP && y0 > 0) {
			y0--; update(FULL_UPDATE);
		} else if (e.getKeyCode()==KeyEvent.VK_DOWN && y0<imp.getHeight()-1) {
			y0++; update(FULL_UPDATE);
		} else if (e.getKeyCode()==KeyEvent.VK_LEFT && x0>0) {
			x0--; update(FULL_UPDATE);
		} else if (e.getKeyCode()==KeyEvent.VK_RIGHT && x0<imp.getWidth()-1) {
			x0++; update(FULL_UPDATE);
		} else if (e.getSource() instanceof Button)
			ij.keyPressed(e);  //forward other keys from the panel to ImageJ
		Overlay overlay = imp.getOverlay();
		if (overlay==null) return;
		int index = getIndex(overlay, TITLE);
		if (index>=0) {
			overlay.remove(index);
			Roi roi = new Roi(x0-radius, y0-radius, radius*2+1, radius*2+1);
			roi.setName(TITLE);
			roi.setStrokeColor(Color.red);
			overlay.add(roi);
			imp.setOverlay(overlay);	
	   }
	}

	public void mousePressed(MouseEvent e) {
		showDialog();
	}   
	public void mouseEntered(MouseEvent e) {}   
	public void mouseExited(MouseEvent e) {}   
	public void mouseClicked(MouseEvent e) {}   
	public void mouseReleased(MouseEvent e) {}   

	/** In the Overlay class in imageJ 1.46g and later. */
	static int getIndex(Overlay overlay, String name) {
		if (name==null) return -1;
		Roi[] rois = overlay.toArray();
		for (int i=rois.length-1; i>=0; i--) {
			if (name.equals(rois[i].getName()))
				return i;
		}
		return -1;
	}

	public void keyReleased(KeyEvent e) {}
	public void keyTyped(KeyEvent e) {}

	void update(ImagePlus imp, int whichUpdate, int x, int y) {
		if (imp!=this.imp) {
			removeImageListeners();
			removeOutline();
			this.imp = imp;
			addImageListeners();
			//setTitle("Pixels of "+imp.getTitle());
		}
		this.x0 = x;
		this.y0 = y;
		update(whichUpdate);
	}

	synchronized void update(int whichUpdate) {
		if (nextUpdate < whichUpdate)
			nextUpdate = whichUpdate;
		notify();		//wake up the background thread
	}

	// the background thread for updating the table
	public void run() {
		boolean doFullUpdate = false;
		while (true) {
			if (doFullUpdate) {
				setCalibration();
			}
			writeNumbers();
			IJ.wait(50);

			synchronized(this) {
				if (nextUpdate == 0) {
					try {wait();}				//notify wakes up the thread
					catch(InterruptedException e) { //interrupted tells the thread to exit
						return;
					}
				} else {
					doFullUpdate = nextUpdate == FULL_UPDATE;
					nextUpdate = 0;
				}
			}
		} //while (true)
	}

	/** get the surrounding pixels and display them */
	void writeNumbers() {
		if (imp.getID()!=id || imp.getBitDepth()!=bitDepth) {	//has the image changed?
			removeImageListeners();
			addImageListeners();
			initializeLabels();
			this.pack();
			id = imp.getID();
			bitDepth = imp.getBitDepth();
			nextUpdate = FULL_UPDATE;
			return;
		}
		ImageProcessor ip = imp.getProcessor();
		if (ip == null) return;
		int width = ip.getWidth();
		int height = ip.getHeight();
		int x0 = this.x0;		//class variables may change asynchronously, fixed values needed here
		int y0 = this.y0;
		int p = 1;	  //pointer in labels array
		for (int x = x0-radius; x <= x0+radius; x++,p++)
			labels[p].setText(x>=0&&x<width ? Integer.toString(x) : " ");
		for (int y = y0-radius; y <= y0+radius; y++) {
			boolean yInside = y>=0&&y<height;
			int yDisplay =	(Analyzer.getMeasurements() & Measurements.INVERT_Y)!=0 ? height-y-1 : y;
			labels[p].setText(yInside ? Integer.toString(yDisplay) : " ");
			p++;
			for (int x = x0-radius; x <= x0+radius; x++,p++) {
				if (x>=0&&x<width&&yInside) {
					if (ip instanceof ColorProcessor && rgbDisplayType == RGB_RGB) {
						int c = ip.getPixel(x,y);
						int r = (c&0xff0000)>>16;
						int g = (c&0xff00)>>8;
						int b = c&0xff;
						labels[p].setText(r+","+g+","+b);
					} else if (ip instanceof ColorProcessor && rgbDisplayType == RGB_HEX)
						labels[p].setText(int2hex(ip.getPixel(x,y),6));
					else if ((ip instanceof ByteProcessor || ip instanceof ShortProcessor) && grayDisplayType == GRAY_RAW)
						labels[p].setText(Integer.toString(ip.getPixel(x,y)));
					else if ((ip instanceof ByteProcessor || ip instanceof ShortProcessor) && grayDisplayType == GRAY_HEX)
						labels[p].setText(int2hex(ip.getPixel(x,y), ip instanceof ByteProcessor ? 2 : 4));
					else
						labels[p].setText(stringOf(ip.getPixelValue(x,y), digits, expMode));
				} else
					labels[p].setText(" ");
			}
		} //for y
	}

	/** initialize content of the labels to make sure we have enough space */
	void initializeLabels() {
		Color bgColor = new Color(0xcccccc);	//background for row/column header
		String placeHolder = "000000.00";		//how much space to reserve (enough for float, calibrated, rgb)
		ImageProcessor ip = imp.getProcessor();
		if (ip instanceof ByteProcessor && grayDisplayType==GRAY_RAW) {
			placeHolder = "000";
		} else if (ip instanceof ByteProcessor || ip instanceof ShortProcessor) {
			if (grayDisplayType == GRAY_RAW || grayDisplayType == GRAY_HEX)
				placeHolder = "00000";			//minimum space, needed for header (max 99k pixels)
		} else if (ip instanceof ColorProcessor) {
			if (rgbDisplayType == RGB_RGB)
				placeHolder = "000,000,000";
			if (rgbDisplayType == RGB_GRAY)
				placeHolder = "000.00";
			else if (rgbDisplayType == RGB_HEX)
				placeHolder = "CCCCCC";
		}
		if (placeHolder.length()<5 && (ip.getWidth()>9999 || ip.getHeight()>9999))
			placeHolder = "00000";
		if (placeHolder.length()<4 && (ip.getWidth()>999 || ip.getHeight()>999))
			placeHolder = "0000";
		int p = 0;								//pointer in labels array
		int size = 2*radius+1;
		for (int y = 0; y<size+1; y++) {		//header line and data lines
			if (y > 0)							//no label in top-left corner
				labels[p].setText(placeHolder);
			p++;
			for (int x = 0; x<size; x++,p++)
				labels[p].setText(placeHolder);
		}
		labels[radius+1].setForeground(Color.RED); //write current position in red
		labels[(2*radius+2)*(radius+1)].setForeground(Color.RED);
		labels[(2*radius+2)*(radius+1)+radius+1].setForeground(Color.RED);
		for (int i=0; i<size; i++) {			//header lines have a darker background
			labels[i+1].setBackground(bgColor);
			labels[(2*radius+2)*(i+1)].setBackground(bgColor);		
		}
		for (int i=1; i<labels.length; i++)
			labels[i].setAlignment(Label.RIGHT);
	}

	/* set the pixel value calibration of the ImageProcessor and the output format */
	void setCalibration() {
		Calibration cal = imp.getCalibration();
		float[] cTable = cal.getFunction()==Calibration.NONE ? null : cal.getCTable();
		ImageProcessor ip = imp.getProcessor();
		if (ip != null) ip.setCalibrationTable(cTable);
		if (ip instanceof FloatProcessor || cTable != null) {
			float[] data = (ip instanceof FloatProcessor) ? (float[])ip.getPixels() : cTable;
			double[] minmax = Tools.getMinMax(data);
			double maxDataValue = Math.max(Math.abs(minmax[0]), Math.abs(minmax[1]));
			digits = (int)(6-Math.log(maxDataValue)/Math.log(10));
			if (maxDataValue==0.0)
				digits = 6;
			expMode = digits<-1 || digits>7;
			if (Math.min(minmax[0], minmax[1]) < 0)
				digits--; //more space needed for minus sign
		} else {
			digits = 2;
			expMode = false;
		}
	}

	/** Converts a number to a string in decimal or exp format.
	 *	The number of digits is chosen to make the value fit into
	 *	a cell the size of "000000.00"
	 */
	String stringOf(float v, int digits, boolean expMode) {
		if (expMode) {
			int exp = (int)Math.floor(Math.log(Math.abs(v))/Math.log(10));
			double mant = v/Math.pow(10,exp);
			digits = (exp > 0 && exp < 10) ? 5 : 4;
			if (v<0) digits--;		//space needed for minus
			return IJ.d2s(mant,digits)+"e"+exp;
		} else
			return IJ.d2s(v, digits);
	}

	void copyToClipboard() {
		final char delim = '\t';
		int size = 2*radius+1;
		int p = 1;
		StringBuffer sb = new StringBuffer();
		if (copyType == COPY_XY) {
			sb.append(labels[radius+1].getText()); sb.append(delim);
			sb.append(labels[(2*radius+2)*(radius+1)].getText()); sb.append('\n');
		} else if (copyType == COPY_HEADER) {
			for (int x=0; x<size; x++,p++) {
				sb.append(delim);
				sb.append(labels[p].getText());
			}
			sb.append('\n');
		}
		p = size + 1;
		for (int y=0; y<size; y++) {
			if (copyType == COPY_HEADER) {
				sb.append(labels[p].getText()); sb.append(delim);
			}
			p++;
			for (int x=0; x<size; x++,p++) {
				if (x > 0)
					sb.append(delim);
				sb.append(labels[p].getText());
			}
			sb.append('\n');
		}
		String s = new String(sb);
		Clipboard clip = getToolkit().getSystemClipboard();
		if (clip==null) return;
		StringSelection contents = new StringSelection(s);
		clip.setContents(contents, contents);
		IJ.showStatus(size*size+" pixel values copied to clipboard");
	}

	/** Preferences dialog */
	void showDialog() {
		GenericDialog gd = new GenericDialog("Pixel Inspector Prefs...");
		gd.addNumericField("Radius:", radius, 0, 6, "(1-"+MAX_RADIUS+")");
		gd.addChoice("Grayscale readout:",GRAY_DISPLAY_TYPES,GRAY_DISPLAY_TYPES[grayDisplayType]);
		gd.addChoice("RGB readout:",RGB_DISPLAY_TYPES,RGB_DISPLAY_TYPES[rgbDisplayType]);
		gd.addChoice("Copy to clipboard:", COPY_TYPES, COPY_TYPES[copyType]);
		gd.addMessage("Use arrow keys to move red outline.\nPress 'c' to copy data to clipboard.", null, Color.darkGray);
		Point loc = Prefs.getLocation(LOC_KEY);
		if (loc!=null) {
			gd.centerDialog(false);
			gd.setLocation (loc);
		}
		gd.showDialog();
		if (gd.wasCanceled())
			return;
		radius = (int)gd.getNextNumber();
		if (radius<1) radius=1;
		if (radius>MAX_RADIUS) radius=MAX_RADIUS;
		grayDisplayType = gd.getNextChoiceIndex();
		rgbDisplayType = gd.getNextChoiceIndex();
		copyType = gd.getNextChoiceIndex();
		boolean keyOK = false;
		init();
		update(POSITION_UPDATE);
		Prefs.set(PREFS_KEY+"radius", radius);
		Prefs.saveLocation(LOC_KEY, gd.getLocation());
	}

	static String int2hex(int i, int digits) {
		boolean addHexSign = digits<6;
		char[] buf = new char[addHexSign ? digits+1 : digits];
		for (int pos=buf.length-1; pos>=buf.length-digits; pos--) {
			buf[pos] = Tools.hexDigits[i&0xf];
			i >>>= 4;
			if (addHexSign) buf[0] = 'x';
		}
		return new String(buf);
	}
}
