use std::collections::BTreeMap;
use std::io::Write;
use std::path::PathBuf;

use canonical_path::CanonicalPathBuf;
use clap::Clap;
use libpijul::change::*;
use libpijul::{MutTxnT, MutTxnTExt};
use serde_derive::Serialize;

use crate::repository::*;

#[derive(Clap, Debug)]
pub struct Diff {
    /// Set the repository where this command should run. Defaults to the first ancestor of the current directory that contains a `.pijul` directory.
    #[clap(long = "repository")]
    pub repo_path: Option<PathBuf>,
    /// Output the diff in JSON format instead of the default change text format.
    #[clap(long = "json")]
    pub json: bool,
    /// Compare with this channel.
    #[clap(long = "channel")]
    pub channel: Option<String>,
    /// Add all the changes of this channel as dependencies (except changes implied transitively), instead of the minimal dependencies.
    #[clap(long = "tag")]
    pub tag: bool,
    /// Show a short version of the diff.
    #[clap(long = "short")]
    pub short: bool,
    /// Only diff those paths (files or directories). If missing, diff the entire repository.
    pub prefixes: Vec<PathBuf>,
}

impl Diff {
    pub fn run(mut self) -> Result<(), anyhow::Error> {
        let mut repo = Repository::find_root(self.repo_path.clone())?;
        let mut txn = repo.pristine.mut_txn_begin();
        let mut stdout = std::io::stdout();
        let mut channel =
            txn.open_or_create_channel(repo.config.get_current_channel(self.channel.as_ref()))?;

        let mut state = libpijul::RecordBuilder::new();
        if self.prefixes.is_empty() {
            txn.record(
                &mut state,
                libpijul::Algorithm::default(),
                &mut channel,
                &mut repo.working_copy,
                &repo.changes,
                "",
            )?
        } else {
            self.fill_relative_prefixes()?;
            repo.working_copy.record_prefixes(
                &mut txn,
                &mut channel,
                &repo.changes,
                &mut state,
                CanonicalPathBuf::canonicalize(&repo.path)?,
                &self.prefixes,
                num_cpus::get(),
            )?;
        }
        let rec = state.finish();
        if rec.actions.is_empty() {
            return Ok(());
        }
        let actions = rec
            .actions
            .into_iter()
            .map(|rec| rec.globalize(&txn))
            .collect();
        let mut change = LocalChange::make_change(
            &txn,
            &channel,
            actions,
            rec.contents,
            ChangeHeader::default(),
            Vec::new(),
        );

        let (dependencies, extra_known) = if self.tag {
            full_dependencies(&txn, &channel)
        } else {
            dependencies(&txn, &channel, change.changes.iter())
        };
        change.dependencies = dependencies;
        change.extra_known = extra_known;

        super::pager();
        if self.json {
            let mut changes = BTreeMap::new();
            for ch in change.changes.iter() {
                changes
                    .entry(ch.path())
                    .or_insert_with(Vec::new)
                    .push(Status {
                        operation: match ch {
                            Record::FileMove { .. } => "file move",
                            Record::FileDel { .. } => "file del",
                            Record::FileUndel { .. } => "file undel",
                            Record::SolveNameConflict { .. } => "solve name conflict",
                            Record::UnsolveNameConflict { .. } => "unsolve name conflict",
                            Record::FileAdd { .. } => "file add",
                            Record::Edit { .. } => "edit",
                            Record::Replacement { .. } => "replacement",
                            Record::SolveOrderConflict { .. } => "solve order conflict",
                            Record::UnsolveOrderConflict { .. } => "unsolve order conflict",
                            Record::ResurrectZombies { .. } => "resurrect zombies",
                        },
                        line: ch.line(),
                    });
            }
            serde_json::to_writer_pretty(&mut std::io::stdout(), &changes)?;
            writeln!(stdout)?;
        } else if self.short {
            let mut changes = Vec::new();
            for ch in change.changes.iter() {
                changes.push(match ch {
                    Record::FileMove { path, .. } => format!("MV {}\n", path),
                    Record::FileDel { path, .. } => format!("D  {}\n", path),
                    Record::FileUndel { path, .. } => format!("UD {}\n", path),
                    Record::FileAdd { path, .. } => format!("A  {}", path),
                    Record::SolveNameConflict { path, .. } => format!("SC {}", path),
                    Record::UnsolveNameConflict { path, .. } => format!("UC {}", path),
                    Record::Edit {
                        local: Local { path, .. },
                        ..
                    } => format!("M  {}", path),
                    Record::Replacement {
                        local: Local { path, .. },
                        ..
                    } => format!("R  {}", path),
                    Record::SolveOrderConflict {
                        local: Local { path, .. },
                        ..
                    } => format!("SC {}", path),
                    Record::UnsolveOrderConflict {
                        local: Local { path, .. },
                        ..
                    } => format!("UC {}", path),
                    Record::ResurrectZombies {
                        local: Local { path, .. },
                        ..
                    } => format!("RZ {}", path),
                });
            }
            changes.sort_unstable();
            changes.dedup();
            for ch in changes {
                println!("{}", ch);
            }
        } else {
            change.write(
                &repo.changes,
                None,
                |local: &libpijul::change::Local, _| -> String {
                    format!("{}:{}", local.path, local.line)
                },
                true,
                &mut std::io::stdout(),
            )?
        }
        Ok(())
    }

    fn fill_relative_prefixes(&mut self) -> Result<(), anyhow::Error> {
        let cwd = std::env::current_dir()?;
        for p in self.prefixes.iter_mut() {
            if p.is_relative() {
                *p = cwd.join(&p);
            }
        }
        Ok(())
    }
}

#[derive(Debug, Serialize)]
struct Status {
    operation: &'static str,
    line: Option<usize>,
}
