//! Pure text generators for the index, index-strings, and CSS assets.
//!
//! These reproduce the asset text from a caller supplied list of emoji strings.
//! They match the embedded assets exactly when given the same input the build
//! used. The regex patterns are not generated here. They come from an external
//! trie minimizer and ship as embedded data.

use std::cmp::Ordering;
use std::collections::BTreeSet;
use std::fmt::Write;

/// Extra emoji sequences added to every version.
///
/// These are included even when a version's `emoji-test.txt` does not list them.
/// The first ten are handshake and wrestler skin-tone sequences. The rest are
/// overqualified sequences a base character followed by VS16 (U+FE0F) as typed
/// through the iOS emoji picker.
///
/// Entries are never removed. Older versions keep producing the same superset.
/// The list is not deduplicated against a version's sequences, so a sequence
/// present in both appears twice before sorting.
pub const EXTRA_SEQUENCES: [&str; 41] = [
    // Handshake skin-tone sequences, officially added in Emoji 14.0.
    "\u{1F91D}\u{1F3FB}", // handshake: light skin
    "\u{1F91D}\u{1F3FC}", // handshake: medium-light skin
    "\u{1F91D}\u{1F3FD}", // handshake: medium skin
    "\u{1F91D}\u{1F3FE}", // handshake: medium-dark skin
    "\u{1F91D}\u{1F3FF}", // handshake: dark skin
    "\u{1F93C}\u{1F3FB}", // wrestlers: light skin
    "\u{1F93C}\u{1F3FC}", // wrestlers: medium-light skin
    "\u{1F93C}\u{1F3FD}", // wrestlers: medium skin
    "\u{1F93C}\u{1F3FE}", // wrestlers: medium-dark skin
    "\u{1F93C}\u{1F3FF}", // wrestlers: dark skin
    // Overqualified sequences: base char + VS16 (U+FE0F).
    "\u{231A}\u{FE0F}",  // watch
    "\u{231B}\u{FE0F}",  // hourglass
    "\u{25FE}\u{FE0F}",  // black medium small square
    "\u{2614}\u{FE0F}",  // umbrella with rain drops
    "\u{2615}\u{FE0F}",  // hot beverage
    "\u{2648}\u{FE0F}",  // Aries
    "\u{2649}\u{FE0F}",  // Taurus
    "\u{264A}\u{FE0F}",  // Gemini
    "\u{264B}\u{FE0F}",  // Cancer
    "\u{264C}\u{FE0F}",  // Leo
    "\u{264D}\u{FE0F}",  // Virgo
    "\u{264E}\u{FE0F}",  // Libra
    "\u{264F}\u{FE0F}",  // Scorpius
    "\u{2650}\u{FE0F}",  // Sagittarius
    "\u{2651}\u{FE0F}",  // Capricorn
    "\u{2652}\u{FE0F}",  // Aquarius
    "\u{2653}\u{FE0F}",  // Pisces
    "\u{267F}\u{FE0F}",  // wheelchair symbol
    "\u{26AA}\u{FE0F}",  // medium white circle
    "\u{26BD}\u{FE0F}",  // soccer ball
    "\u{26BE}\u{FE0F}",  // baseball
    "\u{26C4}\u{FE0F}",  // snowman without snow
    "\u{26F2}\u{FE0F}",  // fountain
    "\u{26F3}\u{FE0F}",  // flag in hole
    "\u{26F5}\u{FE0F}",  // sailboat
    "\u{26FA}\u{FE0F}",  // tent
    "\u{2757}\u{FE0F}",  // heavy exclamation mark symbol
    "\u{2B1B}\u{FE0F}",  // black large square
    "\u{2B1C}\u{FE0F}",  // white large square
    "\u{2B55}\u{FE0F}",  // heavy large circle
    "\u{1F004}\u{FE0F}", // mahjong tile red dragon
];

/// Compare two emoji strings for the sequence sort.
///
/// Longer strings sort first, measured by code-point count. Equal length ties
/// break by lexicographic order of UTF-16 code units, ascending.
///
/// Two details are load-bearing:
///
/// - Length is the code-point count, not the UTF-8 byte length and not the
///   UTF-16 length. An astral character counts as one.
/// - The tie break compares UTF-16 code units. JavaScript string comparison
///   works this way. An astral character's leading high surrogate
///   (U+D800..=U+DBFF) sorts below BMP characters in U+E000..=U+FFFF. Comparing
///   by Unicode scalar value would order those cases the other way.
#[must_use]
pub fn compare(a: &str, b: &str) -> Ordering {
    let a_len = a.chars().count();
    let b_len = b.chars().count();
    match b_len.cmp(&a_len) {
        Ordering::Equal => utf16_cmp(a, b),
        non_equal => non_equal,
    }
}

/// Compare two strings by UTF-16 code units, matching JavaScript `<` and `>`.
fn utf16_cmp(a: &str, b: &str) -> Ordering {
    a.encode_utf16().cmp(b.encode_utf16())
}

/// Sort emoji strings the way the sequence list is sorted.
///
/// Applies [`compare`] with a stable sort, so equal entries keep input order.
pub fn sort_sequences(sequences: &mut [String]) {
    sequences.sort_by(|a, b| compare(a, b));
}

/// Sort emoji strings by UTF-16 code units, matching JavaScript `Array.sort`
/// with no comparator.
///
/// The index and index-strings assets use this order. The sort is stable.
pub fn default_sort(sequences: &mut [String]) {
    sequences.sort_by(|a, b| utf16_cmp(a, b));
}

/// Build the index text: one line per emoji, `<U+XXXX ...> \u{2192} <emoji>`.
///
/// Each code point is rendered `U+` followed by uppercase hex, zero-padded to a
/// minimum width of four. Code points are space joined. The separator is a
/// space, U+2192 RIGHTWARDS ARROW, then a space. Lines are newline joined with
/// a single trailing newline.
#[must_use]
pub fn generate_index<S: AsRef<str>>(strings: &[S]) -> String {
    let mut buf = String::new();
    for string in strings {
        let string = string.as_ref();
        let mut first = true;
        for ch in string.chars() {
            if !first {
                buf.push(' ');
            }
            first = false;
            // Writing to a String never fails.
            let _ = write!(buf, "U+{:04X}", ch as u32);
        }
        buf.push_str(" \u{2192} ");
        buf.push_str(string);
        buf.push('\n');
    }
    buf
}

/// Build the index-strings text: one emoji per line with a trailing newline.
#[must_use]
pub fn generate_index_strings<S: AsRef<str>>(strings: &[S]) -> String {
    let mut buf = String::new();
    for string in strings {
        buf.push_str(string.as_ref());
        buf.push('\n');
    }
    buf
}

/// Build the CSS `unicode-range` value from the code points in the input.
///
/// Collects every code point that appears in any input string, deduplicates
/// them, and coalesces consecutive code points into inclusive ranges. Emits
/// `U+<HEX>` for a single code point and `U+<HEX>-<HEX>` for a range. Hex is
/// uppercase with no zero padding. Tokens are comma joined. There is no trailing
/// newline. Input order does not matter.
#[must_use]
pub fn generate_css_unicode_range<S: AsRef<str>>(strings: &[S]) -> String {
    let mut code_points: BTreeSet<u32> = BTreeSet::new();
    for string in strings {
        for ch in string.as_ref().chars() {
            code_points.insert(ch as u32);
        }
    }

    let mut buf = String::new();
    let mut iter = code_points.into_iter().peekable();
    let mut first = true;
    while let Some(start) = iter.next() {
        let mut end = start;
        while let Some(&next) = iter.peek() {
            if next == end + 1 {
                end = next;
                iter.next();
            } else {
                break;
            }
        }
        if !first {
            buf.push(',');
        }
        first = false;
        // Writing to a String never fails.
        if start == end {
            let _ = write!(buf, "U+{start:X}");
        } else {
            let _ = write!(buf, "U+{start:X}-{end:X}");
        }
    }
    buf
}
