use std::collections::HashSet;
use std::path::PathBuf;

use clap::Clap;
use libpijul::vertex_buffer::VertexBuffer;
use libpijul::{ChangeId, Channel, EdgeFlags, TxnT, TxnTExt, Vertex};
use log::debug;

use crate::repository::Repository;
use crate::Error;

#[derive(Clap, Debug)]
pub struct Credit {
    /// 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")]
    repo_path: Option<PathBuf>,
    /// Use this channel instead of the current channel
    #[clap(long = "channel")]
    channel: Option<String>,
    /// The file to annotate
    file: PathBuf,
}

impl Credit {
    pub fn run(self) -> Result<(), anyhow::Error> {
        let has_repo_path = self.repo_path.is_some();
        let mut repo = Repository::find_root(self.repo_path)?;
        let txn = repo.pristine.txn_begin()?;
        let channel_name = repo.config.get_current_channel(self.channel.as_ref());
        let channel = if let Some(channel) = txn.load_channel(&channel_name) {
            channel
        } else {
            return Err((Error::NoSuchChannel {
                channel: channel_name.to_string(),
            })
            .into());
        };
        if self.channel.is_some() {
            repo.config.current_channel = self.channel;
            repo.save_config()?;
        }
        let (pos, _ambiguous) = if has_repo_path {
            let root = std::fs::canonicalize(repo.path.join(&self.file))?;
            let path = root.strip_prefix(&repo.path)?.to_str().unwrap();
            txn.follow_oldest_path(&repo.changes, &channel, &path)?
        } else {
            let mut root = crate::current_dir()?;
            root.push(&self.file);
            let root = std::fs::canonicalize(&root)?;
            let path = root.strip_prefix(&repo.path)?.to_str().unwrap();
            txn.follow_oldest_path(&repo.changes, &channel, &path)?
        };
        let channel_ = channel.borrow();
        super::pager();
        txn.output_file(
            &repo.changes,
            &channel,
            pos,
            &mut Creditor::new(std::io::stdout(), &txn, &channel_),
        )?;
        Ok(())
    }
}

pub struct Creditor<'a, W: std::io::Write, T: TxnTExt> {
    w: W,
    buf: Vec<u8>,
    new_line: bool,
    changes: HashSet<ChangeId>,
    txn: &'a T,
    channel: &'a Channel<T>,
}

impl<'a, W: std::io::Write, T: TxnTExt> Creditor<'a, W, T> {
    pub fn new(w: W, txn: &'a T, channel: &'a Channel<T>) -> Self {
        Creditor {
            w,
            new_line: true,
            buf: Vec::new(),
            txn,
            channel,
            changes: HashSet::new(),
        }
    }
}

impl<'a, W: std::io::Write, T: TxnTExt> VertexBuffer for Creditor<'a, W, T> {
    fn output_line<E, C: FnOnce(&mut Vec<u8>) -> Result<(), E>>(
        &mut self,
        v: Vertex<ChangeId>,
        c: C,
    ) -> Result<(), E>
    where
        E: From<std::io::Error>,
    {
        debug!("outputting vertex {:?}", v);
        self.buf.clear();
        c(&mut self.buf)?;

        use libpijul::Base32;
        if !v.change.is_root() {
            self.changes.clear();
            for e in self
                .txn
                .iter_adjacent(self.channel, v, EdgeFlags::PARENT, EdgeFlags::all())
                .filter(|e| !e.introduced_by.is_root())
            {
                self.changes.insert(e.introduced_by);
            }
            if !self.new_line {
                writeln!(self.w)?;
            }
            writeln!(self.w)?;
            let mut is_first = true;
            for c in self.changes.drain() {
                write!(
                    self.w,
                    "{}{}",
                    if is_first { "" } else { ", " },
                    c.to_base32()
                )?;
                is_first = false;
            }
            writeln!(self.w, "\n")?;
        }
        let ends_with_newline = self.buf.ends_with(b"\n");
        if let Ok(s) = std::str::from_utf8(&self.buf[..]) {
            for l in s.lines() {
                self.w.write_all(b"> ")?;
                self.w.write_all(l.as_bytes())?;
                self.w.write_all(b"\n")?;
            }
        }
        if !self.buf.is_empty() {
            // empty "lines" (such as in the beginning of a file)
            // don't change the status of self.new_line.
            self.new_line = ends_with_newline;
        }
        Ok(())
    }

    fn output_conflict_marker(&mut self, s: &str) -> Result<(), std::io::Error> {
        if !self.new_line {
            self.w.write_all(s.as_bytes())?;
        } else {
            self.w.write_all(&s.as_bytes()[1..])?;
        }
        Ok(())
    }
}
