import std.bitmanip;
import std.conv;

import dayutil;

immutable int MEM_SIZE = 36;

enum Operation {
	WRITE_BITMASK,
	WRITE_MEM
}

enum Value {
	ZERO,
	ONE,
	X,
}

struct Instruction {
	Operation op;
	long address;
	Value[] value;
}

Variant run(int part, File input, bool bigboy, string[] args) {
	auto parsed = input.byLineCopy.parseInput;

	return Variant(parts!long(part,
				() => part1(parsed),
				() => part2(parsed)));
}

Instruction[] parseInput(Range)(Range range) if (isInputRange!Range && isSomeString!(ElementType!Range)) {
	return range.map!((x) {
		Instruction instr;
		if (x.startsWith("mask = ")) {
			instr.op = Operation.WRITE_BITMASK;
			instr.value.reserve(x.length - 8);
			foreach(c; x[7..$]) {
				switch(c) {
				case 'X':
					instr.value ~= [Value.X];
					break;
				case '1':
					instr.value ~= [Value.ONE];
					break;
				case '0':
					instr.value ~= [Value.ZERO];
					break;
				default:
					assert(false, "Invalid input");
				}
			}
		} else {
			instr.op = Operation.WRITE_MEM;
			size_t secondBracket = x.countUntil(']');
			instr.address = to!long(x[4..secondBracket]);
			long tmp = to!long(x[secondBracket + 4..$]);
			instr.value = toStupidBinary(tmp);
		}
		return instr;
	}).array;
}

Value[] toStupidBinary(long val) {
	Value[] result;
	while (val != 0) {
		result ~= [(val & 1) == 1 ? Value.ONE : Value.ZERO];
		val >>= 1;
	}
	result.length = MEM_SIZE;
	return result.reverse;
}

long fromStupidBinary(Value[] val) {
	long result;
	foreach(bit; val) {
		result <<= 1;
		if (bit == Value.ONE) {
			result |= 1;
		}
	}
	return result;
}

long part1(Instruction[] instructions) {
	Value[] bitmask;
	long[long] memory;
	foreach(instruction; instructions) {
		final switch(instruction.op) {
			case Operation.WRITE_BITMASK:
				bitmask = instruction.value;
				break;
			case Operation.WRITE_MEM:
				Value[] tmp = instruction.value;
				foreach(ref bit, mask; lockstep(tmp.retro, bitmask.retro)) {
					final switch(mask) {
						case Value.ZERO:
							bit = Value.ZERO;
							break;
						case Value.ONE:
							bit = Value.ONE;
							break;
						case Value.X: break;
					}
				}
				//writeln(tmp);
				memory[instruction.address] = fromStupidBinary(tmp);
				break;
		}
	}
	long sum = 0;
	foreach(e; memory.byValue()) {
		//writeln(e);
		sum += e;
	}
	return sum;
}

unittest {
	string input = q"EOS
mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X
mem[8] = 11
mem[7] = 101
mem[8] = 0
EOS";
	auto parsed = parseInput(input.lineSplitter);
	assert(part1(parsed) == 165);
}

long[] addressPermutations(Value[] address) {
	import std.math;
	long[] addresses;
	addresses ~= [0];
	addresses.reserve(pow(2, addresses.count(Value.X)));

	foreach(idx, bit; address.retro.enumerate) {
		if (bit == Value.X) {
			auto duplicate  = addresses.dup;
			foreach(ref address2; duplicate) {
				address2 += (cast(long) 1 << idx);
			}
			addresses ~= duplicate;

		} else if (bit == Value.ONE) {
			foreach(ref address2; addresses) {
				address2 += (cast(long) 1 << idx);
			}
		}
	}
	// debug writeln(address);
	foreach(address2; addresses) {
		// debug writeln("Address: %064b".format(address2));
	}
	assert(addresses.length == pow(2, address.count(Value.X)));
	assert(addresses.uniq.array.length == addresses.length);
	return addresses;
}

long part2(Instruction[] instructions) {
	Value[] bitmask;
	long[long] memory;
	foreach(instruction; instructions) {
		final switch(instruction.op) {
			case Operation.WRITE_BITMASK:
				bitmask = instruction.value;
				break;
			case Operation.WRITE_MEM:
				Value[] tmp = toStupidBinary(instruction.address);
				foreach(ref bit, mask; lockstep(tmp, bitmask)) {
					final switch(mask) {
						case Value.ZERO: break;
						case Value.ONE:
							bit = Value.ONE;
							break;
						case Value.X: 
							bit = Value.X;
							break;
					}
				}
				//writeln(tmp);
				foreach(long address; addressPermutations(tmp)) {
					//writeln("Writing to address ", address);
					memory[address] = fromStupidBinary(instruction.value);
				}
				break;
		}
	}

	return memory.byValue().sum;
}

unittest {
	string input = q"EOS
mask = 000000000000000000000000000000X1001X
mem[42] = 100
mask = 00000000000000000000000000000000X0XX
mem[26] = 1
EOS";
	auto parsed = parseInput(input.lineSplitter);
	assert(part2(parsed) == 208);
}
