package turgen.generator;

import java.io.*;

/**
 * Generator of tape image with pwmX chunks. This generator is based on
 * information obtained from a8cas-utils project
 *
 */
public class TapeImagePWMGenerator {

    /*Torzo hlavicky data chunku*/
    int[] dataChunkHeader = {100, 97, 116, 97, 0, 0, 0, 0};
    int[] baudChunkHeader = {'b', 'a', 'u', 'd', 0, 0, 0, 0};
    int[] fskChunkHeader = {'f', 's', 'k', ' ', 0, 0, 0, 0};
    int[] fujiChunkHeader = {'F', 'U', 'J', 'I', 0, 0, 0, 0};

    /**
     * Copy of last SETUP instruction encountered
     */
    private final int[] lastSetup = new int[13];

    /**
     * Get tape image data created from passed generator instructions
     *
     * @param instr Generator instructions
     * @return Tape image data
     */
    public int[] getData(int[] instr) {

        /*Program counter*/
        int pc = 0;
        /*Opcode*/
        int op = 0;

        /*Return value*/
        int[] retVal = null;

        /*Cumulated silence in miliseconds*/
        int silenceMiliseconds = 0;
        
        boolean hasPWMs = false;

        /*Stream initialization*/
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        ByteArrayInputStream bais = null;
        DataInputStream dis = null;

        try {

            if (instr[pc] != SignalGenerator.INSTR_FUJI) {
                /*FUJI header*/
                dos.writeInt(70);
                dos.writeInt(85);
                dos.writeInt(74);
                dos.writeInt(73);

                /*Followed with zeroes*/
                for (int i = 0; i < 4; i++) {
                    dos.writeInt(0);
                }
            }

            /*Quit loop tag*/
            boolean quit = false;

            do {
                /*Opcode*/
                if (pc >= instr.length) {
                    break;
                }

                op = instr[pc];
                switch (op) {

                    /*End of instructions*/
                    case SignalGenerator.INSTR_END: {
                        quit = true;
                        break;
                    }

                    /*Block separator*/
                    case SignalGenerator.INSTR_BLOCKSEP: {
                        silenceMiliseconds += 50;
                        pc++;
                        break;
                    }

                    /*Short pulse*/
                    case SignalGenerator.INSTR_NARROW: {

                        addPWMlHeader(dos, 2, silenceMiliseconds, false);
                        addPWMlElement(dos, lastSetup[3] >> 1);
                        addPWMlElement(dos, lastSetup[3] >> 1);
                        pc++;
                        silenceMiliseconds = 0;

                        break;
                    }

                    /*Long pulse*/
                    case SignalGenerator.INSTR_WIDE: {

                        addPWMlHeader(dos, 2, silenceMiliseconds, false);
                        addPWMlElement(dos, lastSetup[2] >> 1);
                        addPWMlElement(dos, lastSetup[2] >> 1);
                        pc++;
                        silenceMiliseconds = 0;
                        break;
                    }

                    /*Sync pulse*/
                    case SignalGenerator.INSTR_SYNC: {

                        addPWMlHeader(dos, 2, silenceMiliseconds, false);
                        addPWMlElement(dos, lastSetup[4] >> 1);
                        addPWMlElement(dos, lastSetup[4] >> 1);
                        pc++;
                        silenceMiliseconds = 0;
                        break;
                    }

                    /*Stop pulse*/
                    case SignalGenerator.INSTR_STOP: {
                        addPWMlHeader(dos, 2, silenceMiliseconds, false);
                        addPWMlElement(dos, lastSetup[5] >> 1);
                        addPWMlElement(dos, lastSetup[5] >> 1);
                        pc++;
                        silenceMiliseconds = 0;
                        break;
                    }

                    /*Short silence 0.25 s*/
                    case SignalGenerator.INSTR_SHORTSILENCE: {
                        silenceMiliseconds += 100;
                        pc++;
                        break;
                    }

                    /*Long silence*/
                    case SignalGenerator.INSTR_LONGSILENCE: {

                        pc++;
                        silenceMiliseconds = (Math.abs(instr[pc])) * 1000;
                        pc++;
                        break;
                    }

                    /*Pilot tone*/
                    case SignalGenerator.INSTR_PILOTTONE: {
                        pc++;
                        addPWMcHeader(dos, silenceMiliseconds, 1);
                        addPWMcElement(dos, instr[pc], lastSetup[1]);
                        pc++;
                        silenceMiliseconds = 0;
                        break;
                    }

                    /*Setup*/
                    case SignalGenerator.INSTR_SETUP: {

                        /*Flush silence if any*/
                        if (silenceMiliseconds != 0 && hasPWMs) {
                            addPWMlHeader(dos, 0, silenceMiliseconds, true);
                            silenceMiliseconds = 0;
                        }

                        /*Add pwms chunk*/
                        addPWMs(dos, instr[pc + 6], instr[pc + 7], 44100);
                        hasPWMs=true;
                        
                        /*Save SETUP instruction*/
                        for (int i = 0; i < 13; i++) {
                            lastSetup[i] = instr[pc + i];
                        }

                        /*Next instruction*/
                        pc += 13;
                        break;

                    }

                    /*Block of data*/
                    case SignalGenerator.INSTR_DATA: {
                        pc++;
                        int dataCount = instr[pc];
                        pc++;

                        int[] dt = new int[dataCount];
                        for (int i = 0; i < dataCount; i++) {
                            dt[i] = instr[pc + i];
                        }

                        /*Flush silence if any*/
                        if (silenceMiliseconds != 0) {
                            addPWMlHeader(dos, 0, silenceMiliseconds, true);
                            silenceMiliseconds = 0;
                        }

                        addPWMd(dos, lastSetup[3], lastSetup[2], dt);

                        pc += dataCount;

                        break;
                    }

                    /*PWMS instruction*/
                    case SignalGenerator.INSTR_PWMS: {
                        pc++;
                        int polarity = instr[pc];
                        pc++;
                        int byteOrder = instr[pc];
                        pc++;
                        int sampleRate = instr[pc];
                        pc++;

                        addPWMs(dos, polarity == SignalGenerator.FLAG_POLARITY_10 ? 1 : 0, byteOrder == SignalGenerator.FLAG_ORDER_HL ? 1 : 0, sampleRate);
                        hasPWMs=true;
                        break;
                    }

                    /*PWMC instruction*/
                    case SignalGenerator.INSTR_PWMC: {
                        pc++;
                        int silence = instr[pc];
                        pc++;
                        int triplets = instr[pc];
                        addPWMcHeader(dos, silence, triplets);
                        pc++;

                        for (int i = 0; i < triplets; i++) {
                            addPWMcElement(dos, instr[pc + 1], instr[pc]);
                            pc += 2;
                        }

                        break;
                    }

                    /*PWMD instruction*/
                    case SignalGenerator.INSTR_PWMD: {
                        pc++;
                        int dataLen = instr[pc];
                        pc++;
                        int shortPulse = instr[pc];
                        pc++;
                        int longPulse = instr[pc];
                        pc++;
                        int[] data = new int[dataLen];
                        for (int i = 0; i < dataLen; i++) {
                            data[i] = instr[pc];
                            pc++;
                        }
                        addPWMd(dos, shortPulse, longPulse, data);
                        break;
                    }

                    case SignalGenerator.INSTR_PWML: {
                        pc++;
                        int silence = instr[pc];
                        pc++;
                        int numElements = instr[pc];
                        pc++;
                        addPWMlHeader(dos, numElements, silence, false);

                        for (int i = 0; i < numElements; i++) {
                            addPWMlElement(dos, instr[pc]);
                            pc++;
                        }
                        break;

                    }
                    case SignalGenerator.INSTR_BAUD: {
                        pc++;
                        int baudRate = instr[pc];
                        baudChunkHeader[6] = baudRate % 256;
                        baudChunkHeader[7] = baudRate / 256;
                        for (int i = 0; i < baudChunkHeader.length; i++) {
                            dos.writeInt(baudChunkHeader[i]);
                        }
                        pc++;
                        break;
                    }
                    case SignalGenerator.INSTR_STDDATA: {
                        pc++;
                        int irgMilis = instr[pc];
                        pc++;
                        int len = instr[pc];
                        pc++;

                        dataChunkHeader[4] = len % 256;
                        dataChunkHeader[5] = len / 256;
                        dataChunkHeader[6] = irgMilis % 256;
                        dataChunkHeader[7] = irgMilis / 256;

                        for (int i = 0; i < dataChunkHeader.length; i++) {
                            dos.writeInt(dataChunkHeader[i]);
                        }

                        for (int i = 0; i < len; i++) {
                            dos.writeInt(instr[pc]);
                            pc++;
                        }
                        break;
                    }
                    case SignalGenerator.INSTR_FSK: {
                        pc++;
                        int irgMilis = instr[pc];
                        pc++;
                        int lenInWords = instr[pc];
                        int lenInBytes = lenInWords * 2;
                        pc++;

                        fskChunkHeader[4] = lenInBytes % 256;
                        fskChunkHeader[5] = lenInBytes / 256;
                        fskChunkHeader[6] = irgMilis % 256;
                        fskChunkHeader[7] = irgMilis / 256;

                        for (int i = 0; i < fskChunkHeader.length; i++) {
                            dos.writeInt(fskChunkHeader[i]);
                        }

                        for (int i = 0; i < lenInWords; i++) {
                            dos.writeInt(instr[pc] % 256);
                            dos.writeInt(instr[pc] / 256);
                            pc++;
                        }

                        break;

                    }
                    case SignalGenerator.INSTR_FUJI: {
                        /*Skip opcode*/
                        pc++;
                        /*Get length*/
                        int len = instr[pc];
                        pc++;
                        /*Header skeleton*/
                        fujiChunkHeader[4] = len % 256;
                        fujiChunkHeader[5] = len / 256;
                        /*Write header*/
                        for (int i = 0; i < fujiChunkHeader.length; i++) {
                            dos.writeInt(fujiChunkHeader[i]);
                        }
                        /*Write data*/
                        for (int i = 0; i < len; i++) {
                            dos.writeInt(instr[pc]);
                            pc++;
                        }
                        break;
                    }
                    case SignalGenerator.INSTR_PAUSE: {
                        /*Just skip opcode*/
                        pc++;
                        break;
                    }

                    default: {
                        pc++;
                        break;
                    }
                }

            } while (quit == false);

            /*Read it back*/
            byte[] b = baos.toByteArray();
            int l = b.length / 4;
            bais = new ByteArrayInputStream(baos.toByteArray());
            dis = new DataInputStream(bais);

            retVal = new int[l];
            for (int i = 0; i < l; i++) {
                retVal[i] = dis.readInt();
            }

            dos.close();
            dis.close();

        } catch (Exception e) {
            e.printStackTrace();
        }

        return retVal;
    }

    /**
     * Add pwms chunk to the tape image
     *
     * @param dos Stream where the chunk is to be written
     * @param polarity Polarity of pulses, 1 (HILO) or 0 (LOHI)
     * @param byteOrder Order of bits in byte 1 MSB first, 0 LSB first
     * @throws Exception
     */
    private void addPWMs(DataOutputStream dos, int polarity, int byteOrder, int sampleRate) throws Exception {

        /*Write 'pwms'*/
        dos.writeInt(0x70);
        dos.writeInt(0x77);
        dos.writeInt(0x6D);
        dos.writeInt(0x73);

        /*Length is 2*/
        dos.writeInt(2);
        dos.writeInt(0);

        /*Aux 1 polarity and byte order*/
        int aux1 = (byteOrder == 1 ? 4 : 0) + (polarity == 1 ? 2 : 1);

        /*Aux 2 is unused*/
        int aux2 = 0;

        /*Write AUX bytes*/
        dos.writeInt(aux1);
        dos.writeInt(aux2);

        /*Sample rate*/
        dos.writeInt(sampleRate % 256);
        dos.writeInt(sampleRate / 256);
    }

    /**
     * Add pwms chunk to the tape image
     *
     * @param dos Stream where the chunk is to be written
     * @param count Number of pilot tone pulses
     * @param pulseLength Length of pilot tone pulse
     * @param silenceLength Length of silence in miliseconds
     * @throws Exception
     */
    private void addPWMcHeader(DataOutputStream dos, int silenceLength, int numTriplets) throws Exception {

        /*Write 'pwmi'*/
        dos.writeInt(0x70);
        dos.writeInt(0x77);
        dos.writeInt(0x6D);
        dos.writeInt(0x63);

        /*Write length*/
        dos.writeInt((numTriplets * 3) % 256);
        dos.writeInt((numTriplets * 3) / 256);

        /*Write AUX*/
        if (silenceLength > 65535) {
            silenceLength = 65535;
        }
        dos.writeInt(silenceLength % 256);
        dos.writeInt(silenceLength / 256);
    }

    private void addPWMcElement(DataOutputStream dos, int pulseCount, int length) throws Exception {
        /*Write pilot tone*/
        dos.writeInt(length);
        dos.writeInt(pulseCount % 256);
        dos.writeInt(pulseCount / 256);

    }

    /**
     * Add pwmd chunk to the tape image
     *
     * @param dos Stream where the chunk is to be written
     * @param shortPulseLength Length of short pulse (0)
     * @param longPulseLength Length of long pulse (1)
     * @param data Data to be held by the pwmd chunk
     * @throws Exception
     */
    private void addPWMd(DataOutputStream dos, int shortPulseLength, int longPulseLength, int[] data) throws Exception {

        /*Write 'pwmd'*/
        dos.writeInt(0x70);
        dos.writeInt(0x77);
        dos.writeInt(0x6D);
        dos.writeInt(0x64);

        /*Write length*/
        int l = data.length;
        dos.writeInt(l % 256);
        dos.writeInt(l / 256);

        /*Write AUX*/
        dos.writeInt(shortPulseLength);
        dos.writeInt(longPulseLength);

        /*Write data*/
        for (int i = 0; i < l; i++) {
            dos.writeInt(data[i]);
        }
    }

    /**
     * Add pwml chunk to the tape image
     *
     * @param dos Stream where the chunk is to be written
     * @param numLevels Number of levels
     * @param silenceLength Length of silence in miliseconds
     * @param silenceOnly Purpose of the chunk is only silence
     * @throws Exception
     */
    private void addPWMlHeader(DataOutputStream dos, int numLevels, int silenceLength, boolean silenceOnly) throws Exception {

        /*Write 'pwml*/
        dos.writeInt(0x70);
        dos.writeInt(0x77);
        dos.writeInt(0x6D);
        dos.writeInt(0x6C);

        /*Write length*/
        if (silenceOnly == true) {
            dos.writeInt(0);
            dos.writeInt(0);
        }
        else {
            dos.writeInt((numLevels * 2) % 256);
            dos.writeInt((numLevels * 2) / 256);
        }


        /*Write AUX silence*/
        if (silenceLength > 65535) {
            silenceLength = 65535;
        }
        dos.writeInt(silenceLength % 256);
        dos.writeInt(silenceLength / 256);

    }

    private void addPWMlElement(DataOutputStream dos, int pulseLength) throws Exception {

        /*Write length of logical states*/
        dos.writeInt((pulseLength) % 256);
        dos.writeInt((pulseLength) / 256);

    }

}
