use rbx_mesh::mesh_data::NormalId2 as MeshDataNormalId2;
use strafesnet_common::model::{self,IndexedVertex,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId};
use strafesnet_common::integer::vec3;

#[allow(dead_code)]
#[derive(Debug)]
pub enum Error{
	Block,
	MissingVertexId(u32),
	Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
	RobloxPhysicsData(rbx_mesh::physics_data::Error),
	RobloxMeshData(rbx_mesh::mesh_data::Error),
}
impl std::fmt::Display for Error{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}

// wacky state machine to make sure all vertices in a face agree upon what NormalId to use.
// Roblox duplicates this information per vertex when it should only exist per-face.
enum MeshDataNormalStatus{
	Agree(MeshDataNormalId2),
	Conflicting,
}
struct MeshDataNormalChecker{
	status:Option<MeshDataNormalStatus>,
}
impl MeshDataNormalChecker{
	fn new()->Self{
		Self{status:None}
	}
	fn check(&mut self,normal:MeshDataNormalId2){
		self.status=match self.status.take(){
			None=>Some(MeshDataNormalStatus::Agree(normal)),
			Some(MeshDataNormalStatus::Agree(old_normal))=>{
				if old_normal==normal{
					Some(MeshDataNormalStatus::Agree(old_normal))
				}else{
					Some(MeshDataNormalStatus::Conflicting)
				}
			},
			Some(MeshDataNormalStatus::Conflicting)=>Some(MeshDataNormalStatus::Conflicting),
		};
	}
	fn into_agreed_normal(self)->Option<MeshDataNormalId2>{
		self.status.and_then(|status|match status{
			MeshDataNormalStatus::Agree(normal)=>Some(normal),
			MeshDataNormalStatus::Conflicting=>None,
		})
	}
}

impl std::error::Error for Error{}
pub fn convert(
	roblox_physics_data:&[u8],
	roblox_mesh_data:&[u8],
	size:glam::Vec3,
	crate::rbx::RobloxPartDescription(part_texture_description):crate::rbx::RobloxPartDescription,
)->Result<model::Mesh,Error>{
	const NORMAL_FACES:usize=6;
	let mut polygon_groups_normal_id=vec![Vec::new();NORMAL_FACES];

	// build graphics and physics meshes
	let mut mb=strafesnet_common::model::MeshBuilder::new();
	// graphics
	let graphics_groups=if !roblox_mesh_data.is_empty(){
		// create per-face texture coordinate affine transforms
		let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{
			t.transform.set_size(1.0,1.0);
			t.to_face_description()
		}));

		let mesh_data=rbx_mesh::read_mesh_data_versioned(
			std::io::Cursor::new(roblox_mesh_data)
		).map_err(Error::RobloxMeshData)?;
		let graphics_mesh=match mesh_data{
			rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
			rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL2(mesh_data2))=>mesh_data2.mesh,
			rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::CSGMDL4(mesh_data4))=>mesh_data4.mesh,
		};
		for [vertex_id0,vertex_id1,vertex_id2] in graphics_mesh.faces{
			let face=[
				graphics_mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
				graphics_mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
				graphics_mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
			];
			let mut normal_agreement_checker=MeshDataNormalChecker::new();
			let face=face.into_iter().map(|vertex|{
				normal_agreement_checker.check(vertex.normal_id);
				let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?);
				let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?);
				let tex_coord=glam::Vec2::from_array(vertex.tex);
				let maybe_face_description=&cube_face_description[vertex.normal_id as usize-1];
				let (tex,color)=match maybe_face_description{
					Some(face_description)=>{
						// transform texture coordinates and set decal color
						let tex=mb.acquire_tex_id(face_description.transform.transform_point2(tex_coord));
						let color=mb.acquire_color_id(face_description.color);
						(tex,color)
					},
					None=>{
						// texture coordinates don't matter and pass through mesh vertex color
						let tex=mb.acquire_tex_id(tex_coord);
						let color=mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)));
						(tex,color)
					},
				};
				Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
			}).collect::<Result<Vec<_>,_>>().map_err(Error::Planar64Vec3)?;
			if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
				polygon_groups_normal_id[normal_id as usize-1].push(face);
			}else{
				panic!("Empty face!");
			}
		}
		(0..NORMAL_FACES).map(|polygon_group_id|{
			model::IndexedGraphicsGroup{
				render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
				groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
			}
		}).collect()
	}else{
		Vec::new()
	};

	//physics
	let physics_convex_meshes=if !roblox_physics_data.is_empty(){
		let physics_data=rbx_mesh::read_physics_data_versioned(
			std::io::Cursor::new(roblox_physics_data)
		).map_err(Error::RobloxPhysicsData)?;
		let physics_convex_meshes=match physics_data{
			rbx_mesh::physics_data::PhysicsData::CSGK(_)
			// have not seen this format in practice
			|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
			=>return Err(Error::Block),
			rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes(meshes))
			=>meshes.meshes,
			rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
			=>vec![pim.mesh],
		};
		physics_convex_meshes
	}else{
		Vec::new()
	};
	let polygon_groups:Vec<PolygonGroup>=polygon_groups_normal_id.into_iter().map(|faces|
		// graphics polygon groups (to be rendered)
		Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
	).chain(physics_convex_meshes.into_iter().map(|mesh|{
		// this can be factored out of the loop but I am lazy
		let color=mb.acquire_color_id(glam::Vec4::ONE);
		let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
		// physics polygon groups (to do physics)
		Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[vertex_id0,vertex_id1,vertex_id2]|{
			let face=[
				mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?,
				mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?,
				mesh.vertices.get(vertex_id2.0 as usize).ok_or(Error::MissingVertexId(vertex_id2.0))?,
			].map(|v|glam::Vec3::from_slice(v)/size);
			let vertex_norm=(face[1]-face[0])
			          .cross(face[2]-face[0]);
			let normal=mb.acquire_normal_id(vec3::try_from_f32_array(vertex_norm.to_array()).map_err(Error::Planar64Vec3)?);
			face.into_iter().map(|vertex_pos|{
				let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
				Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
			}).collect()
		}).collect::<Result<_,_>>()?)))
	})).collect::<Result<_,_>>()?;
	let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
		groups:vec![PolygonGroupId::new(id as u32)]
	}).collect();
	Ok(mb.build(
		polygon_groups,
		graphics_groups,
		physics_groups,
	))
}
