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 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;

pub const dependsOnNeighbors = true;
var branchModels: std.HashMap(HashMapKey, ModelIndex, HashMapKey, std.hash_map.default_max_load_percentage) = undefined;
const HashMapKey = struct {
	radius: u16,
	shellModelId: []const u8,
	textureSlotOffset: u32,

	pub fn hash(_: HashMapKey, val: HashMapKey) u64 {
		var hasher = std.hash.Wyhash.init(0);
		std.hash.autoHashStrat(&hasher, val, .DeepRecursive);
		return hasher.final();
	}
	pub fn eql(_: HashMapKey, val1: HashMapKey, val2: HashMapKey) bool {
		if(val1.radius != val2.radius) return false;
		if(val1.textureSlotOffset != val2.textureSlotOffset) return false;
		return std.mem.eql(u8, val1.shellModelId, val2.shellModelId);
	}
};
pub const BranchData = packed struct(u6) {
	enabledConnections: u6,

	pub inline fn init(blockData: u16) BranchData {
		return .{.enabledConnections = @truncate(blockData)};
	}

	pub inline fn isConnected(self: @This(), neighbor: Neighbor) bool {
		return (self.enabledConnections & Neighbor.bitMask(neighbor)) != 0;
	}

	pub inline fn setConnection(self: *@This(), neighbor: Neighbor, value: bool) void {
		if(value) {
			self.enabledConnections |= Neighbor.bitMask(neighbor);
		} else {
			self.enabledConnections &= ~Neighbor.bitMask(neighbor);
		}
	}
};

pub fn init() void {
	branchModels = .initContext(main.globalAllocator.allocator, undefined);
}

pub fn deinit() void {
	branchModels.deinit();
}

pub fn reset() void {
	branchModels.clearRetainingCapacity();
}

pub const Direction = enum(u2) {
	negYDir = 0,
	posXDir = 1,
	posYDir = 2,
	negXDir = 3,
};

const Pattern = union(enum) {
	dot: void,
	halfLine: Direction,
	line: Direction,
	bend: Direction,
	intersection: Direction,
	cross: void,
};

fn rotateQuad(originalCorners: [4]Vec2f, pattern: Pattern, min: f32, max: f32, side: Neighbor, textureSlotOffset: u32) main.models.QuadInfo {
	var corners: [4]Vec2f = originalCorners;

	switch(pattern) {
		.dot, .cross => {},
		inline else => |typ| {
			const angle: f32 = @as(f32, @floatFromInt(@intFromEnum(typ)))*std.math.pi/2.0;
			corners = .{
				vec.rotate2d(originalCorners[0], angle, @splat(0.5)),
				vec.rotate2d(originalCorners[1], angle, @splat(0.5)),
				vec.rotate2d(originalCorners[2], angle, @splat(0.5)),
				vec.rotate2d(originalCorners[3], angle, @splat(0.5)),
			};
		},
	}

	const offX: f32 = @floatFromInt(@intFromBool(@reduce(.Add, side.textureX()) < 0));
	const offY: f32 = @floatFromInt(@intFromBool(@reduce(.Add, side.textureY()) < 0));

	const corners3d = .{
		@as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[0][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[0][1] - offY)),
		@as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[1][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[1][1] - offY)),
		@as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[2][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[2][1] - offY)),
		@as(Vec3f, @floatFromInt(side.textureX()))*@as(Vec3f, @splat(corners[3][0] - offX)) + @as(Vec3f, @floatFromInt(side.textureY()))*@as(Vec3f, @splat(corners[3][1] - offY)),
	};

	var offset: Vec3f = .{0.0, 0.0, 0.0};
	offset[@intFromEnum(side.vectorComponent())] = if(side.isPositive()) max else min;

	const res: main.models.QuadInfo = .{
		.corners = .{
			corners3d[0] + offset,
			corners3d[1] + offset,
			corners3d[2] + offset,
			corners3d[3] + offset,
		},
		.cornerUV = originalCorners,
		.normal = @as(Vec3f, @floatFromInt(side.relPos())),
		.textureSlot = textureSlotOffset + @intFromEnum(pattern),
	};

	return res;
}

fn addQuads(pattern: Pattern, side: Neighbor, radius: f32, out: *main.List(main.models.QuadInfo), textureSlotOffset: u32) void {
	const min: f32 = (8.0 - radius)/16.0;
	const max: f32 = (8.0 + radius)/16.0;
	switch(pattern) {
		.dot => {
			out.append(rotateQuad(.{
				.{min, min},
				.{min, max},
				.{max, min},
				.{max, max},
			}, pattern, min, max, side, textureSlotOffset));
		},
		.halfLine => {
			out.append(rotateQuad(.{
				.{min, 0.0},
				.{min, max},
				.{max, 0.0},
				.{max, max},
			}, pattern, min, max, side, textureSlotOffset));
		},
		.line => {
			out.append(rotateQuad(.{
				.{min, 0.0},
				.{min, 1.0},
				.{max, 0.0},
				.{max, 1.0},
			}, pattern, min, max, side, textureSlotOffset));
		},
		.bend => {
			out.append(rotateQuad(.{
				.{0.0, 0.0},
				.{0.0, max},
				.{max, 0.0},
				.{max, max},
			}, pattern, min, max, side, textureSlotOffset));
		},
		.intersection => {
			out.append(rotateQuad(.{
				.{0.0, 0.0},
				.{0.0, max},
				.{1.0, 0.0},
				.{1.0, max},
			}, pattern, min, max, side, textureSlotOffset));
		},
		.cross => {
			out.append(rotateQuad(.{
				.{0.0, 0.0},
				.{0.0, 1.0},
				.{1.0, 0.0},
				.{1.0, 1.0},
			}, pattern, min, max, side, textureSlotOffset));
		},
	}
}

pub fn getPattern(data: BranchData, side: Neighbor) ?Pattern {
	const posX = Neighbor.fromRelPos(side.textureX()).?;
	const negX = Neighbor.fromRelPos(side.textureX()).?.reverse();
	const posY = Neighbor.fromRelPos(side.textureY()).?;
	const negY = Neighbor.fromRelPos(side.textureY()).?.reverse();

	const connectedPosX = data.isConnected(posX);
	const connectedNegX = data.isConnected(negX);
	const connectedPosY = data.isConnected(posY);
	const connectedNegY = data.isConnected(negY);

	const count: u6 = @as(u6, @intFromBool(connectedPosX)) + @as(u6, @intFromBool(connectedNegX)) + @as(u6, @intFromBool(connectedPosY)) + @as(u6, @intFromBool(connectedNegY));

	return switch(count) {
		0 => {
			if(data.isConnected(side)) {
				return null;
			}

			return .dot;
		},
		1 => {
			var dir: Direction = .negXDir;
			if(connectedNegY) {
				dir = .negYDir;
			} else if(connectedPosX) {
				dir = .posXDir;
			} else if(connectedPosY) {
				dir = .posYDir;
			}
			return .{.halfLine = dir};
		},
		2 => {
			if((connectedPosX and connectedNegX) or (connectedPosY and connectedNegY)) {
				var dir: Direction = .negYDir;
				if(connectedPosX and connectedNegX) {
					dir = .posXDir;
				}

				return .{.line = dir};
			}

			var dir: Direction = .negXDir;

			if(connectedNegY) {
				dir = .negYDir;
				if(connectedPosX) {
					dir = .posXDir;
				}
			} else if(connectedPosX) {
				dir = .posXDir;
				if(connectedPosY) {
					dir = .posYDir;
				}
			} else if(connectedPosY) {
				dir = .posYDir;
				if(connectedNegX) {
					dir = .negXDir;
				}
			}

			return .{.bend = dir};
		},
		3 => {
			var dir: Direction = undefined;
			if(!connectedPosY) dir = .negYDir;
			if(!connectedNegX) dir = .posXDir;
			if(!connectedNegY) dir = .posYDir;
			if(!connectedPosX) dir = .negXDir;

			return .{.intersection = dir};
		},
		4 => {
			return .cross;
		},
		else => undefined,
	};
}

pub fn createBlockModel(_: Block, modeData: *u16, zon: ZonElement) ModelIndex {
	var radius = zon.get(f32, "radius", 4);
	const radiusForComparisons = std.math.lossyCast(u16, @round(radius*65536.0/16.0));
	radius = @as(f32, @floatFromInt(radiusForComparisons))*16.0/65536.0;
	modeData.* = radiusForComparisons;
	const shellModelId = zon.get([]const u8, "shellModel", "");
	const textureSlotOffset = zon.get(u32, "textureSlotOffset", 0);
	if(branchModels.get(.{.radius = radiusForComparisons, .shellModelId = shellModelId, .textureSlotOffset = textureSlotOffset})) |modelIndex| return modelIndex;

	var shellQuads = main.List(main.models.QuadInfo).init(main.stackAllocator);
	defer shellQuads.deinit();
	if(shellModelId.len != 0) {
		const shellModel = main.models.getModelIndex(shellModelId).model();
		shellModel.getRawFaces(&shellQuads);
	}

	var modelIndex: ModelIndex = undefined;
	for(0..64) |i| {
		var quads = main.List(main.models.QuadInfo).init(main.stackAllocator);
		defer quads.deinit();
		quads.appendSlice(shellQuads.items);

		for(Neighbor.iterable) |neighbor| {
			const pattern = getPattern(BranchData.init(@intCast(i)), neighbor);

			if(pattern) |pat| {
				addQuads(pat, neighbor, radius, &quads, textureSlotOffset);
			}
		}

		const index = main.models.Model.init(quads.items);
		if(i == 0) {
			modelIndex = index;
		}
	}

	branchModels.put(.{.radius = radiusForComparisons, .shellModelId = shellModelId, .textureSlotOffset = textureSlotOffset}, modelIndex) catch unreachable;

	return modelIndex;
}

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

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

	comptime var rotationTable: [4][16]u8 = undefined;
	comptime for(0..16) |i| {
		rotationTable[0][i] = @intCast(i << 2);
	};
	comptime for(1..4) |a| {
		for(0..16) |i| {
			const old: BranchData = .init(rotationTable[a - 1][i]);
			var new: BranchData = .init(0);

			new.setConnection(Neighbor.dirPosX.rotateZ(), old.isConnected(Neighbor.dirPosX));
			new.setConnection(Neighbor.dirNegX.rotateZ(), old.isConnected(Neighbor.dirNegX));
			new.setConnection(Neighbor.dirPosY.rotateZ(), old.isConnected(Neighbor.dirPosY));
			new.setConnection(Neighbor.dirNegY.rotateZ(), old.isConnected(Neighbor.dirNegY));

			rotationTable[a][i] = new.enabledConnections;
		}
	};
	if(data >= 0b111111) return 0;
	const rotationIndex = (data & 0b111100) >> 2;
	const upDownFlags = data & 0b000011;
	return rotationTable[@intFromEnum(angle)][rotationIndex] | upDownFlags;
}

pub fn generateData(
	_: *main.game.World,
	_: Vec3i,
	_: Vec3f,
	_: Vec3f,
	_: Vec3i,
	neighbor: ?Neighbor,
	currentBlock: *Block,
	neighborBlock: Block,
	blockPlacing: bool,
) bool {
	const canConnectToNeighbor = currentBlock.mode() == neighborBlock.mode() and currentBlock.modeData() == neighborBlock.modeData();

	if(blockPlacing or canConnectToNeighbor or !neighborBlock.replacable()) {
		const neighborModel = blocks.meshes.model(neighborBlock).model();

		var currentData = BranchData.init(currentBlock.data);
		// Branch block upon placement should extend towards a block it was placed
		// on if the block is solid or also uses branch model.
		const targetVal = ((!neighborBlock.replacable() and (!neighborBlock.viewThrough() or canConnectToNeighbor)) and (canConnectToNeighbor or neighborModel.isNeighborOccluded[neighbor.?.reverse().toInt()]));
		currentData.setConnection(neighbor.?, targetVal);

		const result: u16 = currentData.enabledConnections;
		if(result == currentBlock.data) return false;

		currentBlock.data = result;
		return true;
	}
	return false;
}

pub fn updateData(block: *Block, neighbor: Neighbor, neighborBlock: Block) bool {
	const canConnectToNeighbor = block.mode() == neighborBlock.mode() and block.modeData() == neighborBlock.modeData();
	var currentData = BranchData.init(block.data);

	// Handle joining with other branches. While placed, branches extend in a
	// opposite direction than they were placed from, effectively connecting
	// to the block they were placed at.
	if(canConnectToNeighbor) {
		const neighborData = BranchData.init(neighborBlock.data);
		currentData.setConnection(neighbor, neighborData.isConnected(neighbor.reverse()));
	} else if(neighborBlock.replacable()) {
		currentData.setConnection(neighbor, false);
	}

	const result: u16 = currentData.enabledConnections;
	if(result == block.data) return false;

	block.data = result;
	return true;
}

fn closestRay(block: Block, relativePlayerPos: Vec3f, playerDir: Vec3f) ?u16 {
	var closestIntersectionDistance: f64 = std.math.inf(f64);
	var resultBitMask: ?u16 = null;
	{
		const modelIndex = blocks.meshes.modelIndexStart(block);
		if(RotationMode.DefaultFunctions.rayModelIntersection(modelIndex, relativePlayerPos, playerDir)) |intersection| {
			closestIntersectionDistance = intersection.distance;
			resultBitMask = 0;
		}
	}
	for(Neighbor.iterable) |direction| {
		const directionBitMask = Neighbor.bitMask(direction);

		if((block.data & directionBitMask) != 0) {
			const modelIndex: ModelIndex = blocks.meshes.modelIndexStart(block).add(directionBitMask);
			if(RotationMode.DefaultFunctions.rayModelIntersection(modelIndex, relativePlayerPos, playerDir)) |intersection| {
				if(@abs(closestIntersectionDistance) > @abs(intersection.distance)) {
					closestIntersectionDistance = intersection.distance;
					resultBitMask = direction.bitMask();
				}
			}
		}
	}
	return resultBitMask;
}

pub fn onBlockBreaking(_: ?main.items.Item, relativePlayerPos: Vec3f, playerDir: Vec3f, currentData: *Block) void {
	if(closestRay(currentData.*, relativePlayerPos, playerDir)) |directionBitMask| {
		// If player destroys a central part of branch block, branch block is completely destroyed.
		if(directionBitMask == 0) {
			currentData.typ = 0;
			currentData.data = 0;
			return;
		}
		// Otherwise only the connection player aimed at is destroyed.
		currentData.data &= ~directionBitMask;
	}
}
