// Copyright © 2023 Kim Altintop <kim@eagain.io>
// SPDX-License-Identifier: GPL-2.0-only

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

use automerge::{
    Change,
    ChangeHash,
};
use tempfile::NamedTempFile;
use thiserror::Error;

use crate::ChangeStore;

pub const FILE_EXTENSION: &str = "am.change";

#[derive(Debug, Error)]
pub enum Error {
    #[error("invalid change")]
    Change(#[from] automerge::LoadChangeError),
    #[error(transparent)]
    Tmp(#[from] tempfile::PersistError),
    #[error(transparent)]
    Io(#[from] io::Error),
}

#[derive(Clone, Debug)]
pub struct FileStore {
    root: PathBuf,
}

impl FileStore {
    pub fn new(root: impl Into<PathBuf>) -> Self {
        Self { root: root.into() }
    }

    fn filename(&self, hash: &ChangeHash) -> PathBuf {
        let mut buf = self.root.clone();

        let hex = hex::encode(hash.0);
        let (x, y) = hex.split_at(2);
        buf.push(x);
        buf.push(y);
        buf.set_extension(FILE_EXTENSION);

        buf
    }
}

impl ChangeStore for FileStore {
    type Error = Error;

    fn write_change(&self, mut change: Change) -> Result<(), Self::Error> {
        let hash = change.hash();
        let compressed = change.bytes();

        let file = self.filename(&hash);
        let dir = file.parent().unwrap();

        std::fs::create_dir_all(dir)?;
        let mut tmp = NamedTempFile::new_in(dir)?;
        tmp.write_all(&compressed)?;
        tmp.persist(&file)?;

        Ok(())
    }

    fn get_change(&self, hash: &ChangeHash) -> Result<Option<Change>, Self::Error> {
        match File::open(self.filename(hash)) {
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e.into()),
            Ok(mut f) => {
                let mut buf = Vec::new();
                f.read_to_end(&mut buf)?;

                Ok(Change::from_bytes(buf).map(Some)?)
            },
        }
    }

    fn has_change(&self, hash: &ChangeHash) -> Result<bool, Self::Error> {
        Ok(self.filename(hash).try_exists()?)
    }
}