An 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),
        }
    }
    return 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) -> FastExportStream {
        return 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() {
            match &op {
                FileOp::Modify { fw } => {
                    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.len() > 0 {
            &c.authors[0]
        } else {
            "<>"
        };
        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);
        match parent {
            Some(p) => match self.commit_marks.get(&p) {
                Some(p_mark) => println!("from :{}", p_mark),
                None => {}
            },
            None => {}
        }
        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(),
                    )
                }
            }
        }
        return 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)
    }
}