/*!
Highlight source code using the tree-sitter highlight module.

*/

use nu_ansi_term::{Color, Style};
use sdml_core::error::Error;
use std::io::Write;
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_highlight::HighlightEvent;
use tree_sitter_highlight::Highlighter;
use tree_sitter_highlight::HtmlRenderer;

// ------------------------------------------------------------------------------------------------
// Public Functions
// ------------------------------------------------------------------------------------------------

const HIGHLIGHT_NAMES: &[&str] = &[
    "boolean",
    "comment",
    "constant",
    //    "constant.builtin",
    "embedded",
    "error",
    "function.call",
    "function.definition",
    "keyword",
    "operator",
    "method.definition",
    "module",
    "module.definition",
    "number",
    "property",
    "punctuation.bracket",
    "punctuation.delimiter",
    "punctuation.separator",
    "string",
    "string.special",
    "type",
    "type.builtin",
    "type.definition",
    "variable",
    "variable.builtin",
    "variable.field",
    "variable.parameter",
];

const DEFAULT_CSS: &str = include_str!("sdml-highlight.css");

const HTML_HEADER: &str = r#"<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />

    <title>Formatted SDML</title>

    <style type="text/css" media="screen">
    body {
      font-family: "Fira Sans",sans;
    }
{css}
    </style>
  </head>

  <body>

    <header>
      <h1>Formatted SDML</h1>
    </header>

    <main>
"#;

const HTML_FOOTER: &str = r#"    </main>

    <footer>
      <p>Generated by sdml <a href="https://github.com/johnstonskj/rust-sdml">command-line tool</a>.</p>
    </footer>

  </body>
</html>
"#;

pub fn write_highlighted_as_ansi<S: AsRef<[u8]>, W: Write>(
    source: S,
    w: &mut W,
) -> Result<(), Error> {
    let (mut highlighter, config) = highlighter_init();

    let events = highlighter
        .highlight(&config, source.as_ref(), None, |_| None)
        .unwrap();

    highlight_as_ansi(&source, w, events)
}

pub fn write_highlighted_as_html<S: AsRef<[u8]>, W: Write>(
    source: S,
    w: &mut W,
    stand_alone: bool,
) -> Result<(), Error> {
    let (mut highlighter, config) = highlighter_init();

    let events = highlighter
        .highlight(&config, source.as_ref(), None, |_| None)
        .unwrap();

    highlight_as_html(&source, w, events, stand_alone)
}

// ------------------------------------------------------------------------------------------------
// Implementations
// ------------------------------------------------------------------------------------------------

// ------------------------------------------------------------------------------------------------
// Private Functions
// ------------------------------------------------------------------------------------------------

fn highlight_as_ansi<S: AsRef<[u8]>, W: Write>(
    source: S,
    w: &mut W,
    events: impl Iterator<Item = Result<HighlightEvent, tree_sitter_highlight::Error>>,
) -> Result<(), Error> {
    let styles: Vec<Style> = vec![
        Style::new().fg(Color::Fixed(58)),            // boolean
        Style::new().fg(Color::Fixed(248)).italic(),  // comment
        Style::new().fg(Color::Fixed(58)),            // constant
        Style::new().fg(Color::Fixed(248)).italic(),  // embedded
        Style::new().fg(Color::Fixed(9)).underline(), // error
        Style::new().fg(Color::Fixed(26)),            // function.call
        Style::new().fg(Color::Fixed(26)).bold(),     // function.definition
        Style::new().fg(Color::Fixed(100)),           // keyword
        Style::new().fg(Color::Fixed(239)).bold(),    // operator
        Style::new().fg(Color::Fixed(26)).bold(),     // method.definition
        Style::new().fg(Color::Fixed(19)),            // module
        Style::new().fg(Color::Fixed(19)).bold(),     // module.definition
        Style::new().fg(Color::Fixed(58)),            // number
        Style::new().fg(Color::Fixed(160)),           // property
        Style::new().fg(Color::Fixed(239)),           // punctuation.bracket
        Style::new().fg(Color::Fixed(239)),           // punctuation.delimiter
        Style::new().fg(Color::Fixed(239)),           // punctuation.separator
        Style::new().fg(Color::Fixed(70)),            // string
        Style::new().fg(Color::Fixed(92)),            // string.special
        Style::new().fg(Color::Fixed(27)),            // type
        Style::new().fg(Color::Fixed(21)),            // type.builtin
        Style::new().fg(Color::Fixed(27)).bold(),     // type.definition
        Style::new().fg(Color::Fixed(67)),            // variable
        Style::new().fg(Color::Fixed(67)),            // variable.builtin
        Style::new().fg(Color::Fixed(67)),            // variable.field
        Style::new().fg(Color::Fixed(67)),            // variable.parameter
    ];

    highlight_as_ansi_inner(source, w, &styles, events)
}

fn highlight_as_ansi_inner<S: AsRef<[u8]>, W: Write>(
    source: S,
    w: &mut W,
    styles: &[Style],
    events: impl Iterator<Item = Result<HighlightEvent, tree_sitter_highlight::Error>>,
) -> Result<(), Error> {
    let source = source.as_ref();
    let mut style_stack = vec![Style::new()];

    for event in events {
        match event.unwrap() {
            HighlightEvent::HighlightStart(highlight) => {
                style_stack.push(styles[highlight.0]);
            }
            HighlightEvent::HighlightEnd => {
                style_stack.pop();
            }
            HighlightEvent::Source { start, end } => {
                style_stack
                    .last()
                    .unwrap()
                    .paint(&source[start..end])
                    .write_to(w)?;
            }
        }
    }

    Ok(())
}

fn highlight_as_html<S: AsRef<[u8]>, W: Write>(
    source: S,
    w: &mut W,
    events: impl Iterator<Item = Result<HighlightEvent, tree_sitter_highlight::Error>>,
    stand_alone: bool,
) -> Result<(), Error> {
    let css_classes: Vec<String> = HIGHLIGHT_NAMES
        .iter()
        .map(|s| format!("class=\"{}\"", s.replace('.', "-")))
        .collect();

    if stand_alone {
        write!(w, "{}", HTML_HEADER.replace("{css}", DEFAULT_CSS))?;
    }

    highlight_as_html_inner(
        source,
        w,
        &css_classes,
        events,
        if stand_alone { "      " } else { "" },
    )?;

    if stand_alone {
        write!(w, "{}", HTML_FOOTER)?;
    }

    Ok(())
}

fn highlight_as_html_inner<S: AsRef<[u8]>, W: Write>(
    source: S,
    w: &mut W,
    css_classes: &[String],
    events: impl Iterator<Item = Result<HighlightEvent, tree_sitter_highlight::Error>>,
    prefix: &str,
) -> Result<(), Error> {
    let mut renderer = HtmlRenderer::new();
    renderer
        .render(events, source.as_ref(), &move |highlight| {
            css_classes[highlight.0].as_bytes()
        })
        .unwrap();

    writeln!(w, "{}<pre>", prefix)?;
    writeln!(w, "{}  <code class=\"sdml\">", prefix)?;
    for line in renderer.lines() {
        write!(w, "{}    {}", prefix, line)?;
    }
    writeln!(w, "{}  </code>", prefix)?;
    writeln!(w, "{}</pre>", prefix)?;

    Ok(())
}

fn highlighter_init() -> (Highlighter, HighlightConfiguration) {
    let highlighter = Highlighter::new();
    let mut config = HighlightConfiguration::new(
        tree_sitter_sdml::language(),
        "sdml",
        tree_sitter_sdml::HIGHLIGHTS_QUERY,
        tree_sitter_sdml::INJECTIONS_QUERY,
        tree_sitter_sdml::LOCALS_QUERY,
    )
    .unwrap();
    config.configure(HIGHLIGHT_NAMES);

    (highlighter, config)
}
