fork of andybalholm/pijul-export - implementation of git fast-export for Pijul
use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;

use chrono::DateTime;
use chrono::Utc;

use serde::Deserialize;

use libpijul::change::Author;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::Pristine;
use libpijul::pristine::sanakirja::Txn;
use libpijul::pristine::ChannelRef;
use libpijul::pristine::InodeMetadata;
use libpijul::working_copy::WorkingCopy;
use libpijul::working_copy::WorkingCopyRead;
use libpijul::DepsTxnT;
use libpijul::GraphTxnT;
use libpijul::Hash;
use libpijul::MutTxnT;
use libpijul::MutTxnTExt;
use libpijul::TxnT;
use libpijul::TxnTExt;

use crate::changestore::EagerChangeStore;

pub struct Repository {
    path: String,
    pristine: Pristine,
    change_store: EagerChangeStore,
    identities: HashMap<String, String>,
}

pub struct Change {
    pub hash: libpijul::Hash,
    pub state: libpijul::Merkle,
    pub message: String,
    pub description: Option<String>,
    pub timestamp: DateTime<Utc>,
    pub authors: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct NoSuchChannelError {
    channel_name: String,
}

impl fmt::Display for NoSuchChannelError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "no such channel: {}", self.channel_name)
    }
}

impl Error for NoSuchChannelError {}

#[derive(Debug, Clone)]
pub struct StateMismatch {
    got: libpijul::Merkle,
    want: libpijul::Merkle,
}

impl fmt::Display for StateMismatch {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "state mismatch: got {:?}, want {:?}",
            self.got, self.want
        )
    }
}

impl Error for StateMismatch {}

#[derive(Deserialize)]
struct IdentityKey {
    key: String,
}

#[derive(Deserialize)]
struct Identity {
    display_name: String,
    email: String,
    public_key: IdentityKey,
}

impl Repository {
    pub fn load(path: &str) -> Result<Repository, Box<dyn Error>> {
        let repo_path = Path::new(path);
        let pristine_path = repo_path.join(".pijul/pristine/db");
        let pristine = Pristine::new(pristine_path)?;
        let change_store = EagerChangeStore::from_root(repo_path);

        let mut identities: HashMap<String, String> = HashMap::new();
        Self::load_identities(&mut identities, &repo_path.join(".pijul/identities")).unwrap_or(());
        if let Ok(home) = env::var("HOME") {
            Self::load_identities(
                &mut identities,
                &Path::new(&home).join(".config/pijul/identities"),
            )
            .unwrap_or(());
        }

        Ok(Repository {
            path: path.to_string(),
            change_store,
            pristine,
            identities,
        })
    }

    fn load_identities(
        identities: &mut HashMap<String, String>,
        dir: &Path,
    ) -> Result<(), Box<dyn Error>> {
        let files = fs::read_dir(dir)?;
        for file in files {
            if let Ok(content) = fs::read_to_string(dir.join(file?.path()).join("identity.toml")) {
                let id: Identity = toml::from_str(&content)?;
                identities.insert(
                    id.public_key.key,
                    format!("{} <{}>", id.display_name, id.email),
                );
            }
        }
        Ok(())
    }

    fn load_channel(
        &mut self,
        txn: &Txn,
        channel_name: &str,
    ) -> Result<ChannelRef<Txn>, Box<dyn Error>> {
        match txn.load_channel(channel_name) {
            Ok(opt) => match opt {
                Some(c) => Ok(c),
                None => Err(Box::new(NoSuchChannelError {
                    channel_name: channel_name.to_string(),
                })),
            },
            Err(err) => Err(err.into()),
        }
    }

    fn author_string(&self, a: &Author) -> String {
        if let Some(key) = a.0.get("key") {
            return self
                .identities
                .get(key)
                .map_or_else(|| format!("{} <>", key), Into::into);
        }

        if let Some(name) = a.0.get("name") {
            return format!(
                "{} <{}>",
                name,
                a.0.get("email").map(|e| e.as_str()).unwrap_or("")
            );
        }

        if let Some(email) = a.0.get("email") {
            return format!("<{}>", email);
        }

        "<>".into()
    }

    pub fn log(&mut self, channel_name: &str) -> Result<Vec<Change>, Box<dyn Error>> {
        let txn = self.pristine.txn_begin()?;
        let channel = self.load_channel(&txn, channel_name)?;
        let log = txn.log(&*channel.read(), 0)?;

        let mut changes: Vec<Change> = Vec::new();
        for pr in log {
            let (_, (h, mrk)) = pr?;
            let hash: libpijul::Hash = h.into();
            let header = self.change_store.get_header(&h.into())?;
            let authors: Vec<String> = header
                .authors
                .iter()
                .map(|a| self.author_string(a))
                .collect();

            changes.push(Change {
                hash,
                state: mrk.into(),
                message: header.message,
                description: header.description,
                timestamp: header.timestamp,
                authors,
            });
        }

        Ok(changes)
    }

    pub fn new_sandbox(&mut self) -> Result<Sandbox, Box<dyn Error>> {
        let change_store = EagerChangeStore::from_root(Path::new(&self.path));
        let txn = self.pristine.arc_txn_begin()?;
        let channel = txn.write().open_or_create_channel("pijul-export-sandbox")?;
        Ok(Sandbox {
            change_store,
            txn,
            channel,
        })
    }
}

// A Sandbox has a temporary channel for applying changes and getting the
// contents of the affected files.
pub struct Sandbox {
    change_store: EagerChangeStore,
    txn: libpijul::pristine::ArcTxn<libpijul::pristine::sanakirja::MutTxn<()>>,
    channel: libpijul::pristine::ChannelRef<libpijul::pristine::sanakirja::MutTxn<()>>,
}

impl Sandbox {
    pub fn add_change(&mut self, change: &Change) -> Result<(), Box<dyn Error>> {
        let (_, new_state) = self.txn.write().apply_change(
            &self.change_store,
            &mut *self.channel.write(),
            &change.hash,
        )?;
        if new_state == change.state {
            Ok(())
        } else {
            Err(Box::new(StateMismatch {
                got: new_state,
                want: change.state,
            }))
        }
    }

    pub fn get_files(&mut self, change: Hash) -> Result<FileSet, Box<dyn Error>> {
        let fs = FileSet {
            operations: Arc::new(Mutex::new(Vec::new())),
        };

        let mut touched_paths = BTreeSet::new();
        let txn = self.txn.read();
        if let Some(int) = txn.get_internal(&change.into())? {
            for inode in txn.iter_rev_touched(int)? {
                let (int_, inode) = inode?;
                match int_.cmp(int) {
                    Ordering::Less => continue,
                    Ordering::Greater => break,
                    Ordering::Equal => {
                        if let Some((path, _)) = libpijul::fs::find_path(
                            &self.change_store,
                            &*txn,
                            &*self.channel.read(),
                            false,
                            *inode,
                        )? {
                            touched_paths.insert(path);
                        } else {
                            touched_paths.clear();
                            break;
                        }
                    }
                }
            }
        }
        if touched_paths.contains("") {
            touched_paths.clear();
        }
        if touched_paths.is_empty() {
            touched_paths.insert(String::from(""));
        }
        std::mem::drop(txn);

        let mut last: Option<&str> = None;
        for path in touched_paths.iter() {
            if let Some(last_path) = last {
                // If `last_path` is a prefix (in the path sense) of `path`, skip.
                if last_path.len() < path.len() {
                    let (pre_last, post_last) = path.split_at(last_path.len());
                    if pre_last == last_path && post_last.starts_with('/') {
                        continue;
                    }
                }
            }
            libpijul::output::output_repository_no_pending(
                &fs,
                &self.change_store,
                &self.txn,
                &self.channel,
                path,
                false,
                None,
                1,
                0,
            )?;
            last = Some(path);
        }

        Ok(fs)
    }
}

#[derive(Clone)]
pub struct FileWriter {
    pub name: String,
    pub content: Arc<Mutex<Vec<u8>>>,
}

impl std::io::Write for FileWriter {
    fn write(&mut self, b: &[u8]) -> Result<usize, std::io::Error> {
        match self.content.lock() {
            Ok(mut c) => c.write(b),
            Err(e) => panic!("{}", e),
        }
    }
    fn flush(&mut self) -> Result<(), std::io::Error> {
        Ok(())
    }
}

pub enum FileOp {
    Modify { fw: FileWriter },
    Delete { path: String },
    Rename { old: String, new: String },
}

#[derive(Clone)]
pub struct FileSet {
    pub operations: Arc<Mutex<Vec<FileOp>>>,
}

#[derive(Debug, Clone)]
pub enum FileSetError {
    ReadNotImplemented,
}

impl Error for FileSetError {}

impl fmt::Display for FileSetError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            FileSetError::ReadNotImplemented => write!(f, "not implemented: WorkingCopyRead"),
        }
    }
}

impl WorkingCopyRead for FileSet {
    type Error = FileSetError;

    fn file_metadata(&self, _file: &str) -> Result<InodeMetadata, Self::Error> {
        Err(FileSetError::ReadNotImplemented)
    }

    fn read_file(&self, _file: &str, _buffer: &mut Vec<u8>) -> Result<(), Self::Error> {
        Err(FileSetError::ReadNotImplemented)
    }

    fn modified_time(&self, _file: &str) -> Result<std::time::SystemTime, Self::Error> {
        Err(FileSetError::ReadNotImplemented)
    }
}

impl WorkingCopy for FileSet {
    type Writer = FileWriter;

    fn create_dir_all(&self, _file: &str) -> Result<(), Self::Error> {
        Ok(())
    }

    fn remove_path(&self, path: &str, _rec: bool) -> Result<(), Self::Error> {
        self.operations.lock().unwrap().push(FileOp::Delete {
            path: path.to_owned(),
        });
        Ok(())
    }

    fn rename(&self, old: &str, new: &str) -> Result<(), Self::Error> {
        self.operations.lock().unwrap().push(FileOp::Rename {
            old: old.to_owned(),
            new: new.to_owned(),
        });
        Ok(())
    }

    fn set_permissions(&self, _file: &str, _permissions: u16) -> Result<(), Self::Error> {
        Ok(())
    }

    fn write_file(&self, file: &str, _: libpijul::Inode) -> Result<Self::Writer, Self::Error> {
        let f = FileWriter {
            name: file.to_string(),
            content: Arc::new(Mutex::new(Vec::new())),
        };
        self.operations
            .lock()
            .unwrap()
            .push(FileOp::Modify { fw: f.clone() });
        Ok(f)
    }
}