///|
fn markdown_session_current_scroll_y(
  visible_scroll_y : Double,
  visible_scroll_state : @core.ScrollState?,
) -> Double {
  match visible_scroll_state {
    Some(state) => state.offset().y
    None => visible_scroll_y
  }
}

///|
fn markdown_session_clamped_scroll_y(
  session : MarkdownDocumentSession,
  font : @core.FontSpec,
  source_mode~ : Bool,
  scroll_y : Double,
  viewport_height : Double,
) -> Double {
  if viewport_height <= 0.0 {
    return max_double(0.0, scroll_y)
  }
  let content_height = session.estimated_content_height(base=font, source_mode~) +
    96.0
  let max_scroll = max_double(0.0, content_height - viewport_height)
  clamp_double(scroll_y, 0.0, max_scroll)
}

///|
fn markdown_session_display_session(
  session : MarkdownDocumentSession,
  display_text : String,
) -> MarkdownDocumentSession {
  if display_text == session.source {
    session
  } else {
    MarkdownDocumentSession::new(display_text)
  }
}

///|
fn markdown_session_visible_window(
  session : MarkdownDocumentSession,
  selection : MarkdownEditorSelection,
  font : @core.FontSpec,
  source_mode~ : Bool,
  focus_mode~ : Bool,
  base_dir~ : String?,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> MarkdownDocumentRichTextWindow {
  let scroll_y = markdown_session_clamped_scroll_y(
    session,
    font,
    source_mode~,
    markdown_session_current_scroll_y(visible_scroll_y, visible_scroll_state),
    visible_viewport_height,
  )
  session.rich_text_window(
    selection,
    base=font,
    source_mode~,
    focus_mode~,
    base_dir~,
    scroll_y~,
    viewport_height=visible_viewport_height,
    overscan=visible_overscan,
  )
}

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

///|
fn[Msg] markdown_session_on_input_edit(
  session : MarkdownDocumentSession,
  on_transaction : (MarkdownEditTransaction) -> Msg,
) -> (String, Int, @core.TextRange?) -> Msg {
  (next_source, caret, next_selection) => {
    on_transaction(
      MarkdownEditTransaction::from_sources(
        session.source,
        next_source,
        caret~,
        selection=next_selection,
        reason=TextInput,
      ),
    )
  }
}

///|
fn markdown_session_editor_paint(
  ctx : @core.ViewPaintContext,
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  placeholder~ : String,
  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?,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewPaintPlan {
  let commands : Array[@core.DrawCommand] = []
  let overlay_commands : Array[@core.DrawCommand] = []
  let rounded = @core.RoundedRect::new(rect=ctx.frame, radius=corner_radius)
  commands.push(@core.DrawCommand::FillRoundedRectBrush(rounded, background))
  append_rich_text_border(
    commands,
    rounded,
    if ctx.focused {
      focused_border
    } else {
      border
    },
  )
  let text = session.source
  let display_text = rich_text_with_composition(
    text,
    ctx.text_control,
    focused=ctx.focused,
  )
  let display_session = markdown_session_display_session(session, display_text)
  let content = rich_text_editor_content_rect(ctx.frame)
  commands.push(@core.DrawCommand::PushClip(content))
  let child_layers : Array[@core.ViewPaintLayer] = []
  if display_text == "" {
    commands.push(
      @core.DrawCommand::DrawText(
        @core.TextRun::new(
          text=placeholder,
          frame=content,
          font~,
          color=placeholder_color,
          align=@core.TextAlign::TextStart,
        ),
      ),
    )
  } else {
    let window = markdown_session_visible_window(
      display_session,
      selection,
      font,
      source_mode~,
      focus_mode~,
      base_dir~,
      visible_scroll_y~,
      visible_viewport_height~,
      visible_overscan~,
      visible_scroll_state~,
    )
    let window_content = window.content_rect(content)
    // Position blocks inside the offset `window_content`, but cull against
    // the original `content` viewport. Without this, the non-windowed cull
    // uses `window_content.origin.y + height` which is shifted up by
    // `top_padding - scroll_y`, cutting off the bottom of the viewport.
    append_rich_text_document_layers(
      child_layers,
      window.document,
      window_content,
      font,
      foreground,
      text_system=ctx.text_system,
      clip_rect=Some(content),
    )
    if ctx.focused {
      append_rich_text_editor_overlay(
        overlay_commands,
        text~,
        display_text~,
        document=window.document,
        content=window_content,
        font~,
        cursor_color~,
        control=ctx.text_control,
        text_system=ctx.text_system,
        clip_rect=Some(content),
      )
    }
  }
  // PopClip must go in post_commands (not commands) so that child_layers —
  // which are rendered between commands and post_commands — stay inside the
  // content clip. Without this, overscan blocks bleed past the editor frame
  // and overlap the chrome/status bar during scrolling.
  {
    commands,
    child_layers,
    post_commands: [@core.DrawCommand::PopClip],
    overlay_commands,
    platform_views: [],
    repaint_boundary: false,
    animating: false,
  }
}

///|
fn[Msg] markdown_session_editor_event(
  ctx : @core.ViewEventContext,
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  on_transaction~ : (MarkdownEditTransaction) -> Msg,
  font~ : @core.FontSpec,
  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,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewEventResult[Msg] {
  let value = @core.Binding::new(get=() => session.source, set=_next => ())
  let on_input_edit = markdown_session_on_input_edit(session, on_transaction)
  match ctx.event {
    @core.AppEvent::Pointer(event) =>
      markdown_session_pointer_event(
        ctx,
        session~,
        selection~,
        font~,
        source_mode~,
        focus_mode~,
        base_dir~,
        on_selection_change~,
        on_prefix_click~,
        on_command_click~,
        event~,
        visible_scroll_y~,
        visible_viewport_height~,
        visible_overscan~,
        visible_scroll_state~,
      )
    @core.AppEvent::Keyboard(event) => {
      // ArrowUp/ArrowDown 需要用 visible_window 的 document 来正确计算
      // 视觉位置(考虑 scroll offset),所以在这里预处理,而不是走
      // rich_text_keyboard_event 的通用 format(text) 路径。
      if event.pressed && (event.key == "ArrowUp" || event.key == "ArrowDown") {
        let text = session.source
        let window = markdown_session_visible_window(
          session,
          selection,
          font,
          source_mode~,
          focus_mode~,
          base_dir~,
          visible_scroll_y~,
          visible_viewport_height~,
          visible_overscan~,
          visible_scroll_state~,
        )
        let content = window.content_rect(
          rich_text_editor_content_rect(ctx.frame),
        )
        return rich_text_vertical_caret_move_result(
          ctx,
          text,
          window.document,
          content,
          font,
          up=event.key == "ArrowUp",
          extend_selection=event.modifiers.shift,
          on_selection_change,
        )
      }
      rich_text_keyboard_event(
        ctx,
        value~,
        format=_source => {
          session.rich_text(
            selection,
            base=font,
            source_mode~,
            focus_mode~,
            base_dir~,
          )
        },
        font~,
        on_input=None,
        on_input_edit=Some(on_input_edit),
        on_selection_change~,
        input_transform=Some(markdown_session_input_transform),
        accepts_tab_input~,
        event~,
      )
    }
    @core.AppEvent::TextInput(event) =>
      rich_text_input_event(
        ctx,
        value~,
        on_input=None,
        on_input_edit=Some(on_input_edit),
        on_selection_change~,
        input_transform=Some(markdown_session_input_transform),
        event~,
        record_history=true,
      )
    _ => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] markdown_session_pointer_event(
  ctx : @core.ViewEventContext,
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  font~ : @core.FontSpec,
  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)?,
  event~ : @core.PointerEvent,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewEventResult[Msg] {
  let inside = ctx.frame.contains(event.position)
  if inside && (event.wheel_delta.x != 0.0 || event.wheel_delta.y != 0.0) {
    return markdown_session_scroll_event(
      session,
      font,
      source_mode~,
      event~,
      visible_viewport_height~,
      visible_scroll_state~,
    )
  }
  match event.phase {
    @core.PointerPhase::Down =>
      if inside {
        let text = session.source
        let window = markdown_session_visible_window(
          session,
          selection,
          font,
          source_mode~,
          focus_mode~,
          base_dir~,
          visible_scroll_y~,
          visible_viewport_height~,
          visible_overscan~,
          visible_scroll_state~,
        )
        let content = window.content_rect(
          rich_text_editor_content_rect(ctx.frame),
        )
        let command_modifiers = event.modifiers.meta || event.modifiers.control
        match (command_modifiers, on_command_click) {
          (true, Some(handler)) => {
            let offset = rich_text_document_source_offset_at_point(
              window.document,
              event.position,
              content,
              font,
              ctx.text_system,
            ).unwrap_or(text_length(text))
            view_event_result(
              changed=true,
              focused=true,
              captured=true,
              text_control=Some(ctx.text_control),
              dirty=@core.ViewDirtyHint::ViewPaintDirty,
              messages=[handler(offset)],
            )
          }
          _ => {
            let prefix_messages = match on_prefix_click {
              Some(handler) =>
                match
                  rich_text_document_prefix_source_offset_at_point(
                    window.document,
                    event.position,
                    content,
                    font,
                    ctx.text_system,
                  ) {
                  Some(offset) => [handler(offset)]
                  None => []
                }
              None => []
            }
            let caret = rich_text_document_source_offset_at_point(
              window.document,
              event.position,
              content,
              font,
              ctx.text_system,
            ).unwrap_or(text_length(text))
            let next_control = {
              ..ctx.text_control,
              caret,
              selection: None,
              text_selection_anchor: Some(caret),
              composition: None,
              composition_cursor: None,
            }
            view_event_result(
              changed=true,
              focused=true,
              captured=true,
              text_control=Some(next_control),
              dirty=@core.ViewDirtyHint::ViewPaintDirty,
              messages=prefix_messages +
                selection_messages(on_selection_change, caret, None),
            )
          }
        }
      } else {
        @core.ViewEventResult::ignored()
      }
    @core.PointerPhase::Move =>
      match ctx.text_control.text_selection_anchor {
        Some(anchor) => {
          let text = session.source
          let window = markdown_session_visible_window(
            session,
            selection,
            font,
            source_mode~,
            focus_mode~,
            base_dir~,
            visible_scroll_y~,
            visible_viewport_height~,
            visible_overscan~,
            visible_scroll_state~,
          )
          let content = window.content_rect(
            rich_text_editor_content_rect(ctx.frame),
          )
          let caret = rich_text_document_source_offset_at_point(
            window.document,
            event.position,
            content,
            font,
            ctx.text_system,
          ).unwrap_or(text_length(text))
          let next_selection = if caret == anchor {
            None
          } else {
            Some(@core.TextRange::new(start=anchor, end=caret))
          }
          let changed = ctx.text_control.caret != caret ||
            ctx.text_control.selection != next_selection
          if changed {
            view_event_result(
              changed=true,
              focused=true,
              captured=true,
              text_control=Some({
                ..ctx.text_control,
                caret,
                selection: next_selection,
                composition: None,
                composition_cursor: None,
              }),
              dirty=@core.ViewDirtyHint::ViewPaintDirty,
              messages=selection_messages(
                on_selection_change, caret, next_selection,
              ),
            )
          } else {
            view_event_result(
              focused=true,
              captured=true,
              text_control=Some(ctx.text_control),
            )
          }
        }
        None => @core.ViewEventResult::ignored()
      }
    @core.PointerPhase::Up =>
      if ctx.text_control.text_selection_anchor is Some(_) {
        view_event_result(
          changed=true,
          focused=true,
          captured=true,
          text_control=Some({ ..ctx.text_control, text_selection_anchor: None }),
          dirty=@core.ViewDirtyHint::ViewPaintDirty,
        )
      } else {
        @core.ViewEventResult::ignored()
      }
    _ => @core.ViewEventResult::ignored()
  }
}

///|
fn[Msg] markdown_session_scroll_event(
  session : MarkdownDocumentSession,
  font : @core.FontSpec,
  source_mode~ : Bool,
  event~ : @core.PointerEvent,
  visible_viewport_height~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewEventResult[Msg] {
  match visible_scroll_state {
    Some(state) => {
      let viewport_height = if visible_viewport_height > 0.0 {
        visible_viewport_height
      } else {
        0.0
      }
      let content_height = session.estimated_content_height(
          base=font,
          source_mode~,
        ) +
        96.0
      let max_scroll = max_double(0.0, content_height - viewport_height)
      let current = state.offset()
      let next_y = min_double(
        max_double(0.0, current.y - event.wheel_delta.y),
        max_scroll,
      )
      state.scroll_to(x=0.0, y=next_y)
      view_event_result(
        changed=next_y != current.y,
        captured=true,
        dirty=@core.ViewDirtyHint::ViewPaintDirty,
      )
    }
    None => @core.ViewEventResult::ignored()
  }
}

///|
fn markdown_session_input_state(
  text~ : String,
  frame~ : @core.Rect,
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  font~ : @core.FontSpec,
  control~ : @core.TextControlStateContext,
  source_mode~ : Bool,
  focus_mode~ : Bool,
  base_dir~ : String?,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
  text_system~ : @core.TextSystem,
) -> @core.TextInputState {
  let control = {
    ..control,
    caret: selection.caret,
    selection: selection.selection,
  }
  let caret = @core.clamp_int(control.caret, 0, session.source_length)
  if frame.size.width <= 0.0 || frame.size.height <= 0.0 {
    return {
      focused: true,
      element_id: None,
      text,
      caret,
      selection: control.selection,
      composition: control.composition,
      composition_cursor: control.composition_cursor,
      frame,
      caret_rect: @core.Rect::new(
        x=frame.origin.x,
        y=frame.origin.y,
        width=1.0,
        height=font.size,
      ),
    }
  }
  let display_text = rich_text_with_composition(text, control, focused=true)
  let display_session = markdown_session_display_session(session, display_text)
  let visual_caret = match control.composition {
    Some(_) =>
      @core.clamp_int(
        rich_text_visual_caret(text, control, focused=true),
        0,
        text_length(display_text),
      )
    None => caret
  }
  let window = markdown_session_visible_window(
    display_session,
    { caret: visual_caret, selection: control.selection },
    font,
    source_mode~,
    focus_mode~,
    base_dir~,
    visible_scroll_y~,
    visible_viewport_height~,
    visible_overscan~,
    visible_scroll_state~,
  )
  let base_content = rich_text_editor_content_rect(frame)
  let content = window.content_rect(base_content)
  let caret_rect = match
    rich_text_document_caret_rect_at_source(
      window.document,
      visual_caret,
      content,
      font,
      text_system~,
    ) {
    Some(rect) => rect
    None =>
      markdown_session_estimated_caret_rect(
        display_session,
        visual_caret,
        base_content,
        font,
        source_mode~,
        scroll_y=window.scroll_y,
      )
  }
  {
    focused: true,
    element_id: None,
    text,
    caret,
    selection: control.selection,
    composition: control.composition,
    composition_cursor: control.composition_cursor,
    frame,
    caret_rect,
  }
}

///|
fn markdown_session_estimated_caret_rect(
  session : MarkdownDocumentSession,
  source_caret : Int,
  content : @core.Rect,
  font : @core.FontSpec,
  source_mode~ : Bool,
  scroll_y~ : Double,
) -> @core.Rect {
  let source_y = session.source_offset_y(source_caret, base=font, source_mode~)
  @core.Rect::new(
    x=content.origin.x,
    y=content.origin.y + source_y - scroll_y + 4.0,
    width=1.5,
    height=max_double(10.0, font.size * 1.55 - 8.0),
  )
}

///|
fn[Msg] markdown_session_editor_command(
  ctx : @core.ViewEventContext,
  session~ : MarkdownDocumentSession,
  selection~ : MarkdownEditorSelection,
  on_transaction~ : (MarkdownEditTransaction) -> Msg,
  on_selection_change~ : ((Int, @core.TextRange?) -> Msg)?,
  font~ : @core.FontSpec,
  source_mode~ : Bool,
  focus_mode~ : Bool,
  base_dir~ : String?,
  intent : @core.CommandIntent,
  paste_text~ : String?,
) -> @core.ViewTextCommandResult[Msg] {
  let value = @core.Binding::new(get=() => session.source, set=_next => ())
  let on_input_edit = markdown_session_on_input_edit(session, on_transaction)
  let format_selection = match intent {
    @core.CommandIntent::Copy | @core.CommandIntent::Cut =>
      ({ caret: -1, selection: None } : MarkdownEditorSelection)
    _ => selection
  }
  let format = display_source => {
    if display_source == session.source {
      session.rich_text(
        format_selection,
        base=font,
        source_mode~,
        focus_mode~,
        base_dir~,
      )
    } else if source_mode {
      markdown_editor_format_source_mode(display_source, font)
    } else {
      markdown_editor_format_for_writing_mode(
        display_source,
        format_selection,
        base=font,
        focus_mode~,
        base_dir~,
      )
    }
  }
  rich_text_editor_command(
    ctx,
    value~,
    format~,
    on_input=None,
    on_input_edit=Some(on_input_edit),
    on_selection_change~,
    input_transform=Some(markdown_session_input_transform),
    intent,
    paste_text~,
  )
}