text editor inspired vim and yi
import ViE.State
import ViE.IO

import ViE.Loader
import ViE.Checkpoint
import ViE.Types
import ViE.Buffer.Content
import ViE.Window.Actions
import ViE.Window.Analysis
import ViE.Buffer
import ViE.Command.Explorer
import ViE.Command.Parser
import ViE.State.Search
import ViE.Data.PieceTable.Tree

open ViE.Window
open ViE.Buffer
open ViE.Feature
open ViE.Command

namespace ViE.Command

def collectMatchesInBytes (haystack : ByteArray) (needle : ByteArray) : Array (Nat × Nat) :=
  let n := needle.size
  let h := haystack.size
  if n == 0 || h < n then
    #[]
  else
    let rec loop (fuel : Nat) (i : Nat) (acc : Array (Nat × Nat)) : Array (Nat × Nat) :=
      match fuel with
      | 0 => acc
      | fuel + 1 =>
          if i + n > h then
            acc
          else
            match ViE.PieceTree.findPatternInBytes haystack needle i with
            | some idx =>
                if idx + n > h then
                  acc
                else
                  let nextI := if idx + n > i then idx + n else i + 1
                  loop fuel nextI (acc.push (idx, idx + n))
            | none => acc
    loop (h + 1) 0 #[]

def collectMatchesInPieceTable (pt : PieceTable) (pattern : ByteArray) : Array (Nat × Nat) :=
  let n := pattern.size
  if n == 0 then
    #[]
  else
    let total := pt.tree.length
    let rec loop (fuel : Nat) (offset : Nat)
      (cache : Lean.RBMap Nat ByteArray compare) (order : Array Nat)
      (acc : Array (Nat × Nat)) : Array (Nat × Nat) :=
      match fuel with
      | 0 => acc
      | fuel + 1 =>
          if offset >= total then
            acc
          else
            let (res, cache', order') :=
              ViE.PieceTree.searchNext pt.tree pt pattern offset ViE.searchChunkSize false cache order 0
            match res with
            | some idx =>
                if idx + n > total then
                  acc
                else
                  let nextOffset := if idx + n > offset then idx + n else offset + 1
                  loop fuel nextOffset cache' order' (acc.push (idx, idx + n))
            | none => acc
    loop (total + 1) 0 Lean.RBMap.empty #[] #[]

def replaceFirstInLine (line : String) (old : String) (new : String) : String :=
  if old.isEmpty then
    line
  else
    let parts := line.splitOn old
    match parts with
    | [] => line
    | p :: rest =>
        if rest.isEmpty then
          line
        else
          p ++ new ++ (String.intercalate old rest)

def replaceAllInLine (line : String) (old : String) (new : String) : String :=
  if old.isEmpty then
    line
  else
    String.intercalate new (line.splitOn old)

def parseSubstitute (cmd : String) : Option (Bool × String × String × String) :=
  let (isGlobal, rest) :=
    if cmd.startsWith "%s/" then
      (true, (cmd.drop 3).toString)
    else if cmd.startsWith "s/" then
      (false, (cmd.drop 2).toString)
    else
      (false, "")
  if rest.isEmpty then
    none
  else
    let parts := rest.splitOn "/"
    match parts with
    | old :: new :: flagsParts =>
        let flags := String.intercalate "/" flagsParts
        some (isGlobal, old, new, flags)
    | _ => none

def parseGlobal (cmd : String) : Option (Bool × String × String) :=
  if cmd.startsWith "g/" || cmd.startsWith "v/" then
    let isInvert := cmd.startsWith "v/"
    let rest := (cmd.drop 2).toString
    let parts := rest.splitOn "/"
    match parts with
    | pat :: cmdParts =>
        let subcmd := String.intercalate "/" cmdParts
        some (isInvert, pat, (subcmd.trimAscii).toString)
    | _ => none
  else
    none

def lineRangeWithNewline (pt : PieceTable) (row : Row) : Option (Nat × Nat) :=
  match pt.getLineRange row.val with
  | some (startOff, len) =>
      let endOff := startOff + len
      if endOff < pt.tree.length then
        let b := ViE.PieceTree.getBytes pt.tree endOff 1 pt
        if b.size == 1 && b[0]! == (UInt8.ofNat 10) then
          some (startOff, len + 1)
        else
          some (startOff, len)
      else
        some (startOff, len)
  | none => none

def collectMatchesInLine (pt : PieceTable) (row : Row) (pattern : ByteArray) : Array (Nat × Nat) :=
  match pt.getLineRange row.val with
  | some (startOff, len) =>
      let bytes := ViE.PieceTree.getBytes pt.tree startOff len pt
      let localMatches := collectMatchesInBytes bytes pattern
      localMatches.map (fun (s, e) => (startOff + s, startOff + e))
  | none => #[]

def clampCursorToActiveBuffer (state : EditorState) : EditorState :=
  let cursor := state.getCursor
  state.setCursor (ViE.clampPointToActiveBuffer state cursor)

def execGlobal (cmd : String) (state : EditorState) : EditorState :=
  match parseGlobal cmd with
  | none => { state with message := s!"Invalid global: {cmd}" }
  | some (isInvert, pat, subcmd) =>
      if pat.isEmpty then
        { state with message := "Empty global pattern" }
      else
        let buf := state.getActiveBuffer
        let cursor := state.getCursor
        let cursorOffset := ViE.getOffsetFromPointInBufferWithTabStop buf cursor.row cursor.col state.config.tabStop |>.getD 0
        let patBytes := pat.toUTF8
        let lineCount := buf.lineCount
        let matchingRows : Array Nat := Id.run do
          let mut rows : Array Nat := #[]
          for row in [0:lineCount] do
            let hasMatch := (collectMatchesInLine buf.table ⟨row⟩ patBytes).size > 0
            if (if isInvert then !hasMatch else hasMatch) then
              rows := rows.push row
          return rows
        if subcmd == "d" then
          let ranges : Array (Nat × Nat) := Id.run do
            let mut acc : Array (Nat × Nat) := #[]
            for row in matchingRows do
              match lineRangeWithNewline buf.table ⟨row⟩ with
              | some (startOff, len) => acc := acc.push (startOff, startOff + len)
              | none => pure ()
            return acc
          let newTable := buf.table.applyDeletions cursorOffset ranges
          let newBuf := { buf with table := newTable, dirty := true }
          clampCursorToActiveBuffer (state.updateActiveBuffer (fun _ => newBuf))
        else if subcmd.startsWith "s/" then
          match parseSubstitute subcmd with
          | none => { state with message := s!"Invalid substitute: {subcmd}" }
          | some (_isGlobal, old, new, flags) =>
              if old.isEmpty then
                { state with message := "Empty substitute pattern" }
              else
                let doGlobal := flags.contains 'g'
                let oldBytes := old.toUTF8
                let matches1 : Array (Nat × Nat) := Id.run do
                  let mut acc : Array (Nat × Nat) := #[]
                  for row in matchingRows do
                    let lineMatches := collectMatchesInLine buf.table ⟨row⟩ oldBytes
                    if doGlobal then
                      acc := acc.append lineMatches
                    else
                      match lineMatches[0]? with
                      | some first => acc := acc.push first
                      | none => pure ()
                  return acc
                let newTable := buf.table.applyReplacements cursorOffset matches1 new
                let newBuf := { buf with table := newTable, dirty := true }
                clampCursorToActiveBuffer (state.updateActiveBuffer (fun _ => newBuf))
        else
          { state with message := s!"Unsupported global subcommand: {subcmd}" }

def execSubstitute (cmd : String) (state : EditorState) : EditorState :=
  match parseSubstitute cmd with
  | none => { state with message := s!"Invalid substitute: {cmd}" }
  | some (isGlobal, old, new, flags) =>
      if old.isEmpty then
        { state with message := "Empty substitute pattern" }
      else
        let doGlobal := flags.contains 'g'
        let buf := state.getActiveBuffer
        let cursor := state.getCursor
        let cursorOffset := ViE.getOffsetFromPointInBufferWithTabStop buf cursor.row cursor.col state.config.tabStop |>.getD 0
        let patBytes := old.toUTF8
        let matches1 :=
          if isGlobal then
            if doGlobal then
              collectMatchesInPieceTable buf.table patBytes
            else
              Id.run do
                let lineCount := buf.lineCount
                let mut acc : Array (Nat × Nat) := #[]
                for row in [0:lineCount] do
                  match buf.table.getLineRange row with
                  | some (startOff, len) =>
                      let bytes := ViE.PieceTree.getBytes buf.table.tree startOff len buf.table
                      match (collectMatchesInBytes bytes patBytes)[0]? with
                      | some (s, e) => acc := acc.push (startOff + s, startOff + e)
                      | none => pure ()
                  | none => pure ()
                return acc
          else
            match buf.table.getLineRange cursor.row.val with
            | some (startOff, len) =>
                let bytes := ViE.PieceTree.getBytes buf.table.tree startOff len buf.table
                let localMatches := collectMatchesInBytes bytes patBytes
                localMatches.map (fun (s, e) => (startOff + s, startOff + e))
            | none => #[]
        let matches2 :=
          if doGlobal || isGlobal then
            matches1
          else
            match matches1[0]? with
            | some first => #[first]
            | none => #[]
        let newTable := buf.table.applyReplacements cursorOffset matches2 new
        let newBuf := { buf with table := newTable, dirty := true }
        clampCursorToActiveBuffer (state.updateActiveBuffer (fun _ => newBuf))

def executeCommand (commands : CommandMap) (state : EditorState) : IO EditorState := do
  let fullCmd := state.inputState.commandBuffer
  if fullCmd.startsWith "s/" || fullCmd.startsWith "%s/" then
    return execSubstitute fullCmd state
  if fullCmd.startsWith "g/" || fullCmd.startsWith "v/" then
    return execGlobal fullCmd state
  let parts := fullCmd.splitOn " "
  match parts with
  | [] => return state
  | cmd :: args =>
    match commands.lookup cmd with
    | some action => action args state
    | none => return { state with message := s!"Unknown command: {fullCmd}" }

end ViE.Command