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

use std::path::Path;

use automerge::{
    Automerge,
    Change,
    ChangeHash,
};
use thiserror::Error;

use crate::{
    changes::{
        self,
        FileStore,
    },
    index,
    traits::{
        ChangeStore,
        ReadableStorage,
        WritableStorage,
    },
};

#[derive(Debug, Error)]
pub enum Error {
    #[error("missing change: {0}")]
    MissingChange(ChangeHash),
    #[error(transparent)]
    Automerge(#[from] automerge::AutomergeError),
    #[error(transparent)]
    Changes(#[from] changes::fs::Error),
    #[error(transparent)]
    Index(#[from] index::Error),
}

pub struct Storage {
    changes: FileStore,
    index: index::Sanakirja,
}

impl Storage {
    pub fn new(root: impl AsRef<Path>) -> Result<Self, Error> {
        let changes = FileStore::new(root.as_ref().join("changes"));
        let index = index::Sanakirja::new(Some(&root.as_ref().join("index")))?;

        Ok(Self { changes, index })
    }

    pub fn readable(&self) -> Result<Readable, Error> {
        Ok(Readable {
            changes: self.changes.clone(),
            tx: self.index.txn_begin()?,
        })
    }

    pub fn writable(&self) -> Result<Writable<()>, Error> {
        Ok(Writable {
            changes: self.changes.clone(),
            tx: self.index.mut_txn_begin()?,
        })
    }
}

pub type Readable = Tx<index::Txn>;
pub type Writable<T> = Tx<index::MutTxn<T>>;

pub struct Tx<T> {
    changes: FileStore,
    tx: T,
}

impl<T: sanakirja::Commit> Tx<T> {
    pub fn commit(self) -> Result<(), Error> {
        Ok(sanakirja::Commit::commit(self.tx).map_err(index::Error::from)?)
    }
}

impl<T> ReadableStorage for Tx<index::GenericTxn<T>>
where
    T: sanakirja::LoadPage<Error = sanakirja::Error>,
{
    type Error = Error;
    type Doc = index::Doc;

    fn load(&self, doc: &str) -> Result<Option<Automerge>, Error> {
        let stored = self.tx.get_doc(doc)?;
        if let Some(doc) = stored {
            let mut am = Automerge::new();
            for change in doc.changes(&self.tx) {
                let (_, hash) = change?;
                let change = self
                    .changes
                    .get_change(&hash)?
                    .ok_or_else(|| Error::MissingChange(hash))?;
                am.apply_changes(Some(change))?;
            }
            Ok(Some(am))
        } else {
            Ok(None)
        }
    }

    fn exists(&self, doc: &str) -> Result<bool, Self::Error> {
        Ok(self.tx.has_doc(doc)?)
    }

    fn doc(&self, doc: &str) -> Result<Option<Self::Doc>, Self::Error> {
        Ok(self.tx.get_doc(doc)?)
    }
}

impl<T> WritableStorage for Writable<T> {
    type Error = Error;

    fn store(
        &mut self,
        doc: &str,
        changes: impl IntoIterator<Item = Change>,
    ) -> Result<(), Self::Error> {
        let mut stored = self.tx.create_doc(doc)?;
        for change in changes {
            let hash = change.hash();
            self.changes.write_change(change)?;
            stored.put_change(&mut self.tx, hash)?;
        }

        Ok(())
    }
}