import Foundation
import OrderedCollections


class G3DbLoader: LoaderProtocol
{
	typealias T = Mesh<VertexPositionNormalTexcoord>

	func load(url: URL) -> T?
	{
		guard var reader = try? G3DbReader(url)
		else { return Optional.none }
		return try? reader.read()
	}

	func load(url: URL, content: inout ContentManager) -> T? { return load(url: url) }
}


fileprivate struct G3DbReader
{
	private let version: [Int16] = [0, 1]
	private var reader: UBJsonReader

	init(_ url: URL) throws
	{
		guard let file = try? FileHandle(forReadingFrom: url)
		else { throw G3DbReaderError.fileNotFound }
		self.reader = UBJsonReader(file: file)
	}

	mutating func read() throws -> Mesh<VertexPositionNormalTexcoord>
	{
		let model = try readModel()

		let mesh = model.meshes[0]

		var vertices = [VertexPositionNormalTexcoord]()
		var indices = [UInt16]()
		var subMeshes = OrderedDictionary<String, Mesh<VertexPositionNormalTexcoord>.SubMesh>()

		let attributeSize =
		{ (attrib: G3DAttribute) in
			switch attrib
			{
			case .position, .normal, .colour, .tangent, .bitangent:
				return 3
			case .texCoord(_), .blendWeight(_):
				return 2
			case .colourPacked:
				return 1
			}
		}

		var stride = 0
		for a in mesh.attributes
		{
			stride += attributeSize(a)
		}
		let numVerts = mesh.vertices.count / stride
		vertices.reserveCapacity(numVerts)
		for i in 0..<numVerts
		{
			var srcIdx = i * stride
			var position: Vec3f = .zero, normal: Vec3f = .zero
			var texCoord: Vec2f = .zero
			for a in mesh.attributes
			{
				switch a
				{
				case .position:
					position = .init(
						mesh.vertices[srcIdx],
						mesh.vertices[srcIdx + 2],
						-mesh.vertices[srcIdx + 1])
				case .normal:
					normal = .init(
						mesh.vertices[srcIdx],
						mesh.vertices[srcIdx + 2],
						-mesh.vertices[srcIdx + 1])
				case .texCoord(id: 0):
					texCoord = .init(
						mesh.vertices[srcIdx],
						1.0 - mesh.vertices[srcIdx + 1])
				default: break
				}
				srcIdx += attributeSize(a)
			}
			vertices.append(.init(position: position, normal: normal, texCoord: texCoord))
		}
		for part in mesh.parts
		{
			subMeshes[part.key] = .init(start: indices.count, length: part.value.indices.count)
			indices += part.value.indices.map { UInt16($0) }
		}
		return Mesh(vertices: vertices, indices: indices, subMeshes: subMeshes)
	}

	mutating func readModel() throws -> G3DModel
	{
		let root = try reader.read()
		let version = try root.getArray(key: "version")
		guard try version.count == 2 && (try version.map({ try $0.int16 }) == self.version)
		else { throw G3DbReaderError.versionMismatch }
		
		var model = G3DModel()
		model.id = try root.getString(key: "id", default: "")
		try readMeshes(root, &model)
		try readMaterials(root, &model)
		return model
	}

	mutating func readMeshes(_ root: UBJsonToken, _ model: inout G3DModel) throws
	{
		let meshes = try root.getArray(key: "meshes")
		for obj in meshes
		{
			var mesh = G3DModelMesh()

			mesh.id = try obj.getString(key: "id", default: "")
			for attrib in try obj.getArray(key: "attributes")
			{
				mesh.attributes.append(try .resolve(try attrib.string))
			}
			mesh.vertices = try obj.getFloatArray(key: "vertices")
			for partObj in try obj.getArray(key: "parts")
			{
				let id = try partObj.getString(key: "id")
				if mesh.parts.keys.contains(id) { throw G3DbReaderError.duplicateIDs }

				var part = G3dModelMeshPart()
				part.mode = try .resolve(try partObj.getString(key: "type"))
				part.indices = try partObj.getInt16Array(key: "indices")

				mesh.parts[id] = part
			}
			model.meshes.append(mesh)
		}
	}

	mutating func readMaterials(_ root: UBJsonToken, _ model: inout G3DModel) throws
	{
		
	}
}

fileprivate struct G3DModel
{
	var id: String = .init()
	var meshes: [G3DModelMesh] = .init()
}

fileprivate struct G3DModelMesh
{
	var id: String = .init()
	var attributes: [G3DAttribute] = .init()
	var vertices: [Float] = .init()
	var parts: Dictionary<String, G3dModelMeshPart> = .init()
}

fileprivate struct G3dModelMeshPart
{
	var mode: G3DPrimativeType = .invalid
	var indices: [Int16] = .init()
}

fileprivate enum G3DAttribute: Equatable
{
	case position
	case normal
	case tangent
	case bitangent
	case colour
	case colourPacked
	case texCoord(id: UInt8)
	case blendWeight(id: UInt8)

	static let order = [ .position, .colour, .colourPacked, .normal,
		texCoord(id: 0), .blendWeight(id: 0), .tangent, .bitangent ]
}

extension G3DAttribute
{
	static func resolve(_ attrib: String) throws -> Self
	{
		let getAttributeId =
		{ (attrib: String, offset: Int) throws -> UInt8 in
			let idIdx = attrib.index(attrib.startIndex, offsetBy: offset)
			let idStr = attrib.suffix(from: idIdx)
			guard let id = UInt8(idStr)
			else { throw G3DbReaderError.badAttribute }
			return id
		}

		return switch attrib
		{
		case "POSITION":    .position
		case "NORMAL":      .normal
		case "COLOR":       .colour
		case "COLORPACKED": .colourPacked
		case "TANGENT":     .tangent
		case "BINORMAL":    .bitangent
		default:
			if attrib.starts(with: "TEXCOORD")
			{
				.texCoord(id: try getAttributeId(attrib, 8))
			}
			else if attrib.starts(with: "BLENDWEIGHT")
			{
				.blendWeight(id: try getAttributeId(attrib, 11))
			}
			else { throw G3DbReaderError.badAttribute }
		}
	}
}

fileprivate enum G3DPrimativeType
{
	case invalid
	case triangles
	case lines
	case points
	case triangleStrip
	case lineStrip
}

extension G3DPrimativeType
{
	static func resolve(_ key: String) throws -> Self
	{
		switch key
		{
		case "TRIANGLES": .triangles
		case "LINES": .lines
		case "POINTS": .points
		case "TRIANGLE_STRIP": .triangleStrip
		case "LINE_STRIP": .lineStrip
		default: throw G3DbReaderError.invalidFormat
		}
	}
}

fileprivate enum G3DbReaderError: Error
{
	case fileNotFound
	case versionMismatch
	case unsupportedFormat
	case invalidFormat
	case badAttribute
	case duplicateIDs
}
