//! Chunk identifiers.
//!
//! # Example
//!
//! ```rust
//! use obnam::id::*;
//!
//! let mut factory = IdFactory::default();
//! let id1 = factory.id();
//! let id2 = factory.id();
//! assert_ne!(id1, id2);
//! ```

use std::fmt;

use base64::engine::{Engine, general_purpose::URL_SAFE_NO_PAD};
use rand::{RngExt, rng, rngs::ThreadRng};
use serde::{Deserialize, Serialize};

/// A factory for chunk IDs.
#[derive(Debug)]
pub struct IdFactory {
    rng: ThreadRng,
}

impl Default for IdFactory {
    fn default() -> Self {
        Self { rng: rng() }
    }
}

impl IdFactory {
    /// Generate a new random [`Id`].
    pub fn id(&mut self) -> Id {
        Id {
            id: self.rng.random(),
        }
    }
}

/// An identifier for a chunk.
///
/// Identifiers are random numbers up to 128 bits.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Id {
    id: u128,
}

impl TryFrom<&str> for Id {
    type Error = IdError;
    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let bytes = URL_SAFE_NO_PAD
            .decode(value)
            .map_err(|err| IdError::Decode(value.to_string(), err))?;
        let bytes = bytes
            .as_array()
            .ok_or_else(|| IdError::NotU128(value.to_string()))?;
        Ok(Self {
            id: u128::from_le_bytes(*bytes),
        })
    }
}

impl TryFrom<&String> for Id {
    type Error = IdError;
    fn try_from(value: &String) -> Result<Self, Self::Error> {
        Self::try_from(value.as_str())
    }
}

impl TryFrom<String> for Id {
    type Error = IdError;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::try_from(value.as_str())
    }
}

impl fmt::Display for Id {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let bytes = self.id.to_le_bytes();
        write!(f, "{}", URL_SAFE_NO_PAD.encode(bytes))
    }
}

/// Errors from chunk identifiers.
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum IdError {
    /// Not base64url.
    #[error("chunk identifier is not base64url encoded: {0:?}")]
    Decode(String, base64::DecodeError),

    /// Not a `u128`.
    #[error("chunk identifier is not a 128-bit integer after base64url decoding: {0:?}")]
    NotU128(String),
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn id_is_equal_to_itself() {
        let mut factory = IdFactory::default();
        let id = factory.id();
        assert_eq!(id, id);
    }

    #[test]
    fn two_ids_are_different() {
        let mut factory = IdFactory::default();
        let id1 = factory.id();
        let id2 = factory.id();
        assert_ne!(id1, id2);
    }

    #[test]
    fn id_round_trip() {
        let mut factory = IdFactory::default();
        let orig = factory.id();
        let s = orig.to_string();
        let decoded = Id::try_from(s.as_str()).unwrap();
        assert_eq!(orig, decoded);
    }

    #[test]
    fn textual_id_is_printable_ascii() {
        let mut factory = IdFactory::default();
        let id = factory.id();
        let s = id.to_string();
        assert!(s.is_ascii());
        assert!(
            s.chars()
                .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
        );
    }

    #[test]
    fn one_is_expected() {
        let id = Id { id: 1 };
        let s = id.to_string();
        assert_eq!(s, "AQAAAAAAAAAAAAAAAAAAAA");
    }
}
