use crate::context::Context;
#[cfg(feature="run-service")]
use crate::scheduler::scheduler_mut;

pub struct Runner{
	lua:mlua::Lua,
}
#[derive(Debug)]
pub enum Error{
	Lua{
		source:String,
		error:mlua::Error
	},
	RustLua(mlua::Error),
	NoServices,
}
impl std::fmt::Display for Error{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		match self{
			Self::Lua{source,error}=>write!(f,"lua error: source:\n{source}\n{error}"),
			Self::RustLua(error)=>write!(f,"rust-side lua error: {error}"),
			other=>write!(f,"{other:?}"),
		}
	}
}
impl std::error::Error for Error{}

fn init(lua:&mlua::Lua)->mlua::Result<()>{
	lua.sandbox(true)?;

	//global environment
	let globals=lua.globals();

	#[cfg(feature="run-service")]
	crate::scheduler::set_globals(lua,&globals)?;
	super::script_signal::set_globals(lua,&globals)?;
	super::r#enum::set_globals(lua,&globals)?;
	super::color3::set_globals(lua,&globals)?;
	super::vector3::set_globals(lua,&globals)?;
	super::cframe::set_globals(lua,&globals)?;
	super::instance::instance::set_globals(lua,&globals)?;
	super::number_sequence::set_globals(lua,&globals)?;
	super::color_sequence::set_globals(lua,&globals)?;

	Ok(())
}

impl Runner{
	pub fn new()->Result<Self,Error>{
		let runner=Self{
			lua:mlua::Lua::new(),
		};
		init(&runner.lua).map_err(Error::RustLua)?;
		Ok(runner)
	}
	pub fn runnable_context<'a>(self,context:&'a mut Context)->Result<Runnable<'a>,Error>{
		let services=context.find_services().ok_or(Error::NoServices)?;
		self.runnable_context_with_services(context,&services)
	}
	pub fn runnable_context_with_services<'a>(self,context:&'a mut Context,services:&crate::context::Services)->Result<Runnable<'a>,Error>{
		{
			let globals=self.lua.globals();
			globals.set("game",super::instance::Instance::new(services.game)).map_err(Error::RustLua)?;
			globals.set("workspace",super::instance::Instance::new(services.workspace)).map_err(Error::RustLua)?;
		}
		//this makes set_app_data shut up about the lifetime
		self.lua.set_app_data::<&'static mut rbx_dom_weak::WeakDom>(unsafe{core::mem::transmute(&mut context.dom)});
		#[cfg(feature="run-service")]
		self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default());
		Ok(Runnable{
			lua:self.lua,
			_lifetime:&std::marker::PhantomData
		})
	}
}

//Runnable is the same thing but has context set, which it holds the lifetime for.
pub struct Runnable<'a>{
	lua:mlua::Lua,
	_lifetime:&'a std::marker::PhantomData<()>
}
impl Runnable<'_>{
	pub fn drop_context(self)->Runner{
		self.lua.remove_app_data::<&'static mut rbx_dom_weak::WeakDom>();
		#[cfg(feature="run-service")]
		self.lua.remove_app_data::<crate::scheduler::Scheduler>();
		Runner{
			lua:self.lua,
		}
	}
	pub fn run_script(&self,script:super::instance::Instance)->Result<(),Error>{
		let (name,source)=super::instance::instance::get_name_source(&self.lua,script).map_err(Error::RustLua)?;
		self.lua.globals().raw_set("script",script).map_err(Error::RustLua)?;
		let f=self.lua.load(source.as_str())
		.set_name(name).into_function().map_err(Error::RustLua)?;
		// TODO: set_environment without losing the ability to print from Lua
		let thread=self.lua.create_thread(f).map_err(Error::RustLua)?;
		thread.resume::<mlua::MultiValue>(()).map_err(|error|Error::Lua{source,error})?;
		// wait() is called from inside Lua and goes to a rust function that schedules the thread and then yields
		// No need to schedule the thread here
		Ok(())
	}
	#[cfg(feature="run-service")]
	pub fn has_scheduled_threads(&self)->Result<bool,mlua::Error>{
		scheduler_mut(&self.lua,|scheduler|
			Ok(scheduler.has_scheduled_threads())
		)
	}
	#[cfg(feature="run-service")]
	pub fn game_tick(&self)->Result<(),mlua::Error>{
		if let Some(threads)=scheduler_mut(&self.lua,|scheduler|Ok(scheduler.tick_threads()))?{
			for thread in threads{
				//TODO: return dt and total run time
				let result=thread.resume::<mlua::MultiValue>((1.0/30.0,0.0))
					.map_err(|error|Error::Lua{source:"source unavailable".to_owned(),error});
				match result{
					Ok(_)=>(),
					Err(e)=>println!("game_tick Error: {e}"),
				}
			}
		}
		Ok(())
	}
	#[cfg(feature="run-service")]
	pub fn run_service_step(&self)->Result<(),mlua::Error>{
		let render_stepped=super::instance::instance::dom_mut(&self.lua,|dom|{
			let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?;
			super::instance::instance::instance_value_store_mut(&self.lua,|instance_value_store|{
				//unwrap because I trust my find_first_child_of_class function to
				let mut instance_values=instance_value_store.get_or_create_instance_values(run_service).ok_or_else(||mlua::Error::runtime("RunService InstanceValues missing"))?;
				let render_stepped=instance_values.get_or_create_value(&self.lua,"RenderStepped")?;
				//let stepped=instance_values.get_or_create_value(&self.lua,"Stepped")?;
				//let heartbeat=instance_values.get_or_create_value(&self.lua,"Heartbeat")?;
				Ok(render_stepped)
			})
		})?;
		if let Some(render_stepped)=render_stepped{
			let signal:&super::script_signal::ScriptSignal=&*render_stepped.borrow()?;
			signal.fire(&mlua::MultiValue::new());
		}
		Ok(())
	}
}
