// Copyright © 2021 The Radicle Link Contributors

use std::{collections::BTreeMap, error::Error};

use fmt::RefString;
use oid::Oid;

use crate::change::EntryId;
use crate::{ObjectId, TypeName};

/// The [`Reference`]s that refer to the commits that make up a
/// [`crate::CollaborativeObject`].
#[derive(Clone, Debug, Default)]
pub struct Objects(Vec<Reference>);

impl Objects {
    pub fn new(reference: Reference) -> Self {
        Self(vec![reference])
    }

    pub fn push(&mut self, reference: Reference) {
        self.0.push(reference)
    }

    /// Return an iterator over the `local` and `remotes` of the given
    /// [`Objects`].
    pub fn iter(&self) -> impl Iterator<Item = &Reference> {
        self.0.iter()
    }
}

impl From<Vec<Reference>> for Objects {
    fn from(refs: Vec<Reference>) -> Self {
        Objects(refs)
    }
}

/// A [`Reference`] that must directly point to the [`Commit`] for a
/// [`crate::CollaborativeObject`].
#[derive(Clone, Debug)]
pub struct Reference {
    /// The `name` of the reference.
    pub name: RefString,
    /// The [`Commit`] that this reference points to.
    pub target: Commit,
}

impl std::fmt::Display for Reference {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} @ {}", self.name, self.target.id)
    }
}

/// A [`Commit`] that holds the data for a given [`crate::CollaborativeObject`].
#[derive(Clone, Debug)]
pub struct Commit {
    /// The content identifier of the commit.
    pub id: Oid,
}

pub trait Storage {
    type ObjectsError: Error + Send + Sync + 'static;
    type TypesError: Error + Send + Sync + 'static;
    type UpdateError: Error + Send + Sync + 'static;
    type RemoveError: Error + Send + Sync + 'static;

    type Namespace;

    /// Get all references which point to a head of the change graph for a
    /// particular object
    fn objects(
        &self,
        typename: &TypeName,
        object_id: &ObjectId,
    ) -> Result<Objects, Self::ObjectsError>;

    /// Get all references to objects of a given type within a particular
    /// identity
    fn types(&self, typename: &TypeName) -> Result<BTreeMap<ObjectId, Objects>, Self::TypesError>;

    /// Update a ref to a particular collaborative object
    fn update(
        &self,
        namespace: &Self::Namespace,
        typename: &TypeName,
        object_id: &ObjectId,
        entry: &EntryId,
    ) -> Result<(), Self::UpdateError>;

    /// Remove a ref to a particular collaborative object
    fn remove(
        &self,
        namespace: &Self::Namespace,
        typename: &TypeName,
        object_id: &ObjectId,
    ) -> Result<(), Self::RemoveError>;
}

pub mod convert {
    use std::str;

    use fmt::RefString;
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum Error {
        #[error("the reference '{name}' does not point to a commit object")]
        NotCommit {
            name: RefString,
            #[cfg(feature = "git2")]
            #[source]
            err: git2::Error,
        },
        #[error(transparent)]
        Ref(#[from] fmt::Error),
        #[error(transparent)]
        Utf8(#[from] str::Utf8Error),
    }

    #[cfg(feature = "git2")]
    impl<'a> TryFrom<git2::Reference<'a>> for super::Reference {
        type Error = Error;

        fn try_from(value: git2::Reference<'a>) -> Result<Self, Self::Error> {
            let name = RefString::try_from(str::from_utf8(value.name_bytes())?)?;
            let target =
                super::Commit::from(value.peel_to_commit().map_err(|err| Error::NotCommit {
                    name: name.clone(),
                    err,
                })?);
            Ok(Self { name, target })
        }
    }

    #[cfg(feature = "git2")]
    impl<'a> From<git2::Commit<'a>> for super::Commit {
        fn from(commit: git2::Commit<'a>) -> Self {
            Self {
                id: commit.id().into(),
            }
        }
    }
}
