use std::path::PathBuf;
use futures::{StreamExt, TryStreamExt};
use tokio::io::AsyncReadExt;

use crate::common::{sanitize,Style,PropertiesOverride};

//holy smokes what am I doing lmao
//This giant machine is supposed to search for files according to style rules
//e.g. ScriptName.server.lua or init.lua
//Obviously I got carried away
//I could use an enum!
//I could use a struct!
//I could use a trait!
//I could use an error!
//I could use a match!
//I could use a function!
//eventually:
#[derive(Debug)]
#[allow(dead_code)]//idk why this thinks it's dead code, the errors are printed out in various places
pub enum QueryResolveError{
	NotFound,//0 results
	Ambiguous,//>1 results
	JoinError(tokio::task::JoinError),
	IO(std::io::Error),
}
impl std::fmt::Display for QueryResolveError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for QueryResolveError{}

struct FileWithName{
	file:tokio::fs::File,
	name:String,
}

async fn get_file_async(mut path:PathBuf,file_name:impl AsRef<std::path::Path>)->Result<FileWithName,QueryResolveError>{
	let name=file_name.as_ref().to_str().unwrap().to_owned();
	path.push(file_name);
	match tokio::fs::File::open(path).await{
		Ok(file)=>Ok(FileWithName{file,name}),
		Err(e)=>match e.kind(){
			std::io::ErrorKind::NotFound=>Err(QueryResolveError::NotFound),
			_=>Err(QueryResolveError::IO(e)),
		},
	}
}
type QueryHintResult=Result<FileHint,QueryResolveError>;
trait Query{
	async fn resolve(self)->QueryHintResult;
}
type QueryHandle=tokio::task::JoinHandle<Result<FileWithName,QueryResolveError>>;
struct QuerySingle{
	script:QueryHandle,
}
impl QuerySingle{
	fn rox(search_path:&PathBuf,search_name:&str)->Self{
		Self{
			script:tokio::spawn(get_file_async(search_path.clone(),format!("{}.lua",search_name)))
		}
	}
}
impl Query for QuerySingle{
	async fn resolve(self)->QueryHintResult{
		match self.script.await{
			Ok(Ok(file))=>Ok(FileHint{file,hint:ScriptHint::ModuleScript}),
			Ok(Err(e))=>Err(e),
			Err(e)=>Err(QueryResolveError::JoinError(e)),
		}
	}
}
struct QueryTriple{
	module:QueryHandle,
	server:QueryHandle,
	client:QueryHandle,
}
impl QueryTriple{
	fn rox_rojo(search_path:&PathBuf,search_name:&str,search_module:bool)->Self{
		//this should be implemented as constructors of Triplet and Quadruplet to fully support Trey's suggestion
		let module_name=if search_module{
			format!("{}.module.lua",search_name)
		}else{
			format!("{}.lua",search_name)
		};
		Self{
			module:tokio::spawn(get_file_async(search_path.clone(),module_name)),
			server:tokio::spawn(get_file_async(search_path.clone(),format!("{}.server.lua",search_name))),
			client:tokio::spawn(get_file_async(search_path.clone(),format!("{}.client.lua",search_name))),
		}
	}
	fn rojo(search_path:&PathBuf)->Self{
		QueryTriple::rox_rojo(search_path,"init",false)
	}
}
//these functions can be achieved with macros, but I have not learned that yet
fn mega_triple_join(query_triplet:(QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{
	match query_triplet{
		//unambiguously locate file
		(Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))
		|(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound))
		|(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f),
		//multiple files located
		(Ok(_),Ok(_),Err(QueryResolveError::NotFound))
		|(Ok(_),Err(QueryResolveError::NotFound),Ok(_))
		|(Err(QueryResolveError::NotFound),Ok(_),Ok(_))
		|(Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous),
		//no files located
		(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound),
		//other error
		(Err(e),_,_)
		|(_,Err(e),_)
		|(_,_,Err(e))=>Err(e),
	}
}
//LETS GOOOOOOOOOOOOOOOO
fn mega_quadruple_join(query_quad:(QueryHintResult,QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{
	match query_quad{
		//unambiguously locate file
		(Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))
		|(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))
		|(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound))
		|(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f),
		//multiple files located
		(Ok(_),Ok(_),Ok(_),Err(QueryResolveError::NotFound))
		|(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Ok(_))
		|(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))
		|(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Ok(_))
		|(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound))
		|(Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_))
		|(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Ok(_))
		|(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Err(QueryResolveError::NotFound))
		|(Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound),Ok(_))
		|(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_),Ok(_))
		|(Ok(_),Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous),
		//no files located
		(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound),
		//other error
		(Err(e),_,_,_)
		|(_,Err(e),_,_)
		|(_,_,Err(e),_)
		|(_,_,_,Err(e))=>Err(e),
	}
}
impl Query for QueryTriple{
	async fn resolve(self)->QueryHintResult{
		let (module,server,client)=tokio::join!(self.module,self.server,self.client);
		mega_triple_join((
			module.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}),
			server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}),
			client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}),
		))
	}
}
struct QueryQuad{
	module_implicit:QueryHandle,
	module_explicit:QueryHandle,
	server:QueryHandle,
	client:QueryHandle,
}
impl QueryQuad{
	fn rox_rojo(search_path:&PathBuf,search_name:&str)->Self{
		let fill=QueryTriple::rox_rojo(search_path,search_name,true);
		Self{
			module_implicit:QuerySingle::rox(search_path,search_name).script,//Script.lua
			module_explicit:fill.module,//Script.module.lua
			server:fill.server,
			client:fill.client,
		}
	}
}
impl Query for QueryQuad{
	async fn resolve(self)->QueryHintResult{
		let (module_implicit,module_explicit,server,client)=tokio::join!(self.module_implicit,self.module_explicit,self.server,self.client);
		mega_quadruple_join((
			module_implicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}),
			module_explicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}),
			server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}),
			client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}),
		))
	}
}

struct ScriptWithOverrides{
	overrides:PropertiesOverride,
	source:String,
}

#[derive(Debug)]
pub enum ScriptWithOverridesError{
	UnimplementedProperty(String),
}
impl std::fmt::Display for ScriptWithOverridesError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for ScriptWithOverridesError{}

impl ScriptWithOverrides{
	fn from_source(mut source:String)->Result<Self,ScriptWithOverridesError>{
		let mut overrides=PropertiesOverride::default();
		let mut count=0;
		for line in source.lines(){
			//only string type properties are supported atm
			if let Some(captures)=lazy_regex::regex!(r#"^\-\-\s*Properties\.([A-Za-z]\w*)\s*\=\s*"(\w+)"$"#)
			.captures(line){
				count+=line.len();
				match &captures[1]{
					"Name"=>overrides.name=Some(captures[2].to_owned()),
					"ClassName"=>overrides.class=Some(captures[2].to_owned()),
					other=>Err(ScriptWithOverridesError::UnimplementedProperty(other.to_owned()))?,
				}
			}else{
				break;
			}
		}
		Ok(ScriptWithOverrides{overrides,source:source.split_off(count)})
	}
}

enum CompileClass{
	Folder,
	Script(String),
	LocalScript(String),
	ModuleScript(String),
	Model(Vec<u8>),
}

struct CompileNode{
	name:String,
	blacklist:Option<String>,
	class:CompileClass,
}

#[derive(Debug)]
pub enum CompileNodeError{
	IO(std::io::Error),
	ScriptWithOverrides(ScriptWithOverridesError),
	InvalidClassOrHint{
		class:Option<String>,
		hint:ScriptHint
	},
	QueryResolveError(QueryResolveError),
	/// Conversion from OsString to String failed
	FileName(std::ffi::OsString),
	ExtensionNotSupportedInStyle{
		extension:String,
		style:Option<Style>,
	},
	UnknownExtension,
}
impl std::fmt::Display for CompileNodeError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for CompileNodeError{}

enum FileDiscernment{
	Model,
	Script(ScriptHint),
}

impl CompileNode{
	async fn script(search_name:&str,mut file:FileWithName,hint:ScriptHint)->Result<Self,CompileNodeError>{
		//read entire file
		let mut buf=String::new();
		file.file.read_to_string(&mut buf).await.map_err(CompileNodeError::IO)?;
		//regex script according to Properties lines at the top
		let script_with_overrides=ScriptWithOverrides::from_source(buf).map_err(CompileNodeError::ScriptWithOverrides)?;
		//script
		Ok(Self{
			blacklist:Some(file.name),
			name:script_with_overrides.overrides.name.unwrap_or_else(||search_name.to_owned()),
			class:match (script_with_overrides.overrides.class.as_deref(),hint){
				(Some("ModuleScript"),_)
				|(None,ScriptHint::ModuleScript)=>CompileClass::ModuleScript(script_with_overrides.source),
				(Some("LocalScript"),_)
				|(None,ScriptHint::LocalScript)=>CompileClass::LocalScript(script_with_overrides.source),
				(Some("Script"),_)
				|(None,ScriptHint::Script)=>CompileClass::Script(script_with_overrides.source),
				(class,hint)=>Err(CompileNodeError::InvalidClassOrHint{class:class.map(|s|s.to_owned()),hint})?,
			},
		})
	}
	async fn model(search_name:&str,mut file:FileWithName)->Result<Self,CompileNodeError>{
		//read entire file
		let mut buf=Vec::new();
		file.file.read_to_end(&mut buf).await.map_err(CompileNodeError::IO)?;
		//model
		Ok(Self{
			blacklist:Some(file.name),
			name:search_name.to_owned(),//wrong but gets overwritten by internal model name
			class:CompileClass::Model(buf),
		})
	}

	async fn from_folder(entry:&tokio::fs::DirEntry,style:Option<Style>)->Result<Self,CompileNodeError>{
		let contents_folder=entry.path();
		let file_name=entry.file_name();
		let search_name=file_name.to_str().unwrap();
		//scan inside the folder for an object to define the class of the folder
		let script_query=async {match style{
			Some(Style::Rox)=>QuerySingle::rox(&contents_folder,search_name).resolve().await,
			Some(Style::RoxRojo)=>QueryQuad::rox_rojo(&contents_folder,search_name).resolve().await,
			Some(Style::Rojo)=>QueryTriple::rojo(&contents_folder).resolve().await,
			//try all three and complain if there is ambiguity
			None=>mega_triple_join(tokio::join!(
				QuerySingle::rox(&contents_folder,search_name).resolve(),
				//true=search for module here to avoid ambiguity with QuerySingle::rox results
				QueryTriple::rox_rojo(&contents_folder,search_name,true).resolve(),
				QueryTriple::rojo(&contents_folder).resolve(),
			))
		}};
		//model files are rox & rox-rojo only, so it's a lot less work...
		let model_query=get_file_async(contents_folder.clone(),format!("{}.rbxmx",search_name));
		//model? script? both?
		Ok(match tokio::join!(script_query,model_query){
			(Ok(FileHint{file,hint}),Err(QueryResolveError::NotFound))=>Self::script(search_name,file,hint).await?,
			(Err(QueryResolveError::NotFound),Ok(file))=>Self::model(search_name,file).await?,
			(Ok(_),Ok(_))=>Err(CompileNodeError::QueryResolveError(QueryResolveError::Ambiguous))?,
			//neither
			(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Self{
				name:search_name.to_owned(),
				blacklist:None,
				class:CompileClass::Folder,
			},
			//other error
			(Err(e),_)
			|(_,Err(e))=>Err(CompileNodeError::QueryResolveError(e))?
		})
	}

	async fn from_file(entry:&tokio::fs::DirEntry,style:Option<Style>)->Result<Self,CompileNodeError>{
		let mut file_name=entry
		.file_name()
		.into_string()
		.map_err(CompileNodeError::FileName)?;
		//reject goobers
		let is_goober=match style{
			Some(Style::Rojo)=>true,
			_=>false,
		};
		let (ext_len,file_discernment)={
			if let Some(captures)=lazy_regex::regex!(r"^.*(\.module\.lua|\.client\.lua|\.server\.lua)$")
			.captures(file_name.as_str()){
				let ext=&captures[1];
				(ext.len(),match ext{
					".module.lua"=>{
						if is_goober{
							Err(CompileNodeError::ExtensionNotSupportedInStyle{extension:ext.to_owned(),style})?;
						}
						FileDiscernment::Script(ScriptHint::ModuleScript)
					},
					".client.lua"=>FileDiscernment::Script(ScriptHint::LocalScript),
					".server.lua"=>FileDiscernment::Script(ScriptHint::Script),
					_=>panic!("Regex failed"),
				})
			}else if let Some(captures)=lazy_regex::regex!(r"^.*(\.rbxmx|\.lua)$")
			.captures(file_name.as_str()){
				let ext=&captures[1];
				(ext.len(),match ext{
					".rbxmx"=>{
						if is_goober{
							Err(CompileNodeError::ExtensionNotSupportedInStyle{extension:ext.to_owned(),style})?;
						}
						FileDiscernment::Model
					},
					".lua"=>FileDiscernment::Script(ScriptHint::ModuleScript),
					_=>panic!("Regex failed"),
				})
			}else{
				return Err(CompileNodeError::UnknownExtension);
			}
		};
		file_name.truncate(file_name.len()-ext_len);
		let file=tokio::fs::File::open(entry.path()).await.map_err(CompileNodeError::IO)?;
		Ok(match file_discernment{
			FileDiscernment::Model=>Self::model(file_name.as_str(),FileWithName{file,name:file_name.clone()}).await?,
			FileDiscernment::Script(hint)=>Self::script(file_name.as_str(),FileWithName{file,name:file_name.clone()},hint).await?,
		})
	}
}

#[derive(Debug)]
pub enum ScriptHint{
	Script,
	LocalScript,
	ModuleScript,
}
struct FileHint{
	file:FileWithName,
	hint:ScriptHint,
}

enum PreparedData{
	Model(rbx_dom_weak::WeakDom),
	Builder(rbx_dom_weak::InstanceBuilder),
}

enum CompileStackInstruction{
	TraverseReferent(rbx_dom_weak::types::Ref,Option<String>),
	PopFolder,
}

fn script_builder(class:&str,name:&str,source:String)->rbx_dom_weak::InstanceBuilder{
	let mut builder=rbx_dom_weak::InstanceBuilder::new(class);
	builder.set_name(name);
	builder.add_property("Source",rbx_dom_weak::types::Variant::String(source));
	builder
}

enum TooComplicated<T>{
	Stop,
	Value(T),
	Skip,
}

pub struct CompileConfig{
	pub input_folder:PathBuf,
	pub style:Option<Style>,
}

#[derive(Debug)]
pub enum CompileError{
	NullChildRef,
	IO(std::io::Error),
	CompileNode(CompileNodeError),
	DecodeError(rbx_xml::DecodeError),
	JoinError(tokio::task::JoinError),
}
impl std::fmt::Display for CompileError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for CompileError{}

pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->Result<(),CompileError>{
	//hack to traverse root folder as the root object
	dom.root_mut().name="src".to_owned();
	//add in scripts and models
	let mut folder=config.input_folder.clone();
	let mut stack:Vec<CompileStackInstruction>=vec![CompileStackInstruction::TraverseReferent(dom.root_ref(),None)];
	while let Some(instruction)=stack.pop(){
		match instruction{
			CompileStackInstruction::TraverseReferent(item_ref,blacklist)=>{
				//scope to avoid holding item ref
				{
					let item=dom.get_by_ref(item_ref).ok_or(CompileError::NullChildRef)?;
					let folder_name=sanitize(item.name.as_str());
					folder.push(folder_name.as_ref());
					//drop item
				}
				stack.push(CompileStackInstruction::PopFolder);
				//check if a folder exists with item.name
				if let Ok(dir)=tokio::fs::read_dir(folder.as_path()).await{
					let mut exist_names:std::collections::HashSet<String>={
						let item=dom.get_by_ref(item_ref).ok_or(CompileError::NullChildRef)?;
						//push existing dom children objects onto stack (unrelated to exist_names)
						stack.extend(item.children().into_iter().map(|&referent|CompileStackInstruction::TraverseReferent(referent,None)));
						//get names of existing objects
						item.children().into_iter().map(|&child_ref|{
							let child=dom.get_by_ref(child_ref).ok_or(CompileError::NullChildRef)?;
							Ok::<_,CompileError>(sanitize(child.name.as_str()).to_string())
						}).collect::<Result<_,CompileError>>()?
					};
					if let Some(dont)=blacklist{
						exist_names.insert(dont);
					}
					//generate children from folder contents UNLESS! item already has a child of the same name

					let style=config.style;
					let exist_names=&exist_names;
					futures::stream::unfold(dir,|mut dir1|async{
						//thread the needle! follow the path that dir takes!
						let ret1={
							//capture a scoped mutable reference so we can forward dir to the next call even on an error
							let dir2=&mut dir1;
							(||async move{//error catcher so I can use ?
								let ret2=if let Some(entry)=dir2.next_entry().await?{
									//cull early even if supporting things with identical names is possible
									if exist_names.contains(entry.file_name().to_str().unwrap()){
										TooComplicated::Skip
									}else{
										TooComplicated::Value(entry)
									}
								}else{
									TooComplicated::Stop
								};
								Ok(ret2)
							})().await
						};
						match ret1{
							Ok(TooComplicated::Stop)=>None,
							Ok(TooComplicated::Skip)=>Some((Ok(None),dir1)),
							Ok(TooComplicated::Value(v))=>Some((Ok(Some(v)),dir1)),
							Err(e)=>Some((Err(CompileError::IO(e)),dir1)),
						}
					})

					//gotta spawn off the worker threads (Model is slow)
					.then(|bog|async{
						match bog{
							Ok(Some(entry))=>tokio::spawn(async move{
								let met=entry.metadata().await.map_err(CompileError::IO)?;
								//discern that bad boy
								let compile_class={
									let result=match met.is_dir(){
										true=>CompileNode::from_folder(&entry,style).await,
										false=>CompileNode::from_file(&entry,style).await,
									};
									match result{
										Ok(compile_class)=>compile_class,
										Err(e)=>{
											println!("Ignoring file {entry:?} due to error {e}");
											return Ok(None);
										},
									}
								};
								//prepare data structure
								Ok(Some((compile_class.blacklist,match compile_class.class{
									CompileClass::Folder=>PreparedData::Builder(rbx_dom_weak::InstanceBuilder::new("Folder").with_name(compile_class.name.as_str())),
									CompileClass::Script(source)=>PreparedData::Builder(script_builder("Script",compile_class.name.as_str(),source)),
									CompileClass::LocalScript(source)=>PreparedData::Builder(script_builder("LocalScript",compile_class.name.as_str(),source)),
									CompileClass::ModuleScript(source)=>PreparedData::Builder(script_builder("ModuleScript",compile_class.name.as_str(),source)),
									CompileClass::Model(buf)=>PreparedData::Model(rbx_xml::from_reader_default(std::io::Cursor::new(buf)).map_err(CompileError::DecodeError)?),
								})))
							}).await.map_err(CompileError::JoinError)?,
							Ok(None)=>Ok(None),
							Err(e)=>Err(e),
						}
					})

					//is this even what I want?
					.map(|f|async{f}).buffer_unordered(32)

					//begin processing immediately
					//TODO: fix dom being &mut &mut inside the closure
					.try_fold((&mut stack,&mut dom),|(stack,dom),bog|async{
						//push child objects onto dom serially as they arrive
						if let Some((blacklist,data))=bog{
							let referent=match data{
								PreparedData::Model(mut model_dom)=>{
									let referent=model_dom.root().children()[0];
									model_dom.transfer(referent,dom,item_ref);
									referent
								},
								PreparedData::Builder(script)=>dom.insert(item_ref,script),
							};
							//new children need to be traversed
							stack.push(CompileStackInstruction::TraverseReferent(referent,blacklist));
						}
						Ok((stack,dom))
					}).await?;
				}
			},
			CompileStackInstruction::PopFolder=>assert!(folder.pop(),"pop folder bad"),
		}
	}
	Ok(())
}