///|
fn markdown_document(
  source : String,
  base? : @core.FontSpec = @core.FontSpec::new(),
) -> RichTextDocument {
  markdown_editor_format(source, base~)
}

///|
fn markdown_input_transform(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
  inserted : String,
) -> RichTextInputTransform? {
  markdown_editor_transform_input(source, caret, selection, inserted)
}

///|
pub fn[Msg] markdown_editor(
  value~ : String,
  placeholder~ : String,
  size~ : @core.Size,
  font~ : @core.FontSpec,
  foreground~ : @core.Color,
  placeholder_color~ : @core.Color,
  background~ : @core.Brush,
  border? : @core.BorderStyle? = None,
  focused_border? : @core.BorderStyle? = None,
  cursor_color~ : @core.Color,
  corner_radius? : Double = 0.0,
  on_input? : ((String) -> Msg)? = None,
  on_selection_change? : ((Int, @core.TextRange?) -> Msg)? = None,
  on_prefix_click? : ((Int) -> Msg)? = None,
  accepts_tab_input? : Bool = true,
  key? : String? = None,
) -> @core.View[Msg] {
  rich_text_editor(
    value~,
    placeholder~,
    size~,
    format=source => markdown_document(source, base=font),
    font~,
    foreground~,
    placeholder_color~,
    background~,
    border~,
    focused_border~,
    cursor_color~,
    corner_radius~,
    on_input~,
    on_selection_change~,
    on_prefix_click~,
    input_transform=Some(markdown_input_transform),
    accepts_tab_input~,
    kind="MarkdownEditor",
    key~,
  )
}

///|
pub fn[Msg] controlled_markdown_editor(
  value~ : String,
  on_input~ : (String) -> Msg,
  placeholder~ : String,
  size~ : @core.Size,
  font~ : @core.FontSpec,
  foreground~ : @core.Color,
  placeholder_color~ : @core.Color,
  background~ : @core.Brush,
  border? : @core.BorderStyle? = None,
  focused_border? : @core.BorderStyle? = None,
  cursor_color~ : @core.Color,
  corner_radius? : Double = 0.0,
  on_selection_change? : ((Int, @core.TextRange?) -> Msg)? = None,
  on_prefix_click? : ((Int) -> Msg)? = None,
  accepts_tab_input? : Bool = true,
  key? : String? = None,
) -> @core.View[Msg] {
  markdown_editor(
    value~,
    placeholder~,
    size~,
    font~,
    foreground~,
    placeholder_color~,
    background~,
    border~,
    focused_border~,
    cursor_color~,
    corner_radius~,
    on_input=Some(on_input),
    on_selection_change~,
    on_prefix_click~,
    accepts_tab_input~,
    key~,
  )
}

///|
pub fn[Msg] controlled_markdown_session_editor(
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  on_transaction~ : (MarkdownEditTransaction) -> Msg,
  placeholder~ : String,
  size~ : @core.Size,
  font~ : @core.FontSpec,
  foreground~ : @core.Color,
  placeholder_color~ : @core.Color,
  background~ : @core.Brush,
  border? : @core.BorderStyle? = None,
  focused_border? : @core.BorderStyle? = None,
  cursor_color~ : @core.Color,
  corner_radius? : Double = 0.0,
  source_mode? : Bool = false,
  focus_mode? : Bool = false,
  base_dir? : String? = None,
  on_selection_change? : ((Int, @core.TextRange?) -> Msg)? = None,
  on_prefix_click? : ((Int) -> Msg)? = None,
  on_command_click? : ((Int) -> Msg)? = None,
  accepts_tab_input? : Bool = true,
  scroll_glide? : MarkdownScrollGlide = MarkdownScrollGlide::settled(0.0),
  on_scroll? : ((Double, Double) -> Msg)? = None,
  visible_viewport_height? : Double = 0.0,
  visible_overscan? : Double = 320.0,
  key? : String? = None,
) -> @core.View[Msg] {
  controlled_markdown_session_editor_snapshot(
    session~,
    selection~,
    on_transaction~,
    placeholder~,
    size~,
    font~,
    foreground~,
    placeholder_color~,
    background~,
    border~,
    focused_border~,
    cursor_color~,
    corner_radius~,
    source_mode~,
    focus_mode~,
    base_dir~,
    on_selection_change~,
    on_prefix_click~,
    on_command_click~,
    accepts_tab_input~,
    scroll_glide~,
    on_scroll~,
    visible_viewport_height~,
    visible_overscan~,
    key~,
  )
}

///|
fn[Msg] controlled_markdown_session_editor_snapshot(
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  on_transaction~ : (MarkdownEditTransaction) -> Msg,
  placeholder~ : String,
  size~ : @core.Size,
  font~ : @core.FontSpec,
  foreground~ : @core.Color,
  placeholder_color~ : @core.Color,
  background~ : @core.Brush,
  border~ : @core.BorderStyle?,
  focused_border~ : @core.BorderStyle?,
  cursor_color~ : @core.Color,
  corner_radius~ : Double,
  source_mode~ : Bool,
  focus_mode~ : Bool,
  base_dir~ : String?,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  on_prefix_click~ : ((Int) -> Msg)?,
  on_command_click~ : ((Int) -> Msg)?,
  accepts_tab_input~ : Bool,
  scroll_glide~ : MarkdownScrollGlide,
  on_scroll~ : ((Double, Double) -> Msg)?,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  key~ : String?,
) -> @core.View[Msg] {
  @core.View::from_node(
    MarkdownSessionEditorViewNode::{
      session,
      selection,
      placeholder,
      size,
      font,
      foreground,
      placeholder_color,
      background,
      border,
      focused_border,
      cursor_color,
      corner_radius,
      source_mode,
      focus_mode,
      base_dir,
      accepts_tab_input,
      scroll_glide,
      visible_viewport_height,
      visible_overscan,
      key,
    },
    event=(node, ctx) => {
      markdown_session_editor_event(
        ctx,
        session=node.session,
        selection=node.selection,
        on_transaction~,
        font=node.font,
        source_mode=node.source_mode,
        focus_mode=node.focus_mode,
        base_dir=node.base_dir,
        on_selection_change~,
        on_prefix_click~,
        on_command_click~,
        accepts_tab_input=node.accepts_tab_input,
        // Hit-testing and visual caret moves use the same frame-local offset
        // as paint. A retained paint may have published a measurement delta
        // before the app rebuilds its glide, so include that pending delta.
        visible_scroll_y=node.scroll_glide.display_offset(
          ctx.now_ms,
          compensation=markdown_scroll_peek_geometry_compensation(),
        ),
        on_scroll~,
        visible_viewport_height=node.visible_viewport_height,
        visible_overscan=node.visible_overscan,
      )
    },
    text_command=(node, ctx, intent, paste_text) => {
      markdown_session_editor_command(
        ctx,
        session=node.session,
        selection=node.selection,
        on_transaction~,
        on_selection_change~,
        font=node.font,
        source_mode=node.source_mode,
        focus_mode=node.focus_mode,
        base_dir=node.base_dir,
        intent,
        paste_text~,
      )
    },
    semantics_actions=node => {
      [
        @core.ViewSemanticsActionHandler::set_text((_ctx, next_source) => {
          @core.ViewSemanticsActionResult::message(
            on_transaction(
              MarkdownEditTransaction::from_sources(
                node.session.source(),
                next_source,
                caret=text_length(next_source),
                selection=None,
              ),
            ),
          )
        }),
      ]
    },
  )
}

///|
priv struct MarkdownSessionEditorViewNode {
  session : MarkdownDocumentSession
  selection : MarkdownEditorSelection
  placeholder : String
  size : @core.Size
  font : @core.FontSpec
  foreground : @core.Color
  placeholder_color : @core.Color
  background : @core.Brush
  border : @core.BorderStyle?
  focused_border : @core.BorderStyle?
  cursor_color : @core.Color
  corner_radius : Double
  source_mode : Bool
  focus_mode : Bool
  base_dir : String?
  accepts_tab_input : Bool
  /// The scroll transition to draw: evaluated at paint/event time from the
  /// runtime animation clock, never resolved into a stored offset.
  scroll_glide : MarkdownScrollGlide
  visible_viewport_height : Double
  visible_overscan : Double
  key : String?
}

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

///|
impl @core.ViewNode for MarkdownSessionEditorViewNode with fn declaration(self) {
  rich_text_declaration(
    layout=rich_text_exact_channel(declaration_size_key(self.size)),
    paint=rich_text_exact_channel(
      rich_text_record_key("MarkdownSessionEditor.paint", [
        // Identity of the painted source via the session's maintained
        // (length, digest) pair. Encoding the whole document into the key
        // here would make every keystroke pay an O(document) UTF-8 pass,
        // a fresh key buffer, and a full memcmp against the previous key.
        ("source_length", @core.DeclarationKey::int(self.session.source_length)),
        (
          "source_digest",
          @core.DeclarationKey::uint64(self.session.source_digest),
        ),
        ("selection_caret", @core.DeclarationKey::int(self.selection.caret)),
        (
          "selection_range",
          declaration_text_range_option_key(self.selection.selection),
        ),
        ("placeholder", @core.DeclarationKey::string(self.placeholder)),
        ("font", declaration_font_key(self.font)),
        ("foreground", declaration_color_key(self.foreground)),
        ("placeholder_color", declaration_color_key(self.placeholder_color)),
        ("background", declaration_brush_key(self.background)),
        ("border", declaration_border_option_key(self.border)),
        ("focused_border", declaration_border_option_key(self.focused_border)),
        ("cursor_color", declaration_color_key(self.cursor_color)),
        ("corner_radius", @core.DeclarationKey::double(self.corner_radius)),
        ("source_mode", @core.DeclarationKey::bool(self.source_mode)),
        ("focus_mode", @core.DeclarationKey::bool(self.focus_mode)),
        (
          "base_dir",
          @core.DeclarationKey::option(
            self.base_dir.map(@core.DeclarationKey::string),
          ),
        ),
        (
          "accepts_tab_input",
          @core.DeclarationKey::bool(self.accepts_tab_input),
        ),
        (
          "scroll_glide",
          @core.DeclarationKey::record(
            type_tag="moui_richtext.MarkdownScrollGlide.v2",
            fields=[
              ("from_y", @core.DeclarationKey::double(self.scroll_glide.from_y)),
              ("to_y", @core.DeclarationKey::double(self.scroll_glide.to_y)),
              (
                "start_ms",
                @core.DeclarationKey::double(self.scroll_glide.start_ms),
              ),
              ("tau_ms", @core.DeclarationKey::double(self.scroll_glide.tau_ms)),
            ],
          ),
        ),
        (
          "viewport_height",
          @core.DeclarationKey::double(self.visible_viewport_height),
        ),
        ("overscan", @core.DeclarationKey::double(self.visible_overscan)),
      ]),
    ),
    semantics=rich_text_exact_channel(
      @core.DeclarationKey::string(self.placeholder),
    ),
  )
}

///|
impl @core.ViewNode for MarkdownSessionEditorViewNode with fn layout(self, ctx) {
  @core.ViewLayoutResult::new(size=ctx.constraints.constrain(self.size))
}

///|
impl @core.ViewNode for MarkdownSessionEditorViewNode with fn paint(self, ctx) {
  markdown_scroll_publish_frame_time(ctx.now_ms)
  // The offset is sampled for THIS frame: the glide is a function of the
  // animation clock, so the position shown is always correct for the frame
  // being drawn (no timer tick grid to beat against the display refresh).
  // A retained node can paint again before the app consumes the measurement
  // message. Include that pending delta in the frame-local offset so a
  // geometry commit cannot move content backward for one frame.
  let pending_geometry_compensation = markdown_scroll_peek_geometry_compensation()
  let (plan, geometry_compensation) = markdown_session_editor_paint(
    ctx,
    session=self.session,
    selection=self.selection,
    placeholder=self.placeholder,
    font=self.font,
    foreground=self.foreground,
    placeholder_color=self.placeholder_color,
    background=self.background,
    border=self.border,
    focused_border=self.focused_border,
    cursor_color=self.cursor_color,
    corner_radius=self.corner_radius,
    source_mode=self.source_mode,
    focus_mode=self.focus_mode,
    base_dir=self.base_dir,
    visible_scroll_y=self.scroll_glide.display_offset(
      ctx.now_ms,
      compensation=pending_geometry_compensation,
      scale=ctx.environment.scale_factor,
    ),
    visible_viewport_height=self.visible_viewport_height,
    visible_overscan=self.visible_overscan,
  )
  // The application owns the persistent glide. Paint only publishes the
  // measurement compensation; mutating this transient view node as well
  // would apply the same delta a second time when the app consumes it.
  ignore(geometry_compensation)
  // Keep the host's animation-driven redraw loop alive until the glide
  // settles; the runtime stops ticking once this reports false.
  { ..plan, animating: self.scroll_glide.is_animating(ctx.now_ms), }
}

///|
impl @core.ViewNode for MarkdownSessionEditorViewNode with fn text_input_state(
  self,
  ctx,
) {
  Some(
    markdown_session_input_state(
      text=self.session.source(),
      frame=ctx.frame,
      session=self.session,
      selection=self.selection,
      font=self.font,
      control=ctx.text_control,
      source_mode=self.source_mode,
      focus_mode=self.focus_mode,
      base_dir=self.base_dir,
      visible_scroll_y=self.scroll_glide.display_offset(
        ctx.now_ms,
        compensation=markdown_scroll_peek_geometry_compensation(),
      ),
      visible_viewport_height=self.visible_viewport_height,
      visible_overscan=self.visible_overscan,
      text_system=ctx.text_system,
    ),
  )
}

///|
impl @core.ViewNode for MarkdownSessionEditorViewNode with fn semantics(self) {
  @core.ViewSemanticsInfo::new(
    composition=Boundary,
    role=TextField,
    label=self.placeholder,
    value="Markdown document",
  )
}

///|
impl @core.ViewNode for MarkdownSessionEditorViewNode with fn focusable(_self) {
  true
}