use std::collections::HashMap;
use crate::error::{RecoverableErrors,CFrameError,CFrameErrorType,DuplicateStageError,InstancePath,NormalIdError,Planar64ConvertError,ParseIntContext,ShapeError};
use crate::loader::{MeshWithSize,MeshIndex};
use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
use strafesnet_common::map;
use strafesnet_common::model;
use strafesnet_common::gameplay_modes::{NormalizedModes,Mode,ModeId,ModeUpdate,ModesBuilder,Stage,StageElement,StageElementBehaviour,StageId,Zone};
use strafesnet_common::gameplay_style;
use strafesnet_common::gameplay_attributes as attr;
use strafesnet_common::integer::{self,vec3,Planar64TryFromFloatError,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3};
use strafesnet_common::model::RenderConfigId;
use strafesnet_deferred_loader::deferred_loader::{RenderConfigDeferredLoader,MeshDeferredLoader};
use strafesnet_deferred_loader::mesh::Meshes;
use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};

// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
	rbx_dom_weak::ustr(s)
}

macro_rules! lazy_regex{
	($r:literal)=>{{
		use regex::Regex;
		use std::sync::LazyLock;
		static RE:LazyLock<Regex>=LazyLock::new(||Regex::new($r).unwrap());
		&RE
	}};
}

fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Result<Planar64Affine3,Planar64TryFromFloatError>{
	Ok(Planar64Affine3::new(
		Planar64Mat3::from_cols([
			(vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x])?
			*integer::try_from_f32(size.x/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
			(vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y])?
			*integer::try_from_f32(size.y/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
			(vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z])?
			*integer::try_from_f32(size.z/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
		]),
		vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z])?
	))
}

enum GetAttributesError{
	ModeIdParseInt(ParseIntContext),
	DuplicateMode(ModeId),
	StageIdParseInt(ParseIntContext),
	DuplicateStage(DuplicateStageError),
	WormholeOutIdParseInt(ParseIntContext),
	DuplicateWormholeOut(u32),
	WormholeInIdParseInt(ParseIntContext),
	JumpLimitParseInt(ParseIntContext),
}

fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap<model::ModelId,u32>,wormhole_id_to_out_model:&mut HashMap<u32,model::ModelId>)->Result<attr::CollisionAttributes,GetAttributesError>{
	let mut general=attr::GeneralAttributes::default();
	let mut intersecting=attr::IntersectingAttributes::default();
	let mut contacting=attr::ContactingAttributes::default();
	let mut force_can_collide=can_collide;
	let mut force_intersecting=false;
	let mut allow_booster=true;
	match name{
		"Water"=>{
			force_can_collide=false;
			allow_booster=false;
			//TODO: read stupid CustomPhysicalProperties
			intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity});
		},
		"Accelerator"=>{
			//although the new game supports collidable accelerators, this is a roblox compatability map loader
			force_can_collide=false;
			// Accelerator is not allowed to be booster in roblox
			allow_booster=false;
			general.accelerator=Some(attr::Accelerator{acceleration:velocity});
		},
		// "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{
		// 	mode_id:0,
		// 	stage_id:0,
		// 	force:false,
		// 	behaviour:model::StageElementBehaviour::Unordered
		// })),
		"SetVelocity"=>{
			allow_booster=false;
			general.trajectory=Some(attr::SetTrajectory::Velocity(velocity));
		},
		"MapStart"=>{
			force_can_collide=false;
			force_intersecting=true;
			let mode_id=ModeId::MAIN;
			modes_builder.insert_mode(
				mode_id,
				Mode::empty(
					gameplay_style::StyleModifiers::roblox_bhop(),
					model_id
				)
			).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
		},
		"MapFinish"=>{
			force_can_collide=false;
			force_intersecting=true;
			modes_builder.push_mode_update(
				ModeId::MAIN,
				ModeUpdate::zone(
					model_id,
					Zone::Finish,
				),
			);
		},
		"MapAnticheat"=>{
			force_can_collide=false;
			force_intersecting=true;
			modes_builder.push_mode_update(
				ModeId::MAIN,
				ModeUpdate::zone(
					model_id,
					Zone::Anticheat,
				),
			);
		},
		"Platform"=>{
			modes_builder.push_mode_update(
				ModeId::MAIN,
				ModeUpdate::element(
					model_id,
					StageElement::new(StageId::FIRST,false,StageElementBehaviour::Platform,None),//roblox does not know which stage the platform belongs to
				),
			);
		},
		other=>{
			let regman=lazy_regex!(r"^(BonusStart|WormholeOut)(\d+)$");
			if let Some(captures)=regman.captures(other){
				match &captures[1]{
					"BonusStart"=>{
						force_can_collide=false;
						force_intersecting=true;
						let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
						modes_builder.insert_mode(
							mode_id,
							Mode::empty(
								gameplay_style::StyleModifiers::roblox_bhop(),
								model_id
							)
						).map_err(|_|GetAttributesError::DuplicateMode(mode_id))?;
					},
					"WormholeOut"=>{
						//the PhysicsModelId has to exist for it to be teleported to!
						force_intersecting=true;
						//this object is not special in strafe client, but the roblox mapping needs to be converted to model id
						let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeOutIdParseInt)?;
						if wormhole_id_to_out_model.insert(wormhole_id,model_id).is_some(){
							return Err(GetAttributesError::DuplicateWormholeOut(wormhole_id));
						}
					},
					_=>(),
				}
			}else if let Some(captures)=lazy_regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
			.captures(other){
				force_intersecting=true;
				let stage_id=StageId::new(ParseIntContext::parse(&captures[3]).map_err(GetAttributesError::StageIdParseInt)?);
				let stage_element=StageElement::new(
					//stage_id:
					stage_id,
					//force:
					match captures.get(1){
						Some(m)=>m.as_str()=="Force",
						None=>false,
					},
					//behaviour:
					match &captures[2]{
						"Spawn"=>{
							let mode_id=ModeId::MAIN;
							modes_builder.insert_stage(
								mode_id,
								stage_id,
								Stage::empty(model_id),
							).map_err(|_|GetAttributesError::DuplicateStage(DuplicateStageError{mode_id,stage_id}))?;
							//TODO: let denormalize handle this
							StageElementBehaviour::SpawnAt
						},
						"SpawnAt"=>StageElementBehaviour::SpawnAt,
						//cancollide false so you don't hit the side
						//NOT a decoration
						"Trigger"=>{force_can_collide=false;StageElementBehaviour::Trigger},
						"Teleport"=>{force_can_collide=false;StageElementBehaviour::Teleport},
						"Platform"=>StageElementBehaviour::Platform,
						_=>unreachable!("regex1[2] messed up bad"),
					},
					None
				);
				modes_builder.push_mode_update(
					ModeId::MAIN,
					ModeUpdate::element(
						model_id,
						stage_element,
					),
				);
			}else if let Some(captures)=lazy_regex!(r"^(Jump|WormholeIn)(\d+)$")
			.captures(other){
				match &captures[1]{
					"Jump"=>modes_builder.push_mode_update(
						ModeId::MAIN,
						ModeUpdate::element(
							model_id,
							//jump_limit:
							StageElement::new(
								StageId::FIRST,
								false,
								StageElementBehaviour::Check,
								Some(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::JumpLimitParseInt)?)
							)
						),
					),
					"WormholeIn"=>{
						force_can_collide=false;
						force_intersecting=true;
						let wormhole_id=ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::WormholeInIdParseInt)?;
						// It is impossible for two different objects to have the same model id
						assert!(wormhole_in_model_to_id.insert(model_id,wormhole_id).is_none(),"Impossible");
					},
					_=>unreachable!("regex2[1] messed up bad"),
				}
			}else if let Some(captures)=lazy_regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
			.captures(other){
				force_can_collide=false;
				force_intersecting=true;
				let mode_id=ModeId::new(ParseIntContext::parse(&captures[2]).map_err(GetAttributesError::ModeIdParseInt)?);
				modes_builder.push_mode_update(
					mode_id,
					ModeUpdate::zone(
						model_id,
						//zone:
						match &captures[1]{
							"Finish"=>Zone::Finish,
							"Anticheat"=>Zone::Anticheat,
							_=>unreachable!("regex3[1] messed up bad"),
						},
					),
				);
			}
			// else if let Some(captures)=lazy_regex::regex!(r"^Stage(\d+)OrderedCheckpoint(\d+)$")
			// .captures(other){
			// 	match &captures[1]{
			// 		"OrderedCheckpoint"=>modes_builder.push_stage_update(
			// 			ModeId::MAIN,
			// 			StageId::new(0),
			// 			StageUpdate::ordered_checkpoint(captures[2].parse::<u32>().unwrap()),
			// 		),
			// 		_=>panic!("regex3[1] messed up bad"),
			// 	}
			// }
		}
	}
	//need some way to skip this
	if allow_booster&&velocity!=vec3::zero(){
		general.booster=Some(attr::Booster::Velocity(velocity));
	}
	Ok(match force_can_collide{
		true=>{
			match name{
				"Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)),
				"Surf"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Surf),
				"Ladder"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Ladder(attr::ContactingLadder{sticky:true})),
				_=>(),
			}
			attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting,general})
		},
		false=>if force_intersecting
		||general.any()
		||intersecting.any()
		{
			attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting,general})
		}else{
			attr::CollisionAttributes::Decoration
		},
	})
}

#[derive(Clone,Copy)]
pub struct RobloxTextureTransform{
	offset_studs_u:f32,
	offset_studs_v:f32,
	studs_per_tile_u:f32,
	studs_per_tile_v:f32,
	size_u:f32,
	size_v:f32,
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
pub struct RobloxTextureTransformBits{
	offset_studs_u:u32,
	offset_studs_v:u32,
	studs_per_tile_u:u32,
	studs_per_tile_v:u32,
	size_u:u32,
	size_v:u32,
}
impl RobloxTextureTransform{
	fn identity()->Self{
		Self{
			offset_studs_u:0.0,
			offset_studs_v:0.0,
			studs_per_tile_u:1.0,
			studs_per_tile_v:1.0,
			size_u:1.0,
			size_v:1.0,
		}
	}
	pub fn to_bits(self)->RobloxTextureTransformBits{
		RobloxTextureTransformBits{
			offset_studs_u:self.offset_studs_u.to_bits(),
			offset_studs_v:self.offset_studs_v.to_bits(),
			studs_per_tile_u:self.studs_per_tile_u.to_bits(),
			studs_per_tile_v:self.studs_per_tile_v.to_bits(),
			size_u:self.size_u.to_bits(),
			size_v:self.size_v.to_bits(),
		}
	}
	pub fn affine(&self)->glam::Affine2{
		glam::Affine2::from_translation(
			glam::vec2(self.offset_studs_u/self.studs_per_tile_u,self.offset_studs_v/self.studs_per_tile_v)
		)
		*glam::Affine2::from_scale(
			glam::vec2(self.size_u/self.studs_per_tile_u,self.size_v/self.studs_per_tile_v)
		)
	}
	pub fn set_size(&mut self,size_u:f32,size_v:f32){
		self.size_u=size_u;
		self.size_v=size_v;
	}
}
impl core::hash::Hash for RobloxTextureTransform{
	fn hash<H:core::hash::Hasher>(&self,state:&mut H){
		self.to_bits().hash(state);
	}
}
#[derive(Clone,Copy,Hash,Eq,PartialEq)]
pub struct RobloxFaceTextureDescriptionBits{
	render:RenderConfigId,
	color:[u32;4],
	transform:RobloxTextureTransformBits,
}
#[derive(Clone,Copy)]
pub struct RobloxFaceTextureDescription{
	pub render:RenderConfigId,
	pub color:glam::Vec4,
	pub transform:RobloxTextureTransform,
}
impl PartialEq for RobloxFaceTextureDescription{
	fn eq(&self,other:&Self)->bool{
		self.to_bits().eq(&other.to_bits())
	}
}
impl Eq for RobloxFaceTextureDescription{}
impl core::hash::Hash for RobloxFaceTextureDescription{
	fn hash<H:core::hash::Hasher>(&self,state:&mut H){
		self.to_bits().hash(state);
	}
}
impl RobloxFaceTextureDescription{
	pub fn to_bits(self)->RobloxFaceTextureDescriptionBits{
		RobloxFaceTextureDescriptionBits{
			render:self.render,
			color:self.color.to_array().map(f32::to_bits),
			transform:self.transform.to_bits(),
		}
	}
	pub fn to_face_description(&self)->FaceDescription{
		FaceDescription{
			render:self.render,
			transform:self.transform.affine(),
			color:self.color,
		}
	}
}
macro_rules! impl_description_index{
	($description:ident,$index:ty)=>{
		impl core::ops::Index<$index> for $description{
			type Output=Option<RobloxFaceTextureDescription>;
			fn index(&self,index:$index)->&Self::Output{
				&self.0[index as usize]
			}
		}
		impl core::ops::IndexMut<$index> for $description{
			fn index_mut(&mut self,index:$index)->&mut Self::Output{
				&mut self.0[index as usize]
			}
		}
	};
}

#[derive(Clone,Default,Eq,Hash,PartialEq)]
pub struct RobloxPartDescription(pub(crate)[Option<RobloxFaceTextureDescription>;6]);
impl_description_index!(RobloxPartDescription,CubeFace);

#[derive(Clone,Default,Eq,Hash,PartialEq)]
pub struct RobloxWedgeDescription(pub(crate)[Option<RobloxFaceTextureDescription>;5]);

#[derive(Clone,Default,Eq,Hash,PartialEq)]
pub struct RobloxCornerWedgeDescription(pub(crate)[Option<RobloxFaceTextureDescription>;5]);

#[derive(Clone,Eq,Hash,PartialEq)]
enum RobloxBasePartDescription{
	Sphere(RobloxPartDescription),
	Part(RobloxPartDescription),
	Cylinder(RobloxPartDescription),
	Wedge(RobloxWedgeDescription),
	CornerWedge(RobloxCornerWedgeDescription),
}

// TODO: initialize all ustrs in a top level struct thrown around as a reference
// struct UstrKludge{
// }

fn get_content_url(content:&rbx_dom_weak::types::Content)->Option<&str>{
	match content.value(){
		rbx_dom_weak::types::ContentType::Uri(uri)=>Some(uri.as_str()),
		_=>None,
	}
}

fn get_texture_description<'a>(
	render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
	recoverable_errors:&mut RecoverableErrors,
	db:&rbx_reflection::ReflectionDatabase,
	dom:&'a rbx_dom_weak::WeakDom,
	object:&rbx_dom_weak::Instance,
	size:&rbx_dom_weak::types::Vector3,
)->RobloxPartDescription{
	//use the biggest one and cut it down later...
	let mut part_texture_description=RobloxPartDescription::default();
	let decal=&db.classes["Decal"];
	let decals=object.children().iter().filter_map(|&referent|{
		let instance=dom.get_by_ref(referent)?;
		db.classes.get(instance.class.as_str()).is_some_and(|class|
			db.has_superclass(class,decal)
		).then_some(instance)
	});
	for decal in decals{
		// decals should always have these properties,
		// but it is not guaranteed by the rbx_dom_weak data structure.
		let (
			Some(rbx_dom_weak::types::Variant::Content(content)),
			Some(rbx_dom_weak::types::Variant::Enum(normalid)),
			Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
			Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
		)=(
			decal.properties.get(&static_ustr("TextureContent")),
			decal.properties.get(&static_ustr("Face")),
			decal.properties.get(&static_ustr("Color3")),
			decal.properties.get(&static_ustr("Transparency")),
		)else{
			recoverable_errors.decal_property.push(InstancePath::new(dom,decal));
			continue;
		};
		let texture_id=get_content_url(content);
		let render_id=render_config_deferred_loader.acquire_render_config_id(texture_id);
		let Ok(cube_face)=normalid.to_u32().try_into()else{
			recoverable_errors.normal_id.push(NormalIdError{
				path:InstancePath::new(dom,decal),
				normal_id:normalid.to_u32(),
			});
			continue;
		};
		let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
			//generate tranform
			if let (
					Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_u)),
					Some(&rbx_dom_weak::types::Variant::Float32(offset_studs_v)),
					Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_u)),
					Some(&rbx_dom_weak::types::Variant::Float32(studs_per_tile_v)),
				) = (
					decal.properties.get(&static_ustr("OffsetStudsU")),
					decal.properties.get(&static_ustr("OffsetStudsV")),
					decal.properties.get(&static_ustr("StudsPerTileU")),
					decal.properties.get(&static_ustr("StudsPerTileV")),
				)
			{
				let (size_u,size_v)=match cube_face{
					CubeFace::Right=>(size.z,size.y),//right
					CubeFace::Top=>(size.x,size.z),//top
					CubeFace::Back=>(size.x,size.y),//back
					CubeFace::Left=>(size.z,size.y),//left
					CubeFace::Bottom=>(size.x,size.z),//bottom
					CubeFace::Front=>(size.x,size.y),//front
				};
				(
					glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
					RobloxTextureTransform{
						offset_studs_u,
						offset_studs_v,
						studs_per_tile_u,
						studs_per_tile_v,
						size_u,
						size_v,
					}
				)
			}else{
				recoverable_errors.texture_property.push(InstancePath::new(dom,decal));
				(glam::Vec4::ONE,RobloxTextureTransform::identity())
			}
		}else{
			(glam::Vec4::ONE,RobloxTextureTransform::identity())
		};
		part_texture_description[cube_face]=Some(RobloxFaceTextureDescription{
			render:render_id,
			color:roblox_texture_color,
			transform:roblox_texture_transform,
		});
	}
	part_texture_description
}
enum Shape{
	Primitive(Primitives),
	MeshPart,
	PhysicsData,
}
enum MeshAvailability{
	Immediate,
	DeferredMesh(RenderConfigId),
	DeferredUnion(RobloxPartDescription),
}
struct DeferredModelDeferredAttributes<'a>{
	render:RenderConfigId,
	model:ModelDeferredAttributes<'a>,
}
struct ModelDeferredAttributes<'a>{
	mesh:model::MeshId,
	deferred_attributes:GetAttributesArgs<'a>,
	color:model::Color4,//transparency is in here
	transform:Planar64Affine3,
}
struct DeferredUnionDeferredAttributes<'a>{
	render:RobloxPartDescription,
	model:ModelDeferredAttributes<'a>,
}
struct ModelOwnedAttributes{
	mesh:model::MeshId,
	attributes:attr::CollisionAttributes,
	color:model::Color4,//transparency is in here
	transform:Planar64Affine3,
}
struct GetAttributesArgs<'a>{
	name:&'a str,
	can_collide:bool,
	velocity:Planar64Vec3,
}
pub fn convert<'a>(
	dom:&'a rbx_dom_weak::WeakDom,
	render_config_deferred_loader:&mut RenderConfigDeferredLoader<&'a str>,
	mesh_deferred_loader:&mut MeshDeferredLoader<MeshIndex<'a>>,
)->PartialMap1<'a>{
	let mut recoverable_errors=RecoverableErrors::default();

	let mut deferred_models_deferred_attributes=Vec::new();
	let mut deferred_unions_deferred_attributes=Vec::new();
	let mut primitive_models_deferred_attributes=Vec::new();
	let mut primitive_meshes=Vec::new();
	let mut mesh_id_from_description=HashMap::new();

	//just going to leave it like this for now instead of reworking the data structures for this whole thing
	let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);

	let db=rbx_reflection_database::get().unwrap();
	let basepart=&db.classes["BasePart"];
	let baseparts=dom.descendants().filter(|&instance|
		db.classes.get(instance.class.as_str()).is_some_and(|class|
			db.has_superclass(class,basepart)
		)
	);
	for object in baseparts{
		let (
			Some(rbx_dom_weak::types::Variant::CFrame(cf)),
			Some(rbx_dom_weak::types::Variant::Vector3(size)),
			Some(rbx_dom_weak::types::Variant::Vector3(velocity)),
			Some(rbx_dom_weak::types::Variant::Float32(transparency)),
			Some(rbx_dom_weak::types::Variant::Color3uint8(color3)),
			Some(&rbx_dom_weak::types::Variant::Bool(can_collide)),
		) = (
			object.properties.get(&static_ustr("CFrame")),
			object.properties.get(&static_ustr("Size")),
			object.properties.get(&static_ustr("Velocity")),
			object.properties.get(&static_ustr("Transparency")),
			object.properties.get(&static_ustr("Color")),
			object.properties.get(&static_ustr("CanCollide")),
		)else{
			recoverable_errors.basepart_property.push(InstancePath::new(dom,object));
			continue;
		};
		let model_transform=match planar64_affine3_from_roblox(cf,size){
			Ok(model_transform)=>{
				if model_transform.matrix3.det().is_zero(){
					recoverable_errors.basepart_cframe.push(CFrameError{
						path:InstancePath::new(dom,object),
						error:CFrameErrorType::ZeroDeterminant,
					});
					continue;
				}
				model_transform
			},
			Err(e)=>{
				recoverable_errors.basepart_cframe.push(CFrameError{
					path:InstancePath::new(dom,object),
					error:CFrameErrorType::Convert(e),
				});
				continue;
			}
		};

		//TODO: also detect "CylinderMesh" etc here
		let shape=match object.class.as_str(){
			"Part"|"Seat"|"SpawnLocation"=>{
				let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get(&static_ustr("Shape"))else{
					recoverable_errors.part_property.push(InstancePath::new(dom,object));
					continue;
				};
				let Ok(shape)=shape.to_u32().try_into()else{
					recoverable_errors.part_shape.push(ShapeError{
						path:InstancePath::new(dom,object),
						shape:shape.to_u32(),
					});
					continue;
				};
				Shape::Primitive(shape)
			},
			"TrussPart"|"VehicleSeat"=>Shape::Primitive(Primitives::Cube),
			"WedgePart"=>Shape::Primitive(Primitives::Wedge),
			"CornerWedgePart"=>Shape::Primitive(Primitives::CornerWedge),
			"MeshPart"=>Shape::MeshPart,
			"UnionOperation"=>Shape::PhysicsData,
			"Terrain"=>continue,
			_=>{
				recoverable_errors.unsupported_class.insert(object.class.as_str().to_owned());
				Shape::Primitive(Primitives::Cube)
			}
		};

		let (availability,mesh_id)=match shape{
			Shape::Primitive(primitive_shape)=>{
				let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
				//obscure rust syntax "slice pattern"
				let RobloxPartDescription([
					f0,//Cube::Right
					f1,//Cube::Top
					f2,//Cube::Back
					f3,//Cube::Left
					f4,//Cube::Bottom
					f5,//Cube::Front
				])=part_texture_description;
				let basepart_description=match primitive_shape{
					Primitives::Sphere=>RobloxBasePartDescription::Sphere(RobloxPartDescription([f0,f1,f2,f3,f4,f5])),
					Primitives::Cube=>RobloxBasePartDescription::Part(RobloxPartDescription([f0,f1,f2,f3,f4,f5])),
					Primitives::Cylinder=>RobloxBasePartDescription::Cylinder(RobloxPartDescription([f0,f1,f2,f3,f4,f5])),
					//use front face texture first and use top face texture as a fallback
					Primitives::Wedge=>RobloxBasePartDescription::Wedge(RobloxWedgeDescription([
						f0,//Cube::Right->Wedge::Right
						f5.or(f1),//Cube::Front|Cube::Top->Wedge::TopFront
						f2,//Cube::Back->Wedge::Back
						f3,//Cube::Left->Wedge::Left
						f4,//Cube::Bottom->Wedge::Bottom
					])),
					//TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top
					Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge(RobloxCornerWedgeDescription([
						f0,//Cube::Right->CornerWedge::Right
						f2.or(f1.clone()),//Cube::Back|Cube::Top->CornerWedge::TopBack
						f3.or(f1),//Cube::Left|Cube::Top->CornerWedge::TopLeft
						f4,//Cube::Bottom->CornerWedge::Bottom
						f5,//Cube::Front->CornerWedge::Front
					])),
				};
				//make new model if unit cube has not been created before
				let mesh_id=if let Some(&mesh_id)=mesh_id_from_description.get(&basepart_description){
					//push to existing texture model
					mesh_id
				}else{
					let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
					mesh_id_from_description.insert(basepart_description.clone(),mesh_id);//borrow checker going crazy
					let mesh=match basepart_description{
						RobloxBasePartDescription::Cylinder(part_texture_description)=>primitives::unit_cylinder(CubeFaceDescription::new(part_texture_description,textureless_render_group)),
						RobloxBasePartDescription::Sphere(part_texture_description)
						|RobloxBasePartDescription::Part(part_texture_description)=>primitives::unit_cube(CubeFaceDescription::new(part_texture_description,textureless_render_group)),
						RobloxBasePartDescription::Wedge(wedge_texture_description)=>primitives::unit_wedge(WedgeFaceDescription::new(wedge_texture_description,textureless_render_group)),
						RobloxBasePartDescription::CornerWedge(cornerwedge_texture_description)=>primitives::unit_cornerwedge(CornerWedgeFaceDescription::new(cornerwedge_texture_description,textureless_render_group)),
					};
					primitive_meshes.push(mesh);
					mesh_id
				};
				(MeshAvailability::Immediate,mesh_id)
			},
			Shape::MeshPart=>{
				let (
					Some(rbx_dom_weak::types::Variant::Content(mesh_content)),
					Some(rbx_dom_weak::types::Variant::Content(texture_content)),
				)=(
					// mesh must exist
					object.properties.get(&static_ustr("MeshContent")),
					// texture is allowed to be none
					object.properties.get(&static_ustr("TextureContent")),
				)else{
					recoverable_errors.meshpart_property.push(InstancePath::new(dom,object));
					continue;
				};
				let mesh_asset_id=match get_content_url(mesh_content){
					Some(mesh_asset_id)=>mesh_asset_id,
					None=>{
						recoverable_errors.meshpart_content.push(InstancePath::new(dom,object));
						// Return an empty string which will fail to parse as an asset id
						""
					}
				};
				let texture_asset_id=get_content_url(texture_content);
				(
					MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
					mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
				)
			},
			Shape::PhysicsData=>{
				let mut content="";
				let mut mesh_data:&[u8]=&[];
				let mut physics_data:&[u8]=&[];
				if let Some(rbx_dom_weak::types::Variant::ContentId(asset_id))=object.properties.get(&static_ustr("AssetId")){
					content=asset_id.as_ref();
				}
				if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("MeshData")){
					mesh_data=data.as_ref();
				}
				if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get(&static_ustr("PhysicsData")){
					physics_data=data.as_ref();
				}
				let part_texture_description=get_texture_description(render_config_deferred_loader,&mut recoverable_errors,db,dom,object,size);
				let mesh_index=MeshIndex::union(content,mesh_data,physics_data,size,part_texture_description.clone());
				let mesh_id=mesh_deferred_loader.acquire_mesh_id(mesh_index);
				(MeshAvailability::DeferredUnion(part_texture_description),mesh_id)
			},
		};
		let velocity=match vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]){
			Ok(velocity)=>velocity,
			Err(e)=>{
				recoverable_errors.basepart_velocity.push(Planar64ConvertError{
					path:InstancePath::new(dom,object),
					error:e,
				});
				continue;
			}
		};
		let model_deferred_attributes=ModelDeferredAttributes{
			mesh:mesh_id,
			transform:model_transform,
			color:glam::vec4(color3.r as f32/255f32,color3.g as f32/255f32,color3.b as f32/255f32,1.0-*transparency),
			deferred_attributes:GetAttributesArgs{
				name:object.name.as_str(),
				can_collide,
				velocity,
			},
		};
		match availability{
			MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
			MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
				render,
				model:model_deferred_attributes
			}),
			MeshAvailability::DeferredUnion(part_texture_description)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
				render:part_texture_description,
				model:model_deferred_attributes,
			}),
		}
	}
	PartialMap1{
		primitive_meshes,
		primitive_models_deferred_attributes,
		deferred_models_deferred_attributes,
		deferred_unions_deferred_attributes,
		recoverable_errors,
	}
}
struct MeshIdWithSize{
	mesh:model::MeshId,
	size:Planar64Vec3,
}
fn acquire_mesh_id_from_render_config_id(
	primitive_meshes:&mut Vec<model::Mesh>,
	mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
	loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
	old_mesh_id:model::MeshId,
	render:RenderConfigId,
)->Option<MeshIdWithSize>{
	//ignore meshes that fail to load completely for now
	loaded_meshes.get(&old_mesh_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
		mesh:*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
		.entry(render).or_insert_with(||{
			let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
			let mut mesh_clone=mesh.clone();
			//set the render group lool
			if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
				graphics_group.render=render;
			}
			primitive_meshes.push(mesh_clone);
			mesh_id
		}),
		size,
	})
}
fn acquire_union_id_from_render_config_id(
	primitive_meshes:&mut Vec<model::Mesh>,
	union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
	loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
	old_union_id:model::MeshId,
	part_texture_description:RobloxPartDescription,
)->Option<MeshIdWithSize>{
	//ignore uniones that fail to load completely for now
	loaded_meshes.get(&old_union_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
		mesh:*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
		.entry(part_texture_description.clone()).or_insert_with(||{
			let union_id=model::MeshId::new(primitive_meshes.len() as u32);
			let mut union_clone=mesh.clone();
			//set the render groups
			for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){
				if let Some(face_texture_description)=maybe_face_texture_description{
					graphics_group.render=face_texture_description.render;
				}
			}
			primitive_meshes.push(union_clone);
			union_id
		}),
		size,
	})
}
pub struct PartialMap1<'a>{
	primitive_meshes:Vec<model::Mesh>,
	primitive_models_deferred_attributes:Vec<ModelDeferredAttributes<'a>>,
	deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes<'a>>,
	deferred_unions_deferred_attributes:Vec<DeferredUnionDeferredAttributes<'a>>,
	recoverable_errors:RecoverableErrors,
}
impl PartialMap1<'_>{
	pub fn add_meshpart_meshes_and_calculate_attributes(
		mut self,
		meshpart_meshes:Meshes<MeshWithSize>,
	)->PartialMap2{
		//calculate attributes
		let mut modes_builder=ModesBuilder::default();
		let mut unique_attributes=Vec::new();
		let mut attributes_id_from_attributes=HashMap::new();

		let mut wormhole_in_model_to_id=HashMap::new();
		let mut wormhole_id_to_out_model=HashMap::new();

		//decode roblox meshes
		//generate mesh_id_map based on meshes that failed to load
		let loaded_meshes:HashMap<model::MeshId,MeshWithSize>=
		meshpart_meshes.consume().collect();

		// SAFETY: I have no idea what I'm doing and this is definitely unsound in some subtle way
		// I just want to chain iterators together man
		let aint_no_way=core::cell::UnsafeCell::new(&mut self.primitive_meshes);

		let mut model_counter=0;
		let mut mesh_id_from_render_config_id=HashMap::new();
		let mut union_id_from_render_config_id=HashMap::new();
		//now that the meshes are loaded, these models can be generated
		let models_owned_attributes:Vec<ModelOwnedAttributes>=
		self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{
			//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
			//insert into primitive_meshes
			let MeshIdWithSize{mesh,size:mesh_size}=acquire_mesh_id_from_render_config_id(
				unsafe{*aint_no_way.get()},
				&mut mesh_id_from_render_config_id,
				&loaded_meshes,
				deferred_model_deferred_attributes.model.mesh,
				deferred_model_deferred_attributes.render
			)?;
			Some(ModelDeferredAttributes{
				mesh,
				deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes,
				color:deferred_model_deferred_attributes.model.color,
				transform:Planar64Affine3::new(
					Planar64Mat3::from_cols([
						(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_1().unwrap(),
						(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_1().unwrap(),
						(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_1().unwrap(),
					]),
					deferred_model_deferred_attributes.model.transform.translation
				),
			})
		}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
			//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
			//insert into primitive_meshes
			let MeshIdWithSize{mesh,size}=acquire_union_id_from_render_config_id(
				unsafe{*aint_no_way.get()},
				&mut union_id_from_render_config_id,
				&loaded_meshes,
				deferred_union_deferred_attributes.model.mesh,
				deferred_union_deferred_attributes.render
			)?;
			Some(ModelDeferredAttributes{
				mesh,
				deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
				color:deferred_union_deferred_attributes.model.color,
				transform:Planar64Affine3::new(
					Planar64Mat3::from_cols([
						(deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().narrow_1().unwrap(),
						(deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().narrow_1().unwrap(),
						(deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().narrow_1().unwrap(),
					]),
					deferred_union_deferred_attributes.model.transform.translation
				),
			})
		}))
		.chain(self.primitive_models_deferred_attributes.into_iter())
		.filter_map(|model_deferred_attributes|{
			let model_id=model::ModelId::new(model_counter);
			let attributes=match get_attributes(
				&model_deferred_attributes.deferred_attributes.name,
				model_deferred_attributes.deferred_attributes.can_collide,
				model_deferred_attributes.deferred_attributes.velocity,
				model_id,
				&mut modes_builder,
				&mut wormhole_in_model_to_id,
				&mut wormhole_id_to_out_model,
			){
				Ok(attributes)=>attributes,
				Err(e)=>{
					match e{
						GetAttributesError::ModeIdParseInt(e)=>self.recoverable_errors.mode_id_parse_int.push(e),
						GetAttributesError::DuplicateMode(mode_id)=>{self.recoverable_errors.duplicate_mode.insert(mode_id);},
						GetAttributesError::StageIdParseInt(e)=>self.recoverable_errors.stage_id_parse_int.push(e),
						GetAttributesError::DuplicateStage(duplicate_stage)=>{self.recoverable_errors.duplicate_stage.insert(duplicate_stage);},
						GetAttributesError::WormholeOutIdParseInt(e)=>self.recoverable_errors.wormhole_out_id_parse_int.push(e),
						GetAttributesError::DuplicateWormholeOut(wormhole_id)=>{self.recoverable_errors.duplicate_wormhole_out.insert(wormhole_id);},
						GetAttributesError::WormholeInIdParseInt(e)=>self.recoverable_errors.wormhole_in_id_parse_int.push(e),
						GetAttributesError::JumpLimitParseInt(e)=>self.recoverable_errors.jump_limit_parse_int.push(e),
					}
					return None;
				}
			};
			model_counter+=1;
			Some(ModelOwnedAttributes{
				mesh:model_deferred_attributes.mesh,
				attributes,
				color:model_deferred_attributes.color,
				transform:model_deferred_attributes.transform,
			})
		}).collect();
		let models=models_owned_attributes.into_iter().enumerate().map(|(model_id,mut model_owned_attributes)|{
			let model_id=model::ModelId::new(model_id as u32);
			//update attributes with wormhole id
			//TODO: errors/prints
			if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){
				if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){
					match &mut model_owned_attributes.attributes{
						attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general})
						|attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general})
						=>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}),
						attr::CollisionAttributes::Decoration=>println!("Not a wormhole"),
					}
				}
			}

			//index the attributes
			let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){
				attributes_id
			}else{
				let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32);
				attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id);
				unique_attributes.push(model_owned_attributes.attributes);
				attributes_id
			};
			model::Model{
				mesh:model_owned_attributes.mesh,
				transform:model_owned_attributes.transform,
				color:model_owned_attributes.color,
				attributes:attributes_id,
			}
		}).collect();
		PartialMap2{
			meshes:self.primitive_meshes,
			models,
			modes:modes_builder.build_normalized(),
			attributes:unique_attributes,
			recoverable_errors:self.recoverable_errors,
		}
	}
}

pub struct PartialMap2{
	meshes:Vec<model::Mesh>,
	models:Vec<model::Model>,
	modes:NormalizedModes,
	attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
	recoverable_errors:RecoverableErrors,
}
impl PartialMap2{
	pub fn add_render_configs_and_textures(
		self,
		render_configs:RenderConfigs,
	)->(map::CompleteMap,RecoverableErrors){
		let (textures,render_configs)=render_configs.consume();
		let (textures,texture_id_map):(Vec<Vec<u8>>,HashMap<model::TextureId,model::TextureId>)
		=textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(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
			//
			// This is because some textures may not exist, so the render config
			// that it points to is unique but is texture.
			//
			// I don't think this needs to be fixed because missing textures
			// should be a conversion error anyways.
			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.meshes,
				models:self.models,
				//the roblox legacy texture thing always works
				textures,
				render_configs,
			},
			self.recoverable_errors,
		)
	}
}
