fork of andybalholm/pijul-export - implementation of git fast-export for Pijul
use std::collections::HashMap;
use std::error::Error;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;

use libpijul::Base32;
use regex::Regex;
use simple_error::SimpleError;

use crate::repo::Change;
use crate::repo::FileOp;
use crate::repo::FileSet;

fn escape_string(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '\n' => result.push_str("\\n"),
            '\\' => result.push_str("\\\\"),
            '\"' => result.push_str("\\\""),
            _ => result.push(c),
        }
    }
    result
}

pub struct FastExportStream {
    pub branch_name: String,

    max_commit_mark: i32,
    commit_marks: HashMap<libpijul::Merkle, i32>,
    mark_file: Vec<u8>,

    max_blob_mark: i32,
    blob_marks: HashMap<String, i32>,
}

impl FastExportStream {
    pub fn new(branch_name: String) -> Self {
        FastExportStream {
            branch_name,

            max_commit_mark: 0,
            commit_marks: HashMap::new(),
            mark_file: Vec::new(),

            max_blob_mark: 1000000000,
            blob_marks: HashMap::new(),
        }
    }

    pub fn write_commit(&mut self, c: &Change, parent: Option<libpijul::Merkle>, files: &FileSet) {
        let mut file_marks = HashMap::new();
        for op in &*files.operations.lock().unwrap() {
            if let FileOp::Modify { fw } = &op {
                let ct = fw.content.lock().unwrap();
                let content: &[u8] = &ct;
                let h = sha256::digest(content);
                let mut new_blob = false;
                let blob_mark = *self.blob_marks.entry(h).or_insert_with(|| {
                    new_blob = true;
                    self.max_blob_mark += 1;
                    self.max_blob_mark
                });
                file_marks.insert(fw.name.clone(), blob_mark);
                if new_blob {
                    println!("blob");
                    println!("mark :{}", blob_mark);
                    println!("data {}", content.len());
                    std::io::stdout().write_all(content).unwrap();
                    println!();
                }
            }
        }

        println!("commit refs/heads/{}", self.branch_name);
        let mark = self.commit_marks.entry(c.state).or_insert_with(|| {
            self.max_commit_mark += 1;
            writeln!(
                &mut self.mark_file,
                ":{} {}",
                self.max_commit_mark,
                c.state.to_base32()
            )
            .unwrap();
            self.max_commit_mark
        });
        println!("mark :{}", mark);
        let committer = if c.authors.is_empty() {
            "<>"
        } else {
            &c.authors[0]
        };
        println!("committer {} {} +0000", committer, c.timestamp.timestamp());
        let message = match &c.description {
            Some(description) => format!("{}\n\n{}", c.message, description),
            None => c.message.to_string(),
        };
        println!("data {}", message.len());
        println!("{}", message);
        if let Some(p) = parent {
            if let Some(p_mark) = self.commit_marks.get(&p) {
                println!("from :{}", p_mark);
            }
        }
        for op in &*files.operations.lock().unwrap() {
            match &op {
                FileOp::Modify { fw } => println!(
                    "M 644 :{} \"{}\"",
                    file_marks.get(&fw.name).unwrap(),
                    escape_string(&fw.name)
                ),
                FileOp::Delete { path } => println!("D \"{}\"", escape_string(path)),
                FileOp::Rename { old, new } => {
                    println!("R \"{}\" \"{}\"", escape_string(old), escape_string(new))
                }
            };
        }
        println!();
    }

    fn parse_line(re: &Regex, line: &str) -> Option<(i32, libpijul::Merkle)> {
        let caps = re.captures(line)?;
        let mark: i32 = caps[1].parse().ok()?;
        let state = libpijul::Merkle::from_base32(caps[2].as_bytes())?;
        Some((mark, state))
    }

    pub fn load_marks(&mut self, filename: &str) -> Result<(), Box<dyn Error>> {
        self.mark_file = std::fs::read(filename)?;
        let re = Regex::new(r"^:(\d+) (\w+)$")?;
        let mark_file_content: &[u8] = &self.mark_file;
        for line in BufReader::new(mark_file_content).lines() {
            let line = line?.to_string();
            match Self::parse_line(&re, &line) {
                Some((mark, state)) => {
                    self.commit_marks.insert(state, mark);
                    if mark > self.max_commit_mark {
                        self.max_commit_mark = mark;
                    }
                }
                None => {
                    return Err(
                        SimpleError::new(format!("syntax error in mark file: {}", line)).into(),
                    )
                }
            }
        }
        Ok(())
    }

    pub fn save_marks(&self, filename: &str) -> Result<(), Box<dyn Error>> {
        std::fs::write(filename, &self.mark_file)?;
        Ok(())
    }

    pub fn already_exported(&self, state: &libpijul::Merkle) -> bool {
        self.commit_marks.contains_key(state)
    }
}