text editor inspired vim and yi
import ViE.State.Config
import ViE.State.Layout
import ViE.State.Movement
import ViE.Types
import ViE.Buffer.Content
import ViE.Data.PieceTable
import ViE.Unicode

namespace ViE

def EditorState.setMode (s : EditorState) (m : Mode) : EditorState :=
  { s with mode := m }

def EditorState.insertChar (s : EditorState) (c : Char) : EditorState :=
  let tabStop := s.config.tabStop
  let cursor := s.getCursor
  -- Use direct PieceTable insert for atomic undo
  let s' := s.updateActiveBuffer fun buffer =>
    match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop with
    | some offset => { buffer with table := buffer.table.insert offset (c.toString) offset
                                   dirty := true }
    | none => buffer

  let delta := ViE.Unicode.charWidthAt tabStop cursor.col.val c
  s'.setCursor { cursor with col := ⟨cursor.col.val + delta⟩ }

def EditorState.insertNewline (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  let cursor := s.getCursor
  -- Use direct PieceTable insert for atomic undo
  let s' := s.updateActiveBuffer fun buffer =>
    match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop with
    | some offset => { buffer with table := buffer.table.insert offset "\n" offset
                                   dirty := true }
    | none => buffer

  s'.setCursor { row := ⟨cursor.row.val + 1⟩, col := 0 }

def EditorState.deleteBeforeCursor (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  let cursor := s.getCursor
  if cursor.col.val > 0 then
    -- Delete char before cursor
    let buffer := s.getActiveBuffer
    let prevC := prevCol tabStop buffer cursor.row cursor.col
    let s' := s.updateActiveBuffer fun buffer =>
       match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row prevC tabStop,
             ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop with
       | some startOff, some endOff =>
            let len := endOff - startOff
            if len > 0 then
              { buffer with table := buffer.table.delete startOff len endOff
                            dirty := true }
            else buffer
       | _, _ => buffer

    s'.setCursor { cursor with col := prevC }
  else if cursor.row.val > 0 then
    -- Join with previous line (delete previous newline)
    let prevRow : Row := ⟨cursor.row.val - 1⟩

    let s' := s.updateActiveBuffer fun buffer =>
       -- Verify we are deleting the newline at the end of prevRow.
       match buffer.table.getLineRange prevRow.val with
       | some (start, len) =>
          -- Newline is at start + len. Length of newline is 1 (for \n).
          { buffer with table := buffer.table.delete (start + len) 1 (start + len)
                        dirty := true }
       | none => buffer

    -- Cursor moves to end of previous line
    let newLen := lineDisplayWidth tabStop s'.getActiveBuffer prevRow
    s'.setCursor { row := prevRow, col := ⟨newLen⟩ }
  else
    s

def EditorState.deleteCharAfterCursor (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  let cursor := s.getCursor
  -- Check if valid char at cursor
  let buffer := s.getActiveBuffer
  let lineLen := lineDisplayWidth tabStop buffer cursor.row
  let nextC := nextCol tabStop buffer cursor.row cursor.col
  let deletedText :=
    match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop,
          ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row nextC tabStop with
    | some startOff, some endOff =>
        if cursor.col.val < lineLen && endOff > startOff then
          PieceTree.getSubstring buffer.table.tree startOff (endOff - startOff) buffer.table
        else
          ""
    | _, _ => ""
  let s' := s.updateActiveBuffer fun buffer =>
    match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row cursor.col tabStop,
          ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row nextC tabStop with
    | some startOff, some endOff =>
        -- Ensure we don't delete newline if at end of line (unless J behavior, but x usually doesn't join lines)
        if cursor.col.val < lineLen && endOff > startOff then
          { buffer with table := buffer.table.delete startOff (endOff - startOff) startOff
                        dirty := true }
        else
          buffer
    | _, _ => buffer

  -- Cursor stays at same position unless it was last char
  let s'' :=
    if deletedText.isEmpty then
      s'
    else
      let reg : Register := {
        kind := .charwise
        text := deletedText
        blockLines := []
        blockWidth := 0
      }
      { s' with clipboard := some reg }
  let newBuffer := s''.getActiveBuffer
  let newLen := lineDisplayWidth tabStop newBuffer cursor.row
  if newLen > 0 && cursor.col.val >= newLen then
     let lineStr := lineString newBuffer cursor.row
     let newCol := ViE.Unicode.prevDisplayColWithTabStop lineStr tabStop newLen
     s''.setCursor { cursor with col := ⟨newCol⟩ }
  else
     s''





def EditorState.deleteCurrentLine (s : EditorState) : EditorState :=
  let cursor := s.getCursor
  let line := ViE.getLineFromBuffer s.getActiveBuffer cursor.row |>.getD ""
  let content := if line.endsWith "\n" then line else line ++ "\n"
  let reg : Register := {
    kind := .linewise
    text := content
    blockLines := []
    blockWidth := 0
  }
  let s' := s.updateActiveBuffer fun buffer =>
    match buffer.table.getLineRange cursor.row.val with
    | some (start, len) =>
       let deleteLen := if cursor.row.val < FileBuffer.lineCount buffer - 1 then len + 1 else len
       -- Restore cursor at start of line
       { buffer with table := buffer.table.delete start deleteLen start
                     dirty := true }
    | none => buffer

  -- Reset cursor logic
  let newBuffer := s'.getActiveBuffer
  let newRowVal := min cursor.row.val (newBuffer.lineCount.pred)
  let newRow : Row := ⟨newRowVal⟩
  let currentLineLen := ViE.getLineLengthFromBuffer newBuffer newRow |>.getD 0
  let newCol := min cursor.col.val currentLineLen
  let s'' := s'.setCursor { row := newRow, col := ⟨newCol⟩ }
  { s'' with
    inputState := { s''.inputState with previousKey := none },
    clipboard := some reg,
    message := "Deleted line"
  }


def EditorState.deleteRange (s : EditorState) (p1 p2 : Point) : EditorState :=
  let tabStop := s.config.tabStop
  let buffer := s.getActiveBuffer
  let (startOffOpt, endOffOpt) :=
    match ViE.getOffsetFromPointInBufferWithTabStop buffer p1.row p1.col tabStop,
          ViE.getOffsetFromPointInBufferWithTabStop buffer p2.row p2.col tabStop with
    | some off1, some off2 => (some (min off1 off2), some (max off1 off2))
    | _, _ => (none, none)
  let deletedText :=
    match (startOffOpt, endOffOpt) with
    | (some startOff, some endOff) =>
        if endOff > startOff then
          PieceTree.getSubstring buffer.table.tree startOff (endOff - startOff) buffer.table
        else
          ""
    | _ => ""
  let s' := s.updateActiveBuffer fun buffer =>
     match (startOffOpt, endOffOpt) with
     | (some startOff, some endOff) =>
         let len := endOff - startOff
         if len > 0 then
            { buffer with table := buffer.table.delete startOff len startOff
                          dirty := true }
         else buffer
     | _ => buffer

  -- Move cursor to start of deleted range
  let newStart := if p1.row < p2.row || (p1.row == p2.row && p1.col < p2.col) then p1 else p2
  let s'' := s'.setCursor newStart
  let s''' :=
    if deletedText.isEmpty then
      s''
    else
      let reg : Register := {
        kind := .charwise
        text := deletedText
        blockLines := []
        blockWidth := 0
      }
      { s'' with clipboard := some reg }
  { s''' with inputState := { s'''.inputState with previousKey := none } }


def EditorState.changeWord (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  let n := if s.getCount == 0 then 1 else s.getCount
  let start := s.getCursor

  -- Apply 'moveWordEnd' n times (recursive helper)
  let rec applyMotionN (st : EditorState) (count : Nat) : EditorState :=
    match count with
    | 0 => st
    | k + 1 => applyMotionN (st.moveWordEnd) k

  let endState := applyMotionN s n
  let endP := endState.getCursor

  -- Inclusive deletion logic for 'e' motion behavior
  let buffer := s.getActiveBuffer
  let lineLen := lineDisplayWidth tabStop buffer endP.row
  let lineStr := lineString buffer endP.row
  let targetCol := if endP.col.val < lineLen then ViE.Unicode.nextDisplayColWithTabStop lineStr tabStop endP.col.val else lineLen
  let targetP := { endP with col := ⟨targetCol⟩ }

  (s.deleteRange start targetP).setMode Mode.insert

def EditorState.insertLineBelow (s : EditorState) : EditorState :=
  let cursor := s.getCursor
  let s' := s.updateActiveBuffer fun buffer =>
    match buffer.table.getLineRange cursor.row.val with
    | some (start, len) =>
       { buffer with table := buffer.table.insert (start + len) "\n" (start + len)
                     dirty := true }
    | none => buffer
  s'.setCursor { row := ⟨cursor.row.val + 1⟩, col := 0 }

def EditorState.insertLineAbove (s : EditorState) : EditorState :=
  let cursor := s.getCursor
  let s' := s.updateActiveBuffer fun buffer =>
    match buffer.table.getLineRange cursor.row.val with
    | some (start, _) =>
       { buffer with table := buffer.table.insert start "\n" start
                     dirty := true }
    | none => buffer
  s'.setCursor { cursor with col := 0 }

def EditorState.yankCurrentLine (s : EditorState) : EditorState :=
  let cursor := s.getCursor
  let line := ViE.getLineFromBuffer s.getActiveBuffer cursor.row |>.getD ""
  let content := if line.endsWith "\n" then line else line ++ "\n"
  let reg : Register := {
    kind := .linewise
    text := content
    blockLines := []
    blockWidth := 0
  }
  { s with clipboard := some reg, message := "Yanked 1 line" }

def EditorState.ensureLineCount (s : EditorState) (count : Nat) : EditorState :=
  s.updateActiveBuffer fun buffer =>
    let lineCount := buffer.table.lineCount
    if count <= lineCount then
      buffer
    else
      let missing := count - lineCount
      let newlines := String.ofList (List.replicate missing '\n')
      let len := buffer.table.tree.length
      { buffer with table := buffer.table.insert len newlines len, dirty := true }

def EditorState.pasteCharwise (s : EditorState) (text : String) (after : Bool) : EditorState :=
  let tabStop := s.config.tabStop
  let cursor := s.getCursor
  let line := ViE.getLineFromBuffer s.getActiveBuffer cursor.row |>.getD ""
  let lineWidth := ViE.Unicode.stringWidthWithTabStop line tabStop
  let targetCol :=
    if after then
      if cursor.col.val < lineWidth then
        ViE.Unicode.nextDisplayColWithTabStop line tabStop cursor.col.val
      else
        cursor.col.val
    else
      cursor.col.val
  let s' := s.updateActiveBuffer fun buffer =>
    match ViE.getOffsetFromPointInBufferWithTabStop buffer cursor.row ⟨targetCol⟩ tabStop with
    | some offset => { buffer with table := buffer.table.insert offset text offset, dirty := true }
    | none => buffer
  let lines := text.splitOn "\n"
  let lastLine := lines.getLastD ""
  let lastWidth := ViE.Unicode.stringWidthWithTabStop lastLine tabStop
  let rowDelta := if lines.length > 0 then lines.length - 1 else 0
  let newRow := cursor.row.val + rowDelta
  let newCol :=
    if lines.length <= 1 then
      if lastWidth == 0 then targetCol else targetCol + lastWidth - 1
    else
      if lastWidth == 0 then 0 else lastWidth - 1
  s'.setCursor { row := ⟨newRow⟩, col := ⟨newCol⟩ }

def firstNonBlankCol (line : String) (tabStop : Nat) : Nat :=
  let rec loop (cs : List Char) (col : Nat) : Nat :=
    match cs with
    | [] => 0
    | c :: rest =>
      if c == ' ' || c == '\t' then
        loop rest (col + ViE.Unicode.charWidthAt tabStop col c)
      else
        col
  loop line.toList 0

def EditorState.pasteLinewise (s : EditorState) (text : String) (below : Bool) : EditorState :=
  let cursor := s.getCursor
  let row := if below then cursor.row.val + 1 else cursor.row.val
  let s' := s.updateActiveBuffer fun buffer =>
    -- Determine insert offset.
    -- Try start of target line. If EOF, append.
    let (offset, textToInsert) := match ViE.getOffsetFromPointInBuffer buffer ⟨row⟩ 0 with
                  | some off => (off, text)
                  | none =>
                      let len := buffer.table.tree.length
                      if len > 0 then
                         if !PieceTable.endsWithNewline buffer.table then
                             (len, "\n" ++ text)
                         else
                             (len, text)
                      else (0, text)
    let pt := buffer.table.commit
    { buffer with table := pt.insert offset textToInsert offset, dirty := true }
  let newRow := if below then cursor.row.val + 1 else cursor.row.val
  let lineStr := ViE.getLineFromBuffer s'.getActiveBuffer ⟨newRow⟩ |>.getD ""
  let col := firstNonBlankCol lineStr s.config.tabStop
  s'.setCursor { row := ⟨newRow⟩, col := ⟨col⟩ }

def EditorState.pasteBlockwise (s : EditorState) (reg : Register) (after : Bool) : EditorState :=
  let tabStop := s.config.tabStop
  let cursor := s.getCursor
  let line := ViE.getLineFromBuffer s.getActiveBuffer cursor.row |>.getD ""
  let lineWidth := ViE.Unicode.stringWidthWithTabStop line tabStop
  let baseCol :=
    if after then
      if cursor.col.val < lineWidth then
        ViE.Unicode.nextDisplayColWithTabStop line tabStop cursor.col.val
      else
        cursor.col.val
    else
      cursor.col.val
  let lines :=
    if reg.blockLines.isEmpty then
      if reg.text.isEmpty then [] else reg.text.splitOn "\n"
    else
      reg.blockLines
  let targetLineCount := cursor.row.val + lines.length
  let s1 := s.ensureLineCount targetLineCount
  let s2 := s1.updateActiveBuffer fun buffer =>
    let rec apply (buf : FileBuffer) (idx : Nat) (ls : List String) : FileBuffer :=
      match ls with
      | [] => buf
      | l :: rest =>
        let row := cursor.row.val + idx
        let lineStr := ViE.getLineFromBuffer buf ⟨row⟩ |>.getD ""
        let lineW := ViE.Unicode.stringWidthWithTabStop lineStr tabStop
        let targetCol := baseCol
        let (buf1, offset) :=
          if targetCol > lineW then
            let pad := targetCol - lineW
            let padStr := String.ofList (List.replicate pad ' ')
            let insertOff := ViE.getOffsetFromPointInBufferWithTabStop buf ⟨row⟩ ⟨lineW⟩ tabStop |>.getD (buf.table.tree.length)
            let buf' := { buf with table := buf.table.insert insertOff padStr insertOff, dirty := true }
            let off' := ViE.getOffsetFromPointInBufferWithTabStop buf' ⟨row⟩ ⟨targetCol⟩ tabStop |>.getD insertOff
            (buf', off')
          else
            let off := ViE.getOffsetFromPointInBufferWithTabStop buf ⟨row⟩ ⟨targetCol⟩ tabStop |>.getD 0
            (buf, off)
        let buf2 := { buf1 with table := buf1.table.insert offset l offset, dirty := true }
        apply buf2 (idx + 1) rest
    apply buffer 0 lines
  s2.setCursor { row := cursor.row, col := ⟨baseCol⟩ }

def EditorState.pasteBelow (s : EditorState) : EditorState :=
  match s.clipboard with
  | none => { s with message := "Clipboard empty" }
  | some reg =>
    match reg.kind with
    | .linewise => s.pasteLinewise reg.text true
    | .charwise => s.pasteCharwise reg.text true
    | .blockwise => s.pasteBlockwise reg true

def EditorState.pasteAbove (s : EditorState) : EditorState :=
  match s.clipboard with
  | none => { s with message := "Clipboard empty" }
  | some reg =>
    match reg.kind with
    | .linewise => s.pasteLinewise reg.text false
    | .charwise => s.pasteCharwise reg.text false
    | .blockwise => s.pasteBlockwise reg false

def EditorState.undo (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  -- Capture current cursor offset (if valid) for redo
  let buf := s.getActiveBuffer
  let cursor := s.getCursor
  let currentOffset := match ViE.getOffsetFromPointInBufferWithTabStop buf cursor.row cursor.col tabStop with
                       | some off => off
                       | none => 0

  let (newTable, cursorOpt) := buf.table.undo currentOffset
  let s' := s.updateActiveBuffer fun _ => { buf with table := newTable, dirty := true }

  match cursorOpt with
  | some off =>
      let p := ViE.getPointFromOffsetInBufferWithTabStop { buf with table := newTable } off tabStop
      let s'' := s'.setCursor p
      { s'' with message := "Undo" }
  | none => { s' with message := "Already at oldest change" }

def EditorState.redo (s : EditorState) : EditorState :=
  let tabStop := s.config.tabStop
  -- Capture current cursor offset
  let buf := s.getActiveBuffer
  let cursor := s.getCursor
  let currentOffset := match ViE.getOffsetFromPointInBufferWithTabStop buf cursor.row cursor.col tabStop with
                       | some off => off
                       | none => 0

  let (newTable, cursorOpt) := buf.table.redo currentOffset
  let s' := s.updateActiveBuffer fun _ => { buf with table := newTable, dirty := true }

  match cursorOpt with
  | some off =>
      let p := ViE.getPointFromOffsetInBufferWithTabStop { buf with table := newTable } off tabStop
      let s'' := s'.setCursor p
      { s'' with message := "Redo" }
  | none => { s' with message := "Already at newest change" }

def EditorState.commitEdit (s : EditorState) : EditorState :=
  s.updateActiveBuffer fun buffer =>
    { buffer with table := buffer.table.commit }


/-- Vim Operators -/

def EditorState.deleteWord (s : EditorState) : EditorState :=
  let p1 := s.getCursor
  let s' := s.moveWordForward
  let p2 := s'.getCursor
  s.deleteRange p1 p2


end ViE