use strafesnet_common::{map,model,integer,gameplay_attributes};

const VALVE_SCALE:f32=1.0/16.0;
fn valve_transform(v:[f32;3])->integer::Planar64Vec3{
	integer::Planar64Vec3::try_from([v[0]*VALVE_SCALE,v[2]*VALVE_SCALE,-v[1]*VALVE_SCALE]).unwrap()
}
pub fn convert_bsp<AcquireRenderConfigId,AcquireMeshId>(
	bsp:&vbsp::Bsp,
	mut acquire_render_config_id:AcquireRenderConfigId,
	mut acquire_mesh_id:AcquireMeshId
)->PartialMap1
where
	AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
	AcquireMeshId:FnMut(&str)->model::MeshId,
{
	//figure out real attributes later
	let mut unique_attributes=Vec::new();
	unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration);
	const TEMP_TOUCH_ME_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=gameplay_attributes::CollisionAttributesId::new(0);

	let mut prop_mesh_count=0;
	//declare all prop models to Loader
	let prop_models=bsp.static_props().map(|prop|{
		//get or create mesh_id
		let mesh_id=acquire_mesh_id(prop.model());
		//not the most failsafe code but this is just for the map tool lmao
		if prop_mesh_count==mesh_id.get(){
			prop_mesh_count+=1;
		};
		let placement=prop.as_prop_placement();
		model::Model{
			mesh:mesh_id,
			attributes:TEMP_TOUCH_ME_ATTRIBUTE,
			transform:integer::Planar64Affine3::new(
				integer::Planar64Mat3::try_from(
					glam::Mat3A::from_diagonal(glam::Vec3::splat(placement.scale))
					//TODO: figure this out
					*glam::Mat3A::from_quat(glam::Quat::from_array(placement.rotation.into()))
				).unwrap(),
				valve_transform(placement.origin.into()),
			),
			color:glam::Vec4::ONE,
		}
	}).collect();

	//TODO: make the main map one single mesh with a bunch of different physics groups and graphics groups

	//the generated MeshIds in here will collide with the Loader Mesh Ids
	//but I can't think of a good workaround other than just remapping one later.
	let world_meshes:Vec<model::Mesh>=bsp.models().map(|world_model|{
		//non-deduplicated
		let mut spam_pos=Vec::new();
		let mut spam_tex=Vec::new();
		let mut spam_normal=Vec::new();
		let mut spam_vertices=Vec::new();
		let mut graphics_groups=Vec::new();
		let mut physics_group=model::IndexedPhysicsGroup::default();
		let polygon_groups=world_model.faces().enumerate().map(|(polygon_group_id,face)|{
			let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
			let face_texture=face.texture();
			let face_texture_data=face_texture.texture_data();
			//this would be better as a 4x2 matrix
			let texture_transform_u=glam::Vec4::from_array(face_texture.texture_transforms_u)/(face_texture_data.width as f32);
			let texture_transform_v=glam::Vec4::from_array(face_texture.texture_transforms_v)/(face_texture_data.height as f32);

			//this automatically figures out what the texture is trying to do and creates
			//a render config for it, and then returns the id to that render config
			let render_id=acquire_render_config_id(Some(face_texture_data.name()));

			//normal
			let normal=face.normal();
			let normal_idx=spam_normal.len() as u32;
			spam_normal.push(valve_transform(normal.into()));
			let mut polygon_iter=face.vertex_positions().map(|vertex_position|{
				//world_model.origin seems to always be 0,0,0
				let vertex_xyz=(world_model.origin+vertex_position).into();
				let pos_idx=spam_pos.len();
				spam_pos.push(valve_transform(vertex_xyz));

				//calculate texture coordinates
				let pos=glam::Vec3::from_array(vertex_xyz).extend(1.0);
				let tex=glam::vec2(texture_transform_u.dot(pos),texture_transform_v.dot(pos));
				let tex_idx=spam_tex.len() as u32;
				spam_tex.push(tex);

				let vertex_id=model::VertexId::new(spam_vertices.len() as u32);
				spam_vertices.push(model::IndexedVertex{
					pos:model::PositionId::new(pos_idx as u32),
					tex:model::TextureCoordinateId::new(tex_idx as u32),
					normal:model::NormalId::new(normal_idx),
					color:model::ColorId::new(0),
				});
				vertex_id
			});
			let polygon_list=std::iter::from_fn(move||{
				match (polygon_iter.next(),polygon_iter.next(),polygon_iter.next()){
					(Some(v1),Some(v2),Some(v3))=>Some(vec![v1,v2,v3]),
					//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
					_=>None,
				}
			}).collect();
			if face.is_visible(){
				//TODO: deduplicate graphics groups by render id
				graphics_groups.push(model::IndexedGraphicsGroup{
					render:render_id,
					groups:vec![polygon_group_id],
				})
			}
			physics_group.groups.push(polygon_group_id);
			model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))
		}).collect();
		model::Mesh{
			unique_pos:spam_pos,
			unique_tex:spam_tex,
			unique_normal:spam_normal,
			unique_color:vec![glam::Vec4::ONE],
			unique_vertices:spam_vertices,
			polygon_groups,
			graphics_groups,
			physics_groups:vec![physics_group],
		}
	}).collect();
	
	let world_models:Vec<model::Model>=
	//one instance of the main world mesh
	std::iter::once((
		//world_model
		model::MeshId::new(0),
		//model_origin
		vbsp::Vector::from([0.0,0.0,0.0]),
		//model_color
		[255.0,255.0,255.0]
	)).chain(
		//entities sprinkle instances of the other meshes around
		bsp.entities.iter()
		.flat_map(|ent|ent.parse())//ignore entity parsing errors
		.filter_map(|ent|match ent{
			vbsp::Entity::Brush(brush)=>Some(brush),
			vbsp::Entity::BrushIllusionary(brush)=>Some(brush),
			vbsp::Entity::BrushWall(brush)=>Some(brush),
			vbsp::Entity::BrushWallToggle(brush)=>Some(brush),
			_=>None,
		}).flat_map(|brush|
			//The first character of brush.model is '*'
			brush.model[1..].parse().map(|mesh_id|//ignore parse int errors
				(model::MeshId::new(mesh_id),brush.origin,brush.color)
			)
		)
	).map(|(mesh_id,model_origin,model_color)|{
		model::Model{
			mesh:mesh_id,
			attributes:TEMP_TOUCH_ME_ATTRIBUTE,
			transform:integer::Planar64Affine3::new(
				integer::Planar64Mat3::default(),
				valve_transform(model_origin.into())
			),
			color:(glam::Vec3::from_array(model_color)/255.0).extend(1.0),
		}
	}).collect();

	PartialMap1{
		attributes:unique_attributes,
		world_meshes,
		prop_models,
		world_models,
		modes:strafesnet_common::gameplay_modes::Modes::new(Vec::new()),
	}
}

//partially constructed map types
pub struct PartialMap1{
	attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
	prop_models:Vec<model::Model>,
	world_meshes:Vec<model::Mesh>,
	world_models:Vec<model::Model>,
	modes:strafesnet_common::gameplay_modes::Modes,
}
impl PartialMap1{
	pub fn add_prop_meshes<AcquireRenderConfigId>(
		self,
		prop_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::ModelData)>,
		mut acquire_render_config_id:AcquireRenderConfigId,
	)->PartialMap2
	where
		AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
	{
		PartialMap2{
			attributes:self.attributes,
			prop_meshes:prop_meshes.into_iter().filter_map(|(mesh_id,model_data)|
				//this will generate new render ids and texture ids
				match convert_mesh(model_data,&mut acquire_render_config_id){
					Ok(mesh)=>Some((mesh_id,mesh)),
					Err(e)=>{
						println!("error converting mesh: {e}");
						None
					}
				}
			).collect(),
			prop_models:self.prop_models,
			world_meshes:self.world_meshes,
			world_models:self.world_models,
			modes:self.modes,
		}
	}
}
pub struct PartialMap2{
	attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
	prop_meshes:Vec<(model::MeshId,model::Mesh)>,
	prop_models:Vec<model::Model>,
	world_meshes:Vec<model::Mesh>,
	world_models:Vec<model::Model>,
	modes:strafesnet_common::gameplay_modes::Modes,
}
impl PartialMap2{
	pub fn add_render_configs_and_textures(
		mut self,
		render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
		textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
	)->map::CompleteMap{
		//merge mesh and model lists, flatten and remap all ids
		let mesh_id_offset=self.world_meshes.len();
		println!("prop_meshes.len()={}",self.prop_meshes.len());
		let (mut prop_meshes,prop_mesh_id_map):(Vec<model::Mesh>,std::collections::HashMap<model::MeshId,model::MeshId>)
		=self.prop_meshes.into_iter().enumerate().map(|(new_mesh_id,(old_mesh_id,mesh))|{
			(mesh,(old_mesh_id,model::MeshId::new((mesh_id_offset+new_mesh_id) as u32)))
		}).unzip();
		self.world_meshes.append(&mut prop_meshes);
		//there is no modes or runtime behaviour with references to the model ids currently
		//so just relentlessly cull them if the mesh is missing
		self.world_models.extend(self.prop_models.into_iter().filter_map(|mut model|
			prop_mesh_id_map.get(&model.mesh).map(|&new_mesh_id|{
				model.mesh=new_mesh_id;
				model
			})
		));
		//let mut models=Vec::new();
		let (textures,texture_id_map):(Vec<Vec<u8>>,std::collections::HashMap<model::TextureId,model::TextureId>)
		=textures.into_iter()
		//.filter_map(f) cull unused textures
		.enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
			(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
		}).unzip();
		let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{
			//this may generate duplicate no-texture render configs but idc
			render_config.texture=render_config.texture.and_then(|texture_id|
				texture_id_map.get(&texture_id).copied()
			);
			render_config
		}).collect();
		map::CompleteMap{
			modes:self.modes,
			attributes:self.attributes,
			meshes:self.world_meshes,
			models:self.world_models,
			textures,
			render_configs,
		}
	}
}

fn convert_mesh<AcquireRenderConfigId>(
	model_data:crate::data::ModelData,
	acquire_render_config_id:&mut AcquireRenderConfigId,
)->Result<model::Mesh,vmdl::ModelError>
where
	AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
	let model=model_data.read_model()?;
	let texture_paths=model.texture_directories();
	if texture_paths.len()!=1{
		println!("WARNING: multiple texture paths");
	}
	let skin=model.skin_tables().nth(0).unwrap();

	let mut spam_pos=Vec::with_capacity(model.vertices().len());
	let mut spam_normal=Vec::with_capacity(model.vertices().len());
	let mut spam_tex=Vec::with_capacity(model.vertices().len());
	let mut spam_vertices=Vec::with_capacity(model.vertices().len());
	for (i,vertex) in model.vertices().iter().enumerate(){
		spam_pos.push(valve_transform(vertex.position.into()));
		spam_normal.push(valve_transform(vertex.normal.into()));
		spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
		spam_vertices.push(model::IndexedVertex{
			pos:model::PositionId::new(i as u32),
			tex:model::TextureCoordinateId::new(i as u32),
			normal:model::NormalId::new(i as u32),
			color:model::ColorId::new(0),
		});
	}
	let mut graphics_groups=Vec::new();
	let mut physics_groups=Vec::new();
	let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
		let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);

		let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
			let mut path=std::path::PathBuf::from(texture_path.as_str());
			path.push(texture_name);
			acquire_render_config_id(path.as_os_str().to_str())
		}else{
			acquire_render_config_id(None)
		};

		graphics_groups.push(model::IndexedGraphicsGroup{
			render:render_id,
			groups:vec![polygon_group_id],
		});
		physics_groups.push(model::IndexedPhysicsGroup{
			groups:vec![polygon_group_id],
		});
		model::PolygonGroup::PolygonList(model::PolygonList::new(
			//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
			mesh.vertex_strip_indices().flat_map(|mut strip|
				std::iter::from_fn(move||{
					match (strip.next(),strip.next(),strip.next()){
						(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
						//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
						_=>None,
					}
				})
			).collect()
		))
	}).collect();
	Ok(model::Mesh{
		unique_pos:spam_pos,
		unique_normal:spam_normal,
		unique_tex:spam_tex,
		unique_color:vec![glam::Vec4::ONE],
		unique_vertices:spam_vertices,
		polygon_groups,
		graphics_groups,
		physics_groups,
	})
}