package ij.io;
import ij.gui.*;
import ij.ImagePlus;
import ij.process.*;
import java.io.*;
import java.util.*;
import java.net.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;

/** This class decodes an ImageJ .roi file. 
	<p>
	Format of the original 64 byte ImageJ/NIH Image
	.roi file header. Two byte numbers are big-endian
	signed shorts. The JavaScript example at
	http://wsr.imagej.net/macros/js/DecodeRoiFile.js
	demonstrates how to use this information to 
	decode a .roi file.
	<pre>
	0-3		"Iout"
	4-5		version (>=217)
	6-7		roi type (encoded as one byte)
	8-9		top
	10-11	left
	12-13	bottom
	14-15	right
	16-17	NCoordinates
	18-33	x1,y1,x2,y2 (straight line) | x,y,width,height (double rect) | size (npoints)
	34-35	stroke width (v1.43i or later)
	36-39   ShapeRoi size (type must be 1 if this value>0)
	40-43   stroke color (v1.43i or later)
	44-47   fill color (v1.43i or later)
	48-49   subtype (v1.43k or later)
	50-51   options (v1.43k or later)
	52-52   arrow style or aspect ratio (v1.43p or later)
	53-53   arrow head size (v1.43p or later)
	54-55   rounded rect arc size (v1.43p or later)
	56-59   position
	60-63   header2 offset
	64-       x-coordinates (short), followed by y-coordinates
	<pre>
	@see <a href="http://wsr.imagej.net/macros/js/DecodeRoiFile.js">DecodeRoiFile.js</a>
*/

public class RoiDecoder {
	// offsets
	public static final int VERSION_OFFSET = 4;
	public static final int TYPE = 6;
	public static final int TOP = 8;
	public static final int LEFT = 10;
	public static final int BOTTOM = 12;
	public static final int RIGHT = 14;
	public static final int N_COORDINATES = 16;
	public static final int X1 = 18;
	public static final int Y1 = 22;
	public static final int X2 = 26;
	public static final int Y2 = 30;
	public static final int XD = 18;
	public static final int YD = 22;
	public static final int WIDTHD = 26;
	public static final int HEIGHTD = 30;
	public static final int SIZE = 18;
	public static final int STROKE_WIDTH = 34;
	public static final int SHAPE_ROI_SIZE = 36;
	public static final int STROKE_COLOR = 40;
	public static final int FILL_COLOR = 44;
	public static final int SUBTYPE = 48;
	public static final int OPTIONS = 50;
	public static final int ARROW_STYLE = 52;
	public static final int FLOAT_PARAM = 52; //ellipse ratio or rotated rect width
	public static final int POINT_TYPE= 52;
	public static final int ARROW_HEAD_SIZE = 53;
	public static final int ROUNDED_RECT_ARC_SIZE = 54;
	public static final int POSITION = 56;
	public static final int HEADER2_OFFSET = 60;
	public static final int COORDINATES = 64;
	// header2 offsets
	public static final int C_POSITION = 4;
	public static final int Z_POSITION = 8;
	public static final int T_POSITION = 12;
	public static final int NAME_OFFSET = 16;
	public static final int NAME_LENGTH = 20;
	public static final int OVERLAY_LABEL_COLOR = 24;
	public static final int OVERLAY_FONT_SIZE = 28; //short
	public static final int GROUP = 30;  //byte
	public static final int IMAGE_OPACITY = 31;  //byte
	public static final int IMAGE_SIZE = 32;  //int
	public static final int FLOAT_STROKE_WIDTH = 36;  //float
	public static final int ROI_PROPS_OFFSET = 40;
	public static final int ROI_PROPS_LENGTH = 44;
	public static final int COUNTERS_OFFSET = 48;

	// subtypes
	public static final int TEXT = 1;
	public static final int ARROW = 2;
	public static final int ELLIPSE = 3;
	public static final int IMAGE = 4;
	public static final int ROTATED_RECT = 5;
	
	// options
	public static final int SPLINE_FIT = 1;
	public static final int DOUBLE_HEADED = 2;
	public static final int OUTLINE = 4;
	public static final int OVERLAY_LABELS = 8;
	public static final int OVERLAY_NAMES = 16;
	public static final int OVERLAY_BACKGROUNDS = 32;
	public static final int OVERLAY_BOLD = 64;
	public static final int SUB_PIXEL_RESOLUTION = 128;
	public static final int DRAW_OFFSET = 256;
	public static final int ZERO_TRANSPARENT = 512;
	public static final int SHOW_LABELS = 1024;
	public static final int SCALE_LABELS = 2048;
	public static final int PROMPT_BEFORE_DELETING = 4096; //points
	public static final int SCALE_STROKE_WIDTH = 8192;
	
	// types
	private final int polygon=0, rect=1, oval=2, line=3, freeline=4, polyline=5, noRoi=6,
		freehand=7, traced=8, angle=9, point=10;
	
	private byte[] data;
	private String path;
	private InputStream is;
	private String name;
	private int size;

	/** Constructs an RoiDecoder using a file path. */
	public RoiDecoder(String path) {
		this.path = path;
	}

	/** Constructs an RoiDecoder using a byte array. */
	public RoiDecoder(byte[] bytes, String name) {
		is = new ByteArrayInputStream(bytes);	
		this.name = name;
		this.size = bytes.length;
	}

	/** Opens the Roi at the specified path. Returns null if there is an error. */
	public static Roi open(String path) {
		Roi roi = null;
		RoiDecoder rd = new RoiDecoder(path);
		try {
			roi = rd.getRoi();
		} catch (IOException e) { }
		return roi;
	}

	/** Returns the ROI. */
	public Roi getRoi() throws IOException {
		if (path!=null) {
			File f = new File(path);
			size = (int)f.length();
			if (!path.endsWith(".roi") && size>5242880)
				throw new IOException("This is not an ROI or file size>5MB)");
			name = f.getName();
			is = new FileInputStream(path);
		}
		data = new byte[size];

		int total = 0;
		while (total<size)
			total += is.read(data, total, size-total);
		is.close();
		if (getByte(0)!=73 || getByte(1)!=111)  //"Iout"
			throw new IOException("This is not an ImageJ ROI");
		int version = getShort(VERSION_OFFSET);
		int type = getByte(TYPE);
		int subtype = getShort(SUBTYPE);
		int top= getShort(TOP);
		int left = getShort(LEFT);
		int bottom = getShort(BOTTOM);
		int right = getShort(RIGHT);
		int width = right-left;
		int height = bottom-top;
		int n = getUnsignedShort(N_COORDINATES);
		if (n==0)
			n = getInt(SIZE);
		int options = getShort(OPTIONS);
		int position = getInt(POSITION);
		int hdr2Offset = getInt(HEADER2_OFFSET);
		int channel=0, slice=0, frame=0;
		int overlayLabelColor=0;
		int overlayFontSize=0;
		int group=0;
		int imageOpacity=0;
		int imageSize=0;
		boolean subPixelResolution = (options&SUB_PIXEL_RESOLUTION)!=0 &&  version>=222;
		boolean drawOffset = subPixelResolution && (options&DRAW_OFFSET)!=0;
		boolean scaleStrokeWidth = true;
		if (version>=228)
			scaleStrokeWidth = (options&SCALE_STROKE_WIDTH)!=0;
		
		boolean subPixelRect = version>=223 && subPixelResolution && (type==rect||type==oval);
		double xd=0.0, yd=0.0, widthd=0.0, heightd=0.0;
		if (subPixelRect) {
			xd = getFloat(XD);
			yd = getFloat(YD);
			widthd = getFloat(WIDTHD);
			heightd = getFloat(HEIGHTD);
		}
		
		if (hdr2Offset>0 && hdr2Offset+IMAGE_SIZE+4<=size) {
			channel = getInt(hdr2Offset+C_POSITION);
			slice = getInt(hdr2Offset+Z_POSITION);
			frame = getInt(hdr2Offset+T_POSITION);
			overlayLabelColor = getInt(hdr2Offset+OVERLAY_LABEL_COLOR);
			overlayFontSize = getShort(hdr2Offset+OVERLAY_FONT_SIZE);
			imageOpacity = getByte(hdr2Offset+IMAGE_OPACITY);
			imageSize = getInt(hdr2Offset+IMAGE_SIZE);
			group = getByte(hdr2Offset+GROUP);
		}
		
		if (name!=null && name.endsWith(".roi"))
			name = name.substring(0, name.length()-4);
		boolean isComposite = getInt(SHAPE_ROI_SIZE)>0;
		
		Roi roi = null;
		if (isComposite) {
			roi = getShapeRoi();
			if (version>=218)
				getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
			roi.setPosition(position);
			if (channel>0 || slice>0 || frame>0)
				roi.setPosition(channel, slice, frame);
			decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
			if (version>=224) {
				String props = getRoiProps();
				if (props!=null)
					roi.setProperties(props);
			}
			if (version>=228 && group>0)
				roi.setGroup(group);
			return roi;
		}

		switch (type) {
			case rect:
				if (subPixelRect)
					roi = new Roi(xd, yd, widthd, heightd);
				else
					roi = new Roi(left, top, width, height);
				int arcSize = getShort(ROUNDED_RECT_ARC_SIZE);
				if (arcSize>0)
					roi.setCornerDiameter(arcSize);
				break;
			case oval:
				if (subPixelRect)
					roi = new OvalRoi(xd, yd, widthd, heightd);
				else
					roi = new OvalRoi(left, top, width, height);
				break;
			case line:
				double x1 = getFloat(X1);		
				double y1 = getFloat(Y1);		
				double x2 = getFloat(X2);		
				double y2 = getFloat(Y2);
				if (subtype==ARROW) {
					roi = new Arrow(x1, y1, x2, y2);		
					((Arrow)roi).setDoubleHeaded((options&DOUBLE_HEADED)!=0);
					((Arrow)roi).setOutline((options&OUTLINE)!=0);
					int style = getByte(ARROW_STYLE);
					if (style>=Arrow.FILLED && style<=Arrow.BAR)
						((Arrow)roi).setStyle(style);
					int headSize = getByte(ARROW_HEAD_SIZE);
					if (headSize>=0 && style<=30)
						((Arrow)roi).setHeadSize(headSize);
				} else {
					roi = new Line(x1, y1, x2, y2);
					roi.setDrawOffset(drawOffset);
				}
				break;
			case polygon: case freehand: case traced: case polyline: case freeline: case angle: case point:
					//IJ.log("type: "+type);
					//IJ.log("n: "+n);
					//IJ.log("rect: "+left+","+top+" "+width+" "+height);
					if (n==0 || n<0) break;
					int[] x = new int[n];
					int[] y = new int[n];
					float[] xf = null;
					float[] yf = null;
					int base1 = COORDINATES;
					int base2 = base1+2*n;
					int xtmp, ytmp;
					for (int i=0; i<n; i++) {
						xtmp = getShort(base1+i*2);
						if (xtmp<0) xtmp = 0;
						ytmp = getShort(base2+i*2);
						if (ytmp<0) ytmp = 0;
						x[i] = left+xtmp;
						y[i] = top+ytmp;
					}
					if (subPixelResolution) {
						xf = new float[n];
						yf = new float[n];
						base1 = COORDINATES+4*n;
						base2 = base1+4*n;
						for (int i=0; i<n; i++) {
							xf[i] = getFloat(base1+i*4);
							yf[i] = getFloat(base2+i*4);
						}
					}
					if (type==point) {
						if (subPixelResolution)
							roi = new PointRoi(xf, yf, n);
						else
							roi = new PointRoi(x, y, n);
						if (version>=226) {
							((PointRoi)roi).setPointType(getByte(POINT_TYPE));
							((PointRoi)roi).setSize(getShort(STROKE_WIDTH));
						}
						if ((options&SHOW_LABELS)!=0 && !ij.Prefs.noPointLabels)
							((PointRoi)roi).setShowLabels(true);
						if ((options&PROMPT_BEFORE_DELETING)!=0)
							((PointRoi)roi).promptBeforeDeleting(true);
						break;
					}
					int roiType;
					if (type==polygon)
						roiType = Roi.POLYGON;
					else if (type==freehand) {
						roiType = Roi.FREEROI;
						if (subtype==ELLIPSE || subtype==ROTATED_RECT) {
							double ex1 = getFloat(X1);		
							double ey1 = getFloat(Y1);		
							double ex2 = getFloat(X2);		
							double ey2 = getFloat(Y2);
							double param = getFloat(FLOAT_PARAM);
							if (subtype==ROTATED_RECT)
								roi = new RotatedRectRoi(ex1,ey1,ex2,ey2,param);
							else
								roi = new EllipseRoi(ex1,ey1,ex2,ey2,param);
							break;
						}
					} else if (type==traced)
						roiType = Roi.TRACED_ROI;
					else if (type==polyline)
						roiType = Roi.POLYLINE;
					else if (type==freeline)
						roiType = Roi.FREELINE;
					else if (type==angle)
						roiType = Roi.ANGLE;
					else
						roiType = Roi.FREEROI;
					if (subPixelResolution) {
						roi = new PolygonRoi(xf, yf, n, roiType);
						roi.setDrawOffset(drawOffset);
					} else
						roi = new PolygonRoi(x, y, n, roiType);
					break;
			default:
				throw new IOException("Unrecognized ROI type: "+type);
		}
		if (roi==null)
			return null;
		roi.setName(getRoiName());
		
		// read stroke width, stroke color and fill color (1.43i or later)
		if (version>=218) {
			getStrokeWidthAndColor(roi, hdr2Offset, scaleStrokeWidth);
			if (type==point)
				roi.setStrokeWidth(0);
			boolean splineFit = (options&SPLINE_FIT)!=0;
			if (splineFit && roi instanceof PolygonRoi)
				((PolygonRoi)roi).fitSpline();
		}
		
		if (version>=218 && subtype==TEXT)
			roi = getTextRoi(roi, version);

		if (version>=221 && subtype==IMAGE)
			roi = getImageRoi(roi, imageOpacity, imageSize, options);

		if (version>=224) {
			String props = getRoiProps();
			if (props!=null)
				roi.setProperties(props);
		}

		if (version>=227) {
			int[] counters = getPointCounters(n);
			if (counters!=null && (roi instanceof PointRoi))
				((PointRoi)roi).setCounters(counters);
		}
		
		// set group (1.52t or later)
		if (version>=228 && group>0)
			roi.setGroup(group);

		roi.setPosition(position);
		if (channel>0 || slice>0 || frame>0)
			roi.setPosition(channel, slice, frame);
		decodeOverlayOptions(roi, version, options, overlayLabelColor, overlayFontSize);
		return roi;
	}
	
	void decodeOverlayOptions(Roi roi, int version, int options, int color, int fontSize) {
		Overlay proto = new Overlay();
		proto.drawLabels((options&OVERLAY_LABELS)!=0);
		proto.drawNames((options&OVERLAY_NAMES)!=0);
		proto.drawBackgrounds((options&OVERLAY_BACKGROUNDS)!=0);
		if (version>=220 && color!=0)
			proto.setLabelColor(new Color(color));
		boolean bold = (options&OVERLAY_BOLD)!=0;
		boolean scalable = (options&SCALE_LABELS)!=0;
		if (fontSize>0 || bold || scalable) {
			proto.setLabelFont(new Font("SansSerif", bold?Font.BOLD:Font.PLAIN, fontSize), scalable);
		}
		roi.setPrototypeOverlay(proto);
	}

	void getStrokeWidthAndColor(Roi roi, int hdr2Offset, boolean scaleStrokeWidth) {
		double strokeWidth = getShort(STROKE_WIDTH);
		if (hdr2Offset>0) {
			double strokeWidthD = getFloat(hdr2Offset+FLOAT_STROKE_WIDTH);
			if (strokeWidthD>0.0)
				strokeWidth = strokeWidthD;
		}
		if (strokeWidth>0.0) {
			if (scaleStrokeWidth)
				roi.setStrokeWidth(strokeWidth);
			else
				roi.setUnscalableStrokeWidth(strokeWidth);
		}
		int strokeColor = getInt(STROKE_COLOR);
		if (strokeColor!=0) {
			int alpha = (strokeColor>>24)&0xff;
			roi.setStrokeColor(new Color(strokeColor, alpha!=255));
		}
		int fillColor = getInt(FILL_COLOR);
		if (fillColor!=0) {
			int alpha = (fillColor>>24)&0xff;
			roi.setFillColor(new Color(fillColor, alpha!=255));
		}
	}

	public Roi getShapeRoi() throws IOException {
		int type = getByte(TYPE);
		if (type!=rect)
			throw new IllegalArgumentException("Invalid composite ROI type");
		int top= getShort(TOP);
		int left = getShort(LEFT);
		int bottom = getShort(BOTTOM);
		int right = getShort(RIGHT);
		int width = right-left;
		int height = bottom-top;
		int n = getInt(SHAPE_ROI_SIZE);

		ShapeRoi roi = null;
		float[] shapeArray = new float[n];
		int base = COORDINATES;
		for(int i=0; i<n; i++) {
			shapeArray[i] = getFloat(base);
			base += 4;
		}
		roi = new ShapeRoi(shapeArray);
		roi.setName(getRoiName());
		return roi;
	}
	
	Roi getTextRoi(Roi roi, int version) {
		Rectangle r = roi.getBounds();
		int hdrSize = RoiEncoder.HEADER_SIZE;
		int size = getInt(hdrSize);
		int styleAndJustification = getInt(hdrSize+4);
		int style = styleAndJustification&255;
		int justification = (styleAndJustification>>8) & 3;
		boolean drawStringMode = (styleAndJustification&1024)!=0;
		int nameLength = getInt(hdrSize+8);
		int textLength = getInt(hdrSize+12);
		char[] name = new char[nameLength];
		char[] text = new char[textLength];
		for (int i=0; i<nameLength; i++)
			name[i] = (char)getShort(hdrSize+16+i*2);
		for (int i=0; i<textLength; i++)
			text[i] = (char)getShort(hdrSize+16+nameLength*2+i*2);
		double angle = version>=225?getFloat(hdrSize+16+nameLength*2+textLength*2):0f;
		Font font = new Font(new String(name), style, size);
		TextRoi roi2 = null;
		if (roi.subPixelResolution()) {
			Rectangle2D fb = roi.getFloatBounds();
			roi2 = new TextRoi(fb.getX(), fb.getY(), fb.getWidth(), fb.getHeight(), new String(text), font);
		} else
			roi2 = new TextRoi(r.x, r.y, r.width, r.height, new String(text), font);
		roi2.setStrokeColor(roi.getStrokeColor());
		roi2.setFillColor(roi.getFillColor());
		roi2.setName(getRoiName());
		roi2.setJustification(justification);
		roi2.setDrawStringMode(drawStringMode);
		roi2.setAngle(angle);
		return roi2;
	}
	
	Roi getImageRoi(Roi roi, int opacity, int size, int options) {
		if (size<=0)
			return roi;
		Rectangle r = roi.getBounds();
		byte[] bytes = new byte[size];
		for (int i=0; i<size; i++)
			bytes[i] = (byte)getByte(COORDINATES+i);
		ImagePlus imp = new Opener().deserialize(bytes);
		ImageRoi roi2 = new ImageRoi(r.x, r.y, imp.getProcessor());
		roi2.setOpacity(opacity/255.0);
		if ((options&ZERO_TRANSPARENT)!=0)
			roi2.setZeroTransparent(true);
		return roi2;
	}

	String getRoiName() {
		String fileName = name;
		int hdr2Offset = getInt(HEADER2_OFFSET);
		if (hdr2Offset==0)
			return fileName;
		int offset = getInt(hdr2Offset+NAME_OFFSET);
		int length = getInt(hdr2Offset+NAME_LENGTH);
		if (offset==0 || length==0)
			return fileName;
		if (offset+length*2>size)
			return fileName;
		char[] name = new char[length];
		for (int i=0; i<length; i++)
			name[i] = (char)getShort(offset+i*2);
		return new String(name);
	}
	
	String getRoiProps() {
		int hdr2Offset = getInt(HEADER2_OFFSET);
		if (hdr2Offset==0)
			return null;
		int offset = getInt(hdr2Offset+ROI_PROPS_OFFSET);
		int length = getInt(hdr2Offset+ROI_PROPS_LENGTH);
		if (offset==0 || length==0)
			return null;
		if (offset+length*2>size)
			return null;
		char[] props = new char[length];
		for (int i=0; i<length; i++)
			props[i] = (char)getShort(offset+i*2);
		return new String(props);
	}
	
	int[] getPointCounters(int n) {
		int hdr2Offset = getInt(HEADER2_OFFSET);
		if (hdr2Offset==0)
			return null;
		int offset = getInt(hdr2Offset+COUNTERS_OFFSET);
		if (offset==0)
			return null;
		if (offset+n*4>data.length)
			return null;
		int[] counters = new int[n];
		for (int i=0; i<n; i++)
			counters[i] = getInt(offset+i*4);
		return counters;
	}


	int getByte(int base) {
		return data[base]&255;
	}

	int getShort(int base) {
		int b0 = data[base]&255;
		int b1 = data[base+1]&255;
		int n = (short)((b0<<8) + b1);
		if (n<-5000)
			n = (b0<<8) + b1; // assume n>32767 and unsigned
		return n;		
	}
	
	int getUnsignedShort(int base) {
		int b0 = data[base]&255;
		int b1 = data[base+1]&255;
		return (b0<<8) + b1;	
	}

	int getInt(int base) {
		int b0 = data[base]&255;
		int b1 = data[base+1]&255;
		int b2 = data[base+2]&255;
		int b3 = data[base+3]&255;
		return ((b0<<24) + (b1<<16) + (b2<<8) + b3);
	}

	float getFloat(int base) {
		return Float.intBitsToFloat(getInt(base));
	}
	
	/** Opens an ROI from a byte array. */
	public static Roi openFromByteArray(byte[] bytes) {
		Roi roi = null;
		if (bytes==null || bytes.length==0)
			return roi;
		try {
			RoiDecoder decoder = new RoiDecoder(bytes, null);
			roi = decoder.getRoi();
		} catch (IOException e) {
			return null;
		}
		return roi;
	}

}
