///|
/// A read-only markdown viewer that renders markdown source using the same
/// `MarkdownDocumentSession` pipeline as the markdown editor, but without
/// editing capabilities (no caret, selection, IME, or keyboard input).
///
/// The viewer is designed for embedding inside a `scroll_view` or similar
/// scrollable parent. Pass `visible_scroll_y`, `visible_viewport_height`, and
/// `visible_overscan` so the paint pass can cull off-screen blocks for
/// performance, matching the session editor's windowed rendering strategy.
pub fn[Msg] markdown_viewer(
  source~ : String,
  size~ : @core.Size,
  font? : @core.FontSpec = @core.FontSpec::new(),
  foreground? : @core.Color = @core.Color::black(),
  background? : @core.Brush = @core.Brush::solid(
    @core.Color::rgba(r=0.0, g=0.0, b=0.0, a=0.0),
  ),
  corner_radius? : Double = 0.0,
  on_link_click? : ((String) -> Msg)? = None,
  visible_scroll_y? : Double = 0.0,
  visible_viewport_height? : Double = 0.0,
  visible_overscan? : Double = 320.0,
  key? : String? = None,
) -> @core.View[Msg] {
  markdown_viewer_snapshot(
    source~,
    size~,
    font~,
    foreground~,
    background~,
    corner_radius~,
    on_link_click~,
    visible_scroll_y~,
    visible_viewport_height~,
    visible_overscan~,
    key~,
  )
}

///|
fn[Msg] markdown_viewer_snapshot(
  source~ : String,
  size~ : @core.Size,
  font~ : @core.FontSpec,
  foreground~ : @core.Color,
  background~ : @core.Brush,
  corner_radius~ : Double,
  on_link_click~ : ((String) -> Msg)?,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  key~ : String?,
) -> @core.View[Msg] {
  @core.View::from_node(
    MarkdownViewerNode::{
      source,
      size,
      font,
      foreground,
      background,
      corner_radius,
      visible_scroll_y,
      visible_viewport_height,
      visible_overscan,
      key,
    },
    event=(node, ctx) => {
      markdown_viewer_event(
        ctx,
        source=node.source,
        font=node.font,
        on_link_click~,
        visible_scroll_y~,
        visible_viewport_height=node.visible_viewport_height,
        visible_overscan=node.visible_overscan,
      )
    },
  )
}

///|
priv struct MarkdownViewerNode {
  source : String
  size : @core.Size
  font : @core.FontSpec
  foreground : @core.Color
  background : @core.Brush
  corner_radius : Double
  visible_scroll_y : Double
  visible_viewport_height : Double
  visible_overscan : Double
  key : String?
}

///|
impl @core.ViewNode for MarkdownViewerNode with fn identity(self) {
  @core.ViewIdentity::new(kind="MarkdownViewer", key=self.key)
}

///|
impl @core.ViewNode for MarkdownViewerNode with fn declaration(self) {
  rich_text_declaration(
    layout=rich_text_exact_channel(
      rich_text_record_key("MarkdownViewer.layout", [
        ("source", @core.DeclarationKey::string(self.source)),
        ("size", declaration_size_key(self.size)),
        ("font", declaration_font_key(self.font)),
      ]),
    ),
    paint=rich_text_exact_channel(
      rich_text_record_key("MarkdownViewer.paint", [
        ("foreground", declaration_color_key(self.foreground)),
        ("background", declaration_brush_key(self.background)),
        ("corner_radius", @core.DeclarationKey::double(self.corner_radius)),
        ("scroll_y", @core.DeclarationKey::double(self.visible_scroll_y)),
        (
          "viewport_height",
          @core.DeclarationKey::double(self.visible_viewport_height),
        ),
        ("overscan", @core.DeclarationKey::double(self.visible_overscan)),
      ]),
    ),
  )
}

///|
impl @core.ViewNode for MarkdownViewerNode with fn layout(self, ctx) {
  // The editor's block list supplies a fixed row height. In that mode layout
  // must not parse and shape the same block just to discover a height that the
  // parent immediately clamps back to `self.size.height`; paint performs the
  // cached preparation only for visible rows. A zero height keeps the general
  // viewer's intrinsic-size behavior for unconstrained callers.
  let measured_height = if self.size.height > 0.0 {
    self.size.height
  } else {
    let cache = RichTextDocumentViewCache::new()
    rich_text_document_session_view_measure_height(
      self.source,
      self.size,
      self.font,
      ctx.text_system,
      cache,
    )
  }
  let measured = @core.Size::new(width=self.size.width, height=measured_height)
  @core.ViewLayoutResult::new(size=ctx.constraints.constrain(measured))
}

///|
impl @core.ViewNode for MarkdownViewerNode with fn paint(self, ctx) {
  let cache = RichTextDocumentViewCache::new()
  rich_text_document_session_view_paint(
    ctx,
    self.source,
    self.font,
    self.foreground,
    self.background,
    self.corner_radius,
    self.visible_scroll_y,
    self.visible_viewport_height,
    self.visible_overscan,
    cache,
  )
}

///|
impl @core.ViewNode for MarkdownViewerNode with fn semantics(_self) {
  @core.ViewSemanticsInfo::new(
    composition=Boundary,
    role=ScrollView,
    label="Markdown document",
  )
}

///|
fn[Msg] markdown_viewer_event(
  ctx : @core.ViewEventContext,
  source~ : String,
  font~ : @core.FontSpec,
  on_link_click~ : ((String) -> Msg)?,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
) -> @core.ViewEventResult[Msg] {
  match on_link_click {
    Some(handler) =>
      match ctx.event {
        Pointer(event) =>
          if event.phase == Up && event.button == 0 {
            match
              rich_text_document_session_view_offset_at_point(
                source,
                event.position,
                ctx.frame,
                font,
                ctx.text_system,
                visible_scroll_y,
                visible_viewport_height,
                visible_overscan,
              ) {
              Some(offset) =>
                match markdown_editor_link_context_at_caret(source, offset) {
                  Some(context) =>
                    match
                      markdown_editor_reference_target(source, context.bounds) {
                      Some(target) =>
                        view_event_result(captured=true, messages=[
                          handler(target),
                        ])
                      None => view_event_result()
                    }
                  None => view_event_result()
                }
              None => view_event_result()
            }
          } else {
            view_event_result()
          }
        _ => view_event_result()
      }
    None => view_event_result()
  }
}