const std = @import("std");

const main = @import("main");
const blocks = main.blocks;
const Block = blocks.Block;
const Neighbor = main.chunk.Neighbor;
const ModelIndex = main.models.ModelIndex;
const rotation = main.rotation;
const Degrees = rotation.Degrees;
const RayIntersectionResult = rotation.RayIntersectionResult;
const RotationMode = rotation.RotationMode;
const vec = main.vec;
const Mat4f = vec.Mat4f;
const Vec2f = vec.Vec2f;
const Vec3f = vec.Vec3f;
const Vec3i = vec.Vec3i;
const ZonElement = main.ZonElement;

var modelIndex: ?ModelIndex = null;

fn subBlockMask(x: u1, y: u1, z: u1) u8 {
	return @as(u8, 1) << ((@as(u3, x)*2 + @as(u3, y))*2 + z);
}
fn hasSubBlock(stairData: u8, x: u1, y: u1, z: u1) bool {
	return stairData & subBlockMask(x, y, z) == 0;
}

pub fn rotateZ(data: u16, angle: Degrees) u16 {
	@setEvalBranchQuota(65_536);

	comptime var rotationTable: [4][256]u8 = undefined;
	comptime for(0..4) |a| {
		for(0..256) |old| {
			var new: u8 = 0b11_11_11_11;

			for(0..2) |i| for(0..2) |j| for(0..2) |k| {
				const sin: f32 = @sin((std.math.pi/2.0)*@as(f32, @floatFromInt(a)));
				const cos: f32 = @cos((std.math.pi/2.0)*@as(f32, @floatFromInt(a)));

				const x: f32 = (@as(f32, @floatFromInt(i)) - 0.5)*2.0;
				const y: f32 = (@as(f32, @floatFromInt(j)) - 0.5)*2.0;

				const rX = @intFromBool(x*cos - y*sin > 0);
				const rY = @intFromBool(x*sin + y*cos > 0);

				if(hasSubBlock(@intCast(old), @intCast(i), @intCast(j), @intCast(k))) {
					new &= ~subBlockMask(rX, rY, @intCast(k));
				}
			};
			rotationTable[a][old] = new;
		}
	};
	if(data >= 256) return 0;
	return rotationTable[@intFromEnum(angle)][data];
}

pub fn init() void {}
pub fn deinit() void {}
pub fn reset() void {
	modelIndex = null;
}

const GreedyFaceInfo = struct {min: Vec2f, max: Vec2f};
fn mergeFaces(faceVisible: [2][2]bool, mem: []GreedyFaceInfo) []GreedyFaceInfo {
	var faces: usize = 0;
	if(faceVisible[0][0]) {
		if(faceVisible[0][1]) {
			if(faceVisible[1][0] and faceVisible[1][1]) {
				// One big face:
				mem[faces] = .{.min = .{0, 0}, .max = .{1, 1}};
				faces += 1;
			} else {
				mem[faces] = .{.min = .{0, 0}, .max = .{0.5, 1}};
				faces += 1;
				if(faceVisible[1][0]) {
					mem[faces] = .{.min = .{0.5, 0}, .max = .{1, 0.5}};
					faces += 1;
				}
				if(faceVisible[1][1]) {
					mem[faces] = .{.min = .{0.5, 0.5}, .max = .{1, 1}};
					faces += 1;
				}
			}
		} else {
			if(faceVisible[1][0]) {
				mem[faces] = .{.min = .{0, 0}, .max = .{1.0, 0.5}};
				faces += 1;
			} else {
				mem[faces] = .{.min = .{0, 0}, .max = .{0.5, 0.5}};
				faces += 1;
			}
			if(faceVisible[1][1]) {
				mem[faces] = .{.min = .{0.5, 0.5}, .max = .{1, 1}};
				faces += 1;
			}
		}
	} else {
		if(faceVisible[0][1]) {
			if(faceVisible[1][1]) {
				mem[faces] = .{.min = .{0, 0.5}, .max = .{1, 1}};
				faces += 1;
			} else {
				mem[faces] = .{.min = .{0, 0.5}, .max = .{0.5, 1}};
				faces += 1;
			}
			if(faceVisible[1][0]) {
				mem[faces] = .{.min = .{0.5, 0}, .max = .{1, 0.5}};
				faces += 1;
			}
		} else {
			if(faceVisible[1][0]) {
				if(faceVisible[1][1]) {
					mem[faces] = .{.min = .{0.5, 0}, .max = .{1, 1.0}};
					faces += 1;
				} else {
					mem[faces] = .{.min = .{0.5, 0}, .max = .{1, 0.5}};
					faces += 1;
				}
			} else if(faceVisible[1][1]) {
				mem[faces] = .{.min = .{0.5, 0.5}, .max = .{1, 1}};
				faces += 1;
			}
		}
	}
	return mem[0..faces];
}

pub fn createBlockModel(_: Block, _: *u16, _: ZonElement) ModelIndex {
	if(modelIndex) |idx| return idx;
	for(0..256) |i| {
		var quads = main.List(main.models.QuadInfo).init(main.stackAllocator);
		defer quads.deinit();
		for(Neighbor.iterable) |neighbor| {
			const xComponent = @abs(neighbor.textureX());
			const yComponent = @abs(neighbor.textureY());
			const normal = Vec3i{neighbor.relX(), neighbor.relY(), neighbor.relZ()};
			const zComponent = @abs(normal);
			const zMap: [2]@Vector(3, u32) = if(@reduce(.Add, normal) > 0) .{@splat(0), @splat(1)} else .{@splat(1), @splat(0)};
			var visibleFront: [2][2]bool = undefined;
			var visibleMiddle: [2][2]bool = undefined;
			for(0..2) |x| {
				for(0..2) |y| {
					const xSplat: @TypeOf(xComponent) = @splat(@intCast(x));
					const ySplat: @TypeOf(xComponent) = @splat(@intCast(y));
					const posFront = xComponent*xSplat + yComponent*ySplat + zComponent*zMap[1];
					const posBack = xComponent*xSplat + yComponent*ySplat + zComponent*zMap[0];
					visibleFront[x][y] = hasSubBlock(@intCast(i), @intCast(posFront[0]), @intCast(posFront[1]), @intCast(posFront[2]));
					visibleMiddle[x][y] = !visibleFront[x][y] and hasSubBlock(@intCast(i), @intCast(posBack[0]), @intCast(posBack[1]), @intCast(posBack[2]));
				}
			}
			const xAxis = @as(Vec3f, @floatFromInt(neighbor.textureX()));
			const yAxis = @as(Vec3f, @floatFromInt(neighbor.textureY()));
			const zAxis = @as(Vec3f, @floatFromInt(normal));
			// Greedy mesh it:
			var faces: [2]GreedyFaceInfo = undefined;
			const frontFaces = mergeFaces(visibleFront, &faces);
			for(frontFaces) |*face| {
				var xLower = @abs(xAxis)*@as(Vec3f, @splat(face.min[0]));
				var xUpper = @abs(xAxis)*@as(Vec3f, @splat(face.max[0]));
				if(@reduce(.Add, xAxis) < 0) std.mem.swap(Vec3f, &xLower, &xUpper);
				var yLower = @abs(yAxis)*@as(Vec3f, @splat(face.min[1]));
				var yUpper = @abs(yAxis)*@as(Vec3f, @splat(face.max[1]));
				if(@reduce(.Add, yAxis) < 0) std.mem.swap(Vec3f, &yLower, &yUpper);
				const zValue: Vec3f = @floatFromInt(zComponent*zMap[1]);
				if(neighbor == .dirNegX or neighbor == .dirPosY) {
					face.min[0] = 1 - face.min[0];
					face.max[0] = 1 - face.max[0];
					const swap = face.min[0];
					face.min[0] = face.max[0];
					face.max[0] = swap;
				}
				if(neighbor == .dirUp) {
					face.min = Vec2f{1, 1} - face.min;
					face.max = Vec2f{1, 1} - face.max;
					std.mem.swap(Vec2f, &face.min, &face.max);
				}
				if(neighbor == .dirDown) {
					face.min[1] = 1 - face.min[1];
					face.max[1] = 1 - face.max[1];
					const swap = face.min[1];
					face.min[1] = face.max[1];
					face.max[1] = swap;
				}
				quads.append(.{
					.normal = zAxis,
					.corners = .{
						xLower + yLower + zValue,
						xLower + yUpper + zValue,
						xUpper + yLower + zValue,
						xUpper + yUpper + zValue,
					},
					.cornerUV = .{.{face.min[0], face.min[1]}, .{face.min[0], face.max[1]}, .{face.max[0], face.min[1]}, .{face.max[0], face.max[1]}},
					.textureSlot = neighbor.toInt(),
				});
			}
			const middleFaces = mergeFaces(visibleMiddle, &faces);
			for(middleFaces) |*face| {
				var xLower = @abs(xAxis)*@as(Vec3f, @splat(face.min[0]));
				var xUpper = @abs(xAxis)*@as(Vec3f, @splat(face.max[0]));
				if(@reduce(.Add, xAxis) < 0) std.mem.swap(Vec3f, &xLower, &xUpper);
				var yLower = @abs(yAxis)*@as(Vec3f, @splat(face.min[1]));
				var yUpper = @abs(yAxis)*@as(Vec3f, @splat(face.max[1]));
				if(@reduce(.Add, yAxis) < 0) std.mem.swap(Vec3f, &yLower, &yUpper);
				const zValue = @as(Vec3f, @floatFromInt(zComponent))*@as(Vec3f, @splat(0.5));
				if(neighbor == .dirNegX or neighbor == .dirPosY) {
					face.min[0] = 1 - face.min[0];
					face.max[0] = 1 - face.max[0];
					const swap = face.min[0];
					face.min[0] = face.max[0];
					face.max[0] = swap;
				}
				if(neighbor == .dirUp) {
					face.min = Vec2f{1, 1} - face.min;
					face.max = Vec2f{1, 1} - face.max;
					std.mem.swap(Vec2f, &face.min, &face.max);
				}
				if(neighbor == .dirDown) {
					face.min[1] = 1 - face.min[1];
					face.max[1] = 1 - face.max[1];
					const swap = face.min[1];
					face.min[1] = face.max[1];
					face.max[1] = swap;
				}
				quads.append(.{
					.normal = zAxis,
					.corners = .{
						xLower + yLower + zValue,
						xLower + yUpper + zValue,
						xUpper + yLower + zValue,
						xUpper + yUpper + zValue,
					},
					.cornerUV = .{.{face.min[0], face.min[1]}, .{face.min[0], face.max[1]}, .{face.max[0], face.min[1]}, .{face.max[0], face.max[1]}},
					.textureSlot = neighbor.toInt(),
				});
			}
		}
		const index = main.models.Model.init(quads.items);
		if(i == 0) {
			modelIndex = index;
		}
	}
	return modelIndex.?;
}

pub fn model(block: Block) ModelIndex {
	return blocks.meshes.modelIndexStart(block).add(block.data & 255);
}

pub fn generateData(_: *main.game.World, _: Vec3i, _: Vec3f, _: Vec3f, _: Vec3i, _: ?Neighbor, currentData: *Block, _: Block, blockPlacing: bool) bool {
	if(blockPlacing) {
		currentData.data = 0;
		return true;
	}
	return false;
}

fn closestRay(comptime typ: enum {bit, intersection}, block: Block, relativePlayerPos: Vec3f, playerDir: Vec3f) if(typ == .intersection) ?RayIntersectionResult else u16 {
	var result: ?RayIntersectionResult = null;
	var resultBit: u16 = 0;
	for([_]u16{1, 2, 4, 8, 16, 32, 64, 128}) |bit| {
		if(block.data & bit == 0) {
			const cornerModelIndex: ModelIndex = blocks.meshes.modelIndexStart(block).add(255 ^ bit);
			if(RotationMode.DefaultFunctions.rayModelIntersection(cornerModelIndex, relativePlayerPos, playerDir)) |intersection| {
				if(result == null or intersection.distance < result.?.distance) {
					result = intersection;
					resultBit = bit;
				}
			}
		}
	}
	if(typ == .bit) return resultBit;
	return result;
}

pub fn rayIntersection(block: Block, item: ?main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f) ?RayIntersectionResult {
	if(item) |_item| {
		switch(_item) {
			.baseItem => |baseItem| {
				if(std.mem.eql(u8, baseItem.id(), "cubyz:chisel")) { // Select only one eighth of a block
					return closestRay(.intersection, block, relativePlayerPos, playerDir);
				}
			},
			else => {},
		}
	}
	return RotationMode.DefaultFunctions.rayIntersection(block, item, relativePlayerPos, playerDir);
}

pub fn onBlockBreaking(item: ?main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f, currentData: *Block) void {
	if(item) |_item| {
		switch(_item) {
			.baseItem => |baseItem| {
				if(std.mem.eql(u8, baseItem.id(), "cubyz:chisel")) { // Break only one eigth of a block
					currentData.data |= closestRay(.bit, currentData.*, relativePlayerPos, playerDir);
					if(currentData.data == 255) currentData.* = .{.typ = 0, .data = 0};
					return;
				}
			},
			else => {},
		}
	}
	return RotationMode.DefaultFunctions.onBlockBreaking(item, relativePlayerPos, playerDir, currentData);
}

pub fn canBeChangedInto(oldBlock: Block, newBlock: Block, item: main.items.ItemStack, shouldDropSourceBlockOnSuccess: *bool) RotationMode.CanBeChangedInto {
	if(oldBlock.typ != newBlock.typ) return RotationMode.DefaultFunctions.canBeChangedInto(oldBlock, newBlock, item, shouldDropSourceBlockOnSuccess);
	if(oldBlock.data == newBlock.data) return .no;
	if(item.item != null and item.item.? == .baseItem and std.mem.eql(u8, item.item.?.baseItem.id(), "cubyz:chisel")) {
		return .yes; // TODO: Durability change, after making the chisel a proper tool.
	}
	return .no;
}
