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

use lazy_static::lazy_static;

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

use xml::writer::{ EventWriter, XmlEvent, Result as XmlResult };

lazy_static! {
    static ref GLOBAL_ID: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));
}

#[derive(Clone)]
struct Opts {
    allow_nested:	bool,

    namespace:		String,
    level:		u32,
    id_suite:		u32,
    is_post:		bool,
}

impl Opts {
    pub fn new_testsuite(&self, id: &str) -> Self {
	let namespace = if self.level > 0 {
	    "."
	} else {
	    ""
	}.to_string() + id;

	Self {
	    allow_nested:	self.allow_nested,
	    namespace:		self.namespace.clone() + &namespace,
	    level:		self.level + 1,
	    id_suite:		if self.allow_nested {
		0
	    } else {
		self.id_suite + 1
	    },
	    is_post:		false,
	}
    }
}

struct CounterStr {
    num_tests:		String,
    num_errors:		String,
    num_failures:	String,
    num_skipped:	String,
}

impl CounterStr {
    pub fn new(counter: &crate::util::counter::Counter) -> Self
    {
	Self {
	    num_tests:     counter.get_total().to_string(),
	    num_errors:    counter.num_panic.to_string(),
	    num_failures:  (counter.num_fail +
			    counter.num_abort).to_string(),
	    num_skipped:   (counter.num_skip +
			    counter.num_todo).to_string(),
	}
    }
}

fn time_to_string(tm: std::time::SystemTime) -> String
{
    use chrono::{ Local, DateTime };

    let t: DateTime<Local> = tm.into();

    t.to_string()
}

trait AddCounter<'a> {
    fn add_counter(self, counter: &'a CounterStr) -> Self;
}

impl <'a> AddCounter<'a> for xml::writer::events::StartElementBuilder<'a>
{
    fn add_counter(self, counter: &'a CounterStr) -> Self
    {
	self
	    .attr("tests",     &counter.num_tests)
	    .attr("errors",    &counter.num_errors)
	    .attr("failures",  &counter.num_failures)
	    .attr("skipped",   &counter.num_skipped)
    }
}

trait AsXml {
    fn as_xml<W>(&self, opts: &mut Opts, w: &mut EventWriter<W>) -> XmlResult<()>
    where
	W: std::io::Write;
}

impl crate::ReportPlan {
}

impl AsXml for crate::ReportPlan {
    fn as_xml<W>(&self, opts: &mut Opts, w: &mut EventWriter<W>) -> XmlResult<()>
    where
	W: std::io::Write
    {
	let mut sub_opts = opts.new_testsuite(&self.generic.name);

	w.write(XmlEvent::start_element("testsuite")
		.attr("id",        &opts.id_suite.to_string())
		.attr("package",   &sub_opts.namespace)
		.attr("name",      &self.generic.name)
		.attr("timestamp", &time_to_string(self.generic.ctime))
		.add_counter(&CounterStr::new(&self.counter))
		.attr("time",      &(self.generic.get_duration()).to_string())
	)?;

	for item in &self.elems {
	    item.as_xml(&mut sub_opts, w)?;
	}

	w.write(XmlEvent::end_element())?;

	if !opts.allow_nested {
	    sub_opts.is_post = true;

	    for item in &self.elems {
		item.as_xml(&mut sub_opts, w)?;
	    }

	    opts.id_suite = sub_opts.id_suite;
	}

	Ok(())
    }
}

impl AsXml for crate::ReportCase {
    fn as_xml<W>(&self, _opts: &mut Opts, w: &mut EventWriter<W>) -> XmlResult<()>
    where
	W: std::io::Write
    {
	w.write(XmlEvent::start_element("testcase")
		.attr("name", &self.generic.name)
		.attr("time", &(self.generic.get_duration()).to_string()))?;

	if !self.status.is_ok() {
	    let e_type = match self.status {
		TestStatus::Panic => "error",
		TestStatus::Fail |
		TestStatus::Abort => "failure",
		_ => panic!("unexpected status"),
	    };

	    w.write(XmlEvent::start_element(e_type)
		    .attr("message", self.reason.as_deref()
			  .unwrap_or("unknown error"))
		    .attr("type", status_to_type(self.status)))?;

	    w.write(XmlEvent::end_element())?;

	    if self.val_expect.is_some() || self.val_got.is_some() {
		// proprietary extension
		w.write(XmlEvent::start_element("values"))?;

		if let Some(s) = &self.val_expect {
		    w.write(XmlEvent::start_element("expected"))?;
		    w.write(XmlEvent::characters(&s))?;
		    w.write(XmlEvent::end_element())?;
		}

		if let Some(s) = &self.val_got {
		    w.write(XmlEvent::start_element("got"))?;
		    w.write(XmlEvent::characters(&s))?;
		    w.write(XmlEvent::end_element())?;
		}

		w.write(XmlEvent::end_element())?;
	    }
	}

	if let Some(s) = self.read_stdout().unwrap() {
	    w.write(XmlEvent::start_element("system-out"))?;
	    w.write(XmlEvent::cdata(&s))?;
	    w.write(XmlEvent::end_element())?;
	}

	if let Some(s) = self.read_stderr().unwrap() {
	    w.write(XmlEvent::start_element("system-err"))?;
	    w.write(XmlEvent::cdata(&s))?;
	    w.write(XmlEvent::end_element())?;
	}

	w.write(XmlEvent::end_element())
    }
}

impl AsXml for ReportItem {
    fn as_xml<W>(&self, opts: &mut Opts, w: &mut EventWriter<W>) -> XmlResult<()>
    where
	W: std::io::Write
    {
	match self {
	    Self::Plan(p) if opts.allow_nested || opts.is_post => {
		let res = p.as_xml(opts, w);

		if opts.allow_nested {
		    opts.id_suite += 1;
		}

		res
	    },
	    Self::Case(c) if !opts.is_post => c.as_xml(opts, w),
	    Self::Comment(c) if !opts.is_post => w.write(XmlEvent::comment(c)),

	    _ => Ok(()),
	}
    }
}

fn status_to_type(status: TestStatus) -> &'static str
{
    match status {
	TestStatus::Init =>	"init",
	TestStatus::Skip =>	"skip",
	TestStatus::Todo =>	"todo",
	TestStatus::Pass =>	"pass",
	TestStatus::Degraded =>	"degraded",
	TestStatus::Fail =>	"fail",
	TestStatus::Panic =>	"panic",
	TestStatus::Abort =>	"abort",
    }
}

pub(crate) struct JUnitFormatter<W>
where
    W: std::io::Write
{
    writer:		RefCell<xml::writer::EventWriter<W>>,
    allow_nested:	bool,
    needs_header:	Cell<bool>,
    needs_footer:	Cell<bool>,
}

impl <W> JUnitFormatter<W>
where
    W: std::io::Write
{
    pub fn new(w: W, allow_nested: bool) -> Self {
	Self {
	    allow_nested:	allow_nested,
	    needs_header:	Cell::new(true),
	    needs_footer:	Cell::new(false),
	    writer:		RefCell::new(xml::writer::EmitterConfig::new()
					     .line_separator("\n")
					     .perform_indent(true)
					     .normalize_empty_elements(true)
					     .create_writer(w)),
	}
    }

    fn emit_header(&self) -> Result<(), ()> {
	if self.needs_header.get() {
	    self.writer.borrow_mut().write(XmlEvent::start_element("testsuites")).unwrap();
	    self.needs_header.set(false);
	    self.needs_footer.set(true);
	}
	Ok(())
    }

    fn emit_footer(&self) -> Result<(), ()> {
	if self.needs_footer.get() {
	    self.writer.borrow_mut().write(XmlEvent::end_element()).unwrap();
	    self.needs_footer.set(false);
	}
	Ok(())
    }
}

impl <W> Drop for JUnitFormatter<W>
where
    W: std::io::Write
{
    fn drop(&mut self) {
	self.emit_footer().unwrap();
    }
}

impl <W> super::PlanFormatter for JUnitFormatter<W>
where
    W: std::io::Write
{
    fn serialize(&self, plan: &crate::ReportPlan) -> Result<(), ()>
    {
	let global_id = GLOBAL_ID.lock().unwrap();

	let mut opts = Opts {
	    allow_nested:	self.allow_nested,
	    namespace:		"".to_string(),
	    level:		0,
	    id_suite:		global_id.get(),
	    is_post:		false,
	};

	self.emit_header()?;

	plan.as_xml(&mut opts, &mut self.writer.borrow_mut()).unwrap();
	global_id.set(opts.id_suite);

	Ok(())
    }
}

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

impl <W> JUnitFormatterBuilder<W>
{
    pub fn new(allow_nested: bool) -> Self {
	Self {
	    allow_nested:	allow_nested,
	    _p:			std::marker::PhantomData
	}
    }
}

impl <W> super::PlanFormatterBuilder<W> for JUnitFormatterBuilder<W>
where
    W: std::io::Write
{
    type Target = JUnitFormatter<W>;

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