text editor inspired vim and yi
import Lean
import ViE.Basic
import ViE.State
import ViE.Terminal
import ViE.UI
import ViE.Actions
import ViE.IO
import ViE.Config
import ViE.Checkpoint
import ViE.Loader

namespace ViE


/-- Resolve startup arguments into workspace path and filename. -/
def resolveStartupTarget (filename : Option String) : IO (Option String × Option String) := do
  match filename with
  | some path =>
    let filePath := System.FilePath.mk path
    if ← filePath.pathExists then
      if ← filePath.isDir then
        -- It's a directory, use as workspace
        pure (some path, none)
      else
        -- It's a file, use its parent directory as workspace
        let parentDir := match filePath.parent with
          | some p => p.toString
          | none => "."
        pure (some parentDir, some path)
    else
      -- Path doesn't exist, treat as new file
      pure (none, some path)
  | none => pure (none, none)

def clampCursorInBuffer (tabStop : Nat) (buffer : FileBuffer) (row : Row) (col : Col) : Point :=
  let lineCount := buffer.lineCount
  let clampedRow := if lineCount == 0 then 0 else min row.val (lineCount - 1)
  let lineLen := ViE.getLineLengthFromBufferWithTabStop buffer ⟨clampedRow⟩ tabStop |>.getD 0
  let clampedCol := if lineLen == 0 then 0 else min col.val (lineLen - 1)
  { row := ⟨clampedRow⟩, col := ⟨clampedCol⟩ }

/-- Build startup workspace from a restored checkpoint session. -/
def buildRestoredWorkspace (settings : EditorConfig) (workspacePath : Option String)
  (files : List String) (activeIdx : Nat) (cursors : List (Nat × Nat)) : IO WorkspaceState := do
  let mut loaded : Array FileBuffer := #[]
  let mut nextId := 0
  for fname in files do
    let buf ← loadBufferByteArrayWithConfig fname settings
    loaded := loaded.push {
      buf with
        id := nextId
        table := { buf.table with undoLimit := settings.historyLimit }
    }
    nextId := nextId + 1

  let wsName := match workspacePath with
    | some path => (System.FilePath.fileName path).getD defaultWorkspaceName
    | none => defaultWorkspaceName

  if loaded.isEmpty then
    let empty : FileBuffer := {
      id := 0
      filename := none
      dirty := false
      table := PieceTable.fromString "" settings.searchBloomBuildLeafBits
      missingEol := false
      cache := { lineMap := Lean.RBMap.empty, rawLineMap := Lean.RBMap.empty, lineIndexMap := Lean.RBMap.empty }
    }
    return {
      name := wsName
      rootPath := workspacePath
      buffers := [empty]
      nextBufferId := 1
      layout := .window 0 { initialView with bufferId := 0 }
      activeWindowId := 0
      nextWindowId := 1
    }

  let activeIdx := if activeIdx < loaded.size then activeIdx else 0
  let activeBuf := loaded[activeIdx]!
  let (row, col) := List.getD cursors activeIdx (0, 0)
  let cursor := clampCursorInBuffer settings.tabStop activeBuf ⟨row⟩ ⟨col⟩
  return {
    name := wsName
    rootPath := workspacePath
    buffers := loaded.toList
    nextBufferId := loaded.size
    layout := .window 0 { initialView with bufferId := activeBuf.id, cursor := cursor }
    activeWindowId := 0
    nextWindowId := 1
  }

/-- Main event loop. -/
def loop (config : Config) (state0 : EditorState) : IO Unit := do
  let mut state := state0
  let mut quit := false
  while !quit do
    -- Only render if state is dirty
    state ← if state.dirty then
      ViE.UI.render state
    else
      pure state

    -- Reset dirty flag after render (or if it was already clean, keep it clean)
    state := { state with dirty := false }

    let currentTime ← IO.monoMsNow
    let c ← ViE.Terminal.readKey

    -- Update window size
    let (rows, cols) ← ViE.Terminal.getWindowSize
    let resized := rows != state.windowHeight || cols != state.windowWidth
    state := { state with windowHeight := rows, windowWidth := cols, dirty := state.dirty || resized }

    -- Handle the case where readKey returns None (no input available)
    match c with
    | none =>
      -- Check for timeout
      let (stateAfterTimeout, keys) := ViE.checkTimeout state currentTime
      let stateAfterTimeout := ViE.maybeRunIncrementalSearch stateAfterTimeout currentTime
      if keys.isEmpty then
         -- No input and no timeout events, sleep briefly to avoid busy loop
         IO.sleep 10 -- 10ms
         state := stateAfterTimeout
      else
         -- Process timeout keys (e.g. flushed Esc)
         let mut finalState := stateAfterTimeout
         for key in keys do
           finalState ← ViE.update config finalState key
         if finalState.shouldQuit then
           quit := true
         else
           state := { finalState with dirty := true }

    | some ch =>
      -- Parse the character into keys using the new parseKey function
      let (stateAfterParse, keys) := ViE.parseKey state ch currentTime

      -- Process all returned keys
      let mut finalState := stateAfterParse
      for key in keys do
        finalState ← ViE.update config finalState key

      if finalState.shouldQuit then
        quit := true
      else
        state := { finalState with dirty := true }

/-- Entry point for the editor.
    Accepts a configuration and command line arguments. -/
def start (config : Config) (args : List String) : IO Unit := do

  -- Check for checkpoint session
  let checkpoint ← ViE.Checkpoint.loadSession

  -- Determine initial args (files to open)
  -- If we have a checkpoint and no args, use the checkpoint files.
  -- Otherwise use provided args.
  let (startArgs, restoreMeta) := match checkpoint with
    | some (files, activeIdx, cursors) =>
       if args.isEmpty then
         (files, some (activeIdx, cursors))
       else
         (args, none)
    | none => (args, none)

  let filename := startArgs.head?

  -- Check if the argument is a directory
  let (workspacePath, actualFilename) ← resolveStartupTarget filename

  let restoreFromCheckpoint := args.isEmpty && restoreMeta.isSome && !startArgs.isEmpty
  let state ←
    if restoreFromCheckpoint then
      match restoreMeta with
      | some (activeIdx, cursors) => do
          let restored ← buildRestoredWorkspace config.settings workspacePath startArgs activeIdx cursors
          let s := { ViE.initialState with
            config := config.settings
            message := s!"Restored {restored.buffers.length} file(s)"
          }
          pure <| s.updateCurrentWorkspace (fun _ => restored)
      | none =>
          pure { ViE.initialState with config := config.settings }
    else do
      -- Load buffer if file exists
      let initialBuffer ← match actualFilename with
        | some fname => loadBufferByteArrayWithConfig fname config.settings
        | none => pure {
            id := 0
            filename := actualFilename
            dirty := false
            table := PieceTable.fromString "" config.settings.searchBloomBuildLeafBits
            missingEol := false
            cache := { lineMap := Lean.RBMap.empty, rawLineMap := Lean.RBMap.empty, lineIndexMap := Lean.RBMap.empty }
          }

      -- Check if initial load had an error
      let firstLine := getLineFromBuffer initialBuffer 0 |>.getD ""
      let hasError := firstLine.startsWith "Error loading file:"

      let s := { ViE.initialState with
        config := config.settings,
        message := if hasError then firstLine
                   else match actualFilename with
                     | some f => s!"\"{f}\" [Read]"
                     | none => match workspacePath with
                       | some ws => s!"Workspace: {ws}"
                       | none => "New File"
      }

      -- Update the first workspace with the initial buffer and root path
      pure <| s.updateCurrentWorkspace fun ws =>
        let wsName := match workspacePath with
          | some path => (System.FilePath.fileName path).getD ws.name
          | none => ws.name
        { ws with
            name := wsName,
            rootPath := workspacePath,
            buffers := [initialBuffer],
            nextBufferId := 1
        }

  ViE.Terminal.enableRawMode
  try
    loop config state
  finally
    ViE.Terminal.disableRawMode
    ViE.Terminal.clearScreen
    IO.println "Bye!"

end ViE