use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use ts_rs::TS;

/// A single mark applied to a text span. Extensible via `ty` string.
///
/// Built-in mark types: "bold", "italic", "underline", "code", "strike"
/// Custom marks: "color", "highlight", "link", "font-size", etc.
///
/// `attrs` carries type-specific properties AND rendering hints:
///   - `tag`: HTML tag to render as (e.g., "strong", "em", "code", "span")
///   - `style`: CSS styles (e.g., { "color": "#ff0000", "font-weight": "700" })
///   - Any custom attributes (e.g., `href` for links)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Mark {
    #[serde(rename = "type")]
    pub ty: String,
    #[serde(default, skip_serializing_if = "Map::is_empty")]
    pub attrs: Map<String, Value>,
}

impl Mark {
    pub fn new(mark_type: impl Into<String>) -> Self {
        Self {
            ty: mark_type.into(),
            attrs: Map::new(),
        }
    }

    pub fn with_attrs(mark_type: impl Into<String>, attrs: Map<String, Value>) -> Self {
        Self {
            ty: mark_type.into(),
            attrs,
        }
    }

    pub fn bold() -> Self {
        Self::new("bold")
    }

    pub fn italic() -> Self {
        Self::new("italic")
    }

    pub fn underline() -> Self {
        Self::new("underline")
    }

    pub fn code() -> Self {
        Self::new("code")
    }

    pub fn strike() -> Self {
        Self::new("strike")
    }

    /// Create a mark with a specific tag and style.
    pub fn styled(mark_type: impl Into<String>, tag: &str, style: Map<String, Value>) -> Self {
        let mut attrs = Map::new();
        attrs.insert("tag".into(), Value::String(tag.to_string()));
        if !style.is_empty() {
            attrs.insert("style".into(), Value::Object(style));
        }
        Self {
            ty: mark_type.into(),
            attrs,
        }
    }

    pub fn tag(&self) -> Option<&str> {
        self.attrs.get("tag").and_then(|v| v.as_str())
    }

    pub fn style(&self) -> Option<&Map<String, Value>> {
        self.attrs.get("style").and_then(|v| v.as_object())
    }
}

impl Eq for Mark {}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TextSpan {
    pub text: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub marks: Vec<Mark>,
}

impl TextSpan {
    pub fn plain(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            marks: Vec::new(),
        }
    }

    pub fn with_marks(text: impl Into<String>, marks: Vec<Mark>) -> Self {
        Self {
            text: text.into(),
            marks,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.text.is_empty()
    }

    /// Returns the number of UTF-16 code units.
    /// This matches JavaScript's `string.length`, keeping DOM ↔ engine offset parity.
    pub fn len(&self) -> usize {
        utf16_len(&self.text)
    }

    pub fn has_mark(&self, mark_type: &str) -> bool {
        self.marks.iter().any(|m| m.ty == mark_type)
    }
}

/// Total number of UTF-16 code units in `s`. Matches `str.length` in JS.
pub fn utf16_len(s: &str) -> usize {
    s.chars().map(char::len_utf16).sum()
}

/// Convert a UTF-16 code unit offset to a byte offset within `s`.
/// Offsets that land inside a surrogate pair clamp forward to the next valid
/// code point boundary.
/// Returns `s.len()` when `utf16_offset >= utf16_len(s)`.
pub fn utf16_to_byte(s: &str, utf16_offset: usize) -> usize {
    let mut units = 0;
    for (byte_idx, ch) in s.char_indices() {
        if units >= utf16_offset {
            return byte_idx;
        }
        units += ch.len_utf16();
    }
    s.len()
}

/// Clamp a UTF-16 code unit offset to the previous valid code point boundary.
pub fn prev_char_boundary_utf16(s: &str, utf16_offset: usize) -> usize {
    let mut units = 0;

    for ch in s.chars() {
        let next = units + ch.len_utf16();
        if utf16_offset == units {
            return units;
        }
        if utf16_offset < next {
            return units;
        }
        units = next;
    }

    units
}

/// Clamp a UTF-16 code unit offset to the next valid code point boundary.
pub fn next_char_boundary_utf16(s: &str, utf16_offset: usize) -> usize {
    let mut units = 0;

    for ch in s.chars() {
        if utf16_offset <= units {
            return units;
        }

        let next = units + ch.len_utf16();
        if utf16_offset <= next {
            return next;
        }

        units = next;
    }

    units
}

#[deprecated(note = "Offsets are UTF-16 code units now; use utf16_to_byte instead")]
pub fn char_to_byte(s: &str, char_offset: usize) -> usize {
    utf16_to_byte(s, char_offset)
}

/// Check if all spans in a range have a given mark type.
pub fn all_have_mark(spans: &[TextSpan], start: usize, end: usize, mark_type: &str) -> bool {
    let mut cumulative = 0;
    for span in spans {
        let span_start = cumulative;
        let span_end = cumulative + span.len();
        cumulative = span_end;

        if span_start < end && span_end > start && !span.has_mark(mark_type) {
            return false;
        }
    }
    true
}

/// Normalize a list of spans: merge adjacent spans with identical marks, remove empty spans.
pub fn normalize_spans(spans: &mut Vec<TextSpan>) {
    spans.retain(|s| !s.text.is_empty());

    if spans.len() <= 1 {
        return;
    }

    let mut merged = Vec::with_capacity(spans.len());
    let mut current = spans[0].clone();

    for span in spans.iter().skip(1) {
        if current.marks == span.marks {
            current.text.push_str(&span.text);
        } else {
            merged.push(current);
            current = span.clone();
        }
    }
    merged.push(current);

    *spans = merged;
}

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

    #[test]
    fn mark_creation() {
        let bold = Mark::bold();
        assert_eq!(bold.ty, "bold");
        assert!(bold.attrs.is_empty());
    }

    #[test]
    fn mark_with_style() {
        let mut style = Map::new();
        style.insert("color".into(), Value::String("#ff0000".into()));
        let mark = Mark::styled("color", "span", style);
        assert_eq!(mark.tag(), Some("span"));
        assert!(mark.style().is_some());
    }

    #[test]
    fn span_has_mark() {
        let span = TextSpan::with_marks("hello", vec![Mark::bold(), Mark::italic()]);
        assert!(span.has_mark("bold"));
        assert!(span.has_mark("italic"));
        assert!(!span.has_mark("underline"));
    }

    #[test]
    fn normalize_merges_adjacent_same_marks() {
        let mut spans = vec![TextSpan::plain("hello"), TextSpan::plain(" world")];
        normalize_spans(&mut spans);
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].text, "hello world");
    }

    #[test]
    fn normalize_removes_empty() {
        let mut spans = vec![
            TextSpan::plain(""),
            TextSpan::plain("hello"),
            TextSpan::plain(""),
        ];
        normalize_spans(&mut spans);
        assert_eq!(spans.len(), 1);
        assert_eq!(spans[0].text, "hello");
    }

    #[test]
    fn normalize_keeps_different_marks() {
        let mut spans = vec![
            TextSpan::plain("hello"),
            TextSpan::with_marks(" world", vec![Mark::bold()]),
        ];
        normalize_spans(&mut spans);
        assert_eq!(spans.len(), 2);
    }

    #[test]
    fn utf16_helpers_clamp_surrogate_pairs() {
        let text = "a😀b";

        assert_eq!(utf16_len(text), 4);
        assert_eq!(prev_char_boundary_utf16(text, 2), 1);
        assert_eq!(next_char_boundary_utf16(text, 2), 3);
        assert_eq!(utf16_to_byte(text, 3), 5);
    }

    #[test]
    fn utf16_len_various_scripts() {
        // ASCII: 1 code unit per char
        assert_eq!(utf16_len("abc"), 3);
        // Latin-1 supplement + extended: still 1 code unit each
        assert_eq!(utf16_len("café"), 4);
        // Korean: Hangul syllables are single code units
        assert_eq!(utf16_len("한글"), 2);
        // CJK ideographs: 1 code unit each
        assert_eq!(utf16_len("中文"), 2);
        // BMP symbols: 1 code unit
        assert_eq!(utf16_len("★☆"), 2);
        // Supplementary plane (emoji): 2 code units (surrogate pair)
        assert_eq!(utf16_len("😀"), 2);
        // Flag emoji: 2 regional indicators × 2 code units each = 4
        assert_eq!(utf16_len("🇰🇷"), 4);
        // ZWJ sequence: 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦
        // = 4 emoji (each 2 units) + 3 ZWJ (each 1 unit) = 11
        assert_eq!(utf16_len("👨‍👩‍👧‍👦"), 11);
        // Empty string
        assert_eq!(utf16_len(""), 0);
    }

    #[test]
    fn utf16_to_byte_boundary_clamp() {
        let text = "a😀b";
        // All valid offsets
        assert_eq!(utf16_to_byte(text, 0), 0); // before 'a'
        assert_eq!(utf16_to_byte(text, 1), 1); // before 😀
        // MID-surrogate offset (2) clamps FORWARD to the next boundary
        // past 😀, which is byte 5 (😀 is 4 bytes starting at byte 1).
        assert_eq!(utf16_to_byte(text, 2), 5);
        assert_eq!(utf16_to_byte(text, 3), 5); // after 😀
        assert_eq!(utf16_to_byte(text, 4), 6); // after 'b' = end
        // Past the end → clamp to len
        assert_eq!(utf16_to_byte(text, 100), text.len());
    }

    #[test]
    fn prev_char_boundary_respects_surrogates() {
        let text = "😀😀😀"; // 3 emoji, 6 UTF-16 units, 12 bytes
        // At a boundary (0) → stays there
        assert_eq!(prev_char_boundary_utf16(text, 0), 0);
        // Mid-surrogate (unit 3) → snap back to second emoji start (unit 2)
        assert_eq!(prev_char_boundary_utf16(text, 3), 2);
        // At end (unit 6) → stays
        assert_eq!(prev_char_boundary_utf16(text, 6), 6);
        // Past end (10) → clamps to end (6)
        assert_eq!(prev_char_boundary_utf16(text, 10), 6);
    }

    #[test]
    fn next_char_boundary_respects_surrogates() {
        let text = "😀😀😀";
        // Already at a boundary → stays
        assert_eq!(next_char_boundary_utf16(text, 0), 0);
        assert_eq!(next_char_boundary_utf16(text, 2), 2);
        // Mid-surrogate (3) → advance to next boundary (4)
        assert_eq!(next_char_boundary_utf16(text, 3), 4);
        // At end
        assert_eq!(next_char_boundary_utf16(text, 6), 6);
    }

    #[test]
    fn utf16_empty_string_handling() {
        assert_eq!(utf16_len(""), 0);
        assert_eq!(utf16_to_byte("", 0), 0);
        assert_eq!(utf16_to_byte("", 10), 0);
        assert_eq!(prev_char_boundary_utf16("", 0), 0);
        assert_eq!(next_char_boundary_utf16("", 0), 0);
    }

    #[test]
    fn utf16_mixed_scripts_mid_string() {
        // "a한😀b" → a=1, 한=1, 😀=2, b=1 = 5 units
        let text = "a한😀b";
        assert_eq!(utf16_len(text), 5);
        assert_eq!(utf16_to_byte(text, 0), 0); // before 'a'
        assert_eq!(utf16_to_byte(text, 1), 1); // before 한 (byte 1)
        assert_eq!(utf16_to_byte(text, 2), 4); // before 😀 (byte 1 + 3 of 한)
        // MID-surrogate (offset 3) clamps FORWARD past 😀 to byte 8
        assert_eq!(utf16_to_byte(text, 3), 8);
        assert_eq!(utf16_to_byte(text, 4), 8); // before 'b'
        assert_eq!(utf16_to_byte(text, 5), 9); // end
    }
}
