#[cfg(feature="cli")]
mod cli;
#[cfg(feature="cli")]
pub use cli::Commands;
#[cfg(feature="cli")]
mod loader;

use std::path::PathBuf;
use std::borrow::Cow;
use std::io::Cursor;

use strafesnet_bsp_loader::{Bsp,Vpk};
#[derive(Clone,Copy)]
pub struct BspFinder<'bsp,'vpk>{
	pub bsp:&'bsp Bsp,
	pub vpks:&'vpk [Vpk],
}
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
	pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
		where
			'bsp:'a,
			'vpk:'a,
	{
		// search bsp
		if let Some(data)=self.bsp.pack_get(path)?{
			return Ok(Some(Cow::Owned(data)));
		}

		//search each vpk
		for vpk in self.vpks{
			if let Some(vpk_entry)=vpk.tree_get(path){
				return Ok(Some(vpk_entry.get()?));
			}
		}

		Ok(None)
	}
}

enum VMTContent{
	VMT(String),
	VTF(String),
	Patch(vmt_parser::material::PatchMaterial),
	Unsupported,//don't want to deal with whatever vmt variant
	Unresolved,//could not locate a texture because of vmt content
}
impl VMTContent{
	fn vtf(opt:Option<String>)->Self{
		match opt{
			Some(s)=>Self::VTF(s),
			None=>Self::Unresolved,
		}
	}
}

fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
	//just grab some texture from somewhere for now
	match material{
		vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
		vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
		vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
		vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
		vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
		vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
		vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
		vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
		vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
		vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
		vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
		vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
		_=>unreachable!(),
	}
}

#[derive(Debug,thiserror::Error)]
pub enum GetVMTError{
	#[error("Bsp error {0:?}")]
	Bsp(#[from]vbsp::BspError),
	#[error("Utf8 error {0:?}")]
	Utf8(#[from]std::str::Utf8Error),
	#[error("Vdf error {0:?}")]
	Vdf(#[from]vmt_parser::VdfError),
	#[error("Vmt not found")]
	NotFound,
}

fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
	let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
	//decode vmt and then write
	let vmt_str=core::str::from_utf8(&vmt_data)?;
	let material=vmt_parser::from_str(vmt_str)?;
	Ok(material)
}

#[derive(Debug,thiserror::Error)]
pub enum LoadVMTError{
	#[error("Bsp error {0:?}")]
	Bsp(#[from]vbsp::BspError),
	#[error("GetVMT error {0:?}")]
	GetVMT(#[from]GetVMTError),
	#[error("FromUtf8 error {0:?}")]
	FromUtf8(#[from]std::string::FromUtf8Error),
	#[error("Vdf error {0:?}")]
	Vdf(#[from]vmt_parser::VdfError),
	#[error("Vmt unsupported")]
	Unsupported,
	#[error("Vmt unresolved")]
	Unresolved,
	#[error("Vmt not found")]
	NotFound,
}
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
	where
		'bsp:'a,
		'vpk:'a,
{
	match get_some_texture(material){
		VMTContent::VMT(mut s)=>{
			s.make_ascii_lowercase();
			recursive_vmt_loader(finder,get_vmt(finder,&s)?)
		},
		VMTContent::VTF(s)=>{
			let mut texture_file_name=PathBuf::from("materials");
			texture_file_name.push(s);
			texture_file_name.set_extension("vtf");
			texture_file_name.as_mut_os_str().make_ascii_lowercase();
			Ok(finder.find(texture_file_name.to_str().unwrap())?)
		},
		VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
			mat.resolve(|search_name|{
				let name_lowercase=search_name.to_lowercase();
				match finder.find(&name_lowercase)?{
					Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
					None=>Err(LoadVMTError::NotFound),
				}
			})?
		),
		VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
		VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
	}
}
pub fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
	where
		'bsp:'a,
		'vpk:'a,
{
	let mut texture_file_name=PathBuf::from("materials");
	//lower case
	texture_file_name.push(texture_name);
	texture_file_name.as_mut_os_str().make_ascii_lowercase();
	//remove stem and search for both vtf and vmt files
	let stem=texture_file_name.file_stem().unwrap().to_owned();
	texture_file_name.pop();
	texture_file_name.push(stem);
	if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
		return Ok(Some(stuff));
	}

	// search for both vmt,vtf
	let mut texture_file_name_vmt=texture_file_name.clone();
	texture_file_name_vmt.set_extension("vmt");

	let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
	match get_vmt_result{
		Ok(material)=>{
			let vmt_result=recursive_vmt_loader(finder,material);
			match vmt_result{
				Ok(Some(stuff))=>return Ok(Some(stuff)),
				Ok(None)
				|Err(LoadVMTError::NotFound)=>(),
				|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
				Err(e)=>return Err(e),
			}
		}
		|Err(GetVMTError::NotFound)=>(),
		Err(e)=>Err(e)?,
	}

	// try looking for vtf
	let mut texture_file_name_vtf=texture_file_name.clone();
	texture_file_name_vtf.set_extension("vtf");

	let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
	match get_vtf_result{
		Ok(material)=>{
			let vtf_result=recursive_vmt_loader(finder,material);
			match vtf_result{
				Ok(Some(stuff))=>return Ok(Some(stuff)),
				Ok(None)
				|Err(LoadVMTError::NotFound)=>(),
				|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
				Err(e)=>return Err(e),
			}
		}
		|Err(GetVMTError::NotFound)=>(),
		Err(e)=>Err(e)?,
	}

	Ok(None)
}

#[derive(Debug,thiserror::Error)]
pub enum ConvertTextureError{
	#[error("Vtf error {0:?}")]
	Vtf(#[from]vtf::Error),
	#[error("DDS encode error {0:?}")]
	DDSEncode(#[from]image_dds::error::SurfaceError),
	#[error("DDS create error {0:?}")]
	DDS(#[from]image_dds::CreateDdsError),
	#[error("DDS write error {0:?}")]
	DDSWrite(#[from]image_dds::ddsfile::Error),
}

pub fn convert_texture_to_dds(vtf_data:&[u8])->Result<Vec<u8>,ConvertTextureError>{
	let vtf_vec=vtf_data.to_vec();
	let image=vtf::from_bytes(&vtf_vec)?.highres_image.decode(0)?.to_rgba8();

	let format=if image.width()%4!=0||image.height()%4!=0{
		image_dds::ImageFormat::Rgba8UnormSrgb
	}else{
		image_dds::ImageFormat::BC7RgbaUnormSrgb
	};
	let dds=image_dds::SurfaceRgba8{
		width:image.width(),
		height:image.height(),
		depth:1,
		layers:1,
		mipmaps:1,
		data:image.as_raw(),
	}.encode(format,image_dds::Quality::Slow,image_dds::Mipmaps::GeneratedAutomatic)?.to_dds()?;

	let mut buf=Vec::new();
	dds.write(&mut Cursor::new(&mut buf))?;
	Ok(buf)
}

pub fn read_vpks(vpk_paths:&[PathBuf])->Result<Vec<Vpk>,vpk::Error>{
	vpk_paths.iter().map(|vpk_path|{
		Ok(Vpk::new(vpk::VPK::read(vpk_path)?))
	}).collect()
}
