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

namespace ViE

def searchChunkSize : Nat := 8192
def incrementalSearchDebounceMs : Nat := 120

def updateSearchCache (state : SearchState) (offset : Nat) : SearchState :=
  let cache := state.cache.push offset
  let cache :=
    if cache.size > state.cacheMax then
      cache.extract (cache.size - state.cacheMax) cache.size
    else
      cache
  { state with cache := cache }

def getCursorOffset (s : EditorState) : Nat :=
  let buf := s.getActiveBuffer
  let cursor := s.getCursor
  ViE.getOffsetFromPointInBufferWithTabStop buf cursor.row cursor.col s.config.tabStop |>.getD 0

def applyMatchOffset (s : EditorState) (offset : Nat) : EditorState :=
  let buf := s.getActiveBuffer
  let p := ViE.getPointFromOffsetInBufferWithTabStop buf offset s.config.tabStop
  s.setCursor p

def startSearch (s : EditorState) (pattern : String) (direction : SearchDirection) (useBloom : Bool) : EditorState :=
  let cursorOffset := getCursorOffset s
  let bufferId := s.getActiveBuffer.id
  let search : SearchState := {
    pattern := pattern
    direction := direction
    useBloom := useBloom
    lastMatch := none
    lastSearchOffset := cursorOffset
    cache := #[]
    cacheMax := 128
    lineCacheBufferId := some bufferId
    lineMatches := Lean.RBMap.empty
    lineOrder := #[]
    lineCacheMax := 512
    bloomCache := Lean.RBMap.empty
    bloomOrder := #[]
    bloomCacheMax := s.config.searchBloomCacheMax
  }
  { s with searchState := some search }

def startOrUpdateSearch (s : EditorState) (pattern : String) (direction : SearchDirection) (useBloom : Bool) : EditorState :=
  match s.searchState with
  | none => startSearch s pattern direction useBloom
  | some st =>
      let cursorOffset := getCursorOffset s
      let bufferId := s.getActiveBuffer.id
      let st' := {
        st with
          pattern := pattern
          direction := direction
          useBloom := useBloom
          lastMatch := none
          lastSearchOffset := cursorOffset
          cache := #[]
          lineCacheBufferId := some bufferId
          lineMatches := Lean.RBMap.empty
          lineOrder := #[]
      }
      { s with searchState := some st' }

def searchNextOffset (pt : PieceTable) (pattern : ByteArray) (startOffset : Nat) (useBloom : Bool)
  (cache : Lean.RBMap Nat ByteArray compare) (order : Array Nat) (cacheMax : Nat)
  : (Option Nat × Lean.RBMap Nat ByteArray compare × Array Nat) :=
  PieceTree.searchNext pt.tree pt pattern startOffset searchChunkSize useBloom cache order cacheMax

def searchPrevOffset (pt : PieceTable) (pattern : ByteArray) (startExclusive : Nat) (useBloom : Bool)
  (cache : Lean.RBMap Nat ByteArray compare) (order : Array Nat) (cacheMax : Nat)
  : (Option Nat × Lean.RBMap Nat ByteArray compare × Array Nat) :=
  PieceTree.searchPrev pt.tree pt pattern startExclusive searchChunkSize useBloom cache order cacheMax

def findNextMatch (s : EditorState) (directionOverride : Option SearchDirection := none) : EditorState :=
  match s.searchState with
  | none => { s with message := "No search pattern" }
  | some st =>
    let direction := directionOverride.getD st.direction
    if st.pattern.isEmpty then
      { s with message := "Empty search pattern" }
    else
      let pt := s.getActiveBuffer.table
      let patBytes := st.pattern.toUTF8
      let startOffset :=
        match direction with
        | .forward =>
          match st.lastMatch with
          | some m => m + 1
          | none => getCursorOffset s
        | .backward =>
          match st.lastMatch with
          | some m => m
          | none => getCursorOffset s
      let useBloom := st.useBloom && patBytes.size >= 3
      let (result, bloomCache, bloomOrder) :=
        match direction with
        | .forward => searchNextOffset pt patBytes startOffset useBloom st.bloomCache st.bloomOrder st.bloomCacheMax
        | .backward => searchPrevOffset pt patBytes startOffset useBloom st.bloomCache st.bloomOrder st.bloomCacheMax
      let (result, wrapped, bloomCache, bloomOrder) :=
        match result, direction with
        | some r, _ => (some r, false, bloomCache, bloomOrder)
        | none, .forward =>
            if startOffset > 0 then
              let (r2, c2, o2) := searchNextOffset pt patBytes 0 useBloom bloomCache bloomOrder st.bloomCacheMax
              (r2, true, c2, o2)
            else
              (none, false, bloomCache, bloomOrder)
        | none, .backward =>
            let total := pt.tree.length
            if startOffset < total then
              let (r2, c2, o2) := searchPrevOffset pt patBytes total useBloom bloomCache bloomOrder st.bloomCacheMax
              (r2, true, c2, o2)
            else
              (none, false, bloomCache, bloomOrder)
      match result with
      | none =>
          let st' := { st with bloomCache := bloomCache, bloomOrder := bloomOrder }
          { s with searchState := some st', message := s!"Pattern not found: {st.pattern}" }
      | some off =>
          let s' := applyMatchOffset (s.pushJumpPoint) off
          let st' := updateSearchCache { st with lastMatch := some off, bloomCache := bloomCache, bloomOrder := bloomOrder } off
          let st'' := { st' with direction := direction }
          let bloomTag := if useBloom then " [bloom]" else ""
          let msg :=
            if wrapped then
              s!"Wrapped: {st.pattern}{bloomTag}"
            else
              s!"Match: {st.pattern}{bloomTag}"
          { s' with searchState := some st'', message := msg }

def EditorState.searchWordUnderCursor (s : EditorState) (direction : SearchDirection) : EditorState :=
  match s.wordUnderCursor with
  | none => { s with message := "No keyword under cursor" }
  | some word =>
      let s1 := s.pushJumpPoint
      let cursor := s1.getCursor
      let cursorOffset :=
        ViE.getOffsetFromPointInBufferWithTabStop s1.getActiveBuffer cursor.row cursor.col s1.config.tabStop |>.getD 0
      let s2 := ViE.startOrUpdateSearch s1 word direction false
      let s3 :=
        match s2.searchState with
        | none => s2
        | some st => { s2 with searchState := some { st with lastMatch := some cursorOffset } }
      ViE.findNextMatch s3 (some direction)

def runIncrementalSearch (s : EditorState) : EditorState :=
  if s.mode != .searchForward && s.mode != .searchBackward then
    s
  else
    let pattern := s.inputState.commandBuffer
    if pattern.isEmpty then
      { s with searchState := none, message := "", dirty := true }
    else
      let direction := if s.mode == .searchBackward then SearchDirection.backward else SearchDirection.forward
      let s' := startOrUpdateSearch s pattern direction false
      findNextMatch s' (some direction)

def maybeRunIncrementalSearch (s : EditorState) (now : Nat) : EditorState :=
  if s.inputState.pendingSearch && now - s.inputState.lastSearchInputTime >= incrementalSearchDebounceMs then
    let s' := runIncrementalSearch s
    { s' with
      dirty := true
      inputState := { s'.inputState with pendingSearch := false, lastSearchRunTime := now }
    }
  else
    s

end ViE