//#set OPENGL3

import Foundation
import SDL2
#if OPENGL3
import OpenGL
#else
import OpenGL.GL3
#endif



class OpenGL: Renderer
{
	private let srgb = true

	private var glCtx: SDL_GLContext? = nil
	private var state = OpenGLState()

	struct Version { let major, minor: Int32 }
	let glVersion: Version

	init(version: Version)
	{
		self.glVersion = version
	}

	func create(sdlWindow: OpaquePointer) throws
	{
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,  Int32(SDL_GL_CONTEXT_PROFILE_COMPATIBILITY.rawValue))
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, glVersion.major)
		SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, glVersion.minor)

		SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1)
		SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE,   0)
		SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 0)

		if let context = SDL_GL_CreateContext(sdlWindow) { glCtx = context }
		else { throw RendererError.sdlError(message: "SDL_GL_CreateContext: \(String(cString: SDL_GetError()))") }

		guard SDL_GL_MakeCurrent(sdlWindow, glCtx) == 0
		else { throw RendererError.sdlError(message: "SDL_GL_MakeCurrent: \(String(cString: SDL_GetError()))") }

		state.enable([.texture2D, .cullFace, .depthTest, .rescaleNormal, .colourMaterial])
		if srgb { state.enable(.frameBufferSrgb) }
		state.cullFace = .back
		state.clearDepth = 1
		state.depthFunc = .less
		state.setHint(target: .fog, hint: .dontCare)
	}

	func delete()
	{
		SDL_GL_DeleteContext(glCtx)
	}

	func resize(width: Int32, height: Int32)
	{
		glViewport(0, 0, width, height)
	}

	func newFrame()
	{
		glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
	}

	private var _clearColour = Colour.black
	var clearColour: Colour
	{
		get { _clearColour }
		set(newColour)
		{
			
			state.clearColour(srgb ? newColour.linear : newColour)
			_clearColour = newColour
		}
	}

	var wireframe: Bool
	{
		get { state.polygonMode == .line }
		set(mode) { state.polygonMode = mode ? .line : .fill }
	}

	func setVsync(mode: VSyncMode) throws
	{
		guard SDL_GL_SetSwapInterval(mode.sdlInterval) == 0
		else { throw RendererError.sdlError(message: "SDL_GL_SetSwapInterval: \(String(cString: SDL_GetError()))") }
	}

	func createMesh<V: Vertex>(mesh: Mesh<V>) throws -> RenderMesh<V>
	{
		var buffers = [GLuint](repeating: 0, count: 2)
		buffers.withUnsafeMutableBufferPointer
		{
			glGenBuffers(2, $0.baseAddress!)
		}

		state.arrayBuffer = buffers[0]
		state.elementArrayBuffer = buffers[1]

		glBufferData(GLenum(GL_ARRAY_BUFFER),
			MemoryLayout<V>.stride * mesh.vertices.count,
			mesh.vertices, GLenum(GL_STATIC_DRAW))
		glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER),
			MemoryLayout<V>.stride * mesh.indices.count,
			mesh.indices, GLenum(GL_STATIC_DRAW))

		state.elementArrayBuffer = OpenGLState.defaultBuffer
		state.arrayBuffer = OpenGLState.defaultBuffer

		var subMeshes = [Mesh<V>.SubMesh]()
		if mesh.subMeshes.isEmpty
		{
			subMeshes.append(Mesh<V>.SubMesh(start: 0, length: mesh.indices.count))
		}
		else
		{
			for subMesh in mesh.subMeshes
			{
				subMeshes.append(Mesh<V>.SubMesh(
					start:  subMesh.value.start,
					length: subMesh.value.length,
					material: subMesh.value.material))
			}
		}

		return RenderMesh(
			vbo: Int(buffers[0]),
			ibo: Int(buffers[1]),
			subMeshes: subMeshes,
			materials: mesh.materials)
	}

	func createTexture(data: UnsafeRawPointer, width: Int, height: Int) throws -> RenderTexture2D
	{
		try createTexture(data: data, width: width, height: height, filter: .linear, mipMode: .off)
	}

	func createTexture(data: UnsafeRawPointer, width: Int, height: Int,
		filter: FilterMode, mipMode: MipMode) throws -> RenderTexture2D
	{
		let min: Int32 = switch mipMode
		{
		case .off:     filter.gl
		case .nearest: filter.glMip
		case .linear:  filter.glLinearMip
		}
		return RenderTexture2D(try loadTexture(data: data,
			width: GLsizei(width), height: GLsizei(height),
			minFilter: min, magFilter: filter.gl))
	}

	private func loadTexture(
		data: UnsafeRawPointer,
		width: GLsizei, height: GLsizei,
		minFilter: Int32 = GL_LINEAR, magFilter: Int32 = GL_LINEAR,
		wrap: Int32 = GL_REPEAT) throws -> GLuint
	{
		// Create & upload raw bytes as texture
		var texId: GLuint = 0
		glGenTextures(1, &texId)
		state.bindTexture2D(active: 0, texture: texId)
		state.texture2DParameter(active: 0, param: .minFilter, int: minFilter)
		state.texture2DParameter(active: 0, param: .magFilter, int: magFilter)
		state.texture2DParameter(active: 0, param: .wrapS, int: wrap)
		state.texture2DParameter(active: 0, param: .wrapT, int: wrap)
		let format: GLint = srgb ? GL_SRGB8 : GL_RGBA
		glTexImage2D(GLenum(GL_TEXTURE_2D),
			0, format, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)

		// Generate mipmaps if needed
		if [GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST]
			.contains(minFilter) { glGenerateMipmap(GLenum(GL_TEXTURE_2D)) }

		state.bindTexture2D(active: 0, texture: OpenGLState.defaultTexture)

		return texId
	}


	func deleteMesh<V: Vertex>(_ mesh: RenderMesh<V>)
	{
		var buffers = [GLuint](repeating: 0, count: 2)
		buffers[0] = GLuint(mesh.iboHnd)
		buffers[1] = GLuint(mesh.vboHnd)

		glDeleteBuffers(1, buffers)
	}

	func deleteTexture(_ texture: RenderTexture2D)
	{
		var texId = GLuint(texture.id)
		glDeleteTextures(1, &texId)
	}


	func setProjection(matrix: Mat4f)
	{
		state.matrixMode = .projection
		loadMatrix(matrix)
	}

	func setView(matrix: Mat4f)
	{
		state.matrixMode = .modelView
		loadMatrix(matrix)
	}


	func setMaterial(_ mat: Material)
	{
		glColor4f(mat.diffuse.r, mat.diffuse.g, mat.diffuse.b, mat.diffuse.a)
		state.bindTexture2D(active: 0, texture: mat.texture.isValid ? mat.texture.id : 0)
	}


	private func draw<V: Vertex>(subMesh: Mesh<V>.SubMesh)
	{
		glDrawElements(
			GLenum(GL_TRIANGLES),
			GLsizei(subMesh.length),
			GLenum(GL_UNSIGNED_SHORT),
			.init(bitPattern: MemoryLayout<Mesh<V>.Index>.stride * subMesh.start));
	}

	private func bindMesh<V: Vertex>(mesh: RenderMesh<V>)
	{
		state.enableClient([ .vertex, .colour, .normal, .textureCoord ])

		state.arrayBuffer = UInt32(mesh.vboHnd)
		state.elementArrayBuffer = UInt32(mesh.iboHnd)

		let stride = GLsizei(MemoryLayout<V>.stride)
		if V.self == VertexPositionNormalTexcoord.self
		{
			glVertexPointer(3, GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.position)!))
			glNormalPointer(GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.normal)!))
			glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalTexcoord.texCoord)!))
		}
		else if V.self == VertexPositionNormalColourTexcoord.self
		{
			glVertexPointer(3, GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.position)!))
			glColorPointer(4, GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.colour)!))
			glNormalPointer(GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.normal)!))
			glTexCoordPointer(2, GLenum(GL_FLOAT), stride,
				UnsafeRawPointer.init(bitPattern: MemoryLayout.offset(of: \VertexPositionNormalColourTexcoord.texCoord)!))
		}
	}

	private func unbindMesh()
	{
		state.elementArrayBuffer = OpenGLState.defaultBuffer
		state.arrayBuffer = OpenGLState.defaultBuffer

		state.disableClient([ .vertex, .normal, .textureCoord ])
	}

	private func draw<V: Vertex>(mesh: RenderMesh<V>)
	{
		bindMesh(mesh: mesh)
		for subMesh in mesh.subMeshes
		{
			draw(subMesh: subMesh)
		}
		unbindMesh()
	}

	func draw<V: Vertex>(mesh: RenderMesh<V>, model: Mat4f, environment env: Environment)
	{
		if (mesh.subMeshes.isEmpty) { return }

		applyEnvironment(env)
		state.matrixMode = .modelView
		glPushMatrix()
		mulMatrix(model)
		draw(mesh: mesh)
		glPopMatrix()
	}

	func draw<V: Vertex>(mesh: RenderMesh<V>, environment env: Environment)
	{
		if (mesh.subMeshes.isEmpty) { return }

		applyEnvironment(env)
		draw(mesh: mesh)
	}

	func draw<V: Vertex>(mesh: RenderMesh<V>, subMesh: Mesh<V>.SubMesh, environment env: Environment)
	{
		applyEnvironment(env)
		bindMesh(mesh: mesh)
		draw(subMesh: subMesh)
		unbindMesh()
	}

	private func loadMatrix(_ matrix: Mat4f)
	{
		withUnsafePointer(to: matrix.columns)
		{
			$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
			{ (values: UnsafePointer<GLfloat>) in
				glLoadMatrixf(values)
			}
		}
	}

	private func mulMatrix(_ matrix: Mat4f)
	{
		withUnsafePointer(to: matrix.columns)
		{
			$0.withMemoryRebound(to: GLfloat.self, capacity: 16)
			{ (values: UnsafePointer<GLfloat>) in
				glMultMatrixf(values)
			}
		}
	}


	private func applyEnvironment(_ env: Environment)
	{
		let coordSource =
		{ (mode: Fog.Mode) -> OpenGLState.FogCoordinateSource in
			switch mode
			{
			case .depth:    .fragmentDepth
			case .distance: .coordinate
			}
		}

		switch env.fog
		{
		case .none:
			state.disable(.fog)
		case .range(let colour, let mode, _, let start, let end):
			state.fogMode = .linear
			state.fogColour = srgb ? colour.linear : colour
			state.fogCoodinateSource = coordSource(mode)
			state.fogStart = start
			state.fogEnd = end
			state.enable(.fog)
			break
		case .factor(let colour, let mode, let type, let density):
			state.fogMode = switch type
			{
			case .exp:  .exp
			case .exp2: .exp2
			}
			state.fogColour = srgb ? colour.linear : colour
			state.fogCoodinateSource = coordSource(mode)
			state.fogDensity = density
			state.enable(.fog)
			break
		}

		state.lightModelAmbient = env.ambient

		let lightCaps: [OpenGLState.Capability] = [
			.light0, .light1, .light2, .light3,
			.light4, .light5, .light6, .light7 ]
		if !env.lights.isEmpty
		{
			state.enable(.lighting)
			for i in 0..<8
			{
				if i < env.lights.count
				{
					switch env.lights[i]
					{
					case .directional(let colour, let direction):
						state.lightPosition[i] = Vec4f(direction, 0)
						state.lightDiffuse[i] = srgb ? colour.linear : colour
					case .point(let colour, let position, let intensity):
						state.lightPosition[i] = Vec4f(position, 1)
						state.lightDiffuse[i] = srgb ? colour.linear : colour
						state.lightConstantAttenuation[i] = 0
						state.lightLinearAttenuation[i] = intensity
						state.lightQuadraticAttenuation[i] = 0
					}
					state.enable(lightCaps[i])
				}
				else
				{
					state.disable(lightCaps[i])
				}
			}
		}
		else
		{
			state.disable(.lighting)
			for cap in lightCaps { state.disable(cap) }
		}
	}

	func drawGizmos(lines: [Line])
	{
		state.disable([ .texture2D, .depthTest ])
		var gizmoEnv = Environment()
		gizmoEnv.fog = .none
		applyEnvironment(gizmoEnv)

		glBegin(GLenum(GL_LINES))
		for line in lines
		{
			let colour = srgb ? line.colour.linear : line.colour
			glColor4f(colour.r, colour.g, colour.b, colour.a)
			glVertex3f(line.from.x, line.from.y, line.from.z)
			glVertex3f(line.to.x, line.to.y, line.to.z)
		}
		glEnd()
		state.enable([ .texture2D, .depthTest ])
	}
}

extension VSyncMode
{
	fileprivate var sdlInterval: Int32
	{
		switch self
		{
		case .off:       0
		case .on:        1
		case .adaptive: -1
		}
	}
}

extension FilterMode
{
	fileprivate var gl: Int32
	{
		switch self
		{
		case .point:  GL_NEAREST
		case .linear: GL_LINEAR
		}
	}

	fileprivate var glMip: Int32
	{
		switch self
		{
		case .point:  GL_NEAREST_MIPMAP_NEAREST
		case .linear: GL_LINEAR_MIPMAP_NEAREST
		}
	}

	fileprivate var glLinearMip: Int32
	{
		switch self
		{
		case .point:  GL_NEAREST_MIPMAP_LINEAR
		case .linear: GL_LINEAR_MIPMAP_LINEAR
		}
	}
}

extension WrapMode
{
	fileprivate var gl: Int32
	{
		switch self
		{
		case .clamping:    GL_CLAMP
		case .clampBorder: GL_CLAMP_TO_BORDER
		case .clampEdge:   GL_CLAMP_TO_EDGE
		case .mirrored:    GL_MIRRORED_REPEAT
		case .repeating:   GL_REPEAT
		}
	}
}
