import Foundation
import simd
import JolkEngine


@main struct Program
{
	static func main()
	{
		Application(
			application: CavesOfJolk(),
			config: ApplicationConfiguration(
				resizable: true,
				vSync: .on,
				windowWidth: 1280,
				windowHeight: 720,
				bundle: Bundle.module)
			).run()
	}
}

class CavesOfJolk: ApplicationImplementation
{
	private var aspect: Float = 0
	private var drawEdges = false

	var env = Environment()
	var worldMesh = RenderMesh.empty, cube = RenderMesh.empty, suzanne = RenderMesh.empty, toybox = RenderMesh.empty
	var texture = Texture2D.empty, jolkTex = Texture2D.empty, suzanneDiffuse = Texture2D.empty

	private lazy var world = Collision()
	private lazy var colin = Colin()
	private lazy var jolkCube = JolkCube(position: .init(0, 1, -4))
	private var lightTheta: Float = 0

	private var frameCount = 0
	private var frameTimer: Float = 1

	func create(render: inout Renderer)
	{
		render.clearColour = XnaColour.CornflowerBlue

		env.setFog(
			colour: XnaColour.CornflowerBlue,
			//mode: .depth, falloff: .range(type: .linear, start: 4, end: 38))
			//mode: .depth, falloff: .range(type: .linear, start: 1.25, end: 24.5))
			//mode: .distance, falloff: .factor(type: .exp2, density: 0.1))
			mode: .depth, falloff: .factor(type: .exp2, density: 0.0222))

		env.addAmbientLight(colour: XnaColour.DarkSlateGray.lighten(by: -0.666))
		env.addDirectionalLight(
			direction: -Vec3f(1, -1, -1).normalised,
			colour: XnaColour.BlanchedAlmond
				.lighten(by: -0.02)
				.mix(with: XnaColour.LightSlateGray, 0.5)
				.mix(with: XnaColour.White, 0.125))
		env.addPointLight(
			position: Vec3f(3, 0.33, -5),
			colour: XnaColour.Green.mix(with: XnaColour.Gray, 0.5),
			intensity: 2)
		env.addPointLight(
			position: Vec3f(5.5, 0.33, -6),
			colour: XnaColour.Red.mix(with: XnaColour.Gray, 0.5),
			intensity: 2)
		env.addPointLight(
			position: Vec3f(4, 0.33, -7),
			colour: XnaColour.Blue.mix(with: XnaColour.Gray, 0.5),
			intensity: 2)
	}

	func loadContent(content: inout ContentManager) throws
	{
		try loadWorld(&content)
		cube = try content.create(mesh: .init(
			vertices: [
				.init(position: Vec3f(-1, -1,  1), normal: .forward, texCoord: Vec2f(0, 0)),
				.init(position: Vec3f( 1, -1,  1), normal: .forward, texCoord: Vec2f(1, 0)),
				.init(position: Vec3f(-1,  1,  1), normal: .forward, texCoord: Vec2f(0, 1)),
				.init(position: Vec3f( 1,  1,  1), normal: .forward, texCoord: Vec2f(1, 1)),
				.init(position: Vec3f( 1, -1,  1), normal:   .right, texCoord: Vec2f(0, 0)),
				.init(position: Vec3f( 1, -1, -1), normal:   .right, texCoord: Vec2f(1, 0)),
				.init(position: Vec3f( 1,  1,  1), normal:   .right, texCoord: Vec2f(0, 1)),
				.init(position: Vec3f( 1,  1, -1), normal:   .right, texCoord: Vec2f(1, 1)),
				.init(position: Vec3f( 1, -1, -1), normal:    .back, texCoord: Vec2f(0, 0)),
				.init(position: Vec3f(-1, -1, -1), normal:    .back, texCoord: Vec2f(1, 0)),
				.init(position: Vec3f( 1,  1, -1), normal:    .back, texCoord: Vec2f(0, 1)),
				.init(position: Vec3f(-1,  1, -1), normal:    .back, texCoord: Vec2f(1, 1)),
				.init(position: Vec3f(-1, -1, -1), normal:    .left, texCoord: Vec2f(0, 0)),
				.init(position: Vec3f(-1, -1,  1), normal:    .left, texCoord: Vec2f(1, 0)),
				.init(position: Vec3f(-1,  1, -1), normal:    .left, texCoord: Vec2f(0, 1)),
				.init(position: Vec3f(-1,  1,  1), normal:    .left, texCoord: Vec2f(1, 1)),
				.init(position: Vec3f(-1, -1, -1), normal:    .down, texCoord: Vec2f(0, 0)),
				.init(position: Vec3f( 1, -1, -1), normal:    .down, texCoord: Vec2f(1, 0)),
				.init(position: Vec3f(-1, -1,  1), normal:    .down, texCoord: Vec2f(0, 1)),
				.init(position: Vec3f( 1, -1,  1), normal:    .down, texCoord: Vec2f(1, 1)),
				.init(position: Vec3f(-1,  1,  1), normal:      .up, texCoord: Vec2f(0, 0)),
				.init(position: Vec3f( 1,  1,  1), normal:      .up, texCoord: Vec2f(1, 0)),
				.init(position: Vec3f(-1,  1, -1), normal:      .up, texCoord: Vec2f(0, 1)),
				.init(position: Vec3f( 1,  1, -1), normal:      .up, texCoord: Vec2f(1, 1))
			],
			indices: [
				 0,  1,  2,  2,  1,  3,
				 4,  5,  6,  6,  5,  7,
				 8,  9, 10, 10,  9, 11,
				12, 13, 14, 14, 13, 15,
				16, 17, 18, 18, 17, 19,
				20, 21, 22, 22, 21, 23 ]))
		suzanne = try content.load("suzanne.g3db")
		toybox = try content.load("toybox.g3db")
		

		let linearMipped = Texture2DParameters(
			minFilter: .linear, magFilter: .linear,
			wrapMode: .repeating,
			mipMode: .linear)
		let linearClamp = Texture2DParameters(
			minFilter: .linear, magFilter: .linear,
			wrapMode: .clampEdge)

		texture = try content.load("cobblestone.png", params: linearMipped)
		jolkTex = try content.load("jolkmeup.jpg", params: linearClamp)
		suzanneDiffuse = try content.load("suzanne_diffuse.png", params: linearMipped)
	}

	func resize(width: Int, height: Int)
	{
		aspect = Float(width) / Float(height)
		print(aspect)
	}

	func update(deltaTime: Float)
	{
		colin.update(deltaTime: deltaTime, world: world)
		jolkCube.update(deltaTime: deltaTime, world: world)

		lightTheta += deltaTime

		frameCount += 1
		frameTimer -= deltaTime
		if frameTimer <= 0
		{
			print("FPS: \(frameCount)")
			frameCount = 0
			frameTimer += 1.0
		}
	}

	private func loadWorld(_ content: inout ContentManager) throws
	{
		//let world: Mesh = try content.load("World.obj")
		let obj = try ObjReader.read(url: try content.getResource("World.obj"))
		let mesh: Mesh = try ObjLoader.read(model: obj)
		worldMesh = try content.create(mesh: mesh)

		if let collision = obj.objects["Collision3D"]
		{
			world.build(obj: obj, collision: collision)
		}
	}

		/*
		if let collision = world.subMeshes["Collision"]
		{
			edges.reserveCapacity(collision.length / 6)
			let get2dVertex =
			{ i in
				let vertex = world.vertices[Int(world.indices[i])]
				let position = Vec2f(vertex.position.x, vertex.position.z)
				let normal = Vec2f(vertex.normal.x, vertex.normal.z)
				return (position, normal)
			}
			for i in stride(from: collision.start, to: collision.start + collision.length, by: 3)
			{
				let (v0p, v0n) = get2dVertex(i)
				let (v1p, v1n) = get2dVertex(i + 1)
				let (v2p, v2n) = get2dVertex(i + 2)

				let (p0, p1) = if v0p == v2p || v1p == v2p { (v0p, v1p) }
				else if v0p == v1p { (v0p, v2p) }
				else { throw NSError() }

				let edge = Edge(p: p0.lerp(p1, 0.5), n: (v0n + v1n + v2n).normalised, w: (p0 - p1).len)
				if !edges.contains(where: { edge.p == $0.p && edge.w == $0.w })
				{
					edges.append(edge)
				}
			}
		}
		*/

	func draw(render: inout Renderer, deltaTime: Float)
	{
		// Setup camera
		render.setProjection(matrix:
			.perspective(fovY: Float.rad(fromDeg: colin.fov), aspect: aspect, zNear: 0.1, zFar: 100))
		render.setView(matrix: colin.transform)

		// Update point lights
		var theta = Vec3f(repeating: lightTheta) * Vec3f(0.12, 0.011, 0.056) * 6
		var i = env.lights.startIndex
		while i != env.lights.endIndex
		{
			if case .point(let colour, _, _) = env.lights[i]
			{
				env.lights[i] = .point(
					colour: colour,
					position: Vec3f(
						 4    + 6    * cos(theta[0]),
						 0.33 + 0.33 * sin(theta[1]),
						-6    + 3    * sin(theta[0] * 2)),
					intensity:  3.1  + 1.53 * cos(theta[2]))

				let spacing = 0.5 * Float.pi * (2 / 3.0)
				theta += spacing * Vec3f(1, 0.98, 0.5566)
			}
			i = env.lights.index(after: i)
		}

		// Draw world
		render.setMaterial(Material(
			specular: XnaColour.BlanchedAlmond.mix(with: .black, 0.12),
			texture: texture.id))
		render.draw(mesh: worldMesh, environment: env)

		// Draw jolked up shit
		render.setMaterial(Material(
			specular: XnaColour.Gray,
			gloss: 20.0,
			texture: jolkTex.id))
		render.draw(mesh: cube, model: jolkCube.transform, environment: env)

		render.setMaterial(Material(texture: suzanneDiffuse.id))
		render.draw(mesh: suzanne, model: .translate(.up + Vec3f(3.0, 0.0, -3.5) * 2.5), environment: env)

		render.draw(mesh: toybox,
			model: .translate(Vec3f(6.0, 0.667, -3.5) * Vec3f(2.5, 1, 2.5))
				* .rotate(y: lightTheta * 0.5) * .scale(scalar: 0.25), environment: env)

		if Input.instance.keyPressed(.c) { drawEdges = !drawEdges }
		if drawEdges
		{
			/*
			var lines = [Line](
				repeating: .init(from: .init(), to: .init(), colour: .zero),
				count: edges.count * 3)
			var i: Int = 0
			for edge in edges
			{
				let tangent = Vec2f(edge.n.y, -edge.n.x)
				let a = edge.p - tangent * edge.w * 0.5
				let b = edge.p + tangent * edge.w * 0.5

				lines[i] = Line(
					from: Vec3f(a.x, 0, a.y),
					to: Vec3f(b.x, 0, b.y),
					colour: Colour(r: 0.1, g: 0.9, b: 0.1))
				lines[i + 1] = Line(
					from: Vec3f(edge.p.x, 0, edge.p.y),
					to: Vec3f(edge.p.x + edge.n.x * 0.25, 0, edge.p.y + edge.n.y * 0.25),
					colour: Colour(r: 0.9, g: 0.1, b: 0.1))

				let deltaPos = Vec2f(colin.position.x, colin.position.z) - edge.p
				let something = tangent * deltaPos.cross(edge.n)
				lines[i + 2] = Line(
					from: Vec3f(edge.p.x + something.x, 0.0, edge.p.y + something.y),
					to: Vec3f(edge.p.x + something.x + edge.n.x * 0.5, 0.0, edge.p.y + something.y + edge.n.y * 0.5),
					colour: XnaColour.Azure)
				i += 3
			}
			render.drawGizmos(lines: lines)
			*/

			world.draw(render, position: colin.position)
		}
	}
}
