use std::cell::{ RefCell, Cell };

use serde_derive::Serialize;

use crate::TestStatus;
use crate::report::item::Item as ReportItem;

const INDENT_SIZE: u8 = 2;

#[derive(Serialize)]
struct Diagnostics<'a> {
    severity: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    reason: &'a Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    expect: &'a Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    got: &'a Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    stderr: Option<Vec<String>>,

    #[serde(skip_serializing_if = "Option::is_none")]
    stdout: Option<Vec<String>>,
}

fn status_to_str(status: TestStatus) -> &'static str {
    match status {
	TestStatus::Pass => "ok",
	TestStatus::Degraded => "ok",
	TestStatus::Skip => "ok",
	_ => "not ok",
    }
}

fn generic_to_string(generic: &crate::report::generic::Generic,
		     status: TestStatus) -> String {
    let mut res = generic.name.clone();

    if let Some(d) = &generic.description {
	res = res + " (" + &d + ")";
    }

    if generic.comment.is_some() ||
	status == TestStatus::Skip ||
	status == TestStatus::Degraded
    {
	res += " #";

	res = match status {
	    TestStatus::Skip     => res + " SKIP",
	    TestStatus::Degraded => res + " DEGRADED",
	    _ => res,
	};

	if let Some(d) = &generic.comment {
	    res = res + " " + d;
	}
    }

    res
}

fn println_indent<W>(w: &mut W, level: u16, s: &str) -> std::io::Result<()>
where
    W: std::io::Write,
{
    if ! s.is_empty() {
	for _ in 0..(level * (INDENT_SIZE as u16)) {
	    w.write_all(b" ")?;
	}

	w.write_all(s.as_bytes())?;
    }

    w.write_all(b"\n")
}

fn out_case<W>(w: &mut W, case: &crate::ReportCase, this_num: u32, level: u16) -> std::io::Result<()>
where
    W: std::io::Write,
{
    println_indent(w, level, &format!("{} {} - {}",
				      status_to_str(case.status),
				      this_num,
				      generic_to_string(&case.generic, case.status)))?;

    let mut out = String::new();

    if !case.status.is_ok() {
	let diag = Diagnostics {
	    severity: format!("{:?}", case.status),
	    reason: &case.reason,
	    expect: &case.val_expect,
	    got: &case.val_got,
	    stderr: case.read_stderr()?.map(|v| v.split("\n").map(|v| v.to_owned()).collect()),
	    stdout: case.read_stdout()?.map(|v| v.split("\n").map(|v| v.to_owned()).collect()),
	};

	out = serde_yaml::to_string(&diag).unwrap();
    }

    if !out.is_empty() {
	println_indent(w, level, "# Diagnostics")?;

	for o in out.lines() {
	    println_indent(w, level + 1, &o)?;
	}
    }

    Ok(())
}

fn out_plan<W>(w: &mut W, plan: &crate::ReportPlan, this_num: u32, level: u16) -> std::io::Result<()>
where
    W: std::io::Write,
{
    let mut test_num = 1;

    if level > 0 {
	println_indent(w, level - 1, &format!("{} {} - {}",
					      status_to_str(plan.status),
					      this_num,
					      generic_to_string(&plan.generic, plan.status)))?;
    }

    if let Some(c) = plan.num_planned {
	println_indent(w, level, &format!("1..{}", c))?;
    }

    for e in &plan.elems {
	match e {
	    ReportItem::Plan(p) => out_plan(w, p, test_num, level + 1)?,
	    ReportItem::Case(c) => out_case(w, c, test_num, level)?,
	    ReportItem::Comment(c) => println_indent(w, level, &format!("# {}", c))?,
	}

	test_num += 1;
    }

    Ok(())
}

pub(crate) struct TapFormatter<W>
where
    W: std::io::Write
{
    writer:		RefCell<W>,
    needs_header:	Cell<bool>,
}

impl <W> TapFormatter<W>
where
	W: std::io::Write
{
    fn new(w: W) -> Self {
	Self {
	    writer:		RefCell::new(w),
	    needs_header:	Cell::new(true),
	}
    }

    fn emit_header(&self, w: &mut W) -> std::io::Result<()> {
	if self.needs_header.get() {
	    w.write_all(b"TAP version 13\n")?;
	    self.needs_header.set(false);
	}
	Ok(())
    }
}

impl <W> super::PlanFormatter for TapFormatter<W>
where
    W: std::io::Write + Clone,
{
    fn serialize(&self, plan: &crate::ReportPlan) -> Result<(), ()>
    {
	let mut w = self.writer.borrow().clone();

	self.emit_header(&mut w).unwrap();

	out_plan(&mut w, plan, 1, 0).unwrap();

	Ok(())
    }
}

pub(crate) struct TapFormatterBuilder<W>
{
    _p: std::marker::PhantomData<fn() -> W>,
}

impl <W> TapFormatterBuilder<W>
{
    pub fn new() -> Self {
	Self {
	    _p: std::marker::PhantomData
	}
    }
}

impl <W> super::PlanFormatterBuilder<W> for TapFormatterBuilder<W>
where
    W: std::io::Write + Clone
{
    type Target = TapFormatter<W>;

    fn from_writer(&self, w: W) -> Self::Target {
	Self::Target::new(w)
    }
}
