//! The field of DNA as markup, for the `
` the post-login page hangs it
//! behind.
//!
//! [`crate::art`] draws it; this writes the finished grid out as HTML. The
//! two renderings walk the same [`Canvas::rows`](crate::art::Canvas::rows),
//! so the browser gets the terminal's art glyph for glyph — which is a claim
//! the tests below hold rather than a hope.

use crate::art::{self, Canvas, Ink};

/// The field prerendered into the post-login page, in cells. This one does
/// not follow anything the way `about`'s frame follows a terminal: the page
/// is drawn once, by the process handling the OAuth callback, and has no idea
/// what it will be opened on. So it is cut generously — wider and taller than
/// a laptop screen holds at the type size the page asks for — and the page
/// fades it out at its own edges, which makes a screen too large to cover
/// look like art floating in space rather than a texture that ran out.
///
/// 79 rows is a whole number of half turns past the first, the property
/// [`art::snap_rows`] exists to preserve, so the bottom edge is as wide as
/// the top one. Barely visible under the fade, and free to keep.
const PAGE_COLUMNS: i32 = 240;
const PAGE_ROWS: i32 = 79;

/// The CSS class for a cell's ink, given what a caller wants done with
/// [`Ink::Muted`].
///
/// The bases always get one. Muted is the choice: in the field it is every
/// bond glyph — most of the grid — and wrapping each run of `-` and `=` in
/// an element to say "this is the colour the `
` is already painted in"
/// would multiply the page's largest asset for nothing. On the panel it is
/// the border and the labels, which have to be *dimmer* than the text beside
/// them, so there it is worth an element.
///
/// [`Ink::Default`] never gets one either way. That is what the word means:
/// whatever colour the block it landed in is set in.
fn class_for(ink: Ink, muted: &'static str) -> &'static str {
    match ink {
        Ink::A => "a",
        Ink::T => "t",
        Ink::G => "g",
        Ink::C => "c",
        Ink::Muted => muted,
        Ink::Default => "",
    }
}

/// One canvas as markup: a run of one color becomes one `` with a
/// one-letter class instead of one SGR escape. Uncolored runs are emitted
/// bare, so a field's bonds and blanks cost nothing but their own glyphs.
///
/// Written for a `
`, where the newlines and the leading blanks are the
/// layout. The glyphs are escaped even though the field draws none of the
/// three that need it — the canvas is general, and a page assembled from it
/// should not depend on what happens to be drawn on it.
fn render(canvas: &Canvas, muted: &'static str) -> String {
    let mut out = String::new();
    for row in canvas.rows() {
        let mut open = "";
        for cell in row {
            let class = class_for(cell.ink, muted);
            if class != open {
                if !open.is_empty() {
                    out.push_str("");
                }
                if !class.is_empty() {
                    out.push_str(" out.push_str("<"),
                '>' => out.push_str(">"),
                ch => out.push(ch),
            }
        }
        if !open.is_empty() {
            out.push_str("");
        }
        out.push('\n');
    }
    out
}

/// The field, with nothing punched out of it, ready for a `
`.
///
/// Prerendered here rather than drawn in the browser — it is the same art
/// `atgc about` shows, from the same code, and a page that arrives finished
/// has nothing to lay out twice or to get wrong with a font that loads late.
pub(crate) fn field() -> String {
    let mut canvas = Canvas::new(PAGE_COLUMNS, PAGE_ROWS);
    art::draw_field(&mut canvas);
    render(&canvas, "")
}

/// Any canvas as markup, with the muted ink marked up too: for a block whose
/// text is the subject rather than the texture, where a border has to read
/// as quieter than the line inside it.
///
/// Only `site` wants this, so it is compiled where that is — it is
/// behind the `www` feature, which is why this is not a doc link.
#[cfg(any(feature = "www", test))]
pub(crate) fn panel(canvas: &Canvas) -> String {
    render(canvas, "m")
}

#[cfg(test)]
mod tests {
    use super::*;

    /// CSS class per base, in [`art::STRAND`] order. The mapping lives in
    /// [`class_for`]; this is the list the tests below check it against.
    const CLASSES: [&str; 4] = ["a", "t", "g", "c"];

    /// The inverse of what `render_html` adds: drop the elements, put the
    /// three escaped characters back, and the glyphs are all that is left.
    fn strip_tags(html: &str) -> String {
        let mut out = String::new();
        let mut rest = html;
        while let Some(open) = rest.find('<') {
            out.push_str(&rest[..open]);
            let close = open + rest[open..].find('>').expect("unclosed element");
            rest = &rest[close + 1..];
        }
        out.push_str(rest);
        out.replace("<", "<")
            .replace(">", ">")
            .replace("&", "&")
    }

    // -- the field in HTML -----------------------------------------------

    /// The browser gets the same art as the terminal, from the same code:
    /// strip the markup back out of the HTML and what is left is the plain
    /// render of the same canvas, glyph for glyph. This is the claim the
    /// whole prerendering rests on — everything else about the page is CSS.
    #[test]
    fn the_html_field_is_the_terminal_field_with_markup_around_it() {
        let mut canvas = Canvas::new(PAGE_COLUMNS, PAGE_ROWS);
        art::draw_field(&mut canvas);

        let stripped = strip_tags(&render(&canvas, ""));
        assert_eq!(stripped, canvas.render(false));
        assert_eq!(stripped.lines().count(), PAGE_ROWS as usize);
        // And `field` draws that canvas rather than one of its own.
        assert_eq!(field(), render(&canvas, ""));
    }

    /// Only the bases are wrapped. The bonds are the bulk of the field's
    /// glyphs and they are left bare for the `
` to color, which is what
    /// keeps the page to tens of kilobytes instead of hundreds.
    #[test]
    fn only_the_bases_are_wrapped_in_an_element() {
        let html = field();
        let bases = html
            .chars()
            .filter(|c| matches!(c, 'A' | 'T' | 'G' | 'C'))
            .count();
        assert_eq!(html.matches("").count(), bases);
        // Every class is one of the four, and every one of the four is used.
        for class in CLASSES {
            assert!(
                html.contains(&format!("")),
                "no {class} in the field"
            );
        }
        let classes: Vec<&str> = html
            .split("", Ink::Default);
        canvas.put(3, 0, '<', Ink::of('A'));
        assert_eq!(render(&canvas, ""), "<&><\n");
        assert!(!field().contains("<"), "the field draws no markup");
    }
}