namespace AdventOfCode2019_07_2
{
	const DAY     = 7;
	const PROBLEM = 2;

	export async function run()
	{
		let input = await AdventOfCode.getInput(DAY);
		if (input == null) return;

		let code = input.split(",").map(p => parseInt(p.trim()));

		let outputs: number[] = [];

		for(let i=0; i < 5*5*5*5*5; i++)
		{
			let v = i;
			let a1 = v%5 + 5; v = Math.floor(v/5);
			let a2 = v%5 + 5; v = Math.floor(v/5);
			let a3 = v%5 + 5; v = Math.floor(v/5);
			let a4 = v%5 + 5; v = Math.floor(v/5);
			let a5 = v%5 + 5; v = Math.floor(v/5);

			if ([a1,a2,a3,a4,a5].sort((a, b) => a - b).filter((v,i,s) => s.indexOf(v)===i).length !== 5) continue;

			const r = evalConfiguration(code, a1,a2,a3,a4,a5)
			
			outputs.push(r);
			AdventOfCode.outputConsole([a1,a2,a3,a4,a5].toString() + "  -->  " + r);
		}

		const max = outputs.sort((a, b) => a - b).reverse()[0];

		AdventOfCode.output(DAY, PROBLEM, max.toString());
	}

	function evalConfiguration(code: number[], a1 : number, a2 : number, a3 : number, a4 : number, a5 : number) : number
	{
		const runner1 = new Interpreter(code, [a1, 0]);
		const runner2 = new Interpreter(code, [a2]);
		const runner3 = new Interpreter(code, [a3]);
		const runner4 = new Interpreter(code, [a4]);
		const runner5 = new Interpreter(code, [a5]);

		let optr1 = 0;
		let optr2 = 0;
		let optr3 = 0;
		let optr4 = 0;
		let optr5 = 0;

		while (!runner5.is_halted)
		{
			runner1.singleStep();
			if (runner1.output.length > optr1)
			{
				runner2.inputqueue.push(runner1.output[optr1]); 
				optr1++; 
			}

			runner2.singleStep();
			if (runner2.output.length > optr2)
			{ 
				runner3.inputqueue.push(runner2.output[optr2]); 
				optr2++; 
			}

			runner3.singleStep();
			if (runner3.output.length > optr3)
			{ 
				runner4.inputqueue.push(runner3.output[optr3]); 
				optr3++; 
			}

			runner4.singleStep();
			if (runner4.output.length > optr4)
			{ 
				runner5.inputqueue.push(runner4.output[optr4]); 
				optr4++; 
			}

			runner5.singleStep();
			if (runner5.output.length > optr5)
			{
				runner1.inputqueue.push(runner5.output[optr5]);
				optr5++;
			}
		}

		return runner5.output.reverse()[0];
	}

	class Interpreter
	{
		program: number[];
		inputqueue: number[];
		instructionpointer: number;
		output: number[];

		is_halted: boolean = false;

		constructor(prog: number[], input: number[])
		{
			this.program = Object.assign([], prog);
			this.inputqueue = Object.assign([], input);
			this.instructionpointer = 0;
			this.output = [];
		}

		fullRun() : number[]
		{
			while(!this.is_halted)
			{
				const r = this.singleStep();

				if (r === StepResult.EXECUTED) continue;
				if (r === StepResult.HALTED) return this.output;
				if (r === StepResult.WAITING_FOR_IN) throw "not enough input";

				throw "unknown output of singleStep";
			}

			return this.output;
		}

		singleStep() : StepResult
		{
			const cmd = new Op(this.program[this.instructionpointer]);

			if (cmd.opcode == OpCode.ADD)
			{
				const p0 = cmd.getParameter(this, 0);
				const p1 = cmd.getParameter(this, 1);
				const pv = p0 + p1;
				cmd.setParameter(this, 2, pv);

				this.incInstrPtr(cmd);
				
				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.MUL)
			{
				const p0 = cmd.getParameter(this, 0);
				const p1 = cmd.getParameter(this, 1);
				const pv = p0 * p1;
				cmd.setParameter(this, 2, pv);

				this.incInstrPtr(cmd);
				
				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.HALT)
			{
				this.is_halted = true;
				return StepResult.HALTED;
			}
			else if (cmd.opcode == OpCode.IN)
			{
				if (this.inputqueue.length == 0) return StepResult.WAITING_FOR_IN;

				const pv = this.inputqueue[0];
				cmd.setParameter(this, 0, pv);
				this.inputqueue = this.inputqueue.slice(1);

				this.incInstrPtr(cmd);
				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.OUT)
			{
				const p0 = cmd.getParameter(this, 0);
				this.output.push(p0);

				this.incInstrPtr(cmd);

				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.TJMP)
			{
				const p0 = cmd.getParameter(this, 0);
				if (p0 != 0) this.instructionpointer = cmd.getParameter(this, 1);
				else this.incInstrPtr(cmd);
				
				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.FJMP)
			{
				const p0 = cmd.getParameter(this, 0);
				if (p0 == 0) this.instructionpointer = cmd.getParameter(this, 1);
				else this.incInstrPtr(cmd);
				
				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.LT)
			{
				const p0 = cmd.getParameter(this, 0);
				const p1 = cmd.getParameter(this, 1);
				const pv = p0 < p1 ? 1 : 0;
				cmd.setParameter(this, 2, pv);

				this.incInstrPtr(cmd);
				return StepResult.EXECUTED;
			}
			else if (cmd.opcode == OpCode.EQ)
			{
				const p0 = cmd.getParameter(this, 0);
				const p1 = cmd.getParameter(this, 1);
				const pv = p0 == p1 ? 1 : 0;
				cmd.setParameter(this, 2, pv);

				this.incInstrPtr(cmd);
				return StepResult.EXECUTED;
			}
			else throw "Unknown Op: " + cmd.opcode + " @ " + this.instructionpointer;
		}

		private incInstrPtr(cmd: Op)
		{
			this.instructionpointer += 1 + cmd.parametercount;
		}
	}

	enum StepResult { EXECUTED, HALTED, WAITING_FOR_IN }

	enum OpCode 
	{
		ADD  = 1,
		MUL  = 2,
		IN   = 3,
		OUT  = 4,
		TJMP = 5,
		FJMP = 6,
		LT   = 7,
		EQ   = 8,
		HALT = 99,
	}

	enum ParamMode
	{
		POSITION_MODE  = 0,
		IMMEDIATE_MODE = 1,
	}

	class Op
	{
		opcode: OpCode;
		modes: ParamMode[];

		name: string;
		parametercount: number;

		constructor(v: number)
		{
			this.opcode = v%100;
			v = Math.floor(v/100);
			this.modes = [];
			for(let i=0; i<4; i++) 
			{
				this.modes.push(v%10);
				v = Math.floor(v/10);
			}

			     if (this.opcode == OpCode.ADD)  { this.name="ADD";   this.parametercount=3; }
			else if (this.opcode == OpCode.MUL)  { this.name="MUL";   this.parametercount=3; }
			else if (this.opcode == OpCode.HALT) { this.name="HALT";  this.parametercount=0; }
			else if (this.opcode == OpCode.IN)   { this.name="IN";    this.parametercount=1; }
			else if (this.opcode == OpCode.OUT)  { this.name="OUT";   this.parametercount=1; }
			else if (this.opcode == OpCode.TJMP) { this.name="TJMP";  this.parametercount=2; }
			else if (this.opcode == OpCode.FJMP) { this.name="FJMP";  this.parametercount=2; }
			else if (this.opcode == OpCode.LT)   { this.name="LT";    this.parametercount=3; }
			else if (this.opcode == OpCode.EQ)   { this.name="EQ";    this.parametercount=3; }
			else throw "Unknown opcode: "+this.opcode;
		}

		getParameter(proc: Interpreter, index: number): number
		{
			const prog = proc.program;
			const ip   = proc.instructionpointer;

			let p = prog[ip+1+index];

				 if (this.modes[index] == ParamMode.POSITION_MODE)  p = prog[p];
			else if (this.modes[index] == ParamMode.IMMEDIATE_MODE) p = p;
			else throw "Unknown ParamMode: "+this.modes[index];

			return p;
		}

		setParameter(proc: Interpreter, index: number, value: number): void
		{
			const prog = proc.program;
			const ip   = proc.instructionpointer;

			let p = prog[ip+1+index];

				 if (this.modes[index] == ParamMode.POSITION_MODE)  prog[p] = value;
			else if (this.modes[index] == ParamMode.IMMEDIATE_MODE) throw "Immediate mode not allowed in write";
			else throw "Unknown ParamMpde: "+this.modes[index];
		}
	}
}
