use build_html::{HtmlElement, HtmlTag, Html};
use log::debug;
use super::ansi_visitor::ANSIVisitor;
use super::ansi_color::ANSIColor;
use std::error::Error;
use std::fmt::Write as _; // import without risk of name clashing
use std::io::Write;

/// This struct implements the `ANSIVisitor` trait to convert ANSI codes into
/// HTML code.
///
/// The following example converts a text containing an ANSI escape code that
/// set the background to a color, into an HTML code:
/// ```
/// use ansiconv::{ANSI2HTML, ANSIParser};
/// use std::io::Cursor;
///
/// // Create the converter instance and its output writer
/// let mut out_string = Cursor::new(Vec::<u8>::new());
/// let mut converter = ANSI2HTML::new(&mut out_string);
///
/// // Create an ANSI string to parse and the parser instance to use on it
/// let mut in_string = Cursor::new(b"\x1b[48;5;50mHello!\x1b[0m");
/// let mut parser = ANSIParser::new(&mut in_string);
///
/// // Launch the parsing
/// parser.accept(&mut converter);
///
/// // Get the result
/// let s = std::str::from_utf8(out_string.get_ref()).unwrap();
/// println!("{s}");
/// ```
pub struct ANSI2HTML<'a> {
  out_stream: &'a mut dyn Write,
  root: HtmlElement,
  has_text: bool,
  fg_default_color: ANSIColor,
  fg_color: Option<ANSIColor>,
  bg_color: Option<ANSIColor>,
  bold: bool,
  underline: bool,
}

impl<'a> ANSI2HTML<'a> {

  /// Construct a new `ANSI2HTML` instance from an object implementing the
  /// `Write` trait.
  #[must_use]
  pub fn new(out_stream: &'a mut dyn Write) -> Self {

    let fg = ANSIColor::new(0xff, 0xff, 0xff);

    Self {
      out_stream,
      root: HtmlElement::new(HtmlTag::PreformattedText),
      has_text: false,
      fg_default_color: fg,
      fg_color: None,
      bg_color: None,
      bold: false,
      underline: false,
    }
  }

  /// Set a class on the enclosing tag.
  #[allow(unused)]
  pub fn set_class(&mut self, class: &str) {
    self.root.add_attribute("class", class);
  }

  fn reset_bg(&mut self) {
    self.bg_color = None;
  }

  fn reset_fg(&mut self) {
    self.fg_color = None;
  }

  fn reset_all(&mut self) {
    self.reset_fg();
    self.reset_bg();
    self.bold = false;
    self.underline = false;
  }

  fn get_style(&self) -> Result<String, Box<dyn Error>> {

    // FG color
    let fg = self.fg_color.as_ref().unwrap_or(&self.fg_default_color); 
    let mut s = format!("color:{fg};");

    // BG color
    if let Some(ref bg) = self.bg_color {
      write!(s, "background-color:{bg};")?;
    }

    // Bold
    if self.bold {
      write!(s, "font-weight:bold;")?;
    }

    // Underline
    if self.underline {
      write!(s, "text-decoration:underline;")?;
    }

    Ok(s)
  }
}

impl ANSIVisitor for ANSI2HTML<'_> {

  fn visit_begin(&mut self) -> Result<(), Box<dyn Error>> {
    debug!("Visit begin");
    Ok(())
  }

  fn visit_end(&mut self) -> Result<(), Box<dyn Error>> {
    debug!("Visit end");
    if self.has_text {
      write!(self.out_stream, "{}", self.root.to_html_string())?;
    }
    Ok(())
  }

  fn visit_text(&mut self, s: &str) -> Result<(), Box<dyn Error>> {
    if ! s.is_empty() {
      self.has_text = true;
      let style = self.get_style()?;
      self.root.add_child(HtmlElement::new(HtmlTag::Span)
        .with_attribute("style", style).with_child(s.into()).into());
    }
    Ok(())
  }

  fn visit_reset(&mut self) -> Result<(), Box<dyn Error>> {
    debug!("Visit reset");
    self.reset_all();
    Ok(())
  }

  fn visit_reset_fg_color(&mut self) -> Result<(), Box<dyn Error>> {
    debug!("Visit reset FG color");
    self.reset_fg();
    Ok(())
  }

  fn visit_reset_bg_color(&mut self) -> Result<(), Box<dyn Error>> {
    debug!("Visit reset BG color");
    self.reset_bg();
    Ok(())
  }

  fn visit_set_bold(&mut self, bold: bool
    ) -> Result<(), Box<dyn Error>> {
    debug!("Visit set bold {bold}");
    self.bold = bold;
    Ok(())
  }

  fn visit_set_faint(&mut self, faint: bool
    ) -> Result<(), Box<dyn Error>> {
    if faint {
      self.bold = false;
    }
    Ok(())
  }

  fn visit_set_underline(&mut self, underline: bool
    ) -> Result<(), Box<dyn Error>> {
    debug!("Visit set underline {underline}");
    self.underline = underline;
    Ok(())
  }

  fn visit_set_fg_color(&mut self, color: ANSIColor
    ) -> Result<(), Box<dyn Error>> {
    debug!("Visit set FG color {}", color.to_hex());
    self.fg_color = Some(color);
    Ok(())
  }

  fn visit_set_bg_color(&mut self, color: ANSIColor
    ) -> Result<(), Box<dyn Error>> {
    debug!("Visit set BG color {}", color.to_hex());
    self.bg_color = Some(color);
    Ok(())
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::ANSIParser;
  use std::collections::HashMap;
  use std::io::Cursor;

  #[test]
  fn test_empty_output_text() -> Result<(), Box<dyn Error>> {
      for instr in [
        "", // empty
      "\x1b[1m", // Bold, no text
      "\x1b]8;;https://foo/\x1b\\", // OSC
      ] {
        let mut out_string = Cursor::new(Vec::<u8>::new());
        let mut converter = ANSI2HTML::new(&mut out_string);
        let mut in_string = Cursor::new(instr);
        let mut parser = ANSIParser::new(&mut in_string);
        parser.accept(&mut converter)?;
        let s = std::str::from_utf8(out_string.get_ref())?;
        assert!(s.is_empty(), "Output should be empty: {s}");
      }
    Ok(())
  }

  #[test]
  fn test_conversion() -> Result<(), Box<dyn Error>> {
    let mut cases = HashMap::new();
    cases.insert("ABC", // Text
      "<pre><span style=\"color:#ffffff;\">ABC</span></pre>");
    cases.insert("\x1b[38;2;10;10;10mABC", // Text
      "<pre><span style=\"color:#0a0a0a;\">ABC</span></pre>");
    cases.insert("\x1b[48;2;64;64;64mABC", // Text
      "<pre><span style=\"color:#ffffff;background-color:#404040;\">ABC</span></pre>");
    cases.insert("\x1b[1mABC", // Text
      "<pre><span style=\"color:#ffffff;font-weight:bold;\">ABC</span></pre>");
    cases.insert("\x1b[4mABC", // Text
      "<pre><span style=\"color:#ffffff;text-decoration:underline;\">ABC</span></pre>");

    for (input, expected) in cases {
      let mut out_string = Cursor::new(Vec::<u8>::new());
      let mut converter = ANSI2HTML::new(&mut out_string);
      let mut in_stream = Cursor::new(input);
      let mut parser = ANSIParser::new(&mut in_stream);
      parser.accept(&mut converter)?;
      let s = std::str::from_utf8(out_string.get_ref())?;
      assert_eq!(expected, s, "Got \"{s}\" while expecting \"{expected}\".");
    }
    Ok(())
  }

  #[test]
  fn test_conversion_with_class() -> Result<(), Box<dyn Error>> {
    let mut cases = HashMap::new();
    cases.insert("ABC", // Text
      "<pre class=\"code\"><span style=\"color:#ffffff;\">ABC</span></pre>");

    for (input, expected) in cases {
      let mut out_string = Cursor::new(Vec::<u8>::new());
      let mut converter = ANSI2HTML::new(&mut out_string);
      converter.set_class("code");
      let mut in_stream = Cursor::new(input);
      let mut parser = ANSIParser::new(&mut in_stream);
      parser.accept(&mut converter)?;
      let s = std::str::from_utf8(out_string.get_ref())?;
      assert_eq!(expected, s, "Got \"{s}\" while expecting \"{expected}\".");
    }
    Ok(())
  }
}
