text editor inspired vim and yi
import ViE.UI.Primitives
import ViE.Terminal
import ViE.Color

namespace ViE.UI
open ViE

def shouldRenderMessageAsFloat (msg : String) : Bool :=
  let m := msg.trimAscii.toString
  if m.isEmpty then
    false
  else
    m.startsWith "Error" ||
    m.startsWith "Cannot" ||
    m.startsWith "Invalid" ||
    m.startsWith "Unknown" ||
    m.startsWith "No " ||
    m.startsWith "Empty " ||
    m.startsWith "Usage:" ||
    m.startsWith "failed" ||
    m.startsWith "Failed" ||
    m.contains "not found"

def renderStatusBar (state : EditorState) : String :=
  let plainMessage := state.message.trimAscii.toString
  let floatMessage := shouldRenderMessageAsFloat plainMessage
  if state.mode == .command then
    s!":{state.inputState.commandBuffer}"
  else if state.mode == .searchForward then
    s!"/{state.inputState.commandBuffer}"
  else if state.mode == .searchBackward then
    s!"?{state.inputState.commandBuffer}"
  else if floatMessage then
    ""
  else
    plainMessage

def messageOverlayForState (state : EditorState) : Option FloatingOverlay :=
  let plainMessage := state.message.trimAscii.toString
  let floatMessage := shouldRenderMessageAsFloat plainMessage
  if state.floatingOverlay.isNone &&
      state.mode != .command &&
      state.mode != .searchForward &&
      state.mode != .searchBackward &&
      floatMessage then
    if plainMessage.isEmpty then
      none
    else
      some {
        title := "Message"
        lines := (plainMessage.splitOn "\n").toArray
        maxWidth := 0
        cursorRow := 0
        cursorCol := 0
      }
  else
    none

structure FloatingOverlayLayout where
  top : Nat
  left : Nat
  innerWidth : Nat
  titleRows : Nat
  contentRows : Nat
  deriving Inhabited

def computeFloatingOverlayLayout (rows cols : Nat) (tabStop : Nat) (overlay : FloatingOverlay) : Option FloatingOverlayLayout := Id.run do
  let availableRows := if rows > 1 then rows - 1 else rows
  if cols < 8 || availableRows < 4 then
    return none

  let lines := if overlay.lines.isEmpty then #[""] else overlay.lines
  let titleText := if overlay.title.isEmpty then "" else s!"[{overlay.title}]"
  let titleRows := if titleText.isEmpty then 0 else 1

  let naturalWidthContent := lines.foldl (fun acc ln => max acc (Unicode.stringWidthWithTabStop ln tabStop)) 0
  let naturalWidth := max naturalWidthContent (Unicode.stringWidthWithTabStop titleText tabStop)
  let maxInnerWidth := if cols > 8 then cols - 8 else 1
  let targetWidth :=
    if overlay.maxWidth > 0 then
      overlay.maxWidth
    else
      naturalWidth
  let innerWidth := max 1 (min targetWidth maxInnerWidth)

  let maxContentRows :=
    if availableRows > titleRows + 2 then
      availableRows - titleRows - 2
    else
      0
  if maxContentRows == 0 then
    return none
  let naturalRows := lines.size
  let targetRows := naturalRows
  let contentRows := max 1 (min targetRows maxContentRows)
  let boxHeight := contentRows + titleRows + 2
  let boxWidth := innerWidth + 4
  let top := (availableRows - boxHeight) / 2
  let left := (cols - boxWidth) / 2
  return some {
    top := top
    left := left
    innerWidth := innerWidth
    titleRows := titleRows
    contentRows := contentRows
  }

def renderFloatingOverlay (rows cols : Nat) (tabStop : Nat) (overlay : FloatingOverlay) : Array String := Id.run do
  let some layout := computeFloatingOverlayLayout rows cols tabStop overlay | return #[]
  let lines := if overlay.lines.isEmpty then #[""] else overlay.lines
  let titleText := if overlay.title.isEmpty then "" else s!"[{overlay.title}]"
  let top := layout.top
  let left := layout.left
  let innerWidth := layout.innerWidth
  let titleRows := layout.titleRows
  let contentRows := layout.contentRows
  let border := "+" ++ "".pushn '-' (innerWidth + 2) ++ "+"
  let style := (ViE.Color.toBg .brightBlack) ++ (ViE.Color.toFg .white)

  let mut out : Array String := #[]
  out := out.push (Terminal.moveCursorStr top left)
  out := out.push style
  out := out.push border

  if titleRows == 1 then
    out := out.push (Terminal.moveCursorStr (top + 1) left)
    let clippedTitle := Unicode.takeByDisplayWidthWithTabStop titleText.toRawSubstring tabStop innerWidth
    let titleW := Unicode.stringWidthWithTabStop clippedTitle tabStop
    let titlePad := if titleW < innerWidth then "".pushn ' ' (innerWidth - titleW) else ""
    out := out.push s!"| {clippedTitle}{titlePad} |"

  for i in [0:contentRows] do
    let raw := lines[i]?.getD ""
    let clipped := Unicode.takeByDisplayWidthWithTabStop raw.toRawSubstring tabStop innerWidth
    let clippedW := Unicode.stringWidthWithTabStop clipped tabStop
    let pad := if clippedW < innerWidth then "".pushn ' ' (innerWidth - clippedW) else ""
    let row := top + 1 + titleRows + i
    out := out.push (Terminal.moveCursorStr row left)
    out := out.push s!"| {clipped}{pad} |"

  let bottom := top + contentRows + titleRows + 1
  out := out.push (Terminal.moveCursorStr bottom left)
  out := out.push border
  out := out.push ViE.Color.reset
  return out

end ViE.UI