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


/** Saves an ROI to a file or stream. RoiDecoder.java has a description of the file format.
    @see ij.io.RoiDecoder
    @see ij.plugin.RoiReader
*/
public class RoiEncoder {
    static final int HEADER_SIZE = 64;
    static final int HEADER2_SIZE = 64;
    static final int VERSION = 228; // v1.52t (roi groups, scale stroke width)
    private String path;
    private OutputStream f;
    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 roiName;
    private int roiNameSize;
    private String roiProps;
    private int roiPropsSize;
    private int countersSize;
    private int[] counters;

    
    /** Creates an RoiEncoder using the specified path. */
    public RoiEncoder(String path) {
        this.path = path;
    }

    /** Creates an RoiEncoder using the specified OutputStream. */
    public RoiEncoder(OutputStream f) {
        this.f = f;
    }
    
    /** Saves the specified ROI as a file, returning 'true' if successful. */
    public static boolean save(Roi roi, String path) {
        RoiEncoder re = new RoiEncoder(path);
        try {
            re.write(roi);
        } catch (IOException e) {
            return false;
        }
        return true;
    }

    /** Save the Roi to the file of stream. */
    public void write(Roi roi) throws IOException {
        if (f!=null) {
            write(roi, f);
        } else {
            f = new FileOutputStream(path);
            write(roi, f);
            f.close();
        }
    }
    
    /** Saves the specified ROI as a byte array. */
    public static byte[] saveAsByteArray(Roi roi) {
        if (roi==null) return null;
        byte[] bytes = null;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
            RoiEncoder encoder = new RoiEncoder(out);
            encoder.write(roi);
            out.close();
            bytes = out.toByteArray(); 
        } catch (IOException e) {
            return null;
        }
        return bytes;
    }

    void write(Roi roi, OutputStream f) throws IOException {
        Rectangle r = roi.getBounds();
        if (r.width>65535||r.height>65535||r.x>65535||r.y>65535)
            roi.enableSubPixelResolution();
        int roiType = roi.getType();
        int type = rect;
        int options = 0;
        if (roi.getScaleStrokeWidth())
            options |= RoiDecoder.SCALE_STROKE_WIDTH;
        roiName = roi.getName();
        if (roiName!=null)
            roiNameSize = roiName.length()*2;
        else
            roiNameSize = 0;
        
        roiProps = roi.getProperties();
        if (roiProps!=null)
            roiPropsSize = roiProps.length()*2;
        else
            roiPropsSize = 0;

        switch (roiType) {
            case Roi.POLYGON: type=polygon; break;
            case Roi.FREEROI: type=freehand; break;
            case Roi.TRACED_ROI: type=traced; break;
            case Roi.OVAL: type=oval; break;
            case Roi.LINE: type=line; break;
            case Roi.POLYLINE: type=polyline; break;
            case Roi.FREELINE: type=freeline; break;
            case Roi.ANGLE: type=angle; break;
            case Roi.COMPOSITE: type=rect; break; // shape array size (36-39) will be >0 to indicate composite type
            case Roi.POINT: type=point; break;
            default: type = rect; break;
        }
        
        if (roiType==Roi.COMPOSITE) {
            saveShapeRoi(roi, type, f, options);
            return;
        }

        int n=0;
        int[] x=null, y=null;
        float[] xf=null, yf=null;
        int floatSize = 0;
        if (roi instanceof PolygonRoi) {
            PolygonRoi proi = (PolygonRoi)roi;
            Polygon p = proi.getNonSplineCoordinates();
            n = p.npoints;
            x = p.xpoints;
            y = p.ypoints;
            if (roi.subPixelResolution()) {
                FloatPolygon fp = null;
                if (proi.isSplineFit())
                    fp = proi.getNonSplineFloatPolygon();
                else
                    fp = roi.getFloatPolygon();
                if (n==fp.npoints) {
                    options |= RoiDecoder.SUB_PIXEL_RESOLUTION;
                    if (roi.getDrawOffset())
                        options |= RoiDecoder.DRAW_OFFSET;
                    xf = fp.xpoints;
                    yf = fp.ypoints;
                    floatSize = n*8;
                }
            }
        }
        
        countersSize = 0;
        if (roi instanceof PointRoi) {
            counters = ((PointRoi)roi).getCounters();
            if (counters!=null && counters.length>=n)
                countersSize = n*4;
        }
        
        data = new byte[HEADER_SIZE+HEADER2_SIZE+n*4+floatSize+roiNameSize+roiPropsSize+countersSize];
        data[0]=73; data[1]=111; data[2]=117; data[3]=116; // "Iout"
        putShort(RoiDecoder.VERSION_OFFSET, VERSION);
        data[RoiDecoder.TYPE] = (byte)type;
        putShort(RoiDecoder.TOP, r.y);
        putShort(RoiDecoder.LEFT, r.x);
        putShort(RoiDecoder.BOTTOM, r.y+r.height);
        putShort(RoiDecoder.RIGHT, r.x+r.width);    
        if (roi.subPixelResolution() && (type==rect||type==oval)) {
            FloatPolygon p = null;
            if (roi instanceof OvalRoi)
                p = ((OvalRoi)roi).getFloatPolygon4();
            else {
                int d = roi.getCornerDiameter();
                if (d>0) {
                    roi.setCornerDiameter(0);
                    p = roi.getFloatPolygon();
                    roi.setCornerDiameter(d);
                } else
                    p = roi.getFloatPolygon();
            }
            if (p.npoints==4) {
                putFloat(RoiDecoder.XD, p.xpoints[0]);
                putFloat(RoiDecoder.YD, p.ypoints[0]);
                putFloat(RoiDecoder.WIDTHD, p.xpoints[1]-p.xpoints[0]);
                putFloat(RoiDecoder.HEIGHTD, p.ypoints[2]-p.ypoints[1]);    
                options |= RoiDecoder.SUB_PIXEL_RESOLUTION;
                putShort(RoiDecoder.OPTIONS, options);
            }
        }
        if (n>65535 && type!=point) {
            if (type==polygon || type==freehand || type==traced) {
                String name = roi.getName();
                roi = new ShapeRoi(roi);
                if (name!=null) roi.setName(name);
                saveShapeRoi(roi, rect, f, options);
                return;
            }
            ij.IJ.beep();
            ij.IJ.log("Non-polygonal selections with more than 65k points cannot be saved.");
            n = 65535;
        }
        if (type==point && n>65535)
            putInt(RoiDecoder.SIZE, n);
        else 
            putShort(RoiDecoder.N_COORDINATES, n);
        putInt(RoiDecoder.POSITION, roi.getPosition());
        
        if (type==rect) {
            int arcSize = roi.getCornerDiameter();
            if (arcSize>0)
                putShort(RoiDecoder.ROUNDED_RECT_ARC_SIZE, arcSize);
        }
        
        if (roi instanceof Line) {
            Line line = (Line)roi;
            putFloat(RoiDecoder.X1, (float)line.x1d);
            putFloat(RoiDecoder.Y1, (float)line.y1d);
            putFloat(RoiDecoder.X2, (float)line.x2d);
            putFloat(RoiDecoder.Y2, (float)line.y2d);
            if (roi instanceof Arrow) {
                putShort(RoiDecoder.SUBTYPE, RoiDecoder.ARROW);
                if (((Arrow)roi).getDoubleHeaded())
                    options |= RoiDecoder.DOUBLE_HEADED;
                if (((Arrow)roi).getOutline())
                    options |= RoiDecoder.OUTLINE;
                putShort(RoiDecoder.OPTIONS, options);
                putByte(RoiDecoder.ARROW_STYLE, ((Arrow)roi).getStyle());
                putByte(RoiDecoder.ARROW_HEAD_SIZE, (int)((Arrow)roi).getHeadSize());
            } else {
                if (roi.getDrawOffset())
                    options |= RoiDecoder.SUB_PIXEL_RESOLUTION+RoiDecoder.DRAW_OFFSET;
            }
        }
        
        if (roi instanceof PointRoi) {
            PointRoi point = (PointRoi)roi;
            putByte(RoiDecoder.POINT_TYPE, point.getPointType());
            putShort(RoiDecoder.STROKE_WIDTH, point.getSize());
            if (point.getShowLabels())
                options |= RoiDecoder.SHOW_LABELS;
            if (point.promptBeforeDeleting())
                options |= RoiDecoder.PROMPT_BEFORE_DELETING;
        }

        if (roi instanceof RotatedRectRoi || roi instanceof EllipseRoi) {
            double[] p = null;
            if (roi instanceof RotatedRectRoi) {
                putShort(RoiDecoder.SUBTYPE, RoiDecoder.ROTATED_RECT);
                p = ((RotatedRectRoi)roi).getParams();
            } else {
                putShort(RoiDecoder.SUBTYPE, RoiDecoder.ELLIPSE);
                p = ((EllipseRoi)roi).getParams();
            }
            putFloat(RoiDecoder.X1, (float)p[0]);
            putFloat(RoiDecoder.Y1, (float)p[1]);
            putFloat(RoiDecoder.X2, (float)p[2]);
            putFloat(RoiDecoder.Y2, (float)p[3]);
            putFloat(RoiDecoder.FLOAT_PARAM, (float)p[4]);
        }
                
        // save stroke width, stroke color and fill color (1.43i or later)
        if (VERSION>=218) {
            saveStrokeWidthAndColor(roi);
            if ((roi instanceof PolygonRoi) && ((PolygonRoi)roi).isSplineFit()) {
                options |= RoiDecoder.SPLINE_FIT;
                putShort(RoiDecoder.OPTIONS, options);
            }
        }
        
        if (n==0 && roi instanceof TextRoi)
            saveTextRoi((TextRoi)roi);
        else if (n==0 && roi instanceof ImageRoi)
            options = saveImageRoi((ImageRoi)roi, options);
        else
            putHeader2(roi, HEADER_SIZE+n*4+floatSize);
            
        if (n>0) {
            int base1 = 64;
            int base2 = base1+2*n;
            for (int i=0; i<n; i++) {
                putShort(base1+i*2, x[i]);
                putShort(base2+i*2, y[i]);
            }
            if (xf!=null) {
                base1 = 64+4*n;
                base2 = base1+4*n;
                for (int i=0; i<n; i++) {
                    putFloat(base1+i*4, xf[i]);
                    putFloat(base2+i*4, yf[i]);
                }
            }
        }
        
        saveOverlayOptions(roi, options);
        f.write(data);
    }

    void saveStrokeWidthAndColor(Roi roi) {
        BasicStroke stroke = roi.getStroke();
        if (stroke!=null)
            putShort(RoiDecoder.STROKE_WIDTH, (int)stroke.getLineWidth());
        Color strokeColor = roi.getStrokeColor();
        if (strokeColor!=null)
            putInt(RoiDecoder.STROKE_COLOR, strokeColor.getRGB());
        Color fillColor = roi.getFillColor();
        if (fillColor!=null)
            putInt(RoiDecoder.FILL_COLOR, fillColor.getRGB());
    }

    void saveShapeRoi(Roi roi, int type, OutputStream f, int options) throws IOException {
        float[] shapeArray = ((ShapeRoi)roi).getShapeAsArray();
        if (shapeArray==null) return;
        BufferedOutputStream bout = new BufferedOutputStream(f);
        Rectangle r = roi.getBounds();
        data  = new byte[HEADER_SIZE+HEADER2_SIZE+shapeArray.length*4+roiNameSize+roiPropsSize];
        data[0]=73; data[1]=111; data[2]=117; data[3]=116; // "Iout"
        
        putShort(RoiDecoder.VERSION_OFFSET, VERSION);
        data[RoiDecoder.TYPE] = (byte)type;
        putShort(RoiDecoder.TOP, r.y);
        putShort(RoiDecoder.LEFT, r.x);
        putShort(RoiDecoder.BOTTOM, r.y+r.height);
        putShort(RoiDecoder.RIGHT, r.x+r.width);    
        putInt(RoiDecoder.POSITION, roi.getPosition());
        //putShort(16, n);
        putInt(36, shapeArray.length); // non-zero segment count indicate composite type
        if (VERSION>=218) saveStrokeWidthAndColor(roi);
        saveOverlayOptions(roi, options);

        // handle the actual data: data are stored segment-wise, i.e.,
        // the type of the segment followed by 0-6 control point coordinates.
        int base = 64;
        for (int i=0; i<shapeArray.length; i++) {
            putFloat(base, shapeArray[i]);
            base += 4;
        }
        int hdr2Offset = HEADER_SIZE+shapeArray.length*4;
        //ij.IJ.log("saveShapeRoi: "+HEADER_SIZE+"  "+shapeArray.length);
        putHeader2(roi, hdr2Offset);
        bout.write(data,0,data.length);
        bout.flush();
    }
    
    void saveOverlayOptions(Roi roi, int options) {
        Overlay proto = roi.getPrototypeOverlay();
        if (proto.getDrawLabels())
            options |= RoiDecoder.OVERLAY_LABELS;
        if (proto.getDrawNames())
            options |= RoiDecoder.OVERLAY_NAMES;
        if (proto.getDrawBackgrounds())
            options |= RoiDecoder.OVERLAY_BACKGROUNDS;
        Font font = proto.getLabelFont();
        if (font!=null && font.getStyle()==Font.BOLD)
            options |= RoiDecoder.OVERLAY_BOLD;
        if (proto.scalableLabels())
            options |= RoiDecoder.SCALE_LABELS;
        putShort(RoiDecoder.OPTIONS, options);
    }
    
    void saveTextRoi(TextRoi roi) {
        Font font = roi.getCurrentFont();
        String fontName = font.getName();
        int size = font.getSize();
        int drawStringMode = roi.getDrawStringMode()?1024:0;
        int style = font.getStyle() + roi.getJustification()*256+drawStringMode;
        String text = roi.getText();
        float angle = (float)roi.getAngle();
        int angleLength = 4;
        int fontNameLength = fontName.length();
        int textLength = text.length();
        int textRoiDataLength = 16+fontNameLength*2+textLength*2 + angleLength;
        byte[] data2 = new byte[HEADER_SIZE+HEADER2_SIZE+textRoiDataLength+roiNameSize+roiPropsSize];
        System.arraycopy(data, 0, data2, 0, HEADER_SIZE);
        data = data2;
        putShort(RoiDecoder.SUBTYPE, RoiDecoder.TEXT);
        putInt(HEADER_SIZE, size);
        putInt(HEADER_SIZE+4, style);
        putInt(HEADER_SIZE+8, fontNameLength);
        putInt(HEADER_SIZE+12, textLength);
        for (int i=0; i<fontNameLength; i++)
            putShort(HEADER_SIZE+16+i*2, fontName.charAt(i));
        for (int i=0; i<textLength; i++)
            putShort(HEADER_SIZE+16+fontNameLength*2+i*2, text.charAt(i));
        int hdr2Offset = HEADER_SIZE+textRoiDataLength;
        //ij.IJ.log("saveTextRoi: "+HEADER_SIZE+"  "+textRoiDataLength+"  "+fontNameLength+"  "+textLength);
        putFloat(hdr2Offset-angleLength, angle);
        putHeader2(roi, hdr2Offset);
    }
    
    private int saveImageRoi(ImageRoi roi, int options) {
        byte[] bytes = roi.getSerializedImage();
        int imageSize = bytes.length;
        byte[] data2 = new byte[HEADER_SIZE+HEADER2_SIZE+imageSize+roiNameSize+roiPropsSize];
        System.arraycopy(data, 0, data2, 0, HEADER_SIZE);
        data = data2;
        putShort(RoiDecoder.SUBTYPE, RoiDecoder.IMAGE);
        for (int i=0; i<imageSize; i++)
            putByte(HEADER_SIZE+i, bytes[i]&255);
        int hdr2Offset = HEADER_SIZE+imageSize;
        double opacity = roi.getOpacity();
        putByte(hdr2Offset+RoiDecoder.IMAGE_OPACITY, (int)(opacity*255.0));
        putInt(hdr2Offset+RoiDecoder.IMAGE_SIZE, imageSize);
        if (roi.getZeroTransparent())
            options |= RoiDecoder.ZERO_TRANSPARENT;
        putHeader2(roi, hdr2Offset);
        return options;
    }

    void putHeader2(Roi roi, int hdr2Offset) {
        //ij.IJ.log("putHeader2: "+hdr2Offset+" "+roiNameSize+"  "+roiName);
        putInt(RoiDecoder.HEADER2_OFFSET, hdr2Offset);
        putInt(hdr2Offset+RoiDecoder.C_POSITION, roi.getCPosition());
        putInt(hdr2Offset+RoiDecoder.Z_POSITION, roi.hasHyperStackPosition()?roi.getZPosition():0);
        putInt(hdr2Offset+RoiDecoder.T_POSITION, roi.getTPosition());
        Overlay proto = roi.getPrototypeOverlay();
        Color overlayLabelColor = proto.getLabelColor();
        if (overlayLabelColor!=null)
            putInt(hdr2Offset+RoiDecoder.OVERLAY_LABEL_COLOR, overlayLabelColor.getRGB());
        Font font = proto.getLabelFont();
        if (font!=null)
            putShort(hdr2Offset+RoiDecoder.OVERLAY_FONT_SIZE, font.getSize());
        if (roiNameSize>0)
            putName(roi, hdr2Offset);
        double strokeWidth = roi.getStrokeWidth();
        if (roi.getStroke()==null)
            strokeWidth = 0.0;
        putFloat(hdr2Offset+RoiDecoder.FLOAT_STROKE_WIDTH, (float)strokeWidth);
        if (roiPropsSize>0)
            putProps(roi, hdr2Offset);
        if (countersSize>0)
            putPointCounters(roi, hdr2Offset);
        putByte(hdr2Offset+RoiDecoder.GROUP, roi.getGroup());
    }

    void putName(Roi roi, int hdr2Offset) {
        int offset = hdr2Offset+HEADER2_SIZE;
        int nameLength = roiNameSize/2;
        putInt(hdr2Offset+RoiDecoder.NAME_OFFSET, offset);
        putInt(hdr2Offset+RoiDecoder.NAME_LENGTH, nameLength);
        for (int i=0; i<nameLength; i++)
            putShort(offset+i*2, roiName.charAt(i));
    }

    void putProps(Roi roi, int hdr2Offset) {
        int offset = hdr2Offset+HEADER2_SIZE+roiNameSize;
        int roiPropsLength = roiPropsSize/2;
        putInt(hdr2Offset+RoiDecoder.ROI_PROPS_OFFSET, offset);
        putInt(hdr2Offset+RoiDecoder.ROI_PROPS_LENGTH, roiPropsLength);
        for (int i=0; i<roiPropsLength; i++)
            putShort(offset+i*2, roiProps.charAt(i));
    }

    void putPointCounters(Roi roi, int hdr2Offset) {
        int offset = hdr2Offset+HEADER2_SIZE+roiNameSize+roiPropsSize;
        putInt(hdr2Offset+RoiDecoder.COUNTERS_OFFSET, offset);
        for (int i=0; i<countersSize/4; i++)
            putInt(offset+i*4, counters[i]);
        countersSize = 0;
    }

    void putByte(int base, int v) {
        data[base] = (byte)v;
    }

    void putShort(int base, int v) {
        data[base] = (byte)(v>>>8);
        data[base+1] = (byte)v;
    }

    void putFloat(int base, float v) {
        int tmp = Float.floatToIntBits(v);
        data[base]   = (byte)(tmp>>24);
        data[base+1] = (byte)(tmp>>16);
        data[base+2] = (byte)(tmp>>8);
        data[base+3] = (byte)tmp;
    }

    void putInt(int base, int i) {
        data[base]   = (byte)(i>>24);
        data[base+1] = (byte)(i>>16);
        data[base+2] = (byte)(i>>8);
        data[base+3] = (byte)i;
    }
    
    
}