use crate::mesh::MeshWithSize;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS,FaceDescription};

use rbx_mesh::mesh_data::{VertexId as MeshDataVertexId,NormalId as MeshDataNormalId,NormalId2 as MeshDataNormalId2,NormalId5 as MeshDataNormalId5};
use rbx_mesh::physics_data::VertexId as PhysicsDataVertexId;
use strafesnet_common::model::{self,IndexedVertex,MeshBuilder,PolygonGroup,PolygonGroupId,PolygonList,RenderConfigId,VertexId};
use strafesnet_common::integer::vec3;

#[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(MeshDataNormalId),
	Conflicting,
}
struct MeshDataNormalChecker{
	status:Option<MeshDataNormalStatus>,
}
impl MeshDataNormalChecker{
	fn new()->Self{
		Self{status:None}
	}
	fn check(&mut self,normal:MeshDataNormalId){
		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<MeshDataNormalId>{
		self.status.and_then(|status|match status{
			MeshDataNormalStatus::Agree(normal)=>Some(normal),
			MeshDataNormalStatus::Conflicting=>None,
		})
	}
}

fn build_mesh2(
	mb:&mut MeshBuilder,
	polygon_groups_normal_id:&mut [Vec<Vec<VertexId>>;NORMAL_FACES],
	cube_face_description:&[Option<FaceDescription>;NORMAL_FACES],
	mesh:rbx_mesh::mesh_data::Mesh2,
)->Result<(),Error>{
	//autoscale to size, idk what roblox is doing with the graphics mesh size
	let mut pos_min=glam::Vec3::MAX;
	let mut pos_max=glam::Vec3::MIN;
	for vertex in &mesh.vertices{
		let p=vertex.pos.into();
		pos_min=pos_min.min(p);
		pos_max=pos_max.max(p);
	}
	let graphics_size=pos_max-pos_min;
	for [MeshDataVertexId(vertex_id0),MeshDataVertexId(vertex_id1),MeshDataVertexId(vertex_id2)] in mesh.faces{
		let face=[
			mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
			mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
			mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
		];
		let mut normal_agreement_checker=MeshDataNormalChecker::new();
		let face=face.into_iter().map(|vertex|{
			let MeshDataNormalId2(normal_id)=vertex.normal_id;
			normal_agreement_checker.check(normal_id);
			let pos=glam::Vec3::from_array(vertex.pos)/graphics_size;
			let pos=mb.acquire_pos_id(vec3::try_from_f32_array(pos.to_array())?);
			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[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{
			print!("[union] Empty face!");
		}
	}
	Ok(())
}
fn build_mesh5(
	mb:&mut MeshBuilder,
	polygon_groups_normal_id:&mut [Vec<Vec<VertexId>>;NORMAL_FACES],
	cube_face_description:&[Option<FaceDescription>;NORMAL_FACES],
	mesh:rbx_mesh::mesh_data::CSGMDL5,
)->Result<(),Error>{
	//autoscale to size, idk what roblox is doing with the graphics mesh size
	let mut pos_min=glam::Vec3::MAX;
	let mut pos_max=glam::Vec3::MIN;
	for &pos in &mesh.positions{
		let p=pos.into();
		pos_min=pos_min.min(p);
		pos_max=pos_max.max(p);
	}
	let graphics_size=pos_max-pos_min;
	for face in mesh.faces.indices.chunks_exact(3){
		let mut normal_agreement_checker=MeshDataNormalChecker::new();
		let face=face.into_iter().map(|&vertex_id|{
			let vertex_index=vertex_id as usize;
			let &pos=mesh.positions.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
			let &MeshDataNormalId5(normal_id)=mesh.normal_ids.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
			let &norm=mesh.normals.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
			let &tex=mesh.tex.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
			let &color=mesh.colors.get(vertex_index).ok_or(Error::MissingVertexId(vertex_id))?;
			normal_agreement_checker.check(normal_id);
			let pos=glam::Vec3::from_array(pos)/graphics_size;
			let pos=mb.acquire_pos_id(vec3::try_from_f32_array(pos.to_array()).map_err(Error::Planar64Vec3)?);
			let normal=mb.acquire_normal_id(vec3::try_from_f32_array(norm).map_err(Error::Planar64Vec3)?);
			let tex_coord=glam::Vec2::from_array(tex);
			let maybe_face_description=&cube_face_description[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(color.map(|f|f as f32/255.0f32)));
					(tex,color)
				},
			};
			Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
		}).collect::<Result<Vec<_>,_>>()?;
		if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
			polygon_groups_normal_id[normal_id as usize-1].push(face);
		}else{
			print!("[union] Empty face!");
		}
	}
	Ok(())
}

fn new_polygon_list_checked(list:Vec<model::IndexedVertexList>)->Option<PolygonList>{
	(!list.is_empty()).then_some(PolygonList::new(list))
}

const NORMAL_FACES:usize=6;
impl std::error::Error for Error{}
pub fn convert(
	roblox_physics_data:&[u8],
	roblox_mesh_data:&[u8],
	size:glam::Vec3,
	RobloxPartDescription(part_texture_description):RobloxPartDescription,
)->Result<MeshWithSize,Error>{

	// build graphics and physics meshes
	let mut mb=MeshBuilder::new();
	// graphics
	let (polygon_groups_normal_id,graphics_groups)=if !roblox_mesh_data.is_empty(){
		let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
		// 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)?;
		match mesh_data{
			rbx_mesh::mesh_data::MeshData::CSGK(_)=>return Err(Error::Block),
			rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V2(mesh_data2))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data2.mesh)?,
			rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V4(mesh_data4))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4.mesh)?,
			rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V5(mesh_data4))=>build_mesh5(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4)?,
		};
		let graphics_groups=polygon_groups_normal_id
			.iter()
			.enumerate()
			.filter(|&(_,group)|!group.is_empty())
			.enumerate()
			.map(|(polygon_group_id,(normal_id,_))|{
				model::IndexedGraphicsGroup{
					render:cube_face_description[normal_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
					groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
				}
			}).collect();
		(polygon_groups_normal_id,graphics_groups)
	}else{
		([vec![],vec![],vec![],vec![],vec![],vec![]],Vec::new())
	};

	//physics
	let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().filter_map(|faces|
		// graphics polygon groups (to be rendered)
		Some(PolygonGroup::PolygonList(new_polygon_list_checked(faces)?))
	);
	let polygon_groups:Vec<PolygonGroup>=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::V3(meshes))
			|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V5(meshes))
			=>meshes.meshes,
			rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V6(meshes))
			=>vec![meshes.mesh],
			rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V7(meshes))
			=>meshes.meshes,
		};
		let physics_convex_meshes_it=physics_convex_meshes.into_iter().filter_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)
			let polygons=mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
				let face=[
					mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
					mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
					mesh.vertices.get(vertex_id2 as usize).ok_or(Error::MissingVertexId(vertex_id2))?,
				].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<_,_>>();
			let polygon_list=match polygons{
				Ok(polygons)=>new_polygon_list_checked(polygons)?,
				Err(e)=>return Some(Err(e)),
			};
			Some(Ok(PolygonGroup::PolygonList(polygon_list)))
		});
		polygon_groups_normal_it.map(Ok).chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
	}else{
		// generate a unit cube as default physics
		let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
		let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
		let normal=mb.acquire_normal_id(vec3::zero());
		let color=mb.acquire_color_id(glam::Vec4::ONE);
		let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
			mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})
		).to_vec()).to_vec()));
		polygon_groups_normal_it.chain([polygon_group]).collect()
	};
	let physics_groups=(graphics_groups.len()..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
		groups:vec![PolygonGroupId::new(id as u32)]
	}).collect();
	let mesh=mb.build(
		polygon_groups,
		graphics_groups,
		physics_groups,
	);
	Ok(MeshWithSize::new(mesh,vec3::ONE))
}
