#![doc = include_str!("../README.md")]

mod parsing;
mod predefined;

use std::collections::BTreeMap;
use std::sync::Arc;

use gufo_common::types::Rational;
use gufo_common::xmp::Namespace;
use xml::name::OwnedName;

pub use crate::parsing::Value;

#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct Tag {
    namespace: Namespace,
    name: String,
}

impl Tag {
    pub fn new(namespace: Namespace, name: String) -> Self {
        Self { namespace, name }
    }

    fn from_name(name: &OwnedName) -> Option<Self> {
        if let Some(namespace_url) = &name.namespace {
            let namespace = Namespace::from_url(namespace_url);

            let name = name.local_name.to_owned();
            Some(Self::new(namespace, name))
        } else {
            None
        }
    }

    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn namespace(&self) -> &Namespace {
        &self.namespace
    }
}

impl<T: gufo_common::xmp::Field> From<T> for Tag {
    fn from(_: T) -> Self {
        Self {
            name: T::NAME.to_string(),
            namespace: T::NAMESPACE,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Xmp {
    inner: Vec<u8>,
    entries: BTreeMap<Tag, Value>,
}

#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    #[error("XmlReader: {0}")]
    XmlReader(xml::reader::Error),
    #[error("XmlWriter: {0}")]
    XmlWriter(Arc<xml::writer::Error>),
    #[error("Other: {0}")]
    Other(String),
}

impl Error {
    fn other(s: &str) -> Self {
        Self::Other(s.to_string())
    }
}

impl From<xml::reader::Error> for Error {
    fn from(value: xml::reader::Error) -> Self {
        Self::XmlReader(value)
    }
}

impl From<xml::writer::Error> for Error {
    fn from(value: xml::writer::Error) -> Self {
        Self::XmlWriter(Arc::new(value))
    }
}

impl Xmp {
    pub fn new(data: Vec<u8>) -> Result<Self, Error> {
        let entries = Self::lookup(&data)?;

        Ok(Self {
            inner: data,
            entries,
        })
    }

    pub fn update(&mut self, updates: BTreeMap<Tag, Value>) -> Result<(), Error> {
        let (entries, data) = Self::lookup_and_update(&self.inner, updates)?;
        self.entries = entries;
        self.inner = data;

        Ok(())
    }

    pub fn lookup_generic(&self, tag: impl Into<Tag>) -> Option<&str> {
        let Some(x) = self.entries.get(&tag.into()) else {
            return None;
        };

        if let Value::Generic(s) = x {
            Some(s)
        } else {
            None
        }
    }

    pub fn get_frac(&self, tag: impl Into<Tag>) -> Option<Rational<u32>> {
        let (x, y) = self.lookup_generic(tag)?.split_once('/')?;
        let x = x.parse().ok()?;
        let y = y.parse().ok()?;

        Some(Rational::new(x, y))
    }

    pub fn get_frac_f32(&self, tag: impl Into<Tag>) -> Option<f32> {
        let res = self.get_frac(tag)?.as_f32();
        if res.is_finite() { Some(res) } else { None }
    }

    pub fn get_u16(&self, tag: impl Into<Tag>) -> Option<u16> {
        self.lookup_generic(tag)?.parse().ok()
    }

    #[cfg(feature = "chrono")]
    pub fn get_date_time(&self, tag: impl Into<Tag>) -> Option<gufo_common::datetime::DateTime> {
        Some(gufo_common::datetime::DateTime::FixedOffset(
            self.lookup_generic(tag)
                .and_then(|x| chrono::DateTime::parse_from_rfc3339(x).ok())?,
        ))
    }

    pub fn entries(&self) -> &BTreeMap<Tag, Value> {
        &self.entries
    }

    pub fn into_inner(self) -> Vec<u8> {
        self.inner
    }
}
