/*
https://github.com/eightbitjim/commodore-tape-maker/blob/master/maketape.py

# MIT License
#
# Copyright (c) 2018 eightbitjim
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
*/

export class OutputSoundFile {
    options: any;
    sampleRate: number;
    soundData: number[];
    tapData: number[];

    constructor(options: any) {
        this.options = options;
        this.sampleRate = 44100.0;
        this.soundData = [];
        //00000000  43 36 34 2d 54 41 50 45  2d 52 41 57 01 00 00 00  |C64-TAPE-RAW....|
        //00000010  1e 62 03 00 
        this.tapData = [0x43,0x36,0x34,0x2d,0x54,0x41,0x50,0x45,0x2d,0x52,0x41,0x57,0x01,0x00,0x00,0x00,0,0,0,0];
    }

    getWAVHeader() {
        const header = new Uint8Array(44);
        const view = new DataView(header.buffer);
        view.setUint32(0, 0x52494646, false); // "RIFF"
        view.setUint32(4, 44 + this.soundData.length, true); // ChunkSize
        view.setUint32(8, 0x57415645, false); // "WAVE"
        view.setUint32(12, 0x666d7420, false); // "fmt "
        view.setUint32(16, 16, true); // Subchunk1Size
        view.setUint16(20, 1, true); // AudioFormat (PCM)
        view.setUint16(22, 1, true); // NumChannels
        view.setUint32(24, this.sampleRate, true); // SampleRate
        view.setUint32(28, this.sampleRate * 2, true); // ByteRate
        view.setUint16(32, 1, true); // BlockAlign
        view.setUint16(34, 8, true); // BitsPerSample
        view.setUint32(36, 0x64617461, false); // "data"
        view.setUint32(40, this.soundData.length, true); // Subchunk2Size
        return header;
    }

    addSilence(lengthInSeconds: number): void {
        const numberOfSamples = Math.floor(this.sampleRate * lengthInSeconds);
        for (let i = 0; i < numberOfSamples; i++) {
            this.soundData.push(0);
        }
        //For v1 and v2 TAP files, a $00 value is followed by 3 bytes containing the actual duration measured in clock cycles (not divided by 8). These 3 bytes are in low-high format.
        const numCycles = TAPFile.CLOCK_RATE * lengthInSeconds;
        this.tapData.push(0);
        this.tapData.push(numCycles & 0xff);
        this.tapData.push((numCycles >> 8) & 0xff);
        this.tapData.push((numCycles >> 16) & 0xff);
    }

    addCycle(cycles: number): void {
        this.tapData.push(cycles);
        const numberOfSamples = Math.floor(this.sampleRate * TAPFile.TAP_LENGTH_IN_SECONDS * cycles);
        for (let i = 0; i < numberOfSamples; i++) {
            let value;
            if (this.options.sine_wave) {
                value = - Math.sin((i / numberOfSamples) * 2.0 * Math.PI);
            } else {
                if (i < numberOfSamples / 2) {
                    value = -1;
                } else {
                    value = 1;
                }
            }
            if (this.options.invert_waveform) {
                value = -value;
            }
            this.soundData.push(Math.round(128 + value * 127));
        }
    }

    updateTAPHeader() {
        let datalen = this.tapData.length - 0x14;
        // set bytes 0x10-0x13 to length
        this.tapData[0x10] = datalen & 0xff;
        this.tapData[0x11] = (datalen >> 8) & 0xff;
        this.tapData[0x12] = (datalen >> 16) & 0xff;
        this.tapData[0x13] = (datalen >> 24) & 0xff;
    }

    getTAPData() {
        this.updateTAPHeader();
        return new Uint8Array(this.tapData);
    }

    getSoundData() {
        let header = this.getWAVHeader();
        let data = new Uint8Array(header.length + this.soundData.length);
        data.set(header, 0);
        data.set(new Uint8Array(this.soundData), header.length);
        return data;
    }
}

export class TAPFile {
    
    static CLOCK_RATE = 985248.0;
    static TAP_LENGTH_IN_SECONDS = 8.0 / this.CLOCK_RATE;
    static FILENAME_BUFFER_SIZE = 0x10;
    static FILE_TYPE_NONE = 0;
    static FILE_TYPE_RELOCATABLE = 1;
    static FILE_TYPE_SEQUENTIAL = 2;
    static FILE_TYPE_NON_RELOCATABLE = 3;
    static LEADER_TYPE_HEADER = 0;
    static LEADER_TYPE_CONTENT = 1;
    static LEADER_TYPE_REPEATED = 2;
    static NUMBER_OF_PADDING_BYTES = 171;
    static PADDING_CHARACTER = 0x20;
    static SHORT_PULSE = 0x30;
    static MEDIUM_PULSE = 0x42;
    static LONG_PULSE = 0x56;

    options: any;
    checksum: number;
    data: Uint8Array;
    filenameData: number[];
    startAddress: number;
    endAddress: number;
    fileType: number;
    waveFile: OutputSoundFile;

    constructor(filename: string, options?: any) {
        this.options = options;
        this.checksum = 0;
        this.data = new Uint8Array(0);
        this.filenameData = this.makeFilename(filename);
        this.startAddress = 0;
        this.endAddress = 0;
        this.fileType = TAPFile.FILE_TYPE_NONE;
        this.waveFile = null;
    }

    makeFilename(filename: string): number[] {
        const filenameBuffer = [];
        const space = 0x20;
        filename = filename.toUpperCase(); // for PETSCII
        for (let i = 0; i < TAPFile.FILENAME_BUFFER_SIZE; i++) {
            if (filename.length <= i) {
                filenameBuffer.push(space);
            } else {
                let ch = filename.charCodeAt(i);
                filenameBuffer.push(ch);
            }
        }
        return filenameBuffer;
    }

    setContent(inputFile: { data: Uint8Array, startAddress: number, type: number }): void {
        this.data = inputFile.data;
        this.startAddress = inputFile.startAddress;
        this.endAddress = inputFile.startAddress + inputFile.data.length;
        this.fileType = inputFile.type;
    }

    generateSound(outputWaveFile: OutputSoundFile): void {
        this.waveFile = outputWaveFile;
        this.addHeader(false);
        this.addHeader(true);
        outputWaveFile.addSilence(0.1);
        this.addFile();
    }

    addTapCycle(tapValue: number): void {
        this.waveFile.addCycle(tapValue);
    }

    addBit(value: number): void {
        if (value === 0) {
            this.addTapCycle(TAPFile.SHORT_PULSE);
            this.addTapCycle(TAPFile.MEDIUM_PULSE);
        } else {
            this.addTapCycle(TAPFile.MEDIUM_PULSE);
            this.addTapCycle(TAPFile.SHORT_PULSE);
        }
    }

    addDataMarker(moreToFollow: boolean): void {
        if (moreToFollow) {
            this.addTapCycle(TAPFile.LONG_PULSE);
            this.addTapCycle(TAPFile.MEDIUM_PULSE);
        } else {
            this.addTapCycle(TAPFile.LONG_PULSE);
            this.addTapCycle(TAPFile.SHORT_PULSE);
        }
    }

    resetChecksum(): void {
        this.checksum = 0;
    }

    addByteFrame(value: number, moreToFollow: boolean): void {
        let checkBit = 1;
        for (let i = 0; i < 8; i++) {
            const bit = (value & (1 << i)) !== 0 ? 1 : 0;
            this.addBit(bit);
            checkBit ^= bit;
        }
        this.addBit(checkBit);
        this.addDataMarker(moreToFollow);
        this.checksum ^= value;
    }

    addLeader(fileType: number): void {
        let numberofPulses;
        if (fileType === TAPFile.LEADER_TYPE_HEADER) {
            numberofPulses = 0x6a00;
        } else if (fileType === TAPFile.LEADER_TYPE_CONTENT) {
            numberofPulses = 0x1a00;
        } else {
            numberofPulses = 0x4f;
        }
        for (let i = 0; i < numberofPulses; i++) {
            this.addTapCycle(TAPFile.SHORT_PULSE);
        }
    }

    addSyncChain(repeated: boolean): void {
        let value;
        if (repeated) {
            value = 0x09;
        } else {
            value = 0x89;
        }
        let count = 9;
        while (count > 0) {
            this.addByteFrame(value, true);
            value -= 1;
            count -= 1;
        }
    }

    addData(): void {
        for (let i = 0; i < this.data.length; i++) {
            this.addByteFrame(this.data[i], true);
        }
    }

    addFilename(): void {
        for (let i = 0; i < this.filenameData.length; i++) {
            this.addByteFrame(this.filenameData[i], true);
        }
    }

    addHeader(repeated: boolean): void {
        if (repeated) {
            this.addLeader(TAPFile.LEADER_TYPE_REPEATED);
        } else {
            this.addLeader(TAPFile.LEADER_TYPE_HEADER);
        }
        this.addDataMarker(true);
        this.addSyncChain(repeated);
        this.resetChecksum();
        this.addByteFrame(this.fileType, true);
        this.addByteFrame(this.startAddress & 0x00ff, true);
        this.addByteFrame((this.startAddress & 0xff00) >> 8, true);
        this.addByteFrame(this.endAddress & 0x00ff, true);
        this.addByteFrame((this.endAddress & 0xff00) >> 8, true);
        this.addFilename();
        for (let i = 0; i < TAPFile.NUMBER_OF_PADDING_BYTES; i++) {
            this.addByteFrame(TAPFile.PADDING_CHARACTER, true);
        }
        this.addByteFrame(this.checksum, false);
    }

    addFile(): void {
        let repeated = false;
        for (let i = 0; i < 2; i++) {
            if (!repeated) {
                this.addLeader(TAPFile.LEADER_TYPE_CONTENT);
            } else {
                this.addLeader(TAPFile.LEADER_TYPE_REPEATED);
            }
            this.addDataMarker(true);
            this.addSyncChain(repeated);
            this.resetChecksum();
            this.addData();
            this.addByteFrame(this.checksum, false);
            repeated = true;
        }
        this.addLeader(1);
    }
}
