//! Compute cryptographic checksums.

use std::{
    fs::File,
    io::Read,
    path::{Path, PathBuf},
};

use indicatif::{ProgressBar, ProgressStyle};
use sha2::{Digest, Sha256};

/// A computed checksum.
#[derive(Debug, Eq, PartialEq)]
pub enum Checksum {
    /// A SHA-256 checksum.
    Sha256(String),
}

impl std::fmt::Display for Checksum {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::Sha256(s) => s,
        };
        write!(f, "{s}")
    }
}

/// Compute a checksum.
pub struct Checksummer {
    filename: PathBuf,
}

const BUF_SIZE: usize = 1025 * 1024;

impl Checksummer {
    /// Create a new checsum computer for a file.
    pub fn new(filename: &Path) -> Self {
        Self {
            filename: filename.into(),
        }
    }

    /// Compute checksum for a file using SHA-256.
    pub fn sha256(&self) -> Result<Checksum, ChecksummerError> {
        let metadata = self
            .filename
            .metadata()
            .map_err(|err| ChecksummerError::GetMetadata(self.filename.clone(), err))?;
        let bar = ProgressBar::new(metadata.len()).with_style(
            ProgressStyle::with_template(
                "read {binary_bytes}/{binary_total_bytes} speed {binary_bytes_per_sec} {wide_bar} ETA {eta}",
            )
            .map_err(ChecksummerError::ProgressStyle)?,
        );

        let mut sha256 = Sha256::new();
        let mut file = File::open(&self.filename)
            .map_err(|err| ChecksummerError::Open(self.filename.clone(), err))?;

        let mut buf = vec![0; BUF_SIZE];
        loop {
            let mut n = file
                .read(&mut buf)
                .map_err(|err| ChecksummerError::Read(self.filename.clone(), err))?;

            if n == 0 {
                break;
            }

            sha256.update(&buf[..n]);

            while n >= u64::MAX as usize {
                bar.inc(u64::MAX);
                n -= u64::MAX as usize;
            }
            bar.inc(n as u64); // this safe as we've ensured n fits into u64.
        }

        let checksum = Checksum::Sha256(hex::encode(sha256.finalize()));
        Ok(checksum)
    }
}

/// Possible errors when computing checksums.
#[derive(Debug, thiserror::Error)]
pub enum ChecksummerError {
    /// Can't get file metadata.
    #[error("failed to get file metadata for {0}")]
    GetMetadata(PathBuf, #[source] std::io::Error),

    /// Can't create a progress bar.
    #[error("failed to create a progress bar")]
    ProgressStyle(#[source] indicatif::style::TemplateError),

    /// Can't open file for reading.
    #[error("failed to open file for reading: {0}")]
    Open(PathBuf, #[source] std::io::Error),

    /// Can't read from file.
    #[error("failed to read from file: {0}")]
    Read(PathBuf, #[source] std::io::Error),
}
