use crate::terms::{Iri, Term};

use std::{collections::HashMap, sync::Arc};

use iref::IriRefBuf;
use thiserror::Error;

use bimap::BiMap;

#[derive(Error, Debug, PartialEq)]
pub enum ProfileError {
    #[error("Invalid characters in prefix name")]
    InvalidPrefix,
    #[error("Invalid CURIE")]
    InvalidCurie,
    #[error("Tried to resolve an unknown prefix")]
    UnknownPrefix,
    #[error("Tried to resolve an unknown term")]
    UnknownTerm,
    #[error("Tried to shrink a term other than a named node, or register a term mapping with invalid characters: {0}")]
    InvalidTerm(String),
}

#[derive(Debug, Clone)]
pub struct BasicPrefixMap(HashMap<String, String>);

impl BasicPrefixMap {
    pub fn new() -> Self {
        Self(HashMap::new())
    }
}

pub trait PrefixMap {
    fn validate_prefix(prefix: &str) -> Result<(), ProfileError> {
        if prefix.contains(':') {
            Err(ProfileError::InvalidPrefix)
        } else {
            Ok(())
        }
    }
    fn set(&mut self, prefix: String, iri: String) -> Result<(), ProfileError>;
    fn remove(&mut self, prefix: &str);
    fn resolve(&self, curie: &str) -> Result<Arc<Term>, ProfileError>;
    fn shrink(&self, term: Arc<Term>) -> Result<String, ProfileError>;
    fn set_default(&mut self, iri: String) -> Result<(), ProfileError> {
        self.set("".to_string(), iri)
    }
    fn iter(&self) -> impl Iterator<Item = (&String, &String)> + Send;
}

impl PrefixMap for BasicPrefixMap {
    fn set(&mut self, prefix: String, iri: String) -> Result<(), ProfileError> {
        Self::validate_prefix(&prefix)?;
        self.0.remove(&prefix);
        self.0.insert(prefix.clone(), iri.clone());
        Ok(())
    }

    fn remove(&mut self, prefix: &str) {
        self.0.remove(prefix);
    }

    /// # Example
    ///
    /// ```
    /// use taganak_core::environment::profile::{BasicPrefixMap, PrefixMap};
    ///
    /// let pm = BasicPrefixMap::default();
    ///
    /// let i1 = pm.resolve("rdf:type").unwrap();
    /// let i2 = pm.resolve("xml:lang").unwrap();
    ///
    /// assert_eq!(i1.to_string(), "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>");
    /// assert_eq!(i2.to_string(), "<http://www.w3.org/XML/1998/namespace/lang>");
    /// ```
    fn resolve(&self, curie: &str) -> Result<Arc<Term>, ProfileError> {
        let (prefix, ln) = curie.split_once(":").ok_or(ProfileError::InvalidCurie)?;
        let iri = self.0.get(prefix).ok_or(ProfileError::InvalidPrefix)?;

        Ok(Arc::new(Term::NamedNode(
            Iri::new(format!("{}{}", iri, ln)).map_err(|e| ProfileError::InvalidCurie)?,
        )))
    }

    /// # Example
    ///
    /// ```
    /// use taganak_core::environment::profile::{BasicPrefixMap, PrefixMap};
    /// use std::sync::Arc;
    ///
    /// let pm = BasicPrefixMap::default();
    ///
    /// let c1 = pm.shrink(Arc::new("<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>".try_into().unwrap())).unwrap();
    /// let c2 = pm.shrink(Arc::new("<http://www.w3.org/XML/1998/namespace/lang>".try_into().unwrap())).unwrap();
    ///
    /// assert_eq!(c1, "rdf:type");
    /// assert_eq!(c2, "xml:lang");
    /// ```
    fn shrink(&self, term: Arc<Term>) -> Result<String, ProfileError> {
        match *term {
            Term::NamedNode(ref iri) => {
                let iri = iri.to_string();
                for (prefix, base_iri) in self.0.iter() {
                    match iri.strip_prefix(base_iri) {
                        Some(ln) => return Ok(format!("{}:{}", prefix, ln)),
                        None => continue,
                    }
                }

                Err(ProfileError::InvalidPrefix)
            }
            _ => Err(ProfileError::InvalidTerm(term.to_string())),
        }
    }

    fn iter(&self) -> impl Iterator<Item = (&String, &String)> + Send {
        self.0.iter()
    }
}

impl Default for BasicPrefixMap {
    fn default() -> Self {
        let mut pm = Self::new();

        pm.set(
            "owl".to_string(),
            "http://www.w3.org/2002/07/owl#".to_string(),
        )
        .unwrap();
        pm.set(
            "rdf".to_string(),
            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
        )
        .unwrap();
        pm.set(
            "rdfs".to_string(),
            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
        )
        .unwrap();
        pm.set("rdfa".to_string(), "http://www.w3.org/ns/rdfa#".to_string())
            .unwrap();
        pm.set(
            "xhv".to_string(),
            "http://www.w3.org/1999/xhtml/vocab#".to_string(),
        )
        .unwrap();
        pm.set(
            "xml".to_string(),
            "http://www.w3.org/XML/1998/namespace/".to_string(),
        )
        .unwrap();
        pm.set(
            "xsd".to_string(),
            "http://www.w3.org/2001/XMLSchema#".to_string(),
        )
        .unwrap();

        pm
    }
}

#[derive(Debug, Default, Clone)]
pub struct BasicTermMap {
    default: Option<String>,
    map: BiMap<String, Arc<Term>>,
}

impl BasicTermMap {
    pub fn new() -> Self {
        Self::default()
    }
}

pub trait TermMap {
    fn validate_label(label: &str) -> Result<(), ProfileError> {
        // FIXME is this complete?
        if label.contains(':') || label.contains(char::is_whitespace) {
            return Err(ProfileError::InvalidTerm(label.to_string()));
        }

        Ok(())
    }

    fn set_unchecked(&mut self, label: String, term: Arc<Term>);
    fn resolve_unchecked(&self, label: &str) -> Option<Arc<Term>>;

    fn set(&mut self, label: String, term: Arc<Term>) -> Result<(), ProfileError> {
        Self::validate_label(label.as_str())?;

        self.set_unchecked(label, term);

        Ok(())
    }
    fn set_default(&mut self, term: String);
    fn remove(&mut self, label: &str);
    fn resolve(&self, label: &str) -> Result<Arc<Term>, ProfileError> {
        Self::validate_label(label)?;

        match self.resolve_unchecked(label) {
            Some(t) => Ok(t),
            None => Err(ProfileError::InvalidTerm(label.to_string())),
        }
    }
    fn shrink(&self, term: Arc<Term>) -> Result<String, ProfileError>;
}

impl TermMap for BasicTermMap {
    fn set_unchecked(&mut self, label: String, term: Arc<Term>) {
        self.map.insert(label, term);
    }

    fn set_default(&mut self, term: String) {
        self.default = Some(term);
    }

    fn remove(&mut self, label: &str) {
        self.map.remove_by_left(label);
    }

    /// # Example
    ///
    /// ```
    /// use taganak_core::environment::profile::{BasicTermMap, TermMap};
    /// use taganak_core::terms::Term;
    /// use std::sync::Arc;
    ///
    /// let mut tm = BasicTermMap::default();
    ///
    /// tm.set_default("https://example.com/foo/".to_string());
    /// tm.set("something".to_string(), Arc::new("<https://example.com/bar/something>".try_into().unwrap())).unwrap();
    ///
    /// assert_eq!(tm.resolve_unchecked("something").unwrap().to_string(), "<https://example.com/bar/something>");
    /// assert_eq!(tm.resolve_unchecked("else").unwrap().to_string(), "<https://example.com/foo/else>");
    /// ```
    fn resolve_unchecked(&self, label: &str) -> Option<Arc<Term>> {
        match self.map.get_by_left(label) {
            Some(t) => Some(t.clone()),
            None => match &self.default {
                Some(ref d) => {
                    let mut t = String::with_capacity(d.len() + label.len());
                    t.push_str(d);
                    t.push_str(label);

                    Some(Arc::new(Term::NamedNode(
                        Iri::new(t)
                            .map_err(|e| ProfileError::InvalidTerm(e.to_string()))
                            .ok()?,
                    )))
                }
                None => None,
            },
        }
    }

    /// # Example
    ///
    /// ```
    /// use taganak_core::environment::profile::{BasicTermMap, TermMap};
    /// use taganak_core::terms::Term;
    /// use std::sync::Arc;
    ///
    /// let mut tm = BasicTermMap::default();
    ///
    /// tm.set("something".to_string(), Arc::new("<https://example.com/bar/something>".try_into().unwrap())).unwrap();
    ///
    /// assert_eq!(tm.shrink(Arc::new("<https://example.com/bar/something>".try_into().unwrap())).unwrap(), "something");
    /// assert_eq!(tm.shrink(Arc::new("<https://example.com/bar/else>".try_into().unwrap())).unwrap(), "https://example.com/bar/else");
    /// ```
    fn shrink(&self, term: Arc<Term>) -> Result<String, ProfileError> {
        match self.map.get_by_right(&term) {
            Some(label) => Ok(label.clone()),
            None => match *term {
                Term::NamedNode(ref iri) => Ok(iri.to_string()),
                _ => Err(ProfileError::InvalidTerm(term.to_string())),
            },
        }
    }
}

#[derive(Debug, Default, Clone)]
pub struct BasicProfile {
    pub(crate) prefixes: BasicPrefixMap,
    pub(crate) terms: BasicTermMap,
    pub(crate) base_iri: Option<Iri>,
}

pub trait Profile: Default {
    fn resolve(&self, toresolve: &str) -> Result<Arc<Term>, ProfileError>;

    fn set_default_vocabulary(&mut self, term: String);
    fn set_term(&mut self, label: String, term: Arc<Term>) -> Result<(), ProfileError>;

    fn set_default_prefix(&mut self, iri: String) -> Result<(), ProfileError>;
    fn set_prefix(&mut self, prefix: String, iri: String) -> Result<(), ProfileError>;
    fn iter_prefices(&self) -> impl Iterator<Item = (&String, &String)> + Send;

    fn base_iri(&self) -> Option<&Iri>;
    fn set_base_iri(&mut self, iri: Option<Iri>);
    fn resolve_iri_ref(&self, toresolve: IriRefBuf) -> Result<Iri, ProfileError>;

    fn shrink(&self, term: Arc<Term>) -> Result<String, ProfileError>;
}

impl BasicProfile {
    pub fn new() -> Self {
        Self {
            prefixes: BasicPrefixMap::new(),
            terms: BasicTermMap::new(),
            base_iri: None,
        }
    }
}

impl Profile for BasicProfile {
    /// # Example
    ///
    /// ```
    /// use taganak_core::environment::profile::{BasicProfile, Profile};
    ///
    /// use std::sync::Arc;
    ///
    /// let mut p = BasicProfile::default();
    ///
    /// let i1 = p.resolve("rdf:type").unwrap();
    /// let i2 = p.resolve("xml:lang").unwrap();
    /// assert_eq!(i1.to_string(), "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>");
    /// assert_eq!(i2.to_string(), "<http://www.w3.org/XML/1998/namespace/lang>");
    ///
    /// p.set_default_vocabulary("https://example.com/foo/".to_string());
    /// p.set_term("something".to_string(), Arc::new("<https://example.com/bar/something>".try_into().unwrap())).unwrap();
    ///
    /// assert_eq!(p.resolve("something").unwrap().to_string(), "<https://example.com/bar/something>");
    /// assert_eq!(p.resolve("else").unwrap().to_string(), "<https://example.com/foo/else>");
    /// ```
    fn resolve(&self, toresolve: &str) -> Result<Arc<Term>, ProfileError> {
        if toresolve.contains(":") {
            self.prefixes.resolve(toresolve)
        } else {
            self.terms.resolve(toresolve)
        }
    }

    fn set_default_vocabulary(&mut self, term: String) {
        self.terms.set_default(term)
    }
    fn set_term(&mut self, label: String, term: Arc<Term>) -> Result<(), ProfileError> {
        self.terms.set(label, term)
    }

    fn set_default_prefix(&mut self, iri: String) -> Result<(), ProfileError> {
        self.prefixes.set_default(iri)
    }
    fn set_prefix(&mut self, prefix: String, iri: String) -> Result<(), ProfileError> {
        self.prefixes.set(prefix, iri)
    }
    fn iter_prefices(&self) -> impl Iterator<Item = (&String, &String)> + Send {
        self.prefixes.iter()
    }

    fn base_iri(&self) -> Option<&Iri> {
        self.base_iri.as_ref()
    }
    fn set_base_iri(&mut self, iri: Option<Iri>) {
        self.base_iri = iri;
    }
    fn resolve_iri_ref(&self, toresolve: IriRefBuf) -> Result<Iri, ProfileError> {
        if let Some(ref base_iri) = self.base_iri {
            Ok(toresolve.into_resolved(base_iri))
        } else {
            Err(ProfileError::InvalidTerm(toresolve.to_string()))
        }
    }

    fn shrink(&self, term: Arc<Term>) -> Result<String, ProfileError> {
        let shrunk_via_termmap = self.terms.shrink(term.clone())?;
        if *term.to_iri().unwrap() != shrunk_via_termmap {
            return Ok(shrunk_via_termmap);
        }
        match self.prefixes.shrink(term.clone()) {
            Ok(s) => Ok(s),
            Err(e) => Err(e),
        }
    }
}
