use chrono;
use env_logger;
use git2;
use libpijul;
use rand;
#[macro_use]
extern crate clap;
#[macro_use]
extern crate log;
use chrono::TimeZone;
use clap::{App, Arg};
use git2::*;
use libpijul::fs_representation;
use libpijul::fs_representation::{RepoPath, RepoRoot};
use libpijul::patch::PatchFlags;
use rand::Rng;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::BufRead;
use std::io::BufReader;
use std::io::BufWriter;
use std::io::Write;
use std::path::{Path, PathBuf};

fn main() {
    env_logger::init();
    let matches = App::new("Git -> Pijul converter")
        .version(crate_version!())
        .about("Converts a Git repository into a Pijul one")
        .arg(
            Arg::with_name("INPUT")
                .help("Sets the input Git repository.")
                .required(true)
                .index(1),
        )
        .get_matches();

    let repo_root = Path::new(matches.value_of("INPUT").unwrap());

    let repo = Repository::open(repo_root).unwrap();
    let commits = get_commits(&repo);

    let pijul_dir = repo_root.join(".pijul");
    let commit_log_name = pijul_dir.join("git-converted-commits");
    match File::open(&commit_log_name) {
        Ok(prev_commit_log) => {
            let reader = BufReader::new(prev_commit_log);
            for line in reader.lines() {
                println!("already processed {}", line.unwrap());
            }
        }
        Err(_) => {
            debug!("create new repo");
            std::fs::remove_dir_all(&pijul_dir).unwrap_or(());
            fs_representation::create(&repo_root, rand::thread_rng()).unwrap();
            File::create(&commit_log_name).unwrap();
        }
    };

    let commit_log = OpenOptions::new()
        .append(true)
        .open(&commit_log_name)
        .unwrap();
    let mut writer = BufWriter::new(commit_log);
    let repo_root = if let Some(r) = fs_representation::find_repo_root(&repo_root) {
        r
    } else {
        error!("no repo");
        return;
    };
    let pristine_dir = repo_root.pristine_dir();

    let mut current_branch_name = "master";

    for &(commit_id, ref branch_name, ref forks) in commits.iter() {
        let mut increase = 409600;
        if current_branch_name != branch_name {
            let _res = loop {
                match switch_branch(current_branch_name, branch_name, &repo_root, increase) {
                    Err(ref e) if e.lacks_space() => increase *= 2,
                    e => break e,
                }
            };
            current_branch_name = branch_name;
        }

        debug!("id {:?}", commit_id);
        let commit = repo.find_commit(commit_id).unwrap();
        let mut checkout = build::CheckoutBuilder::new();
        checkout.force();
        repo.checkout_tree(commit.as_object(), Some(&mut checkout))
            .unwrap();

        let files = loop {
            match file_moves(&repo, &repo_root, &commit, &pristine_dir, increase) {
                Err(ref e) if e.lacks_space() => increase *= 2,
                e => break e,
            }
        };

        let author = commit.author();
        record(
            &repo_root,
            &branch_name,
            libpijul::PatchHeader {
                authors: vec![format!(
                    "{} <{}>",
                    author.name().unwrap().to_string(),
                    author.email().unwrap().to_string()
                )],
                name: commit.message().unwrap().to_string(),
                description: None,
                timestamp: chrono::Utc.timestamp(author.when().seconds(), 0),
                flag: PatchFlags::empty(),
            },
            files.unwrap(),
        );

        let new_repo = libpijul::Repository::open(&pristine_dir, None).unwrap();

        for fork in forks {
            debug!("creating branch {:?}", fork);

            let mut txn = new_repo.mut_txn_begin(rand::thread_rng()).unwrap();
            let branch = txn.open_branch(&branch_name).unwrap();
            let new_branch = txn.fork(&branch, &fork).unwrap();
            let _res1 = txn.commit_branch(branch);
            let _res2 = txn.commit_branch(new_branch);
            let _res3 = txn.commit();
        }

        writeln!(writer, "{}", commit_id);
        writer.flush().unwrap();
    }
}

fn get_commits(
    repo: &git2::Repository,
) -> Vec<(
    git2::Oid,
    std::string::String,
    std::vec::Vec<std::string::String>,
)> {
    let mut walk = repo.revwalk().unwrap();
    walk.set_sorting(git2::Sort::TOPOLOGICAL);

    let mut commit_to_branch = HashMap::new();
    let branches = match repo.branches(Some(git2::BranchType::Local)) {
        Err(e) => {
            eprint!("{}", e.message());
            panic!(e)
        }
        Ok(r) => r,
    };
    for branch in branches {
        let branch = branch.unwrap().0;
        let commit = branch.get().target().unwrap();
        let _res = walk.push(commit);
        commit_to_branch.insert(
            commit,
            (branch.name().unwrap().unwrap().to_owned(), Vec::new()),
        );
    }

    let mut commits_reverse = Vec::new();
    for commit in walk {
        let commit = commit.unwrap();
        let (current_branch, forks) = commit_to_branch.get(&commit).unwrap().clone();

        for parent in repo.find_commit(commit).unwrap().parents() {
            match commit_to_branch.entry(parent.id()) {
                // put the parent into the same branch as the child, if it's not already on one
                Vacant(entry) => {
                    entry.insert((current_branch.clone(), Vec::new()));
                }
                // otherwise this is a fork point at the parent
                Occupied(mut entry) => {
                    let ref mut parent_forks = entry.get_mut().1;
                    parent_forks.push(current_branch.clone());
                }
            };
        }

        debug!(
            "commit {:?} in branch {:?} with forks {:?}",
            commit, current_branch, forks
        );
        commits_reverse.push((commit, current_branch, forks));
    }

    commits_reverse.reverse();
    commits_reverse
}

fn switch_branch(
    current_branch_name: &str,
    branch_name: &str,
    repo_root: &RepoRoot<PathBuf>,
    increase: u64,
) -> libpijul::Result<()> {
    debug!("switch branch {:?}", branch_name);
    let new_repo = libpijul::Repository::open(repo_root.pristine_dir(), Some(increase))?;
    let mut txn = new_repo.mut_txn_begin(rand::thread_rng()).unwrap();
    let mut branch = if let Some(branch) = txn.get_branch(branch_name) {
        branch
    } else {
        // no existing branch - should only be for when the base branch is not called "master"
        let branch = txn.open_branch(&current_branch_name)?;
        let new_branch = txn.fork(&branch, &branch_name)?;
        let _res1 = txn.commit_branch(branch);
        let _res2 = txn.commit_branch(new_branch);
        txn.open_branch(&branch_name)?
    };

    use libpijul::ToPrefixes;
    let pref = (&[][..] as &[RepoPath<&Path>]).to_prefixes(&txn, &branch);

    txn.output_repository(
        &mut branch,
        &repo_root,
        &pref,
        &libpijul::patch::UnsignedPatch::empty().leave_unsigned(),
        &BTreeSet::new(),
    )?;
    txn.commit_branch(branch)?;
    txn.commit()?;

    repo_root.set_current_branch(branch_name)?;
    Ok(())
}

fn file_moves(
    repo: &Repository,
    repo_root: &RepoRoot<PathBuf>,
    commit: &Commit<'_>,
    pristine_dir: &Path,
    increase: u64,
) -> libpijul::Result<Vec<PathBuf>> {
    debug!("file_moves, commit {:?}", commit.id());
    debug!("commit msg: {:?}", commit.message());

    let tree1 = commit.tree().unwrap();
    let new_repo = match libpijul::Repository::open(&pristine_dir, Some(increase)) {
        Ok(repo) => repo,
        Err(x) => return Err(x),
    };
    let mut txn = new_repo.mut_txn_begin(rand::thread_rng()).unwrap();

    let mut has_parents = false;
    let mut files = Vec::new();
    for parent in commit.parents() {
        has_parents = true;
        debug!("parent: {:?}", parent.id());
        let tree0 = parent.tree().unwrap();
        let mut diff = repo
            .diff_tree_to_tree(Some(&tree0), Some(&tree1), None)
            .unwrap();
        diff.find_similar(None).unwrap();

        files.extend(
            diff.deltas()
                .map(&mut |delta| file_cb(&mut txn, delta, repo_root)),
        );
    }

    if !has_parents {
        let mut diff = repo.diff_tree_to_tree(None, Some(&tree1), None).unwrap();
        diff.find_similar(None).unwrap();
        files.extend(
            diff.deltas()
                .map(&mut |delta| file_cb(&mut txn, delta, repo_root)),
        );
    }

    txn.commit().unwrap();

    Ok(files)
}

fn file_cb<'a, 'b, R: Rng>(
    txn: &'b mut libpijul::MutTxn<'_, R>,
    delta: DiffDelta<'a>,
    repo_root: &'b RepoRoot<PathBuf>,
) -> PathBuf {
    debug!("nfiles: {:?}", delta.nfiles());
    debug!("old: {:?}", delta.old_file().path());
    debug!("new: {:?}", delta.new_file().path());
    debug!("status {:?}", delta.status());
    let old = delta.old_file().path().unwrap();
    let new = delta.new_file().path().unwrap();
    match delta.status() {
        Delta::Renamed => {
            debug!("moving {:?} to {:?}", old, new);
            txn.move_file(&RepoPath(old), &RepoPath(new), false)
                .unwrap();
        }
        Delta::Added => {
            debug!("added {:?}", new);
            let m = std::fs::metadata(&repo_root.absolutize(&RepoPath(new))).unwrap();
            txn.add_file(&RepoPath(new), m.is_dir()).unwrap_or(())
        }
        Delta::Deleted => {
            debug!("deleted {:?}", new);
            txn.remove_file(&RepoPath(new)).unwrap()
        }
        _ => {}
    }
    new.to_path_buf()
}

fn record(
    repo_root: &RepoRoot<PathBuf>,
    branch_name: &str,
    header: libpijul::PatchHeader,
    files: Vec<PathBuf>,
) {
    let (patch, hash, syncs) = {
        let mut increase = 409600;
        loop {
            match patch_no_resize(repo_root, branch_name, &header, &files, increase) {
                Err(ref e) if e.lacks_space() => {
                    debug!("patch_no_resize increase: {:?}", increase);
                    increase *= 2
                }
                res => break res.unwrap(),
            }
        }
    };
    debug!("hash recorded: {:?}", hash);
    let mut increase = 409600;
    let pristine_dir = repo_root.pristine_dir();
    let res = loop {
        match record_no_resize(
            &pristine_dir,
            &repo_root,
            branch_name,
            &hash,
            &patch,
            &syncs,
            increase,
        ) {
            Err(ref e) if e.lacks_space() => increase *= 2,
            e => break e,
        }
    };
    res.unwrap();
}

fn patch_no_resize(
    repo_root: &RepoRoot<PathBuf>,
    branch_name: &str,
    header: &libpijul::PatchHeader,
    files: &Vec<PathBuf>,
    increase: u64,
) -> libpijul::Result<(
    libpijul::Patch,
    libpijul::Hash,
    BTreeSet<libpijul::InodeUpdate>,
)> {
    let new_repo = libpijul::Repository::open(repo_root.pristine_dir(), Some(increase))?;

    let mut new_txn = new_repo.mut_txn_begin(rand::thread_rng())?;
    use libpijul::*;
    let mut record = RecordState::new();

    let mut prefixes = BTreeSet::new();
    files.iter().for_each(|file| {
        let mut prefix = RepoPath(file.to_path_buf());
        loop {
            let inode = new_txn.find_inode(&prefix).unwrap();
            if let Some(parent) = new_txn.get_revtree(inode) {
                if parent.parent_inode.is_root() {
                    prefixes.insert(prefix);
                    break;
                } else if let Some(_) = new_txn.get_inodes(parent.parent_inode) {
                    prefixes.insert(prefix);
                    break;
                }
            }
            prefix = prefix.parent().unwrap().to_owned();
        }
    });

    let mut branch = new_txn.open_branch(branch_name)?;
    prefixes.iter().for_each(|prefix| {
        debug!("recording: {:?}", prefix);
        new_txn
            .record(
                libpijul::DiffAlgorithm::Myers,
                &mut record,
                &mut branch,
                &repo_root,
                &prefix,
            )
            .unwrap()
    });
    let (changes, syncs) = record.finish();
    let changes: Vec<_> = changes
        .into_iter()
        .flat_map(|x| new_txn.globalize_record(x).into_iter())
        .collect();
    let patch = new_txn.new_patch(
        &branch,
        header.authors.clone(),
        header.name.clone(),
        header.description.clone(),
        header.timestamp.clone(),
        changes,
        std::iter::empty(), // extra_deps.into_iter(),
        PatchFlags::empty(),
    );

    let patches_dir = repo_root.patches_dir();
    // std::fs::create_dir_all(&patches_dir)?;
    let hash = patch.save(&patches_dir, None)?;

    new_txn.commit_branch(branch)?;
    new_txn.commit()?;

    Ok((patch, hash, syncs))
}

fn record_no_resize(
    pristine_dir: &Path,
    repo_root: &RepoRoot<PathBuf>,
    branch_name: &str,
    hash: &libpijul::Hash,
    patch: &libpijul::Patch,
    syncs: &BTreeSet<libpijul::InodeUpdate>,
    increase: u64,
) -> libpijul::Result<Option<libpijul::Hash>> {
    use libpijul::*;
    let size_increase = increase + patch.size_upper_bound() as u64;
    let repo = match Repository::open(&pristine_dir, Some(size_increase)) {
        Ok(repo) => repo,
        Err(x) => return Err(x),
    };
    let mut txn = repo.mut_txn_begin(rand::thread_rng())?;
    // save patch
    let mut branch = txn.open_branch(branch_name)?;
    txn.apply_local_patch(&mut branch, repo_root, &hash, &patch, syncs, false)?;
    txn.commit_branch(branch)?;
    txn.commit()?;
    debug!("Recorded patch {}", hash.to_base58());
    Ok(Some(hash.clone()))
}