package kr.util.sample;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

import kr.midireader.krwriter.IMA4;
import kr.midireader.krwriter.IMA4State;
import kr.util.audio.AudioClip;
import kr.util.audio.MemAudioClip;

/**
 * KR wav file format is a stereo MS IMA4 format with a block align value of
 * 0x48. Also the format type is set to 0x69 rather than 0x11.
 */
public class KRWavWriter {

	private int blockAlign;

	private int bytesPerSecond;

	protected int maxBufSize = 32768; // Don't reduce this value below 2048

	protected byte[] buf = new byte[maxBufSize];

	protected int bufOffset;

	protected int bufLength;

	private int filePointer;

	private RandomAccessFile stream;

	private int dataSizeOffset;

	private int fileSize;

	private int inputframeSizeInBytes = 65; // to produce the correct block
												// align
	
	private IMA4State ima4stateL, ima4stateR;

	private AudioClip ac;

	private int numberOfSamplesOffset;

	public KRWavWriter(AudioClip ac, File outFile) throws FileNotFoundException {
		this.ac = ac;
		outFile.delete();
		stream = new RandomAccessFile(outFile, "rw");
		ima4stateL = new IMA4State();
		ima4stateR = new IMA4State();
	}

	protected void writeHeader() throws IOException {
		int formatSize = 20; // Minimum formatSize
		// bufClear();
		
		int sampleSizeInBits = 4; 

		bufWriteBytes("RIFF");
		bufWriteInt(0); // This is filled in later when filesize is known
		bufWriteBytes("WAVE");

		bufWriteBytes("fmt "); // Format Chunk

		bufWriteIntLittleEndian(formatSize);
		int frameSizeInBits = ((inputframeSizeInBytes - 1) * 8 / 2 + 4 * 8) * 2;
		if (frameSizeInBits > 0) {
			blockAlign = frameSizeInBits / 8;
		} else {
			blockAlign = (sampleSizeInBits / 8) * 2;
		}

		bufWriteShortLittleEndian((short) 0x11); // encoding

		bufWriteShortLittleEndian((short) 2); //channels

		bufWriteIntLittleEndian((int) ac.getRate());

		bytesPerSecond = ((int)ac.getRate()) * 2 / 8 *
		frameSizeInBits / (inputframeSizeInBytes*2);

		bufWriteIntLittleEndian(bytesPerSecond);

		bufWriteShortLittleEndian((short) blockAlign);

		bufWriteShortLittleEndian((short) sampleSizeInBits);
		
		bufWriteShortLittleEndian((short)2); //extra format data length
		bufWriteShortLittleEndian((short)inputframeSizeInBytes); //extra format data length
		
	    bufWriteBytes("fact"); // Fact Chunk
	    bufWriteIntLittleEndian(4);
	    numberOfSamplesOffset = filePointer;
	    bufWriteInt(0);   // This is filled in later when numberOfSamples is known

		bufWriteBytes("data"); // Data Chunk
		dataSizeOffset = (int) filePointer;
		bufWriteInt(0); // This is filled in later when datasize is known
		/*
		 * fileSize = 4 + // for RIFF 4 + // file length 4 + // for WAVE
		 * 
		 * 4 + // 'fmt ' chunk id 4 + // 'fmt ' chunk size field formatSize + 4 + //
		 * 'data' chunk id 4; // 'data' chunk size field
		 */
		bufFlush();
	}

	protected void writeBody() throws IOException {

		int inCount = 0;
		int outCount = 0;

		final int frames = ac.length() / (2 * inputframeSizeInBytes);

		int iterations = (inputframeSizeInBytes - 1);

		// MSDVI stereo format is a mess !
		// header left (same as mono header)
		// header right (same as mono header)
		// 4 bytes left (8 samples)
		// 4 bytes right (8 samples)
		// 4 bytes left (8 samples)
		// 4 bytes right (8 samples)
		// : : :
		// : : :
		// 4 bytes left (8 samples)
		// 4 bytes right (8 samples)

		// ima4stateL,R are stored between chunks encoding.
		
		int readPtr = 0;
		
		byte [] inpData = new byte[16];
		byte [] outData = new byte[4];

		// System.out.println("iter "+iterations +" frames "+frames);
		for (int frameCounter = 0; frameCounter < frames; frameCounter++) {
			// LEFT or mono

			int valprev;
			if(readPtr + inCount > ac.length()) valprev = 0;
			else valprev = ((int)ac.byteAt(readPtr + (inCount++))& 0xff) + ((int)ac.byteAt(readPtr + (inCount++))) * 256;

			ima4stateL.valprev = valprev;
			// validate index legality
			if (ima4stateL.index > 88) {
				ima4stateL.index = 88;
			} else if (ima4stateL.index < 0) {
				ima4stateL.index = 0;
			}

			bufWriteShortLittleEndian((short)valprev);
			bufWriteShortLittleEndian((short)ima4stateL.index);

			if(readPtr + inCount > ac.length()) valprev = 0;
			else 
			{
				valprev = ((int)ac.byteAt(readPtr + (inCount++))& 0xff) + ((int)ac.byteAt(readPtr + (inCount++))) * 256;
			}

			ima4stateR.valprev = valprev;
			// validate index legality
			if (ima4stateR.index > 88) {
				ima4stateR.index = 88;
			} else if (ima4stateR.index < 0) {
				ima4stateR.index = 0;
			}

			bufWriteShortLittleEndian((short)valprev);
			bufWriteShortLittleEndian((short)ima4stateR.index);
			

			for (int loop = 0; loop < iterations / 8; loop++) {
				for(int i = 0; i < 8; i++)
				{
					if(i*4+inCount < ac.length())
					{
						inpData[i*2+1] = ac.byteAt(i*4 + inCount+1 );
						inpData[i*2] = ac.byteAt(i*4 + inCount );						
					}
					else inpData[i*2+1] = 0;
				}

				IMA4.encode(inpData, 0, outData, 0, 8, ima4stateL, 0);
				bufWriteBytes(outData);
				outCount += (8 >> 1);

				for(int i = 0; i < 8; i++)
				{
					if(i*4+inCount+3 < ac.length())
					{
						inpData[i*2+1] = ac.byteAt(i*4 + inCount+3 );
						inpData[i*2] = ac.byteAt(i*4 + inCount+2 );						
					}
					else inpData[i*2+1] = 0;
				}

				IMA4.encode(inpData, 0, outData,
						0, 8, ima4stateR, 0);
				bufWriteBytes(outData);
				outCount += (8 >> 1);
				inCount += (8 << 2);

			}
			
			bufFlush();

		}
	}

	protected void writeFooter() throws IOException {
		// Write the file size
		seek(4);
		bufClear();
		bufWriteIntLittleEndian(fileSize - 8); // -8 since we should skip the
		// first
		// two ints
		bufFlush();
		// Write the data size
		seek(dataSizeOffset);
		int dataSize = fileSize - (dataSizeOffset + 4);
		bufWriteIntLittleEndian(dataSize);
	    bufFlush();

		// Calculate numberOfSamples
	    // bytesPerSecond won't be zero
	    float duration = (float) dataSize / bytesPerSecond;
	    int numberOfSamples = (int) (duration * ac.getRate());
	    seek(numberOfSamplesOffset);
	    bufWriteIntLittleEndian(numberOfSamples);
	    bufFlush();
	}

	protected void bufClear() {
		bufOffset = 0;
		bufLength = 0;
	}

	protected void bufSkip(int size) {
		bufOffset += size;
		bufLength += size;
		filePointer += size;
	}

	protected void bufWriteBytes(String s) {
		byte[] bytes = s.getBytes();
		bufWriteBytes(bytes);
	}

	protected void bufWriteBytes(byte[] bytes) {
		System.arraycopy(bytes, 0, buf, bufOffset, bytes.length);
		bufOffset += bytes.length;
		bufLength += bytes.length;
		filePointer += bytes.length;
	}

	protected void bufWriteInt(int value) {
		buf[bufOffset + 0] = (byte) ((value >> 24) & 0xFF);
		buf[bufOffset + 1] = (byte) ((value >> 16) & 0xFF);
		buf[bufOffset + 2] = (byte) ((value >> 8) & 0xFF);
		buf[bufOffset + 3] = (byte) ((value >> 0) & 0xFF);
		bufOffset += 4;
		bufLength += 4;
		filePointer += 4;
	}

	protected void bufWriteIntLittleEndian(int value) {
		buf[bufOffset + 3] = (byte) ((value >>> 24) & 0xFF);
		buf[bufOffset + 2] = (byte) ((value >>> 16) & 0xFF);
		buf[bufOffset + 1] = (byte) ((value >>> 8) & 0xFF);
		buf[bufOffset + 0] = (byte) ((value >>> 0) & 0xFF);
		bufOffset += 4;
		bufLength += 4;
		filePointer += 4;
	}

	protected void bufWriteShort(short value) {
		buf[bufOffset + 0] = (byte) ((value >> 8) & 0xFF);
		buf[bufOffset + 1] = (byte) ((value >> 0) & 0xFF);
		bufOffset += 2;
		bufLength += 2;
		filePointer += 2;
	}

	protected void bufWriteShortLittleEndian(short value) {
		buf[bufOffset + 1] = (byte) ((value >> 8) & 0xFF);
		buf[bufOffset + 0] = (byte) ((value >> 0) & 0xFF);
		bufOffset += 2;
		bufLength += 2;
		filePointer += 2;
	}

	protected void bufWriteByte(byte value) {
		buf[bufOffset] = value;
		bufOffset++;
		bufLength++;
		filePointer++;
	}

	protected void bufFlush() throws IOException {
		filePointer -= bufLength; // It is going to be incremented in write()
		write(buf, 0, bufLength);
		bufClear();
	}

	protected int seek(int location) throws IOException {
		stream.seek(location);
		return location;
	}

	protected int write(byte[] data, int offset, int length) throws IOException {
		if (length > 0) {
			filePointer += length;
			if (filePointer > fileSize)
				fileSize = filePointer;
		}

		stream.write(data, offset, length);

		return length;
	}
	
	public void writeWav() throws IOException
	{
		writeHeader();
		writeBody();
		writeFooter();
	}

}

class KRWavWriterTest
{
	public static void main(String [] argv) throws Exception
	{
		MemAudioClip ac = new MemAudioClip(2,48000,16);
		
		KRWavWriter w = new KRWavWriter(ac, new File(argv[1]));
		
		w.writeWav();
		System.exit(0);
	}
}
