// Copyright 2026 Ling71671
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Semantic styling role of an inline text span.
pub(all) enum TextSpanKind {
  Normal
  Bold
  Code
  Link(String)
} derive(Eq, Debug)

///|
/// Segment of rich inline text.
pub struct TextSpan {
  text : String
  kind : TextSpanKind
} derive(Eq, Debug)

///|
pub fn TextSpan::normal(text : String) -> TextSpan {
  { text, kind: Normal, }
}

///|
pub fn TextSpan::bold(text : String) -> TextSpan {
  { text, kind: Bold, }
}

///|
pub fn TextSpan::code(text : String) -> TextSpan {
  { text, kind: Code, }
}

///|
pub fn TextSpan::link(text : String, url : String) -> TextSpan {
  { text, kind: Link(url), }
}

///|
/// Response from interactive rich text layout containing clicked URL.
pub struct RichTextResponse {
  clicked_url : String?
  resp : Response
} derive(Eq, Debug)

///|
priv struct RichToken {
  text : String
  kind : TextSpanKind
  is_cjk : Bool
  has_trailing_space : Bool
}

///|
fn tokenize_text_span(span : TextSpan) -> Array[RichToken] {
  let tokens : Array[RichToken] = []
  let text = span.text
  let len = text.length()
  let mut i = 0

  while i < len {
    let ch = text[i]
    let code = ch.to_int()
    if code == 32 || code == 9 || code == 10 {
      if tokens.length() > 0 {
        let last_idx = tokens.length() - 1
        let last = tokens[last_idx]
        tokens[last_idx] = { ..last, has_trailing_space: true, }
      }
      i = i + 1
    } else if is_cjk_char(code) {
      let char_end = next_char_boundary(text, i)
      let char_str = string_slice(text, i, char_end)
      tokens.push({
        text: char_str,
        kind: span.kind,
        is_cjk: true,
        has_trailing_space: false,
      })
      i = char_end
    } else {
      let word_start = i
      while i < len {
        let cur_ch = text[i]
        let cur_code = cur_ch.to_int()
        if cur_code == 32 ||
          cur_code == 9 ||
          cur_code == 10 ||
          is_cjk_char(cur_code) {
          break
        }
        i = next_char_boundary(text, i)
      }
      let word_str = string_slice(text, word_start, i)
      if word_str.length() > 0 {
        tokens.push({
          text: word_str,
          kind: span.kind,
          is_cjk: false,
          has_trailing_space: false,
        })
      }
    }
  }
  tokens
}

///|
/// Render inline rich text with word-wrapping, mixed formatting (normal, bold,
/// inline code pills, hyperlinks), and link click detection.
pub fn rich_text(
  ctx : UIContext,
  id_salt : String,
  spans : Array[TextSpan],
  wrap_width? : Double = 0.0,
) -> RichTextResponse {
  let font_sz = ctx.style.font_normal * ctx.style.scale
  let line_h = (ctx.style.control_h_sm - ctx.style.radius_xs) * ctx.style.scale
  let max_w = if wrap_width > 0.0 { wrap_width } else { ctx.available_width() }
  let space_w = ctx.measure_text(" ", font_sz).x

  // Pre-calculate line breaking and item positioning
  let mut current_x = 0.0
  let mut current_line = 0
  let mut measured_max_w = 0.0

  // (text, kind, rel_rect)
  let layout_items : Array[(String, TextSpanKind, @math.Rect)] = []

  for span in spans {
    let tokens = tokenize_text_span(span)
    for token in tokens {
      let sz = ctx.measure_text(token.text, font_sz)
      let token_w = sz.x +
        (if token.kind == Code { 6.0 * ctx.style.scale } else { 0.0 })
      let trailing_w = if token.has_trailing_space { space_w } else { 0.0 }

      let is_closing = token.is_cjk &&
        token.text.length() > 0 &&
        is_cjk_closing_punct(token.text[0].to_int())
      if current_x + token_w > max_w && current_x > 0.0 && !is_closing {
        current_line = current_line + 1
        current_x = 0.0
      }

      let rel_rect = @math.Rect::new(
        current_x,
        current_line.to_double() * line_h,
        token_w,
        line_h,
      )
      layout_items.push((token.text, token.kind, rel_rect))

      current_x = current_x + token_w + trailing_w
      if current_x > measured_max_w {
        measured_max_w = current_x
      }
    }
  }

  let total_h = (current_line + 1).to_double() * line_h
  let total_w = if wrap_width > 0.0 { wrap_width } else { measured_max_w }

  ctx.push_id(id_salt)
  let (id, container_rect, resp) = ctx.allocate_space(
    @math.Vec2::new(total_w, total_h),
  )
  ctx.pop_id()

  // Collect all link item indices in layout_items
  let link_indices : Array[Int] = []
  for i = 0; i < layout_items.length(); i = i + 1 {
    let (_, kind, _) = layout_items[i]
    match kind {
      Link(_) => link_indices.push(i)
      _ => ()
    }
  }

  // Register focusable if interactive links exist
  if link_indices.length() > 0 {
    ctx.register_focusable(id)
  }
  let is_focused = resp.has_focus() || ctx.has_focus(id)

  let mut clicked_url : String? = None
  let mut kb_link_idx : Int? = None

  if is_focused && link_indices.length() > 0 {
    let mut cur = ctx.memory().get_int_or_default(id, 0, tag="rich_text_kb")
    if cur >= link_indices.length() {
      cur = link_indices.length() - 1
    }
    if cur < 0 {
      cur = 0
    }
    if ctx.input.key_pressed(Key::ArrowRight) ||
      ctx.input.key_pressed(Key::ArrowDown) {
      cur = if cur + 1 < link_indices.length() { cur + 1 } else { cur }
      ctx.input.consume_key(Key::ArrowRight)
      ctx.input.consume_key(Key::ArrowDown)
    }
    if ctx.input.key_pressed(Key::ArrowLeft) ||
      ctx.input.key_pressed(Key::ArrowUp) {
      cur = if cur - 1 >= 0 { cur - 1 } else { cur }
      ctx.input.consume_key(Key::ArrowLeft)
      ctx.input.consume_key(Key::ArrowUp)
    }
    if ctx.input.key_pressed(Key::Home) {
      cur = 0
      ctx.input.consume_key(Key::Home)
    }
    if ctx.input.key_pressed(Key::End) {
      cur = link_indices.length() - 1
      ctx.input.consume_key(Key::End)
    }
    ctx.memory().set_int(id, cur, tag="rich_text_kb")
    kb_link_idx = Some(link_indices[cur])

    if ctx.input.key_pressed(Key::Enter) || ctx.input.key_pressed(Key::Space) {
      ctx.input.consume_key(Key::Enter)
      ctx.input.consume_key(Key::Space)
      let (_, target_kind, _) = layout_items[link_indices[cur]]
      match target_kind {
        Link(target_url) => clicked_url = Some(target_url)
        _ => ()
      }
    }
  }

  let theme = ctx.theme
  let scale = ctx.style.scale

  for i = 0; i < layout_items.length(); i = i + 1 {
    let item = layout_items[i]
    let (text, kind, rel) = item
    let abs_rect = @math.Rect::new(
      container_rect.x + rel.x,
      container_rect.y + rel.y,
      rel.w,
      rel.h,
    )
    let is_hovered = ctx.is_hovered(abs_rect)
    if is_hovered {
      match kind {
        Link(_) => ()
        _ => ctx.set_cursor_icon("text")
      }
    }

    match kind {
      Normal => {
        let text_pos = @math.Vec2::new(
          abs_rect.x,
          abs_rect.y + (line_h - font_sz) * 0.5,
        )
        ctx.painter().add_text(text_pos, text, font_sz, theme.text_body)
      }
      Bold => {
        let text_pos = @math.Vec2::new(
          abs_rect.x,
          abs_rect.y + (line_h - font_sz) * 0.5,
        )
        ctx
        .painter()
        .add_text(text_pos, text, font_sz, theme.text_strong, font_weight=700)
      }
      Code => {
        let pad_x = 3.0 * scale
        let code_bg_rect = @math.Rect::new(
          abs_rect.x,
          abs_rect.y + 2.0 * scale,
          abs_rect.w,
          line_h - 4.0 * scale,
        )
        let code_radius = ctx.style.radius_sm * scale
        ctx.painter().add_rect(code_bg_rect, theme.bg_subtle, code_radius)
        ctx
        .painter()
        .add_rect_stroke(
          code_bg_rect,
          theme.border_muted,
          1.0 * scale,
          code_radius,
        )

        let text_pos = @math.Vec2::new(
          abs_rect.x + pad_x,
          abs_rect.y + (line_h - font_sz) * 0.5,
        )
        ctx
        .painter()
        .add_text(
          text_pos,
          text,
          font_sz,
          theme.accent_primary,
          font_family="monospace",
          font_weight=500,
        )
      }
      Link(url) => {
        let is_kb_focused = kb_link_idx == Some(i)
        if is_hovered {
          ctx.set_cursor_icon("pointer")
          if ctx.input.mouse_pressed {
            clicked_url = Some(url)
          }
        }

        let link_col = if is_hovered || is_kb_focused {
          theme.accent_hover
        } else {
          theme.accent_primary
        }

        let text_pos = @math.Vec2::new(
          abs_rect.x,
          abs_rect.y + (line_h - font_sz) * 0.5,
        )
        ctx.painter().add_text(text_pos, text, font_sz, link_col)

        // Underline when hovered or keyboard focused
        if is_hovered || is_kb_focused {
          let line_y = abs_rect.y + line_h - 2.0 * scale
          ctx
          .painter()
          .add_line(
            @math.Vec2::new(abs_rect.x, line_y),
            @math.Vec2::new(abs_rect.x + abs_rect.w, line_y),
            link_col,
            1.0 * scale,
          )
        }

        // Focus ring on active keyboard-focused link
        if is_kb_focused {
          let focus_r = @math.Rect::new(
            abs_rect.x - 2.0 * scale,
            abs_rect.y + 1.0 * scale,
            abs_rect.w + 4.0 * scale,
            line_h - 2.0 * scale,
          )
          ctx
          .painter()
          .add_rect_stroke(
            focus_r,
            theme.border_focus,
            1.5 * scale,
            ctx.style.radius_xs * scale,
          )
        }
      }
    }
  }

  { clicked_url, resp, }
}

///|
/// Render a standalone text hyperlink with hover underline and pointer cursor.
pub fn hyperlink(
  ctx : UIContext,
  text : String,
  _url : String,
) -> (Bool, Response) {
  let scale = ctx.style.scale
  let font_sz = ctx.style.font_normal * scale
  let text_sz = ctx.measure_text(text, font_sz)
  let h = 20.0 * scale
  let w = text_sz.x

  let (id, rect, resp) = ctx.allocate_space(@math.Vec2::new(w, h))
  ctx.register_focusable(id)
  let is_focused = resp.has_focus() || ctx.has_focus(id)
  let is_hovered = ctx.is_hovered(rect)

  if is_hovered {
    ctx.set_cursor_icon("pointer")
  }

  let mut clicked = is_hovered && ctx.input.mouse_pressed
  if is_focused {
    if ctx.input.key_pressed(Key::Enter) || ctx.input.key_pressed(Key::Space) {
      ctx.input.consume_key(Key::Enter)
      ctx.input.consume_key(Key::Space)
      clicked = true
    }
  }

  let theme = ctx.theme
  let link_col = if is_hovered || is_focused {
    theme.accent_hover
  } else {
    theme.accent_primary
  }

  let text_pos = @math.Vec2::new(rect.x, rect.y + (h - font_sz) * 0.5)
  ctx.painter().add_text(text_pos, text, font_sz, link_col)

  if is_hovered || is_focused {
    let line_y = rect.y + h - 2.0 * scale
    ctx
    .painter()
    .add_line(
      @math.Vec2::new(rect.x, line_y),
      @math.Vec2::new(rect.x + w, line_y),
      link_col,
      1.0 * scale,
    )
  }

  if is_focused {
    ctx
    .painter()
    .add_rect_stroke(
      rect.expand(2.0 * scale),
      theme.border_focus,
      1.5 * scale,
      ctx.style.radius_xs * scale,
    )
  }

  (clicked, resp)
}