// Copyright 2001-2003 FreeHEP.
package org.freehep.graphicsio.cgm;

import java.awt.Color;
import java.awt.geom.Point2D;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

import org.freehep.util.io.ActionHeader;
import org.freehep.util.io.Tag;
import org.freehep.util.io.TagHeader;
import org.freehep.util.io.TaggedOutputStream;
import org.freehep.util.io.UndefinedTagException;

/**
 * CGM Binary Output Stream. Tags written with this OutputStream will
 * produce a binary CGM file. The class also holds the state
 * for the various precisions, defaults, etc...
 *
 * @author Mark Donszelmann
 * @author Charles Loomis
 * @author Ian Graham - support for long commands through partitioning
 * @version $Id: CGMOutputStream.java,v 1.1.1.1 2004/05/31 05:08:53 kuromaru Exp $
 */
public class CGMOutputStream extends TaggedOutputStream {

    private static short COMMAND_PARTITION_SIZE = Short.MAX_VALUE;

    private int version;
    private boolean direct = true;
    private int colorIndexPrecision = 8;
    private int directColorPrecision = 8;
    private int indexPrecision = 16;
    private int integerPrecision = 16;
    private boolean fixedPrecision  = true;
    private boolean doublePrecision = false;

    private boolean vdcReal = false;
    private boolean vdcFixedPrecision = true;
    private boolean vdcDoublePrecision = false;
    private int vdcIntegerPrecision = 16;

    private int namePrecision = 16;

    private int lineWidthSpecificationMode = LineWidthSpecificationMode.ABSOLUTE;
    private int markerSizeSpecificationMode = MarkerSizeSpecificationMode.ABSOLUTE;
    private int edgeWidthSpecificationMode = EdgeWidthSpecificationMode.ABSOLUTE;
    private int interiorStyleSpecificationMode = InteriorStyleSpecificationMode.ABSOLUTE;

    public CGMOutputStream(OutputStream os, int version)
        throws IOException {

        // CGM is big-endian
        super(os, new CGMTagSet(version), null, false);
        this.version = version;
    }

    public CGMOutputStream(OutputStream os)
        throws IOException {

        this(os, 1);
    }

    void setColorMode(boolean direct) {
        this.direct = direct;
    }

    void setColorIndexPrecision(int precision) {
        colorIndexPrecision = precision;
    }

    void setDirectColorPrecision(int precision) {
        directColorPrecision = precision;
    }

    void setIndexPrecision(int precision) {
        indexPrecision = precision;
    }

    void setIntegerPrecision(int precision) {
        integerPrecision = precision;
    }

    void setRealPrecision(boolean fixedPrecision, boolean doublePrecision) {
        this.fixedPrecision = fixedPrecision;
        this.doublePrecision = doublePrecision;
    }

    void setVDCReal(boolean real) {
        vdcReal = real;
    }

    void setVDCIntegerPrecision(int precision) {
        vdcIntegerPrecision = precision;
    }

    void setVDCRealPrecision(boolean fixedPrecision, boolean doublePrecision) {
        this.vdcFixedPrecision = fixedPrecision;
        this.vdcDoublePrecision = doublePrecision;
    }

    void setNamePrecision(int precision) {
        namePrecision = precision;
    }

    void setLineWidthSpecificationMode(int mode) {
        lineWidthSpecificationMode = mode;
    }

    int getLineWidthSpecificationMode() {
        return lineWidthSpecificationMode;
    }

    void setMarkerSizeSpecificationMode(int mode) {
        markerSizeSpecificationMode = mode;
    }

    int getMarkerSizeSpecificationMode() {
        return markerSizeSpecificationMode;
    }

    void setEdgeWidthSpecificationMode(int mode) {
        edgeWidthSpecificationMode = mode;
    }

    int getEdgeWidthSpecificationMode() {
        return edgeWidthSpecificationMode;
    }

    void setInteriorStyleSpecificationMode(int mode) {
        interiorStyleSpecificationMode = mode;
    }

    int getInteriorStyleSpecificationMode() {
        return interiorStyleSpecificationMode;
    }

    // FX
    public void writeFixedPoint(double d) throws IOException {
        int whole = (int)Math.floor(d);
        int frac = (int)Math.floor((d - whole)*0x10000);
        writeShort(whole);
        writeShort(frac);
    }

    // FP
    public void writeFloatingPoint(double d) throws IOException {
        if (doublePrecision) {
            writeDouble(d);
        } else {
            writeFloat((float)d);
        }
    }

    // CI
    public void writeColorIndex(int i) throws IOException {
        switch(colorIndexPrecision) {
            case 8: writeByte(i); break;
            case 16: writeShort(i); break;
            default:
            case 24: // FIXME
            case 32: writeInt(i); break;
        }
    }

    // CCO
    public void writeColorComponent(int c) throws IOException {
        switch(directColorPrecision) {
            default:
            case 8:
            case 16:
            case 24: // FIXME
            case 32:
                writeUnsignedByte(c);
                break;
        }
    }

    // CD
    public void writeColorDirect(Color c) throws IOException {
        // FIXME3: depends on color model from version 3
        writeColorComponent(c.getRed());
        writeColorComponent(c.getGreen());
        writeColorComponent(c.getBlue());
//        writeColorComponent(0);  // Don't do this until color model support is provided, RGB is default
    }

    // IX
    public void writeIntegerIndex(int i) throws IOException {
        switch(indexPrecision) {
            case 8: writeByte(i); break;
            default:
            case 16: writeShort(i); break;
            case 24: // FIXME
            case 32: writeInt(i); break;
        }
    }

    // E
    public void writeEnumerate(int i) throws IOException {
        writeShort(i);
    }

    // I
    public void writeInteger(int i) throws IOException {
        switch(integerPrecision) {
            case 8: writeByte(i); break;
            default:
            case 16: writeShort(i); break;
            case 24: // FIXME
            case 32: writeInt(i); break;
        }
    }

    // R
    public void writeReal(double r) throws IOException {
        if (fixedPrecision) {
            writeFixedPoint(r);
        } else {
            writeFloatingPoint(r);
        }
    }

    // S, SF
    public void writeString(String s) throws IOException {
        int len = s.length();
        if (len < 0xFF) {
            // short
            writeUnsignedByte(len);
        } else {
            // long
            writeUnsignedByte(0xFF);
            writeUnsignedShort(len & 0x7FFF);
        }
        writeBytes(s);
    }

    // D
    public void writeData(byte[] data) throws IOException {
        // FIXME: good unless strings will do encoding
        writeString(new String(data));
    }

    // VDC
    public void writeVDC(double d) throws IOException {
        if (vdcReal) {
            if (vdcFixedPrecision) {
                writeFixedPoint(d);
            } else {
                if (vdcDoublePrecision) {
                    writeDouble(d);
                } else {
                    writeFloat((float)d);
                }
            }
        } else {
            switch(vdcIntegerPrecision) {
                default:
                case 16: writeShort((int)d); break;
                case 24: // FIXME
                case 32: writeInt((int)d); break;
            }
        }
    }

    // P
    public void writePoint(Point2D p) throws IOException {
        writeVDC(p.getX());
        writeVDC(p.getY());
    }

    // CO
    public void writeColor(Color c) throws IOException {
        if (direct) {
            writeColorDirect(c);
        } else {
            // FIXME: we should look up the color
            writeColorIndex((c.getRed()<<16) + (c.getGreen()<<8) + c.getBlue());
        }
    }

    // N
    public void writeName(int name) throws IOException {
        switch(namePrecision) {
            case 8:  writeByte(name); break;
            default:
            case 16: writeShort((int)name); break;
            case 24: // FIXME
            case 32: writeInt((int)name); break;
        }
    }

    protected void writeTagHeader(TagHeader header) throws IOException {

        byteAlign();

        int tagID = header.getTag();
        long length = header.getLength();
        if (length < 0x1F) {
            // short form
            writeUnsignedShort((tagID << 5) + (int)length);
        } else {
            // long form - length is placed in partition rather than header
            writeUnsignedShort((tagID << 5) + 0x001F);
        }
    }


    public void writeTag(Tag tag)
        throws IOException {

        int tagID = tag.getTag();

        if (!tagSet.exists(tagID)) throw new UndefinedTagException(tagID);

        pushBuffer();
        tag.write(tagID, this);
        int align = getTagAlignment();
        int pad = (align - (getBufferLength() % align)) % align;
        for (int i=0; i<pad; i++) {
           write(0);
        }
        int len = getBufferLength();
        byte[] buffer = popBufferBytes();
        TagHeader header = new TagHeader(tagID, len);
        writeTagHeader(header);
        if (len <= 30) {
            write(buffer, 0, len);
        } else {
            writePartitioned(buffer, len);
        }
            
        // pad tag with 0 if necessary
        if ((size() % 2) != 0) {
            writeUnsignedByte(0);
        }
    }

    private void writePartitioned(byte[] buffer, int len) throws IOException {
        int bufferOffset = 0;
        final short partitionSize = COMMAND_PARTITION_SIZE;
        while (len > partitionSize) {
            writeUnsignedShort(0x8000 | partitionSize);
            write(buffer, bufferOffset, partitionSize);
            bufferOffset += partitionSize;
            len -= partitionSize;
        }
        writeUnsignedShort(len);
        write(buffer, bufferOffset, len);
    }

    protected void writeActionHeader(ActionHeader header)
        throws IOException {

        // empty
    }

    public int getVersion() {
        return version;
    }
}
