use std::collections::{hash_map::Entry,HashMap};

use mlua::{FromLua,FromLuaMulti,IntoLua,IntoLuaMulti};
use rbx_types::Ref;
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};

use crate::util::static_ustr;
use crate::runner::number::Number;

pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
	//class functions store
	lua.set_app_data(ClassMethodsStore::default());
	lua.set_app_data(InstanceValueStore::default());

	let table=lua.create_table()?;

	//Instance.new
	table.raw_set("new",
		lua.create_function(|lua,(class_name,parent):(mlua::String,Option<Instance>)|{
			let class_name_str=&*class_name.to_str()?;
			let parent_ref=parent.map_or(Ref::none(),|instance|instance.referent);
			dom_mut(lua,|dom|{
				Ok(Instance::new_unchecked(dom.insert(parent_ref,InstanceBuilder::new(class_name_str))))
			})
		})?
	)?;

	globals.set("Instance",table)?;

	Ok(())
}

// LMAO look at this function!
pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>)->mlua::Result<T>{
	let mut dom=lua.app_data_mut::<crate::context::LuaAppData>().ok_or_else(||mlua::Error::runtime("DataModel missing"))?;
	f(*dom)
}

pub fn class_is_a(class:&str,superclass:&str)->bool{
	let db=rbx_reflection_database::get().unwrap();
	let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
		return false;
	};
	db.has_superclass(class,superclass)
}
fn get_full_name(dom:&WeakDom,instance:&rbx_dom_weak::Instance)->String{
	let mut full_name=instance.name.clone();
	let mut pref=instance.parent();
	while let Some(parent)=dom.get_by_ref(pref){
		full_name.insert(0,'.');
		full_name.insert_str(0,parent.name.as_str());
		pref=parent.parent();
	}
	full_name
}
//helper function for script
pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),mlua::Error>{
	dom_mut(lua,|dom|{
		let instance=script.get(dom)?;
		let source=match instance.properties.get(&static_ustr("Source")){
			Some(rbx_dom_weak::types::Variant::String(s))=>s.clone(),
			_=>Err(mlua::Error::external("Missing script.Source"))?,
		};
		Ok((get_full_name(dom,instance),source))
	})
}

pub fn find_first_child<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
	instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name)
}
pub fn find_first_descendant<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
	dom.descendants_of(instance.referent()).find(|&inst|inst.name==name)
}

pub fn find_first_child_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
	instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.class==class)
}
pub fn find_first_descendant_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
	dom.descendants_of(instance.referent()).find(|&inst|inst.class==class)
}

pub fn find_first_child_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
	let db=rbx_reflection_database::get().unwrap();
	let superclass_descriptor=db.classes.get(superclass)?;
	instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
		db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
	})
}
pub fn find_first_descendant_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
	let db=rbx_reflection_database::get().unwrap();
	let superclass_descriptor=db.classes.get(superclass)?;
	dom.descendants_of(instance.referent()).find(|inst|{
		db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
	})
}

#[derive(Clone,Copy)]
pub struct Instance{
	referent:Ref,
}
impl Instance{
	pub const fn new_unchecked(referent:Ref)->Self{
		Self{referent}
	}
	pub fn new(referent:Ref)->Option<Self>{
		referent.is_some().then_some(Self{referent})
	}
	pub fn get<'a>(&self,dom:&'a WeakDom)->mlua::Result<&'a rbx_dom_weak::Instance>{
		dom.get_by_ref(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
	}
	pub fn get_mut<'a>(&self,dom:&'a mut WeakDom)->mlua::Result<&'a mut rbx_dom_weak::Instance>{
		dom.get_by_ref_mut(self.referent).ok_or_else(||mlua::Error::runtime("Instance missing"))
	}
}
type_from_lua_userdata!(Instance);

impl mlua::UserData for Instance{
	fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
		fn get_parent(lua:&mlua::Lua,this:&Instance)->mlua::Result<Option<Instance>>{
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(Instance::new(instance.parent()))
			})
		}
		fields.add_field_method_get("parent",get_parent);
		fields.add_field_method_get("Parent",get_parent);
		fn set_parent(lua:&mlua::Lua,this:&mut Instance,new_parent:Option<Instance>)->mlua::Result<()>{
			let parent_ref=new_parent.map_or(Ref::none(),|instance|instance.referent);
			dom_mut(lua,|dom|{
				dom.transfer_within(this.referent,parent_ref);
				Ok(())
			})
		}
		fields.add_field_method_set("parent",set_parent);
		fields.add_field_method_set("Parent",set_parent);
		fields.add_field_method_get("Name",|lua,this|{
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(instance.name.clone())
			})
		});
		fields.add_field_method_set("Name",|lua,this,val:mlua::String|{
			dom_mut(lua,|dom|{
				let instance=this.get_mut(dom)?;
				//Why does this need to be cloned?
				instance.name=val.to_str()?.to_owned();
				Ok(())
			})
		});
		fields.add_field_method_get("ClassName",|lua,this|{
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(instance.class.to_owned())
			})
		});
	}
	fn add_methods<M:mlua::UserDataMethods<Self>>(methods:&mut M){
		fn clone(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Instance>{
			dom_mut(lua,|dom|{
				let instance_ref=dom.clone_within(this.referent);
				Ok(Instance::new_unchecked(instance_ref))
			})
		}
		methods.add_method("clone",clone);
		methods.add_method("Clone",clone);
		fn get_children(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<Vec<Instance>>{
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				let children:Vec<_>=instance
					.children()
					.iter()
					.copied()
					.map(Instance::new_unchecked)
					.collect();
				Ok(children)
			})
		}
		methods.add_method("children",get_children);
		methods.add_method("GetChildren",get_children);
		methods.add_method("GetFullName",|lua,this,()|
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(get_full_name(dom,instance))
			})
		);
		fn ffc(lua:&mlua::Lua,this:&Instance,(name,search_descendants):(mlua::String,Option<bool>))->mlua::Result<Option<Instance>>{
			let name_str=&*name.to_str()?;
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(
					match search_descendants.unwrap_or(false){
						true=>find_first_descendant(dom,instance,name_str),
						false=>find_first_child(dom,instance,name_str),
					}
					.map(|instance|
						Instance::new_unchecked(instance.referent())
					)
				)
			})
		}
		methods.add_method("FindFirstChild",ffc);
		methods.add_method("WaitForChild",ffc);
		methods.add_method("FindFirstChildOfClass",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
			let class_str=&*class.to_str()?;
			dom_mut(lua,|dom|{
				let inst=this.get(dom)?;
				Ok(
					match search_descendants.unwrap_or(false){
						true=>find_first_descendant_of_class(dom,inst,class_str),
						false=>find_first_child_of_class(dom,inst,class_str),
					}
					.map(|instance|
						Instance::new_unchecked(instance.referent())
					)
				)
			})
		});
		methods.add_method("FindFirstChildWhichIsA",|lua,this,(class,search_descendants):(mlua::String,Option<bool>)|{
			let class_str=&*class.to_str()?;
			dom_mut(lua,|dom|{
				let inst=this.get(dom)?;
				Ok(
					match search_descendants.unwrap_or(false){
						true=>find_first_descendant_which_is_a(dom,inst,class_str),
						false=>find_first_child_which_is_a(dom,inst,class_str),
					}
					.map(|instance|
						Instance::new_unchecked(instance.referent())
					)
				)
			})
		});
		methods.add_method("GetDescendants",|lua,this,_:()|
			dom_mut(lua,|dom|{
				let children:Vec<_>=dom
					.descendants_of(this.referent)
					.map(|instance|
						Instance::new_unchecked(instance.referent())
					)
					.collect();
				Ok(children)
			})
		);
		methods.add_method("IsAncestorOf",|lua,this,descendant:Instance|
			dom_mut(lua,|dom|{
				let instance=descendant.get(dom)?;
				Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==this.referent))
			})
		);
		methods.add_method("IsDescendantOf",|lua,this,ancestor:Instance|
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(std::iter::successors(Some(instance),|inst|dom.get_by_ref(inst.parent())).any(|inst|inst.referent()==ancestor.referent))
			})
		);
		fn is_a(lua:&mlua::Lua,this:&Instance,classname:mlua::String)->mlua::Result<bool>{
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(class_is_a(instance.class.as_str(),&*classname.to_str()?))
			})
		}
		methods.add_method("isA",is_a);
		methods.add_method("IsA",is_a);
		fn destroy(lua:&mlua::Lua,this:&Instance,_:())->mlua::Result<()>{
			dom_mut(lua,|dom|{
				dom.transfer_within(this.referent,Ref::none());
				Ok(())
			})
		}
		methods.add_method("remove",destroy);
		methods.add_method("Remove",destroy);
		methods.add_method("destroy",destroy);
		methods.add_method("Destroy",destroy);
		methods.add_meta_function(mlua::MetaMethod::ToString,|lua,this:Instance|{
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				Ok(instance.name.clone())
			})
		});
		methods.add_meta_function(mlua::MetaMethod::Index,|lua,(this,index):(Instance,mlua::String)|{
			let index_str=&*index.to_str()?;
			dom_mut(lua,|dom|{
				let instance=this.get(dom)?;
				//println!("__index t={} i={index:?}",instance.name);
				let db=rbx_reflection_database::get().unwrap();
				let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
				// Find existing property
				// Interestingly, ustr can know ahead of time if
				// a property does not exist in any runtime instance
				match Ustr::from_existing(index_str)
					.and_then(|index_ustr|
						instance.properties.get(&index_ustr).cloned()
					)
					//Find default value
					.or_else(||db.find_default_property(class,index_str).cloned())
					//Find virtual property
					.or_else(||db.superclasses_iter(class).find_map(|class|
						find_virtual_property(&instance.properties,class,index_str)
					))
				{
					Some(rbx_types::Variant::Bool(val))=>return val.into_lua(lua),
					Some(rbx_types::Variant::Int32(val))=>return val.into_lua(lua),
					Some(rbx_types::Variant::Int64(val))=>return val.into_lua(lua),
					Some(rbx_types::Variant::Float32(val))=>return val.into_lua(lua),
					Some(rbx_types::Variant::Float64(val))=>return val.into_lua(lua),
					Some(rbx_types::Variant::String(val))=>return val.into_lua(lua),
					Some(rbx_types::Variant::Ref(val))=>return Instance::new_unchecked(val).into_lua(lua),
					Some(rbx_types::Variant::Enum(e))=>return crate::runner::r#enum::EnumItem::from(e).into_lua(lua),
					Some(rbx_types::Variant::Color3(c))=>return crate::runner::color3::Color3::from(c).into_lua(lua),
					Some(rbx_types::Variant::CFrame(cf))=>return crate::runner::cframe::CFrame::from(cf).into_lua(lua),
					Some(rbx_types::Variant::Vector2(v))=>return crate::runner::vector2::Vector2::from(v).into_lua(lua),
					Some(rbx_types::Variant::Vector3(v))=>return crate::runner::vector3::Vector3::from(v).into_lua(lua),
					None=>(),
					other=>return Err(mlua::Error::runtime(format!("Instance.__index Unsupported property type instance={} index={index_str} value={other:?}",instance.name))),
				}
				//find a function with a matching name
				if let Some(function)=class_methods_store_mut(lua,|cf|{
					db.superclasses_iter(class).find_map(|class|{
						let mut class_methods=cf.get_or_create_class_methods(&class.name)?;
						class_methods.get_or_create_function(lua,index_str)
							.transpose()
					}).transpose()
				})?{
					return function.into_lua(lua);
				}

				//find or create an associated userdata object
				if let Some(value)=instance_value_store_mut(lua,|ivs|{
					//TODO: walk class tree somehow
					match ivs.get_or_create_instance_values(&instance){
						Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
						None=>Ok(None)
					}
				})?{
					return value.into_lua(lua);
				}
				//find a child with a matching name
				find_first_child(dom,instance,index_str)
				.map(|instance|Instance::new_unchecked(instance.referent()))
				.into_lua(lua)
			})
		});
		methods.add_meta_function(mlua::MetaMethod::NewIndex,|lua,(this,index,value):(Instance,mlua::String,mlua::Value)|{
			let index_str=&*index.to_str()?;
			dom_mut(lua,|dom|{
				let instance=this.get_mut(dom)?;
				let db=rbx_reflection_database::get().unwrap();
				let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
				let property=db.superclasses_iter(class).find_map(|cls|
					cls.properties.get(index_str)
				).ok_or_else(||
					mlua::Error::runtime(format!("Property '{index_str}' missing on class '{}'",class.name))
				)?;
				let value=match &property.data_type{
					rbx_reflection::DataType::Value(rbx_types::VariantType::Float32)=>{
						let typed_value=Number::from_lua(value.clone(),lua)?.to_f32();
						rbx_types::Variant::Float32(typed_value)
					},
					rbx_reflection::DataType::Enum(enum_name)=>{
						let typed_value=match &value{
							&mlua::Value::Integer(int)=>Ok(rbx_types::Enum::from_u32(int as u32)),
							&mlua::Value::Number(num)=>Ok(rbx_types::Enum::from_u32(num as u32)),
							mlua::Value::String(s)=>{
								let e=db.enums.get(enum_name).ok_or_else(||mlua::Error::runtime("Database DataType Enum name does not exist"))?;
								Ok(rbx_types::Enum::from_u32(*e.items.get(&*s.to_str()?).ok_or_else(||mlua::Error::runtime("Invalid enum item"))?))
							},
							mlua::Value::UserData(any_user_data)=>{
								let e:crate::runner::r#enum::EnumItem=*any_user_data.borrow()?;
								Ok(e.into())
							},
							_=>Err(mlua::Error::runtime("Expected Enum")),
						}?;
						rbx_types::Variant::Enum(typed_value)
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::Color3)=>{
						let typed_value:crate::runner::color3::Color3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Color3"))?.borrow()?;
						rbx_types::Variant::Color3(typed_value.into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::Bool)=>{
						let typed_value=value.as_boolean().ok_or_else(||mlua::Error::runtime("Expected boolean"))?;
						rbx_types::Variant::Bool(typed_value)
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::Int32)=>{
						let typed_value=value.as_i32().ok_or_else(||mlua::Error::runtime("Expected Int32"))?;
						rbx_types::Variant::Int32(typed_value)
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::String)=>{
						let typed_value=match &value{
							mlua::Value::Integer(i)=>i.to_string(),
							mlua::Value::Number(n)=>n.to_string(),
							mlua::Value::String(s)=>s.to_str()?.to_owned(),
							_=>return Err(mlua::Error::runtime("Expected string")),
						};
						rbx_types::Variant::String(typed_value)
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::UDim2)=>{
						let typed_value:&crate::runner::udim2::UDim2=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected UDim2"))?.borrow()?;
						rbx_types::Variant::UDim2(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::NumberRange)=>{
						let typed_value:&crate::runner::number_range::NumberRange=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberRange"))?.borrow()?;
						rbx_types::Variant::NumberRange(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::NumberSequence)=>{
						let typed_value:&crate::runner::number_sequence::NumberSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected NumberSequence"))?.borrow()?;
						rbx_types::Variant::NumberSequence(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::ColorSequence)=>{
						let typed_value:&crate::runner::color_sequence::ColorSequence=&*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected ColorSequence"))?.borrow()?;
						rbx_types::Variant::ColorSequence(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::Vector2)=>{
						let typed_value:crate::runner::vector2::Vector2=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector2"))?.borrow()?;
						rbx_types::Variant::Vector2(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::Vector3)=>{
						let typed_value:crate::runner::vector3::Vector3=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected Vector3"))?.borrow()?;
						rbx_types::Variant::Vector3(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::CFrame)=>{
						let typed_value:crate::runner::cframe::CFrame=*value.as_userdata().ok_or_else(||mlua::Error::runtime("Expected CFrame"))?.borrow()?;
						rbx_types::Variant::CFrame(typed_value.clone().into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
						let typed_value=value.as_string().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_str()?.to_owned();
						rbx_types::Variant::ContentId(typed_value.into())
					},
					rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
						// why clone?
						let typed_value=Option::<Instance>::from_lua(value.clone(),lua)?;
						rbx_types::Variant::Ref(typed_value.map_or(Ref::none(),|instance|instance.referent))
					},
					other=>return Err(mlua::Error::runtime(format!("Unimplemented property type: {other:?}"))),
				};
				// the index is known to be a real property at this point
				// allow creating a permanent ustr (memory leak)
				let index_ustr=rbx_dom_weak::ustr(index_str);
				instance.properties.insert(index_ustr,value);
				Ok(())
			})
		});
	}
}

/// A class function definition shorthand.
macro_rules! cf{
	($f:expr)=>{
		|lua,mut args|{
			let this=Instance::from_lua(args.pop_front().unwrap_or(mlua::Value::Nil),lua)?;
			$f(lua,this,FromLuaMulti::from_lua_multi(args,lua)?)?.into_lua_multi(lua)
		}
	};
}
type ClassFunctionPointer=fn(&mlua::Lua,mlua::MultiValue)->mlua::Result<mlua::MultiValue>;
// TODO: use macros to define these with better organization
/// A double hash map of function pointers.
/// The class tree is walked by the Instance.__index metamethod to find available class methods.
type CFD=phf::Map<&'static str,// Class name
	phf::Map<&'static str,// Method name
		ClassFunctionPointer
	>
>;
const GET_SERVICE:ClassFunctionPointer=cf!(|lua,_this,service:mlua::String|{
	dom_mut(lua,|dom|{
		//dom.root_ref()==this.referent ?
		let service=&*service.to_str()?;
		match service{
			"Lighting"|"RunService"|"Players"|"Workspace"|"MaterialService"|"TweenService"=>{
				let referent=find_first_child_of_class(dom,dom.root(),service)
				.map(|instance|instance.referent())
				.unwrap_or_else(||
					dom.insert(dom.root_ref(),InstanceBuilder::new(service))
				);
				Ok(Instance::new_unchecked(referent))
			},
			other=>Err(mlua::Error::runtime(format!("Service '{other}' not supported"))),
		}
	})
});
const GET_PLAYERS:ClassFunctionPointer=cf!(|_lua,_this,()|->mlua::Result<_>{
	Ok(Vec::<Instance>::new())
});
const NO_OP:ClassFunctionPointer=cf!(|_lua,_this,_:mlua::MultiValue|->mlua::Result<_>{Ok(())});
static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
	"DataModel"=>phf::phf_map!{
		"service"=>GET_SERVICE,
		"GetService"=>GET_SERVICE,
	},
	"Terrain"=>phf::phf_map!{
		"FillBall"=>cf!(|_lua,_,_:(crate::runner::vector3::Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
		"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,crate::runner::vector3::Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
		"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
		"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
	},
	"Players"=>phf::phf_map!{
		"players"=>GET_PLAYERS,
		"GetPlayers"=>GET_PLAYERS,
	},
	"Sound"=>phf::phf_map!{
		"Play"=>NO_OP,
	},
	"TweenService"=>phf::phf_map!{
		"Create"=>cf!(|_lua,_,(instance,tween_info,goal):(Instance,crate::runner::tween_info::TweenInfo,mlua::Table)|->mlua::Result<_>{
			Ok(crate::runner::tween::Tween::create(
				instance,
				tween_info,
				goal,
			))
		}),
	},
};

/// A store of created functions for each Roblox class.
/// Functions are created the first time they are accessed and stored in this data structure.
#[derive(Default)]
struct ClassMethodsStore{
	classes:HashMap<&'static str,//ClassName
		HashMap<&'static str,//Method name
			mlua::Function
		>
	>
}
impl ClassMethodsStore{
	/// return self.classes[class] or create the ClassMethods and then return it
	fn get_or_create_class_methods(&mut self,class:&str)->Option<ClassMethods<'_>>{
		// Use get_entry to get the &'static str keys of the database
		// and use it as a key for the classes hashmap
		CLASS_FUNCTION_DATABASE.get_entry(class)
			.map(|(&static_class_str,method_pointers)|
				ClassMethods{
					method_pointers,
					methods:self.classes.entry(static_class_str)
					.or_insert_with(||HashMap::new()),
				}
			)
	}
}
struct ClassMethods<'a>{
	method_pointers:&'static phf::Map<&'static str,ClassFunctionPointer>,
	methods:&'a mut HashMap<&'static str,mlua::Function>,
}
impl ClassMethods<'_>{
	/// return self.methods[index] or create the function in the hashmap and then return it
	fn get_or_create_function(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::Function>>{
		Ok(match self.method_pointers.get_entry(index){
			Some((&static_index_str,&function_pointer))=>Some(
				match self.methods.entry(static_index_str){
					Entry::Occupied(entry)=>entry.get().clone(),
					Entry::Vacant(entry)=>entry.insert(
						lua.create_function(function_pointer)?
					).clone(),
				}
			),
			None=>None,
		})
	}
}
fn class_methods_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut ClassMethodsStore)->mlua::Result<T>)->mlua::Result<T>{
	let mut cf=lua.app_data_mut::<ClassMethodsStore>().ok_or_else(||mlua::Error::runtime("ClassMethodsStore missing"))?;
	f(&mut *cf)
}

/// A virtual property pointer definition shorthand.
type VirtualPropertyFunctionPointer=fn(&rbx_types::Variant)->Option<rbx_types::Variant>;
const fn vpp(
	property:&'static str,
	pointer:VirtualPropertyFunctionPointer,
)->VirtualProperty{
	VirtualProperty{
		property,
		pointer,
	}
}
struct VirtualProperty{
	property:&'static str,// Source property name
	pointer:VirtualPropertyFunctionPointer,
}
type VPD=phf::Map<&'static str,// Class name
	phf::Map<&'static str,// Virtual property name
		VirtualProperty
	>
>;
static VIRTUAL_PROPERTY_DATABASE:VPD=phf::phf_map!{
	"BasePart"=>phf::phf_map!{
		"Position"=>vpp("CFrame",|c:&rbx_types::Variant|{
			let c=match c{
				rbx_types::Variant::CFrame(c)=>c,
				_=>return None,//fail silently and ungracefully
			};
			Some(rbx_types::Variant::Vector3(c.position))
		}),
	},
};

fn find_virtual_property(
	properties:&rbx_dom_weak::UstrMap<rbx_types::Variant>,
	class:&rbx_reflection::ClassDescriptor,
	index:&str,
)->Option<rbx_types::Variant>{
	//Find virtual property
	let class_virtual_properties=VIRTUAL_PROPERTY_DATABASE.get(&class.name)?;
	let virtual_property=class_virtual_properties.get(index)?;

	//Get source property
	let variant=properties.get(&static_ustr(virtual_property.property))?;

	//Transform Source property with provided function
	(virtual_property.pointer)(variant)
}

// lazy-loaded per-instance userdata values
// This whole thing is a bad idea and a garbage collection nightmare.
// TODO: recreate rbx_dom_weak with my own instance type that owns this data.
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
type LUD=phf::Map<&'static str,// Class name
	phf::Map<&'static str,// Value name
		CreateUserData
	>
>;
fn create_script_signal(lua:&mlua::Lua)->mlua::Result<mlua::AnyUserData>{
	lua.create_any_userdata(crate::runner::script_signal::ScriptSignal::new())
}
static LAZY_USER_DATA:LUD=phf::phf_map!{
	"RunService"=>phf::phf_map!{
		"Stepped"=>create_script_signal,
		"Heartbeat"=>create_script_signal,
		"RenderStepped"=>create_script_signal,
	},
	"Players"=>phf::phf_map!{
		"PlayerAdded"=>create_script_signal,
	},
	"BasePart"=>phf::phf_map!{
		"Touched"=>create_script_signal,
		"TouchEnded"=>create_script_signal,
	},
	"Instance"=>phf::phf_map!{
		"ChildAdded"=>create_script_signal,
		"ChildRemoved"=>create_script_signal,
		"DescendantAdded"=>create_script_signal,
		"DescendantRemoved"=>create_script_signal,
	},
	"ClickDetector"=>phf::phf_map!{
		"MouseClick"=>create_script_signal,
	},
};
#[derive(Default)]
pub struct InstanceValueStore{
	values:HashMap<Ref,
		HashMap<&'static str,
			mlua::AnyUserData
		>
	>,
}
pub struct InstanceValues<'a>{
	named_values:&'static phf::Map<&'static str,CreateUserData>,
	values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
}
impl InstanceValueStore{
	pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues<'_>>{
		LAZY_USER_DATA.get(instance.class.as_str())
			.map(|named_values|
				InstanceValues{
					named_values,
					values:self.values.entry(instance.referent())
					.or_insert_with(||HashMap::new()),
				}
			)
	}
}
impl InstanceValues<'_>{
	pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
		Ok(match self.named_values.get_entry(index){
			Some((&static_index_str,&function_pointer))=>Some(
				match self.values.entry(static_index_str){
					Entry::Occupied(entry)=>entry.get().clone(),
					Entry::Vacant(entry)=>entry.insert(
						function_pointer(lua)?
					).clone(),
				}
			),
			None=>None,
		})
	}
}

pub fn instance_value_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result<T>)->mlua::Result<T>{
	let mut cf=lua.app_data_mut::<InstanceValueStore>().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?;
	f(&mut *cf)
}
