package com.nexuiz.demorecorder.application.democutter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class DemoCutter {

	private static final byte CDTRACK_SEPARATOR = 0x0A;

	private DataInputStream inStream;
	private DataOutputStream outStream;
	private File inFile;
	private File outFile;

	/**
	 * Calls the cutDemo method with reasonable default values for the second and first fast-forward stage.
	 * @param inFile @see other cutDemo method
	 * @param outFile @see other cutDemo method
	 * @param startTime @see other cutDemo method
	 * @param endTime @see other cutDemo method
	 * @param injectAtStart @see other cutDemo method
	 * @param injectBeforeCap @see other cutDemo method
	 * @param injectAfterCap @see other cutDemo method
	 */
	public void cutDemo(File inFile, File outFile, float startTime, float endTime, String injectAtStart, String injectBeforeCap, String injectAfterCap) {
		this.cutDemo(inFile, outFile, startTime, endTime, injectAtStart, injectBeforeCap, injectAfterCap, 100, 10);
	}
	
	/**
	 * Cuts the demo by injecting a 2-phase fast forward command until startTime is reached, then injects the cl_capturevideo 1 command
	 * and once endTime is reached the cl_capturevideo 0 command is injected.
	 * @param inFile the original demo file
	 * @param outFile the new cut demo file
	 * @param startTime when to start capturing (use the gametime in seconds)
	 * @param endTime when to stop capturing
	 * @param injectAtStart a String that will be injected right at the beginning of the demo
	 * 						can be anything that would make sense and can be parsed by DP's console
	 * @param injectBeforeCap a String that will be injected 5 seconds before capturing starts
	 * @param injectAfterCap a String that will be injected shortly after capturing ended
	 * @param ffwSpeedFirstStage fast-forward speed at first stage, when the startTime is still about a minute away (use high values, e.g. 100)
	 * @param ffwSpeedSecondStage fast-forward speed when coming a few seconds close to startTime, use lower values e.g. 5 or 10
	 */
	public void cutDemo(File inFile, File outFile, float startTime, float endTime, String injectAtStart, String injectBeforeCap, String injectAfterCap, int ffwSpeedFirstStage, int ffwSpeedSecondStage) {
		this.inFile = inFile;
		this.outFile = outFile;
		this.prepareStreams();
		this.readCDTrack();
		injectAfterCap = this.checkInjectString(injectAfterCap);
		injectAtStart = this.checkInjectString(injectAtStart);
		injectBeforeCap = this.checkInjectString(injectBeforeCap);

		byte[] data;
		float svctime = -1;
		boolean firstLoop = true;
		String injectBuffer = "";
		int demoStarted = 0;
		boolean endIsReached = false;
		boolean finalInjectionDone = false;
		boolean disconnectIssued = false;
		float firstSvcTime = -1;
		float lastSvcTime = -1;
		
		try {
			while (true) {
				DemoPacket demoPacket = new DemoPacket(this.inStream);
				if (demoPacket.isEndOfFile()) {
					break;
				}
				
				if (demoPacket.isClientToServerPacket()) {
					try {
						this.outStream.write(demoPacket.getOriginalLengthAsByte());
						this.outStream.write(demoPacket.getAngles());
						this.outStream.write(demoPacket.getOriginalData());
					} catch (IOException e) {
						throw new DemoCutterException("Unexpected I/O Exception occurred when writing to the cut demo", e);
					}
					
					continue;
				}

				if (demoPacket.getSvcTime() != -1) {
					svctime = demoPacket.getSvcTime();
				}

				if (svctime != -1) {
					if (firstSvcTime == -1) {
						firstSvcTime = svctime;
					}
					lastSvcTime = svctime;
					
					if (demoStarted < 1 && svctime > (startTime - 50)) {
						injectBuffer = "slowmo " + ffwSpeedSecondStage;
						demoStarted = 1;
					}
					if (demoStarted < 2 && svctime > (startTime - 5)) {
						injectBuffer = "slowmo 1;" + injectBeforeCap;
						demoStarted = 2;
					}
					if (demoStarted < 3 && svctime > startTime) {
						injectBuffer = "cl_capturevideo 1";
						demoStarted = 3;
					}
					if (!endIsReached && svctime > endTime) {
						injectBuffer = "cl_capturevideo 0";
						endIsReached = true;
					}
					if (endIsReached && !finalInjectionDone && svctime > (endTime + 1)) {
						injectBuffer = injectAfterCap;
						finalInjectionDone = true;
					}
					if (finalInjectionDone && !disconnectIssued && svctime > (endTime + 2)) {
						injectBuffer = "disconnect";
						disconnectIssued = true;
					}
					// ensure injectAtStart runs exactly once, before everything else
					if (firstLoop) {
						injectBuffer = injectAtStart + ";slowmo " + ffwSpeedFirstStage + ";" + injectBuffer;
						firstLoop = false;
					}
					// add Buffer head and tail
					if (injectAtStart.length() > 0) {
						injectBuffer = "\011\n" + checkInjectString(injectBuffer) + "\n\000";
					}
				}

				byte[] injectBufferAsBytes = null;
				try {
					injectBufferAsBytes = injectBuffer.getBytes("US-ASCII");
				} catch (UnsupportedEncodingException e) {
					throw new DemoCutterException("Could not convert String to bytes using US-ASCII charset!", e);
				}

				data = demoPacket.getOriginalData();
				if ((injectBufferAsBytes.length + data.length) < 65536) {
					data = DemoCutterUtils.mergeByteArrays(injectBufferAsBytes, data);
					injectBuffer = "";
				}
				
				byte[] newLengthLittleEndian = DemoCutterUtils.convertLittleEndian(data.length);
				try {
					this.outStream.write(newLengthLittleEndian);
					this.outStream.write(demoPacket.getAngles());
					this.outStream.write(data);
				} catch (IOException e) {
					throw new DemoCutterException("Unexpected I/O Exception occurred when writing to the cut demo", e);
				}

			}
			
			if (startTime < firstSvcTime) {
				throw new DemoCutterException("Start time for the demo is " + startTime + ", but demo doesn't start before " + firstSvcTime);
			}
			if (endTime > lastSvcTime) {
				throw new DemoCutterException("End time for the demo is " + endTime + ", but demo already stops at " + lastSvcTime);
			}
		} catch (DemoCutterException e) {
			throw e;
		} catch (Throwable e) {
			throw new DemoCutterException("Internal error in demo cutter sub-route (invalid demo file?)", e);
		} finally {
			try {
				this.outStream.close();
				this.inStream.close();
			} catch (IOException e) {}
		}
	}

	

	/**
	 * Seeks forward in the inStream until CDTRACK_SEPARATOR byte was reached.
	 * All the content is copied to the outStream.
	 */
	private void readCDTrack() {
		byte lastByte;
		try {
			while ((lastByte = inStream.readByte()) != CDTRACK_SEPARATOR) {
				this.outStream.write(lastByte);
			}
			this.outStream.write(CDTRACK_SEPARATOR);
		} catch (EOFException e) {
			throw new DemoCutterException("Unexpected EOF occurred when reading CD track of demo " + inFile.getPath(), e);
		}
		catch (IOException e) {
			throw new DemoCutterException("Unexpected I/O Exception occurred when reading CD track of demo " + inFile.getPath(), e);
		}
	}

	private void prepareStreams() {
		try {
			this.inStream = new DataInputStream(new FileInputStream(this.inFile));
		} catch (FileNotFoundException e) {
			throw new DemoCutterException("Could not open demo file " + inFile.getPath(), e);
		}
		
		try {
			this.outStream = new DataOutputStream(new FileOutputStream(this.outFile));
		} catch (FileNotFoundException e) {
			throw new DemoCutterException("Could not open demo file " + outFile.getPath(), e);
		}
	}
	
	private String checkInjectString(String injectionString) {
		while (injectionString.endsWith(";") || injectionString.endsWith("\n")) {
			injectionString = injectionString.substring(0, injectionString.length()-1);
		}
		return injectionString;
	}
}
