use std::{ffi::OsString, path::PathBuf};

use crate::OutputFormat;

pub struct Context {
    pub limit: Option<usize>,
    pub spec: OsString,
    pub format: OutputFormat,
    pub text: Format,
    pub long_hashes: bool,
}

pub enum Format {
    Text,
    Svg { path: PathBuf },
}
pub const PROGRESS_RANGE: std::ops::RangeInclusive<u8> = 0..=2;

pub(crate) mod function {
    use crate::repository::HexId;
    use crate::{repository::revision::list::Format, OutputFormat};
    use anyhow::{bail, Context};
    use gix::odb::store::RefreshMode;
    use gix::{hashtable::HashMap, revision::walk::Sorting, Progress};
    use layout::{
        backends::svg::SVGWriter,
        core::{base::Orientation, geometry::Point, style::StyleAttr},
        std_shapes::shapes::{Arrow, Element, ShapeKind},
    };

    pub fn list(
        mut repo: gix::Repository,
        mut progress: impl Progress,
        mut out: impl std::io::Write,
        super::Context {
            spec,
            format,
            text,
            limit,
            long_hashes,
        }: super::Context,
    ) -> anyhow::Result<()> {
        if format != OutputFormat::Human {
            bail!("Only human output is currently supported");
        }
        repo.object_cache_size_if_unset(4 * 1024 * 1024);
        repo.objects.refresh = RefreshMode::Never;

        let spec = gix::path::os_str_into_bstr(&spec)?;
        let id = repo
            .rev_parse_single(spec)
            .context("Only single revisions are currently supported")?;
        let commits = id
            .object()?
            .peel_to_kind(gix::object::Kind::Commit)
            .context("Need committish as starting point")?
            .id()
            .ancestors()
            .sorting(Sorting::ByCommitTime(Default::default()))
            .all()?;

        let mut vg = match text {
            Format::Svg { path } => (
                layout::topo::layout::VisualGraph::new(Orientation::TopToBottom),
                path,
                HashMap::default(),
            )
                .into(),
            Format::Text => None,
        };
        progress.init(None, gix::progress::count("commits"));
        progress.set_name("traverse".into());

        let start = std::time::Instant::now();
        for commit in commits {
            if gix::interrupt::is_triggered() {
                bail!("interrupted by user");
            }
            let commit = commit?;
            match vg.as_mut() {
                Some((vg, _path, map)) => {
                    let source = match map.get(&commit.id) {
                        Some(handle) => *handle,
                        None => {
                            let handle = vg.add_node(new_node(commit.id()));
                            map.insert(commit.id, handle);
                            handle
                        }
                    };

                    for parent_id in commit.parent_ids() {
                        let dest = match map.get(parent_id.as_ref()) {
                            Some(handle) => *handle,
                            None => {
                                let dest = vg.add_node(new_node(parent_id));
                                map.insert(parent_id.detach(), dest);
                                dest
                            }
                        };
                        let arrow = Arrow::simple("");
                        vg.add_edge(arrow, source, dest);
                    }
                }
                None => {
                    writeln!(
                        out,
                        "{} {} {}",
                        HexId::new(commit.id(), long_hashes),
                        commit.commit_time.expect("traversal with date"),
                        commit.parent_ids.len()
                    )?;
                }
            }
            progress.inc();
            if limit.is_some_and(|limit| limit == progress.step()) {
                break;
            }
        }

        progress.show_throughput(start);
        if let Some((mut vg, path, _)) = vg {
            let start = std::time::Instant::now();
            progress.set_name("layout graph".into());
            progress.info(format!("writing {}…", path.display()));
            let mut svg = SVGWriter::new();
            vg.do_it(false, false, false, &mut svg);
            std::fs::write(&path, svg.finalize().as_bytes())?;
            open::that(path)?;
            progress.show_throughput(start);
        }
        return Ok(());

        fn new_node(id: gix::Id<'_>) -> Element {
            let pt = Point::new(100., 30.);
            let name = id.shorten_or_id().to_string();
            let shape = ShapeKind::new_box(name.as_str());
            let style = StyleAttr::simple();
            Element::create(shape, style, Orientation::LeftToRight, pt)
        }
    }
}
