use hashbrown::HashSet;
use iref::{Iri, IriBuf};
use mime::Mime;
use rdf_types::vocabulary::{IriVocabulary, IriVocabularyMut};
use static_iref::iri;
use std::{borrow::Cow, hash::Hash};
pub mod chain;
pub mod fs;
pub mod map;
pub mod none;
pub use chain::ChainLoader;
pub use fs::FsLoader;
pub use none::NoLoader;
#[cfg(feature = "reqwest")]
pub mod reqwest;
#[cfg(feature = "reqwest")]
pub use self::reqwest::ReqwestLoader;
pub type LoadingResult<I = IriBuf> = Result<RemoteDocument<I>, LoadError>;
pub type RemoteContextReference<I = IriBuf> = RemoteDocumentReference<I, json_ld_syntax::Context>;
#[derive(Clone)]
pub enum RemoteDocumentReference<I = IriBuf, T = json_syntax::Value> {
	Iri(I),
	Loaded(RemoteDocument<I, T>),
}
impl<I, T> RemoteDocumentReference<I, T> {
	pub fn iri(iri: I) -> Self {
		Self::Iri(iri)
	}
}
impl<I> RemoteDocumentReference<I> {
	pub async fn load_with<V>(self, vocabulary: &mut V, loader: &impl Loader) -> LoadingResult<I>
	where
		V: IriVocabularyMut<Iri = I>,
		I: Clone + Eq + Hash,
	{
		match self {
			Self::Iri(r) => Ok(loader.load_with(vocabulary, r).await?.map(Into::into)),
			Self::Loaded(doc) => Ok(doc),
		}
	}
	pub async fn loaded_with<V>(
		&self,
		vocabulary: &mut V,
		loader: &impl Loader,
	) -> Result<Cow<'_, RemoteDocument<V::Iri>>, LoadError>
	where
		V: IriVocabularyMut<Iri = I>,
		I: Clone + Eq + Hash,
	{
		match self {
			Self::Iri(r) => Ok(Cow::Owned(
				loader
					.load_with(vocabulary, r.clone())
					.await?
					.map(Into::into),
			)),
			Self::Loaded(doc) => Ok(Cow::Borrowed(doc)),
		}
	}
}
#[derive(Debug, thiserror::Error)]
pub enum ContextLoadError {
	#[error(transparent)]
	LoadingDocumentFailed(#[from] LoadError),
	#[error("context extraction failed")]
	ContextExtractionFailed(#[from] ExtractContextError),
}
impl<I> RemoteContextReference<I> {
	pub async fn load_context_with<V, L: Loader>(
		self,
		vocabulary: &mut V,
		loader: &L,
	) -> Result<RemoteContext<I>, ContextLoadError>
	where
		V: IriVocabularyMut<Iri = I>,
		I: Clone + Eq + Hash,
	{
		match self {
			Self::Iri(r) => Ok(loader
				.load_with(vocabulary, r)
				.await?
				.try_map(|d| d.into_ld_context())?),
			Self::Loaded(doc) => Ok(doc),
		}
	}
	pub async fn loaded_context_with<V, L: Loader>(
		&self,
		vocabulary: &mut V,
		loader: &L,
	) -> Result<Cow<'_, RemoteContext<I>>, ContextLoadError>
	where
		V: IriVocabularyMut<Iri = I>,
		I: Clone + Eq + Hash,
	{
		match self {
			Self::Iri(r) => Ok(Cow::Owned(
				loader
					.load_with(vocabulary, r.clone())
					.await?
					.try_map(|d| d.into_ld_context())?,
			)),
			Self::Loaded(doc) => Ok(Cow::Borrowed(doc)),
		}
	}
}
#[derive(Debug, Clone)]
pub struct RemoteDocument<I = IriBuf, T = json_syntax::Value> {
	pub url: Option<I>,
	pub content_type: Option<Mime>,
	pub context_url: Option<I>,
	pub profile: HashSet<Profile<I>>,
	pub document: T,
}
pub type RemoteContext<I = IriBuf> = RemoteDocument<I, json_ld_syntax::context::Context>;
impl<I, T> RemoteDocument<I, T> {
	pub fn new(url: Option<I>, content_type: Option<Mime>, document: T) -> Self {
		Self::new_full(url, content_type, None, HashSet::new(), document)
	}
	pub fn new_full(
		url: Option<I>,
		content_type: Option<Mime>,
		context_url: Option<I>,
		profile: HashSet<Profile<I>>,
		document: T,
	) -> Self {
		Self {
			url,
			content_type,
			context_url,
			profile,
			document,
		}
	}
	pub fn map<U>(self, f: impl Fn(T) -> U) -> RemoteDocument<I, U> {
		RemoteDocument {
			url: self.url,
			content_type: self.content_type,
			context_url: self.context_url,
			profile: self.profile,
			document: f(self.document),
		}
	}
	pub fn try_map<U, E>(self, f: impl Fn(T) -> Result<U, E>) -> Result<RemoteDocument<I, U>, E> {
		Ok(RemoteDocument {
			url: self.url,
			content_type: self.content_type,
			context_url: self.context_url,
			profile: self.profile,
			document: f(self.document)?,
		})
	}
	pub fn map_iris<J>(self, mut f: impl FnMut(I) -> J) -> RemoteDocument<J, T>
	where
		J: Eq + Hash,
	{
		RemoteDocument {
			url: self.url.map(&mut f),
			content_type: self.content_type,
			context_url: self.context_url.map(&mut f),
			profile: self
				.profile
				.into_iter()
				.map(|p| p.map_iri(&mut f))
				.collect(),
			document: self.document,
		}
	}
	pub fn url(&self) -> Option<&I> {
		self.url.as_ref()
	}
	pub fn content_type(&self) -> Option<&Mime> {
		self.content_type.as_ref()
	}
	pub fn context_url(&self) -> Option<&I> {
		self.context_url.as_ref()
	}
	pub fn document(&self) -> &T {
		&self.document
	}
	pub fn document_mut(&mut self) -> &mut T {
		&mut self.document
	}
	pub fn into_document(self) -> T {
		self.document
	}
	pub fn into_url(self) -> Option<I> {
		self.url
	}
	pub fn set_url(&mut self, url: Option<I>) {
		self.url = url
	}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StandardProfile {
	Expanded,
	Compacted,
	Context,
	Flattened,
	Framed,
}
impl StandardProfile {
	pub fn from_iri(iri: &Iri) -> Option<Self> {
		if iri == iri!("http://www.w3.org/ns/json-ld#expanded") {
			Some(Self::Expanded)
		} else if iri == iri!("http://www.w3.org/ns/json-ld#compacted") {
			Some(Self::Compacted)
		} else if iri == iri!("http://www.w3.org/ns/json-ld#context") {
			Some(Self::Context)
		} else if iri == iri!("http://www.w3.org/ns/json-ld#flattened") {
			Some(Self::Flattened)
		} else if iri == iri!("http://www.w3.org/ns/json-ld#framed") {
			Some(Self::Framed)
		} else {
			None
		}
	}
	pub fn iri(&self) -> &'static Iri {
		match self {
			Self::Expanded => iri!("http://www.w3.org/ns/json-ld#expanded"),
			Self::Compacted => iri!("http://www.w3.org/ns/json-ld#compacted"),
			Self::Context => iri!("http://www.w3.org/ns/json-ld#context"),
			Self::Flattened => iri!("http://www.w3.org/ns/json-ld#flattened"),
			Self::Framed => iri!("http://www.w3.org/ns/json-ld#framed"),
		}
	}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Profile<I = IriBuf> {
	Standard(StandardProfile),
	Custom(I),
}
impl Profile {
	pub fn new(iri: &Iri) -> Self {
		match StandardProfile::from_iri(iri) {
			Some(p) => Self::Standard(p),
			None => Self::Custom(iri.to_owned()),
		}
	}
	pub fn iri(&self) -> &Iri {
		match self {
			Self::Standard(s) => s.iri(),
			Self::Custom(c) => c,
		}
	}
}
impl<I> Profile<I> {
	pub fn new_with(iri: &Iri, vocabulary: &mut impl IriVocabularyMut<Iri = I>) -> Self {
		match StandardProfile::from_iri(iri) {
			Some(p) => Self::Standard(p),
			None => Self::Custom(vocabulary.insert(iri)),
		}
	}
	pub fn iri_with<'a>(&'a self, vocabulary: &'a impl IriVocabulary<Iri = I>) -> &'a Iri {
		match self {
			Self::Standard(s) => s.iri(),
			Self::Custom(c) => vocabulary.iri(c).unwrap(),
		}
	}
	pub fn map_iri<J>(self, f: impl FnOnce(I) -> J) -> Profile<J> {
		match self {
			Self::Standard(p) => Profile::Standard(p),
			Self::Custom(i) => Profile::Custom(f(i)),
		}
	}
}
pub type LoadErrorCause = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, thiserror::Error)]
#[error("loading document `{target}` failed: {cause}")]
pub struct LoadError {
	pub target: IriBuf,
	pub cause: LoadErrorCause,
}
impl LoadError {
	pub fn new(target: IriBuf, cause: impl 'static + std::error::Error + Send + Sync) -> Self {
		Self {
			target,
			cause: Box::new(cause),
		}
	}
}
pub trait Loader {
	#[allow(async_fn_in_trait)]
	async fn load_with<V>(&self, vocabulary: &mut V, url: V::Iri) -> LoadingResult<V::Iri>
	where
		V: IriVocabularyMut,
		V::Iri: Clone + Eq + Hash,
	{
		let lexical_url = vocabulary.iri(&url).unwrap();
		let document = self.load(lexical_url).await?;
		Ok(document.map_iris(|i| vocabulary.insert_owned(i)))
	}
	#[allow(async_fn_in_trait)]
	async fn load(&self, url: &Iri) -> Result<RemoteDocument<IriBuf>, LoadError>;
}
impl<'l, L: Loader> Loader for &'l L {
	async fn load_with<V>(&self, vocabulary: &mut V, url: V::Iri) -> LoadingResult<V::Iri>
	where
		V: IriVocabularyMut,
		V::Iri: Clone + Eq + Hash,
	{
		L::load_with(self, vocabulary, url).await
	}
	async fn load(&self, url: &Iri) -> Result<RemoteDocument<IriBuf>, LoadError> {
		L::load(self, url).await
	}
}
impl<'l, L: Loader> Loader for &'l mut L {
	async fn load_with<V>(&self, vocabulary: &mut V, url: V::Iri) -> LoadingResult<V::Iri>
	where
		V: IriVocabularyMut,
		V::Iri: Clone + Eq + Hash,
	{
		L::load_with(self, vocabulary, url).await
	}
	async fn load(&self, url: &Iri) -> Result<RemoteDocument<IriBuf>, LoadError> {
		L::load(self, url).await
	}
}
#[derive(Debug, thiserror::Error)]
pub enum ExtractContextError {
	#[error("unexpected {0}")]
	Unexpected(json_syntax::Kind),
	#[error("missing `@context` entry")]
	NoContext,
	#[error("duplicate `@context` entry")]
	DuplicateContext,
	#[error("JSON-LD context syntax error: {0}")]
	Syntax(json_ld_syntax::context::InvalidContext),
}
impl ExtractContextError {
	fn duplicate_context(
		json_syntax::object::Duplicate(_, _): json_syntax::object::Duplicate<
			json_syntax::object::Entry,
		>,
	) -> Self {
		Self::DuplicateContext
	}
}
pub trait ExtractContext {
	fn into_ld_context(self) -> Result<json_ld_syntax::context::Context, ExtractContextError>;
}
impl ExtractContext for json_syntax::Value {
	fn into_ld_context(self) -> Result<json_ld_syntax::context::Context, ExtractContextError> {
		match self {
			Self::Object(mut o) => match o
				.remove_unique("@context")
				.map_err(ExtractContextError::duplicate_context)?
			{
				Some(context) => {
					use json_ld_syntax::TryFromJson;
					json_ld_syntax::context::Context::try_from_json(context.value)
						.map_err(ExtractContextError::Syntax)
				}
				None => Err(ExtractContextError::NoContext),
			},
			other => Err(ExtractContextError::Unexpected(other.kind())),
		}
	}
}