/-
Copyright (c) 2024 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Marc Huisinga
-/
prelude
import Lean.Server.InfoUtils
import Lean.Server.Completion.CompletionUtils

namespace Lean.Server.Completion
open Elab

private def findBest?
    (infoTree : InfoTree)
    (gt : α → α → Bool)
    (f : ContextInfo → Info → PersistentArray InfoTree → Option α)
    : Option α :=
  (Id.run <| infoTree.visitM (postNode := choose)).join
where
  choose
      (ctx : ContextInfo)
      (info : Info)
      (cs : PersistentArray InfoTree)
      (childValues : List (Option (Option α)))
      : Option α :=
    let bestChildValue := childValues.map (·.join) |>.foldl (init := none) fun v best =>
      if isBetter v best then
        v
      else
        best
    if let some v := f ctx info cs then
      if isBetter v bestChildValue then
        v
      else
        bestChildValue
    else
      bestChildValue
  isBetter (a b : Option α) : Bool :=
    match a, b with
    | none,   none   => false
    | some _, none   => true
    | none,   some _ => false
    | some a, some b => gt a b

/--
If there are `Info`s that contain `hoverPos` and have a nonempty `LocalContext`,
yields the closest one of those `Info`s.
Otherwise, yields the closest `Info` that contains `hoverPos` and has an empty `LocalContext`.
-/
private def findClosestInfoWithLocalContextAt?
    (hoverPos : String.Pos)
    (infoTree : InfoTree)
    : Option (ContextInfo × Info) :=
  findBest? infoTree isBetter fun ctx info _ =>
    if info.occursInOrOnBoundary hoverPos then
      (ctx, info)
    else
      none
where
  isBetter (a b : ContextInfo × Info) : Bool :=
    let (_, ia) := a
    let (_, ib) := b
    if !ia.lctx.isEmpty && ib.lctx.isEmpty then
      true
    else if ia.lctx.isEmpty && !ib.lctx.isEmpty then
      false
    else if ia.isSmaller ib then
      true
    else if ib.isSmaller ia then
      false
    else
      false

private def findSyntheticIdentifierCompletion?
    (hoverPos : String.Pos)
    (infoTree : InfoTree)
    : Option ContextualizedCompletionInfo := do
  let some (ctx, info) := findClosestInfoWithLocalContextAt? hoverPos infoTree
    | none
  let some stack := info.stx.findStack? (·.getRange?.any (·.contains hoverPos (includeStop := true)))
    | none
  let stack := stack.dropWhile fun (stx, _) => !(stx matches `($_:ident) || stx matches `($_:ident.))
  let some (stx, _) := stack.head?
    | none
  let isDotIdCompletion := stack.any fun (stx, _) => stx matches `(.$_:ident)
  if isDotIdCompletion then
    -- An identifier completion is never useful in a dotId completion context.
    none
  let some (id, danglingDot) :=
      match stx with
      | `($id:ident) => some (id.getId, false)
      | `($id:ident.) => some (id.getId, true)
      | _ => none
    | none
  let tailPos := stx.getTailPos?.get!
  let hoverInfo :=
    if hoverPos < tailPos then
      HoverInfo.inside (tailPos - hoverPos).byteIdx
    else
      HoverInfo.after
  some { hoverInfo, ctx, info := .id stx id danglingDot info.lctx none }

private partial def isCursorOnWhitespace (fileMap : FileMap) (hoverPos : String.Pos) : Bool :=
  fileMap.source.atEnd hoverPos || (fileMap.source.get hoverPos).isWhitespace

private partial def isCursorInProperWhitespace (fileMap : FileMap) (hoverPos : String.Pos) : Bool :=
  (fileMap.source.atEnd hoverPos || (fileMap.source.get hoverPos).isWhitespace)
    && (fileMap.source.get (hoverPos - ⟨1⟩)).isWhitespace

private partial def isSyntheticTacticCompletion
    (fileMap  : FileMap)
    (hoverPos : String.Pos)
    (cmdStx   : Syntax)
    : Bool := Id.run do
  let hoverFilePos := fileMap.toPosition hoverPos
  go hoverFilePos cmdStx 0
where
  go
      (hoverFilePos : Position)
      (stx : Syntax)
      (leadingWs : Nat)
      : Bool := Id.run do
    match stx.getPos?, stx.getTailPos? with
    | some startPos, some endPos =>
      let isCursorInCompletionRange :=
        startPos.byteIdx - leadingWs <= hoverPos.byteIdx
          && hoverPos.byteIdx <= endPos.byteIdx + stx.getTrailingSize
      if ! isCursorInCompletionRange then
        return false
      let mut wsBeforeArg := leadingWs
      for arg in stx.getArgs do
        if go hoverFilePos arg wsBeforeArg then
          return true
        -- We must account for the whitespace before an argument because the syntax nodes we use
        -- to identify tactic blocks only start *after* the whitespace following a `by`, and we
        -- want to provide tactic completions in that whitespace as well.
        -- This method of computing whitespace assumes that there are no syntax nodes without tokens
        -- after `by` and before the first proper tactic syntax.
        wsBeforeArg := arg.getTrailingSize
      return isCompletionInEmptyTacticBlock stx
        || isCompletionAfterSemicolon stx
        || isCompletionOnTacticBlockIndentation hoverFilePos stx
    | _, _ =>
      -- Empty tactic blocks typically lack ranges since they do not contain any tokens.
      -- We do not perform more precise range checking in this case because we assume that empty
      -- tactic blocks always occur within other syntax with ranges that let us narrow down the
      -- search to the degree that we can be sure that the cursor is indeed in this empty tactic
      -- block.
      return isCompletionInEmptyTacticBlock stx

  isCompletionOnTacticBlockIndentation
      (hoverFilePos : Position)
      (stx : Syntax)
      : Bool := Id.run do
    let some tacticsNode := getTacticsNode? stx
      | return false
    let some firstTacticPos := tacticsNode.getPos?
      | return false
    let firstTacticColumn := fileMap.toPosition firstTacticPos |>.column
    -- This ensures that we do not accidentally provide tactic completions in a term mode proof -
    -- tactic completions are only provided at the same indentation level as the other tactics in
    -- that tactic block.
    let isCursorInTacticBlock := hoverFilePos.column == firstTacticColumn
    return isCursorInProperWhitespace fileMap hoverPos && isCursorInTacticBlock

  isCompletionAfterSemicolon (stx : Syntax) : Bool := Id.run do
    let some tacticsNode := getTacticsNode? stx
      | return false
    let tactics := tacticsNode.getArgs
    -- We want to provide completions in the case of `skip;<CURSOR>`, so the cursor must only be on
    -- whitespace, not in proper whitespace.
    return isCursorOnWhitespace fileMap hoverPos && tactics.any fun tactic => Id.run do
      let some tailPos := tactic.getTailPos?
        | return false
      let isCursorAfterSemicolon :=
        tactic.isToken ";"
          && tailPos.byteIdx <= hoverPos.byteIdx
          && hoverPos.byteIdx <= tailPos.byteIdx + tactic.getTrailingSize
      return isCursorAfterSemicolon

  getTacticsNode? (stx : Syntax) : Option Syntax :=
    if stx.getKind == ``Parser.Tactic.tacticSeq1Indented then
      some stx[0]
    else if stx.getKind == ``Parser.Tactic.tacticSeqBracketed then
      some stx[1]
    else
      none

  isCompletionInEmptyTacticBlock (stx : Syntax) : Bool :=
    isCursorInProperWhitespace fileMap hoverPos && isEmptyTacticBlock stx

  isEmptyTacticBlock (stx : Syntax) : Bool :=
    stx.getKind == ``Parser.Tactic.tacticSeq && isEmpty stx
      || stx.getKind == ``Parser.Tactic.tacticSeq1Indented && isEmpty stx
      || stx.getKind == ``Parser.Tactic.tacticSeqBracketed && isEmpty stx[1]

  isEmpty : Syntax → Bool
    | .missing       => true
    | .ident ..      => false
    | .atom ..       => false
    | .node _ _ args => args.all isEmpty

private partial def findOutermostContextInfo? (i : InfoTree) : Option ContextInfo :=
  go i
where
  go (i : InfoTree) : Option ContextInfo := do
    match i with
  | .context ctx i =>
    match ctx with
    | .commandCtx ctxInfo =>
      some { ctxInfo with }
    | _ =>
      -- This shouldn't happen (see the `PartialContextInfo` docstring),
      -- but let's continue searching regardless
      go i
  | .node _ cs =>
    cs.findSome? go
  | .hole .. =>
    none

private def findSyntheticTacticCompletion?
    (fileMap  : FileMap)
    (hoverPos : String.Pos)
    (cmdStx   : Syntax)
    (infoTree : InfoTree)
    : Option ContextualizedCompletionInfo := do
  let ctx ← findOutermostContextInfo? infoTree
  if ! isSyntheticTacticCompletion fileMap hoverPos cmdStx then
    none
  -- Neither `HoverInfo` nor the syntax in `.tactic` are important for tactic completion.
  return { hoverInfo := HoverInfo.after, ctx, info := .tactic .missing }

private def findExpectedTypeAt (infoTree : InfoTree) (hoverPos : String.Pos) : Option (ContextInfo × Expr) := do
  let (ctx, .ofTermInfo i) ← infoTree.smallestInfo? fun i => Id.run do
      let some pos := i.pos?
        | return false
      let some tailPos := i.tailPos?
        | return false
      let .ofTermInfo ti := i
        | return false
      return ti.expectedType?.isSome && pos <= hoverPos && hoverPos <= tailPos
    | none
  (ctx, i.expectedType?.get!)

private partial def foldWithLeadingToken [Inhabited α]
    (f    : α → Option Syntax → Syntax → α)
    (init : α)
    (stx  : Syntax)
    : α :=
  let (_, r) := go none init stx
  r
where
  go [Inhabited α] (leadingToken? : Option Syntax) (acc : α) (stx : Syntax) : Option Syntax × α :=
    let acc := f acc leadingToken? stx
    match stx with
    | .missing  => (none, acc)
    | .atom ..  => (stx, acc)
    | .ident .. => (stx, acc)
    | .node _ _ args => Id.run do
      let mut acc := acc
      let mut lastToken? := none
      for arg in args do
        let (lastToken'?, acc') := go (lastToken? <|> leadingToken?) acc arg
        lastToken? := lastToken'? <|> lastToken?
        acc := acc'
      return (lastToken?, acc)

private def findWithLeadingToken?
    (p : Option Syntax → Syntax → Bool)
    (stx : Syntax)
    : Option Syntax :=
  foldWithLeadingToken (stx := stx) (init := none) fun foundStx? leadingToken? stx =>
    match foundStx? with
    | some foundStx => foundStx
    | none =>
      if p leadingToken? stx then
        some stx
      else
        none

private def isSyntheticStructFieldCompletion
    (fileMap  : FileMap)
    (hoverPos : String.Pos)
    (cmdStx   : Syntax)
    : Bool := Id.run do
  let isCursorOnWhitespace := isCursorOnWhitespace fileMap hoverPos
  let isCursorInProperWhitespace := isCursorInProperWhitespace fileMap hoverPos
  if ! isCursorOnWhitespace then
    return false

  let hoverFilePos := fileMap.toPosition hoverPos

  return Option.isSome <| findWithLeadingToken? (stx := cmdStx) fun leadingToken? stx => Id.run do
    let some leadingToken := leadingToken?
      | return false

    if stx.getKind != ``Parser.Term.structInstFields then
      return false

    let fieldsAndSeps := stx[0].getArgs
    let some outerBoundsStart := leadingToken.getTailPos? (canonicalOnly := true)
      | return false
    let some outerBoundsStop :=
        stx.getTrailingTailPos? (canonicalOnly := true)
          <|> leadingToken.getTrailingTailPos? (canonicalOnly := true)
      | return false
    let outerBounds : String.Range := ⟨outerBoundsStart, outerBoundsStop⟩

    let isCompletionInEmptyBlock :=
      fieldsAndSeps.isEmpty && outerBounds.contains hoverPos (includeStop := true)
    if isCompletionInEmptyBlock then
      return true

    let isCompletionAfterSep := fieldsAndSeps.zipIdx.any fun (fieldOrSep, i) => Id.run do
      if i % 2 == 0 || !fieldOrSep.isAtom then
        return false
      let sep := fieldOrSep
      let some sepTailPos := sep.getTailPos?
        | return false
      return sepTailPos <= hoverPos
        && hoverPos.byteIdx <= sepTailPos.byteIdx + sep.getTrailingSize
    if isCompletionAfterSep then
      return true

    let isCompletionOnIndentation := Id.run do
      if ! isCursorInProperWhitespace then
        return false
      let some firstFieldPos := stx.getPos?
        | return false
      let firstFieldColumn := fileMap.toPosition firstFieldPos |>.column
      let isCursorInBlock := hoverFilePos.column == firstFieldColumn
      return isCursorInBlock
    return isCompletionOnIndentation

private def findSyntheticFieldCompletion?
  (fileMap  : FileMap)
  (hoverPos : String.Pos)
  (cmdStx   : Syntax)
  (infoTree : InfoTree)
  : Option ContextualizedCompletionInfo := do
  if ! isSyntheticStructFieldCompletion fileMap hoverPos cmdStx then
    none
  let (ctx, expectedType) ← findExpectedTypeAt infoTree hoverPos
  let .const typeName _ := expectedType.getAppFn
    | none
  if ! isStructure ctx.env typeName then
    none
  return { hoverInfo := HoverInfo.after, ctx, info := .fieldId .missing none .empty typeName }

def findSyntheticCompletions
    (fileMap  : FileMap)
    (hoverPos : String.Pos)
    (cmdStx   : Syntax)
    (infoTree : InfoTree)
    : Array ContextualizedCompletionInfo :=
  let syntheticCompletionData? : Option ContextualizedCompletionInfo :=
    findSyntheticTacticCompletion? fileMap hoverPos cmdStx infoTree <|>
      findSyntheticFieldCompletion? fileMap hoverPos cmdStx infoTree <|>
        findSyntheticIdentifierCompletion? hoverPos infoTree
  syntheticCompletionData?.map (#[·]) |>.getD #[]

end Lean.Server.Completion
