require("mineunit")

fixture("mesecons_fpga")

describe("FPGA logic", function()
	local pos = {x = 0, y = 0, z = 0}
	local pos_a = {x = -1, y = 0, z =  0}
	local pos_b = {x =  0, y = 0, z =  1}
	local pos_c = {x =  1, y = 0, z =  0}
	local pos_d = {x =  0, y = 0, z = -1}

	local fpga_set = false

	local function set_fpga()
		if not fpga_set then
			world.set_node(pos, "mesecons_fpga:fpga0000")
			fpga_set = true
		end
	end
	before_each(set_fpga)

	local function reset_world()
		if fpga_set then
			mesecon._test_reset()
			world.clear()
			fpga_set = false
		end
	end
	after_each(reset_world)

	local function test_program(inputs, outputs, program)
		set_fpga()

		mesecon._test_program_fpga(pos, program)

		if inputs.a then mesecon._test_place(pos_a, "mesecons:test_receptor_on") end
		if inputs.b then mesecon._test_place(pos_b, "mesecons:test_receptor_on") end
		if inputs.c then mesecon._test_place(pos_c, "mesecons:test_receptor_on") end
		if inputs.d then mesecon._test_place(pos_d, "mesecons:test_receptor_on") end
		mineunit:execute_globalstep() -- Execute receptor_on actions
		mineunit:execute_globalstep() -- Execute activate/change actions

		local expected_name = "mesecons_fpga:fpga"
				.. (outputs.d and 1 or 0) .. (outputs.c and 1 or 0)
				.. (outputs.b and 1 or 0) .. (outputs.a and 1 or 0)
		assert.equal(expected_name, world.get_node(pos).name)

		reset_world()
	end

	it("operator and", function()
		local prog = {{"A", "AND", "B", "C"}}
		test_program({}, {}, prog)
		test_program({a = true}, {}, prog)
		test_program({b = true}, {}, prog)
		test_program({a = true, b = true}, {c = true}, prog)
	end)

	it("operator or", function()
		local prog = {{"A", "OR", "B", "C"}}
		test_program({}, {}, prog)
		test_program({a = true}, {c = true}, prog)
		test_program({b = true}, {c = true}, prog)
		test_program({a = true, b = true}, {c = true}, prog)
	end)

	it("operator not", function()
		local prog = {{"NOT", "A", "B"}}
		test_program({}, {b = true}, prog)
		test_program({a = true}, {}, prog)
	end)

	it("operator xor", function()
		local prog = {{"A", "XOR", "B", "C"}}
		test_program({}, {}, prog)
		test_program({a = true}, {c = true}, prog)
		test_program({b = true}, {c = true}, prog)
		test_program({a = true, b = true}, {}, prog)
	end)

	it("operator nand", function()
		local prog = {{"A", "NAND", "B", "C"}}
		test_program({}, {c = true}, prog)
		test_program({a = true}, {c = true}, prog)
		test_program({b = true}, {c = true}, prog)
		test_program({a = true, b = true}, {}, prog)
	end)

	it("operator buf", function()
		local prog = {{"=", "A", "B"}}
		test_program({}, {}, prog)
		test_program({a = true}, {b = true}, prog)
	end)

	it("operator xnor", function()
		local prog = {{"A", "XNOR", "B", "C"}}
		test_program({}, {c = true}, prog)
		test_program({a = true}, {}, prog)
		test_program({b = true}, {}, prog)
		test_program({a = true, b = true}, {c = true}, prog)
	end)

	it("operator nor", function()
		local prog = {{"A", "NOR", "B", "C"}}
		test_program({}, {c = true}, prog)
		test_program({a = true}, {}, prog)
		test_program({b = true}, {}, prog)
		test_program({a = true, b = true}, {}, prog)
	end)

	it("rejects duplicate operands", function()
		test_program({a = true}, {}, {{"A", "OR", "A", "B"}})
		test_program({a = true}, {}, {{"=", "A", "0"}, {"0", "OR", "0", "B"}})
	end)

	it("rejects unassigned memory operands", function()
		test_program({a = true}, {}, {{"A", "OR", "0", "B"}})
		test_program({a = true}, {}, {{"0", "OR", "A", "B"}})
	end)

	it("rejects double memory assignment", function()
		test_program({a = true}, {}, {{"=", "A", "0"}, {"=", "A", "0"}, {"=", "0", "B"}})
	end)

	it("rejects assignment to memory operand", function()
		test_program({a = true}, {}, {{"=", "A", "0"}, {"A", "OR", "0", "0"}, {"=", "0", "B"}})
	end)

	it("allows double port assignment", function()
		test_program({a = true}, {b = true}, {{"NOT", "A", "B"}, {"=", "A", "B"}})
	end)

	it("allows assignment to port operand", function()
		test_program({a = true}, {b = true}, {{"A", "OR", "B", "B"}})
	end)

	it("preserves initial pin states", function()
		test_program({a = true}, {b = true}, {{"=", "A", "B"}, {"=", "B", "C"}})
	end)

	it("rejects binary operations with single operands", function()
		test_program({a = true}, {}, {{"=", "A", "B"}, {" ", "OR", "A", "C"}})
		test_program({a = true}, {}, {{"=", "A", "B"}, {"A", "OR", " ", "C"}})
	end)

	it("rejects unary operations with first operands", function()
		test_program({a = true}, {}, {{"=", "A", "B"}, {"A", "=", " ", "C"}})
	end)

	it("rejects operations without destinations", function()
		test_program({a = true}, {}, {{"=", "A", "B"}, {"=", "A", " "}})
	end)

	it("allows blank statements", function()
		test_program({a = true}, {b = true, c = true}, {
			{" ", " ", " ", " "},
			{"=", "A", "B"},
			{" ", " ", " ", " "},
			{" ", " ", " ", " "},
			{"=", "A", "C"},
		})
	end)

	it("transmits output signals to adjacent nodes", function()
		mesecon._test_program_fpga(pos, {
			{"=", "A", "B"},
			{"=", "A", "C"},
			{"NOT", "A", "D"},
		})
		mesecon._test_place(pos_b, "mesecons:test_effector")
		mesecon._test_place(pos_c, "mesecons:test_effector")
		mesecon._test_place(pos_d, "mesecons:test_effector")
		mineunit:execute_globalstep() -- Execute receptor_on actions
		mineunit:execute_globalstep() -- Execute activate/change actions

		-- Makes an object from the last three effector events in the list for use with assert.same.
		-- This is necessary to ignore the ordering of events.
		local function event_tester(list)
			local o = {list[#list - 2], list[#list - 1], list[#list - 0]}
			table.sort(o, function(a, b)
				local fmt = "%s %d %d %d"
				return fmt:format(a[1], a[2].x, a[2].y, a[2].z) < fmt:format(b[1], b[2].x, b[2].y, b[2].z)
			end)
			return o
		end

		mesecon._test_place(pos_a, "mesecons:test_receptor_on")
		mineunit:execute_globalstep() -- Execute receptor_on action
		mineunit:execute_globalstep() -- Execute activate/change actions
		mineunit:execute_globalstep() -- Execute receptor_on/receptor_off actions
		mineunit:execute_globalstep() -- Execute activate/deactivate/change actions
		assert.equal("mesecons_fpga:fpga0110", world.get_node(pos).name)
		assert.same(event_tester({{"on", pos_b}, {"on", pos_c}, {"off", pos_d}}), event_tester(mesecon._test_effector_events))

		mesecon._test_dig(pos_a)
		mineunit:execute_globalstep() -- Execute receptor_off action
		mineunit:execute_globalstep() -- Execute deactivate/change actions
		mineunit:execute_globalstep() -- Execute receptor_on/receptor_off actions
		mineunit:execute_globalstep() -- Execute activate/deactivate/change actions
		assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name)
		assert.same(event_tester({{"off", pos_b}, {"off", pos_c}, {"on", pos_d}}), event_tester(mesecon._test_effector_events))
	end)

	it("considers past outputs in determining inputs", function()
		-- Memory cell: Turning on A turns on C; turning on B turns off C.
		mesecon._test_program_fpga(pos, {
			{"A", "OR", "C", "0"},
			{"B", "OR", "D", "1"},
			{"NOT", "A", "2"},
			{"NOT", "B", "3"},
			{"0", "AND", "3", "C"},
			{"1", "AND", "2", "D"},
		})

		mesecon._test_place(pos_a, "mesecons:test_receptor_on")
		mineunit:execute_globalstep() -- Execute receptor_on actions
		mineunit:execute_globalstep() -- Execute activate/change actions
		assert.equal("mesecons_fpga:fpga0100", world.get_node(pos).name)

		mesecon._test_dig(pos_a)
		mineunit:execute_globalstep() -- Execute receptor_off actions
		mineunit:execute_globalstep() -- Execute deactivate/change actions
		assert.equal("mesecons_fpga:fpga0100", world.get_node(pos).name)

		mesecon._test_place(pos_b, "mesecons:test_receptor_on")
		mineunit:execute_globalstep() -- Execute receptor_on actions
		mineunit:execute_globalstep() -- Execute activate/change actions
		assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name)

		mesecon._test_dig(pos_b)
		mineunit:execute_globalstep() -- Execute receptor_off actions
		mineunit:execute_globalstep() -- Execute deactivate/change actions
		assert.equal("mesecons_fpga:fpga1000", world.get_node(pos).name)
	end)
end)