package com.jpexs.decompiler.flash.flv;

import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

/**
 *
 * @author JPEXS
 */
public class FLVOutputStream extends OutputStream {

    private OutputStream os;
    private int bitPos = 0;
    private int tempByte = 0;
    private long pos = 0;

    public FLVOutputStream(OutputStream os) {
        this.os = os;
    }

    public long getPos() {
        return pos;
    }

    /**
     * Writes byte to the stream
     *
     * @param b byte to write
     * @throws IOException
     */
    @Override
    public void write(int b) throws IOException {
        alignByte();
        os.write(b);
        pos++;
    }

    private void alignByte() throws IOException {
        if (bitPos > 0) {
            bitPos = 0;
            write(tempByte);
            tempByte = 0;
        }
    }

    /**
     * Writes UI8 (Unsigned 8bit integer) value to the stream
     *
     * @param val UI8 value to write
     * @throws IOException
     */
    public void writeUI8(int val) throws IOException {
        write(val);
    }

    /**
     * Writes UI24 (Unsigned 24bit integer) value to the stream
     *
     * @param value UI32 value
     * @throws IOException
     */
    public void writeUI24(long value) throws IOException {
        write((int) ((value >> 16) & 0xff));
        write((int) ((value >> 8) & 0xff));
        write((int) (value & 0xff));

    }

    /**
     * Writes UI32 (Unsigned 32bit integer) value to the stream
     *
     * @param value UI32 value
     * @throws IOException
     */
    public void writeUI32(long value) throws IOException {
        write((int) ((value >> 24) & 0xff));
        write((int) ((value >> 16) & 0xff));
        write((int) ((value >> 8) & 0xff));
        write((int) (value & 0xff));
    }

    /**
     * Writes UI16 (Unsigned 16bit integer) value to the stream
     *
     * @param value UI16 value
     * @throws IOException
     */
    public void writeUI16(int value) throws IOException {
        write((int) ((value >> 8) & 0xff));
        write((int) (value & 0xff));
    }

    /**
     * Writes UB[nBits] (Unsigned-bit value) value to the stream
     *
     * @param nBits Number of bits which represent value
     * @param value Unsigned value to write
     * @throws IOException
     */
    public void writeUB(int nBits, long value) throws IOException {
        for (int bit = 0; bit < nBits; bit++) {
            int nb = (int) ((value >> (nBits - 1 - bit)) & 1);
            tempByte += nb * (1 << (7 - bitPos));
            bitPos++;
            if (bitPos == 8) {
                bitPos = 0;
                write(tempByte);
                tempByte = 0;
            }
        }
    }

    public void writeHeader(boolean audio, boolean video) throws IOException {
        write("FLV".getBytes());
        write(1); //version
        writeUB(5, 0); //must be 0
        writeUB(1, audio ? 1 : 0); //audio present
        writeUB(1, 0); //reserved
        writeUB(1, video ? 1 : 0); //video present
        writeUI32(9);  //header size            
        writeUI32(0);
    }

    public void writeTag(FLVTAG tag) throws IOException {
        long posBefore = getPos();
        writeUI8(tag.tagType);
        byte data[] = tag.data.getBytes();
        writeUI24(data.length);
        writeUI24(tag.timeStamp & 0xffffff);
        writeUI8((int) ((tag.timeStamp >> 24) & 0xff));
        writeUI24(0);
        write(data); //codecId 4, frameType 1
        long posAfter = getPos();
        long size = posAfter - posBefore;
        writeUI32(size);
    }

    public void writeSCRIPTDATASTRING(String s) throws IOException {
        int len = s.getBytes("UTF-8").length;
        writeUI16(len);
        write(s.getBytes("UTF-8"));
    }

    public void writeSCRIPTDATALONGSTRING(String s) throws IOException {
        int len = s.getBytes("UTF-8").length;
        writeUI32(len);
        write(s.getBytes("UTF-8"));
    }

    private void writeLong(long value) throws IOException {
        byte writeBuffer[] = new byte[8];
        writeBuffer[3] = (byte) (value >>> 56);
        writeBuffer[2] = (byte) (value >>> 48);
        writeBuffer[1] = (byte) (value >>> 40);
        writeBuffer[0] = (byte) (value >>> 32);
        writeBuffer[7] = (byte) (value >>> 24);
        writeBuffer[6] = (byte) (value >>> 16);
        writeBuffer[5] = (byte) (value >>> 8);
        writeBuffer[4] = (byte) (value);
        write(writeBuffer);
    }

    public void writeDOUBLE(double value) throws IOException {
        writeLong(Double.doubleToLongBits(value));
    }

    public void writeSCRIPTDATAOBJECT(SCRIPTDATAOBJECT o) throws IOException {
        writeSCRIPTDATASTRING(o.objectName);
        writeSCRIPTDATAVALUE(o.objectData);
    }

    public void writeSCRIPTDATAVARIABLE(SCRIPTDATAVARIABLE v) throws IOException {
        writeSCRIPTDATASTRING(v.variableName);
        writeSCRIPTDATAVALUE(v.variableData);
    }

    public void writeSCRIPTDATAVALUE(SCRIPTDATAVALUE v) throws IOException {
        writeUI8(v.type);
        switch (v.type) {
            case 0:
                writeDOUBLE((double) (Double) v.value);
                break;
            case 1:
                writeUI8((boolean) (Boolean) v.value ? 1 : 0);
                break;
            case 2:
                writeSCRIPTDATASTRING((String) v.value);
                break;
            case 3:
                @SuppressWarnings("unchecked") List<SCRIPTDATAOBJECT> objects = (List<SCRIPTDATAOBJECT>) v.value;
                for (SCRIPTDATAOBJECT o : objects) {
                    writeSCRIPTDATAOBJECT(o);
                }
                writeUI24(9);//SCRIPTDATAOBJECTEND
                break;
            case 4:
                writeSCRIPTDATASTRING((String) v.value);
                break;
            case 5:
                //null
                break;
            case 6:
                //undefined
                break;
            case 7:
                writeUI16((int) (Integer) v.value);
                break;
            case 8:
                @SuppressWarnings("unchecked") List<SCRIPTDATAVARIABLE> variables = (List<SCRIPTDATAVARIABLE>) v.value;
                writeUI32(variables.size());
                for (SCRIPTDATAVARIABLE var : variables) {
                    writeSCRIPTDATAVARIABLE(var);
                }
                writeUI24(9);//SCRIPTDATAVARIABLEEND
                break;
            case 9:
                //reserved
                break;
            case 10:
                @SuppressWarnings("unchecked") List<SCRIPTDATAVARIABLE> stvariables = (List<SCRIPTDATAVARIABLE>) v.value;
                writeUI32(stvariables.size());
                for (SCRIPTDATAVARIABLE var : stvariables) {
                    writeSCRIPTDATAVARIABLE(var);
                }
                break;
            case 11:
                writeSCRIPTDATADATE((SCRIPTDATADATE) v.value);
                break;
            case 12:
                writeSCRIPTDATALONGSTRING((String) v.value);
                break;

        }
    }

    public void writeSI16(int value) throws IOException {
        writeUI16(value);
    }

    public void writeSCRIPTDATADATE(SCRIPTDATADATE d) throws IOException {
        writeDOUBLE(d.dateTime);
        writeSI16(d.localDateTimeOffset);
    }
}
