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

use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
pub use rbxassetid::RobloxAssetId;
use rbx_dom_weak::Instance;

// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
	rbx_dom_weak::ustr(s)
}

#[derive(Debug)]
pub enum LoadDomError{
	IO(std::io::Error),
	Binary(rbx_binary::DecodeError),
	Xml(rbx_xml::DecodeError),
	UnknownFormat,
}
impl std::fmt::Display for LoadDomError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for LoadDomError{}
pub fn load_dom(data:&[u8])->Result<rbx_dom_weak::WeakDom,LoadDomError>{
	let mut input=Cursor::new(data);
	let mut first_8=[0u8;8];
	input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
	input.rewind().map_err(LoadDomError::IO)?;
	match &first_8{
		b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
		b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
		_=>Err(LoadDomError::UnknownFormat),
	}
}

fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
	let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
		println!("property={} does not exist for class={}",property,object.class.as_str());
		return;
	};
	let rbx_dom_weak::types::ContentType::Uri(uri)=content.value()else{
		println!("ContentType is not Uri");
		return;
	};
	let Ok(asset_id)=uri.parse()else{
		println!("Content failed to parse into AssetID: {:?}",content);
		return;
	};
	content_list.insert(asset_id);
}
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
	let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
		println!("property={} does not exist for class={}",property,object.class.as_str());
		return;
	};
	let Ok(asset_id)=content.as_str().parse()else{
		println!("Content failed to parse into AssetID: {:?}",content);
		return;
	};
	content_list.insert(asset_id);
}

#[derive(Default)]
pub struct UniqueAssets{
	pub meshes:HashSet<RobloxAssetId>,
	pub unions:HashSet<RobloxAssetId>,
	pub textures:HashSet<RobloxAssetId>,
}
impl UniqueAssets{
	fn collect(&mut self,object:&Instance){
		match object.class.as_str(){
			"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
			"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
			"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
			"MeshPart"=>{
				accumulate_content(&mut self.textures,object,"TextureContent");
				accumulate_content(&mut self.meshes,object,"MeshContent");
			},
			"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
			"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
			"Sky"=>{
				accumulate_content_id(&mut self.textures,object,"MoonTextureId");
				accumulate_content_id(&mut self.textures,object,"SkyboxBk");
				accumulate_content_id(&mut self.textures,object,"SkyboxDn");
				accumulate_content_id(&mut self.textures,object,"SkyboxFt");
				accumulate_content_id(&mut self.textures,object,"SkyboxLf");
				accumulate_content_id(&mut self.textures,object,"SkyboxRt");
				accumulate_content_id(&mut self.textures,object,"SkyboxUp");
				accumulate_content_id(&mut self.textures,object,"SunTextureId");
			},
			"SurfaceAppearance"=>{
				accumulate_content(&mut self.textures,object,"ColorMapContent");
				accumulate_content(&mut self.textures,object,"MetalnessMapContent");
				accumulate_content(&mut self.textures,object,"NormalMapContent");
				accumulate_content(&mut self.textures,object,"RoughnessMapContent");
			}
			"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
			"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
			_=>(),
		}
	}
}

pub fn get_unique_assets(dom:&rbx_dom_weak::WeakDom)->UniqueAssets{
	let mut assets=UniqueAssets::default();
	for object in dom.descendants(){
		assets.collect(object);
	}
	assets
}

#[derive(Debug,thiserror::Error)]
pub enum ConvertTextureError{
	#[error("Image error {0:?}")]
	Image(#[from]image::ImageError),
	#[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(data:&[u8])->Result<Vec<u8>,ConvertTextureError>{
	let image=image::load_from_memory(data)?.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)
}

#[derive(Debug)]
pub enum DownloadAssetError{
	Get(rbx_asset::cookie::GetError),
	IO(std::io::Error),
}
impl std::fmt::Display for DownloadAssetError{
	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
		write!(f,"{self:?}")
	}
}
impl std::error::Error for DownloadAssetError{}

pub async fn download_asset(context:&rbx_asset::cookie::Context,asset_id:u64)->Result<Vec<u8>,DownloadAssetError>{
	let data=context.get_asset(rbx_asset::cookie::GetAssetRequest{
		asset_id,
		version:None,
	}).await.map_err(DownloadAssetError::Get)?;
	Ok(data.to_vec().map_err(DownloadAssetError::IO)?)
}

pub(crate) const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
const RETRY_LIMIT:u32=12;

pub enum DownloadRetryResult{
	Ok(Vec<u8>),
	TimedOut,
}

pub async fn download_asset_retry(context:&rbx_asset::cookie::Context,asset_id:u64)->Result<DownloadRetryResult,DownloadAssetError>{
	let mut retry=0u32;
	let mut backoff=1000f32;
	loop{
		match context.get_asset(rbx_asset::cookie::GetAssetRequest{
			asset_id,
			version:None,
		}).await{
			Ok(data)=>{
				let bytes=data.to_vec().map_err(DownloadAssetError::IO)?;
				break Ok(DownloadRetryResult::Ok(bytes));
			},
			Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{
				if status_code.as_u16()==429{
					if retry==RETRY_LIMIT{
						break Ok(DownloadRetryResult::TimedOut);
					}
					tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
					backoff*=BACKOFF_MUL;
					retry+=1;
				}else{
					break Err(DownloadAssetError::Get(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body})));
				}
			},
			Err(e)=>{
				break Err(DownloadAssetError::Get(e));
			},
		}
	}
}
