/*
 * @(#)TIFFOutputStream.java  1.0  2011-02-27
 * 
 * Copyright (c) 2011 Werner Randelshofer, Immensee, Switzerland.
 * All rights reserved.
 * 
 * You may not use, copy or modify this file, except in compliance with the
 * license agreement you entered into with Werner Randelshofer.
 * For details see accompanying license terms.
 */
package org.monte.media.tiff;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteOrder;
import java.util.Stack;
import javax.imageio.stream.ImageOutputStream;

/**
 * {@code TIFFOutputStream}.
 * <p>
 * References:
 * <p>
 * TIFF TM Revision 6.0. Final — June 3, 1992.
 * Adobe Systems Inc.
 * http://www.exif.org/specifications.html
 *
 * @author Werner Randelshofer
 * @version 1.0 2011-02-27 Created.
 */
public class TIFFOutputStream extends OutputStream {

    private ImageOutputStream out;
    private long offset;
    private Stack<IFD> ifdStack=new Stack<IFD>();

    private enum State {

        INITIALIZED, STARTED, FINISHED
    };
    private State state = State.INITIALIZED;
    private long firstIFDOffset = 8;

    public TIFFOutputStream(ImageOutputStream out) throws IOException {
        this.out = out;
        this.offset = out.getStreamPosition();
    }

    public void setByteOrder(ByteOrder bo) {
        if (state == State.INITIALIZED && bo != out.getByteOrder()) {
            throw new IllegalStateException("Can't change byte order within TIFF file");
        }
        out.setByteOrder(bo);
    }

    public ByteOrder getByteOrder() {
        return out.getByteOrder();
    }

    public long getStreamPosition() throws IOException {
        return out.getStreamPosition() - offset;
    }

    public void seek(long position) throws IOException {
        out.seek(position + offset);
    }

    @Override
    public void write(int b) throws IOException {
        ensureStarted();
        out.write(b);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        ensureStarted();
        out.write(b, off, len);
    }

    public void writeIFD(IFD ifd, long nextIFD) throws IOException {
        ensureStarted();
        writeSHORT(ifd.getCount());
        long ifdOffset=getStreamPosition();
        long valueOffset=getStreamPosition()+12*ifd.getCount()+4;
        for (int i=0,n=ifd.getCount();i<n;i++) {
            IFDEntry entry=ifd.get(i);
            writeSHORT(entry.getTagNumber());
            writeSHORT(entry.getTypeNumber());
            if (entry.isDataInValueOffset()) {
                writeLONG(entry.getValueOffset());
            } else {
                writeLONG(valueOffset);
                valueOffset+=entry.getLength();
            }
        }
        writeLONG(nextIFD);

        for (int i=0,n=ifd.getCount();i<n;i++) {
            IFDEntry entry=ifd.get(i);
            if (!entry.isDataInValueOffset()) {
                write((byte[])entry.getData());
            }
        }
    }

    public long getFirstIFDOffset() {
        return firstIFDOffset;
    }

    public void setFirstIFDOffset(long newValue) {
        firstIFDOffset = newValue;
    }

    private void ensureStarted() throws IOException {
        if (state == State.INITIALIZED) {
            if (getByteOrder() == ByteOrder.LITTLE_ENDIAN) {
                writeSHORT(0x4949); // "II" little endian marker
            } else {
                writeSHORT(0x4D4D); // "MM" big endian marker
            }
            writeSHORT(42); // magic number

            state = State.STARTED;
        }
    }

    public void finish() throws IOException {
        ensureStarted();
        if (state == State.STARTED) {
            state = State.FINISHED;

            long pos = getStreamPosition();
            seek(4);
            writeLONG(firstIFDOffset);
            seek(pos);
        }
    }

    /** Writes a 32-bit unsigned integer. */
    public void writeLONG(long v) throws IOException {
        out.writeInt((int) v);
    }
    /** Writes a 12-bit unsigned integer. */
    public void writeSHORT(int v) throws IOException {
        out.writeShort((short) v);
    }

    @Override
    public void close() throws IOException {
        finish();
        out.close();
    }
}
