use std::collections::HashSet;
use std::num::ParseIntError;

use strafesnet_common::gameplay_modes::{StageId,ModeId};
use strafesnet_common::integer::{FixedFromFloatError,Planar64TryFromFloatError};

/// A collection of errors which can be ignored at your peril
#[derive(Debug,Default)]
pub struct RecoverableErrors{
	/// A basepart has an invalid / missing property.
	pub basepart_property:Vec<InstancePath>,
	/// A part has an unconvertable CFrame.
	pub basepart_cframe:Vec<CFrameError>,
	/// A part has an unconvertable Velocity.
	pub basepart_velocity:Vec<Planar64ConvertError>,
	/// A part has an invalid / missing property.
	pub part_property:Vec<InstancePath>,
	/// A part has an invalid shape.
	pub part_shape:Vec<ShapeError>,
	/// A meshpart has an invalid / missing property.
	pub meshpart_property:Vec<InstancePath>,
	/// A meshpart has no mesh.
	pub meshpart_content:Vec<InstancePath>,
	/// A basepart has an unsupported subclass.
	pub unsupported_class:HashSet<String>,
	/// A decal has an invalid / missing property.
	pub decal_property:Vec<InstancePath>,
	/// A SurfaceAppearance has an invalid / missing property.
	pub surface_appearance_property:Vec<InstancePath>,
	/// A decal has an invalid normal_id.
	pub normal_id:Vec<NormalIdError>,
	/// A texture has an invalid / missing property.
	pub texture_property:Vec<InstancePath>,
	/// A mode_id failed to parse.
	pub mode_id_parse_int:Vec<ParseIntContext>,
	/// There is a duplicate mode.
	pub duplicate_mode:HashSet<ModeId>,
	/// A mode_id failed to parse.
	pub stage_id_parse_int:Vec<ParseIntContext>,
	/// A Stage was duplicated leading to undefined behaviour.
	pub duplicate_stage:HashSet<DuplicateStageError>,
	/// A WormholeOut id failed to parse.
	pub wormhole_out_id_parse_int:Vec<ParseIntContext>,
	/// A WormholeOut was duplicated leading to undefined behaviour.
	pub duplicate_wormhole_out:HashSet<u32>,
	/// A WormholeIn id failed to parse.
	pub wormhole_in_id_parse_int:Vec<ParseIntContext>,
	/// A jump limit failed to parse.
	pub jump_limit_parse_int:Vec<ParseIntContext>,
}

impl RecoverableErrors{
	pub fn count(&self)->usize{
		self.basepart_property.len()+
		self.basepart_cframe.len()+
		self.basepart_velocity.len()+
		self.part_property.len()+
		self.part_shape.len()+
		self.meshpart_property.len()+
		self.meshpart_content.len()+
		self.unsupported_class.len()+
		self.decal_property.len()+
		self.surface_appearance_property.len()+
		self.normal_id.len()+
		self.texture_property.len()+
		self.mode_id_parse_int.len()+
		self.duplicate_mode.len()+
		self.stage_id_parse_int.len()+
		self.duplicate_stage.len()+
		self.wormhole_out_id_parse_int.len()+
		self.duplicate_wormhole_out.len()+
		self.wormhole_in_id_parse_int.len()+
		self.jump_limit_parse_int.len()
	}
}

fn write_comma_separated<T>(
	f:&mut std::fmt::Formatter<'_>,
	mut it:impl Iterator<Item=T>,
	custom_write:impl Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result
)->std::fmt::Result{
	if let Some(t)=it.next(){
		custom_write(f,t)?;
		for t in it{
			write!(f,", ")?;
			custom_write(f,t)?;
		}
	}
	Ok(())
}

macro_rules! write_instance_path_error{
	($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal,$problem:literal)=>{
		let len=$self.$field.len();
		if len!=0{
			let plural=if len==1{$class}else{$class_plural};
			write!($f,"The following {plural} {}: ",$problem)?;
			write_comma_separated($f,$self.$field.iter(),|f,InstancePath(path)|
				write!(f,"{path}")
			)?;
			writeln!($f)?;
		}
	};
}
macro_rules! write_duplicate_error{
	($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal)=>{
		let len=$self.$field.len();
		if len!=0{
			let plural=if len==1{$class}else{$class_plural};
			write!($f,"The following {plural} duplicates: ")?;
			write_comma_separated($f,$self.$field.iter(),|f,id|
				write!(f,"{id}")
			)?;
			writeln!($f)?;
		}
	};
}
macro_rules! write_bespoke_error{
	($f:ident,$self:ident,$field:ident,$class:literal,$class_plural:literal,$problem:literal,$path_field:ident,$error_field:ident)=>{
		let len=$self.$field.len();
		if len!=0{
			let plural=if len==1{$class}else{$class_plural};
			write!($f,"The following {plural} {}: ",$problem)?;
			write_comma_separated($f,$self.$field.iter(),|f,context|
				write!(f,"{} ({})",context.$path_field,context.$error_field)
			)?;
			writeln!($f)?;
		}
	};
}

impl core::fmt::Display for RecoverableErrors{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write_instance_path_error!(f,self,basepart_property,"BasePart is","BaseParts are","missing a property");
		write_bespoke_error!(f,self,basepart_cframe,"BasePart","BaseParts","CFrame float convert failed",path,error);
		write_bespoke_error!(f,self,basepart_velocity,"BasePart","BaseParts","Velocity float convert failed",path,error);
		write_instance_path_error!(f,self,part_property,"Part is","Parts are","missing a property");
		write_bespoke_error!(f,self,part_shape,"Part","Parts","Shape is invalid",path,shape);
		write_instance_path_error!(f,self,meshpart_property,"MeshPart is","MeshParts are","missing a property");
		write_instance_path_error!(f,self,meshpart_content,"MeshPart has","MeshParts have","no mesh");
		{
			let len=self.unsupported_class.len();
			if len!=0{
				let plural=if len==1{"Class is"}else{"Classes are"};
				write!(f,"The following {plural} not supported: ")?;
				write_comma_separated(f,self.unsupported_class.iter(),|f,classname|write!(f,"{classname}"))?;
				writeln!(f)?;
			}
		}
		write_instance_path_error!(f,self,decal_property,"Decal is","Decals are","missing a property");
		write_instance_path_error!(f,self,surface_appearance_property,"SurfaceAppearance is","SurfaceAppearances are","missing a property");
		write_bespoke_error!(f,self,normal_id,"Decal","Decals","NormalId is invalid",path,normal_id);
		write_instance_path_error!(f,self,texture_property,"Texture is","Textures are","missing a property");
		write_bespoke_error!(f,self,mode_id_parse_int,"ModeId","ModeIds","failed to parse",context,error);
		write_duplicate_error!(f,self,duplicate_mode,"ModeId has","ModeIds have");
		write_bespoke_error!(f,self,stage_id_parse_int,"StageId","StageIds","failed to parse",context,error);
		write_duplicate_error!(f,self,duplicate_stage,"StageId has","StageIds have");
		write_bespoke_error!(f,self,wormhole_out_id_parse_int,"WormholeOutId","WormholeOutIds","failed to parse",context,error);
		write_duplicate_error!(f,self,duplicate_wormhole_out,"WormholeOutId has","WormholeOutIds have");
		write_bespoke_error!(f,self,wormhole_in_id_parse_int,"WormholeInId","WormholeInIds","failed to parse",context,error);
		write_bespoke_error!(f,self,jump_limit_parse_int,"jump limit","jump limits","failed to parse",context,error);
		Ok(())
	}
}

/// A Decal was missing required properties
#[derive(Debug)]
pub struct InstancePath(pub String);

impl core::fmt::Display for InstancePath{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		self.0.fmt(f)
	}
}

impl InstancePath{
	pub fn new(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->InstancePath{
		let mut names:Vec<_>=core::iter::successors(
			Some(instance),
			|i|dom.get_by_ref(i.parent())
		).map(
			|i|i.name.as_str()
		).collect();
		// discard the name of the root object
		names.pop();
		names.reverse();
		InstancePath(names.join("."))
	}
}

#[derive(Debug)]
pub struct ParseIntContext{
	pub context:String,
	pub error:ParseIntError,
}
impl ParseIntContext{
	pub fn parse<T:core::str::FromStr<Err=ParseIntError>>(input:&str)->Result<T,Self>{
		input.parse().map_err(|error|ParseIntContext{
			context:input.to_owned(),
			error,
		})
	}
}

#[derive(Debug)]
pub struct NormalIdError{
	pub path:InstancePath,
	pub normal_id:u32,
}

#[derive(Debug)]
pub struct ShapeError{
	pub path:InstancePath,
	pub shape:u32,
}

#[derive(Debug)]
pub enum CFrameErrorType{
	ZeroDeterminant,
	Convert(FixedFromFloatError),
}
impl core::fmt::Display for CFrameErrorType{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}

#[derive(Debug)]
pub struct CFrameError{
	pub path:InstancePath,
	pub error:CFrameErrorType,
}

#[derive(Debug)]
pub struct Planar64ConvertError{
	pub path:InstancePath,
	pub error:Planar64TryFromFloatError,
}

#[derive(Debug,Hash,Eq,PartialEq)]
pub struct DuplicateStageError{
	pub mode_id:ModeId,
	pub stage_id:StageId,
}
impl core::fmt::Display for DuplicateStageError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{}-Spawn{}",self.mode_id,self.stage_id.get())
	}
}
