/-
Copyright (c) 2021 Wojciech Nawrocki. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Authors: Wojciech Nawrocki
-/
module

prelude
public import Lean.DocString
public import Lean.PrettyPrinter
meta import Lean.Parser.Term

public section

namespace Lean.Elab

open Lean.Parser.Tactic.Doc (alternativeOfTactic getTacticExtensionString)

/-- Elaborator information with elaborator context.

It can be thought of as a "thunked" elaboration computation that allows us
to retroactively extract type information, symbol locations, etc.
through arbitrary invocations of `runMetaM` (where the necessary context and state
can be reconstructed from `ctx` and `info.lctx`).

W.r.t. widgets, this is used to tag different parts of expressions in `ppExprTagged`.
This is the input to the RPC call `Lean.Widget.InteractiveDiagnostics.infoToInteractive`.
It carries over information about delaborated
`Info` nodes in a `CodeWithInfos`, and the associated pretty-printing
functionality is purpose-specific to showing the contents of infoview popups.

For use in standard LSP go-to-definition (see `Lean.Server.FileWorker.locationLinksOfInfo`),
all the elaborator information we need for similar tasks is already fully recoverable via
the `InfoTree` structure (see `Lean.Elab.InfoTree.visitM`).
There we use this as a convenience wrapper for queried nodes (e.g. the return value of
`Lean.Elab.InfoTree.hoverableInfoAt?`). It also includes the children info nodes
as additional context (this is unused in the RPC case, as delaboration has no notion of child nodes).

NOTE: This type is for internal use in the infoview/LSP. It should not be used in user widgets.
-/
structure InfoWithCtx where
  ctx  : Elab.ContextInfo
  info : Elab.Info
  children : PersistentArray InfoTree

/--
Visit nodes, passing in a surrounding context (the innermost one combined with all outer ones) and
accumulating results on the way back up. If `preNode` returns `false`, the children of the current
node are skipped and `postNode` is invoked with an empty list of results.
-/
partial def InfoTree.visitM [Monad m]
    (preNode  : ContextInfo → Info → (children : PersistentArray InfoTree) → m Bool := fun _ _ _ => pure true)
    (postNode : ContextInfo → Info → (children : PersistentArray InfoTree) → List (Option α) → m α)
    (ctx? : Option ContextInfo := none) : InfoTree → m (Option α) :=
  go ctx?
where go
  | ctx?, context ctx t => go (ctx.mergeIntoOuter? ctx?) t
  | some ctx, node i cs => do
    let visitChildren ← preNode ctx i cs
    if !visitChildren then
      postNode ctx i cs []
    else
      let as ← cs.toList.mapM (go <| i.updateContext? ctx)
      postNode ctx i cs as
  | none, node .. => panic! "unexpected context-free info tree node"
  | _, hole .. => pure none

/-- `InfoTree.visitM` specialized to `Unit` return type -/
def InfoTree.visitM' [Monad m]
    (preNode  : ContextInfo → Info → (children : PersistentArray InfoTree) → m Bool := fun _ _ _ => pure true)
    (postNode : ContextInfo → Info → (children : PersistentArray InfoTree) → m Unit := fun _ _ _ => pure ())
    (ctx? : Option ContextInfo := none) (t : InfoTree) : m Unit :=
  t.visitM preNode (fun ci i cs _ => postNode ci i cs) ctx? |> discard

/--
  Visit nodes bottom-up, passing in a surrounding context (the innermost one) and the union of nested results (empty at leaves). -/
def InfoTree.collectNodesBottomUpM [Monad m] (p : ContextInfo → Info → PersistentArray InfoTree → List α → m (List α)) (i : InfoTree) : m (List α) :=
  (·.getD []) <$> i.visitM (m := m) (postNode := fun ci i cs as => do p ci i cs (as.filterMap id).flatten)

/--
  Visit nodes bottom-up, passing in a surrounding context (the innermost one) and the union of nested results (empty at leaves). -/
def InfoTree.collectNodesBottomUp (p : ContextInfo → Info → PersistentArray InfoTree → List α → List α) (i : InfoTree) : List α :=
  Id.run <| i.collectNodesBottomUpM (pure <| p · · · ·)

/--
  For every branch of the `InfoTree`, find the deepest node in that branch for which `p` returns
  `some _`  and return the union of all such nodes. The visitor `p` is given a node together with
  its innermost surrounding `ContextInfo`. -/
partial def InfoTree.deepestNodesM [Monad m] (p : ContextInfo → Info → PersistentArray InfoTree → m (Option α)) (infoTree : InfoTree) : m (List α) :=
  infoTree.collectNodesBottomUpM fun ctx i cs rs => do
    if rs.isEmpty then
      match ← p ctx i cs with
      | some r => return [r]
      | none   => return []
    else
      return rs

/--
  For every branch of the `InfoTree`, find the deepest node in that branch for which `p` returns
  `some _`  and return the union of all such nodes. The visitor `p` is given a node together with
  its innermost surrounding `ContextInfo`. -/
partial def InfoTree.deepestNodes (p : ContextInfo → Info → PersistentArray InfoTree → Option α) (infoTree : InfoTree) : List α :=
  Id.run <| infoTree.deepestNodesM (pure <| p · · ·)

partial def InfoTree.foldInfo (f : ContextInfo → Info → α → α) (init : α) : InfoTree → α :=
  go none init
where go ctx? a
  | context ctx t => go (ctx.mergeIntoOuter? ctx?) a t
  | node i ts =>
    let a := match ctx? with
      | none => a
      | some ctx => f ctx i a
    ts.foldl (init := a) (go <| i.updateContext? ctx?)
  | hole _ => a

partial def InfoTree.foldInfoM [Monad m] (f : ContextInfo → Info → α → m α) (init : α) : InfoTree → m α :=
  go none init
where go ctx? a
  | context ctx t => go (ctx.mergeIntoOuter? ctx?) a t
  | node i ts => do
    let a ← match ctx? with
      | none => pure a
      | some ctx => f ctx i a
    ts.foldlM (init := a) (go <| i.updateContext? ctx?)
  | hole _ => pure a

/--
Fold an info tree as follows, while ensuring that the correct `ContextInfo` is supplied at each stage:

 * Nodes are combined with the initial value `init` using `f`, and the result is then combined with the children using a left fold
 * On InfoTree holes, we just return the initial value.

This is like `InfoTree.foldInfo`, but it also passes the whole node to `f` instead of just the head.
-/
partial def InfoTree.foldInfoTree (init : α) (f : ContextInfo → InfoTree → α → α) : InfoTree → α :=
  go none init
where
  /-- `foldInfoTree.go` is like `foldInfoTree` but with an additional outer context parameter `ctx?`. -/
  go ctx? a
  | context ctx t => go (ctx.mergeIntoOuter? ctx?) a t
  | t@(node i ts) =>
    let a := match ctx? with
      | none => a
      | some ctx => f ctx t a
    ts.foldl (init := a) (go <| i.updateContext? ctx?)
  | hole _ => a

def InfoTree.collectTermInfoM [Monad m] (t : InfoTree) (f : ContextInfo → TermInfo → m (Option α)) : m (List α) :=
  t.foldInfoM (init := []) fun ctx info result =>
    match info with
    | Info.ofTermInfo ti => do
      match ← f ctx ti with
      | some a => return a :: result
      | none => return result
    | _ => return result

def Info.isTerm : Info → Bool
  | ofTermInfo _ => true
  | _ => false

def Info.isCompletion : Info → Bool
  | ofCompletionInfo .. => true
  | _ => false

def InfoTree.getCompletionInfos (infoTree : InfoTree) : Array (ContextInfo × CompletionInfo) :=
  infoTree.foldInfo (init := #[]) fun ctx info result =>
    match info with
    | Info.ofCompletionInfo info => result.push (ctx, info)
    | _ => result

def Info.stx : Info → Syntax
  | ofTacticInfo i         => i.stx
  | ofTermInfo i           => i.stx
  | ofPartialTermInfo i    => i.stx
  | ofCommandInfo i        => i.stx
  | ofMacroExpansionInfo i => i.stx
  | ofOptionInfo i         => i.stx
  | ofErrorNameInfo i      => i.stx
  | ofFieldInfo i          => i.stx
  | ofCompletionInfo i     => i.stx
  | ofCustomInfo i         => i.stx
  | ofUserWidgetInfo i     => i.stx
  | ofFVarAliasInfo _      => .missing
  | ofFieldRedeclInfo i    => i.stx
  | ofDelabTermInfo i      => i.stx
  | ofChoiceInfo i         => i.stx
  | ofDocInfo i            => i.stx
  | ofDocElabInfo i        => i.stx

def Info.lctx : Info → LocalContext
  | .ofTermInfo i           => i.lctx
  | .ofFieldInfo i          => i.lctx
  | .ofDelabTermInfo i      => i.lctx
  | .ofMacroExpansionInfo i => i.lctx
  | .ofCompletionInfo i     => i.lctx
  | _                       => LocalContext.empty

def Info.pos? (i : Info) : Option String.Pos.Raw :=
  i.stx.getPos? (canonicalOnly := true)

def Info.tailPos? (i : Info) : Option String.Pos.Raw :=
  i.stx.getTailPos? (canonicalOnly := true)

def Info.range? (i : Info) : Option Lean.Syntax.Range :=
  i.stx.getRange? (canonicalOnly := true)

def Info.contains (i : Info) (pos : String.Pos.Raw) (includeStop := false) : Bool :=
  i.range?.any (·.contains pos includeStop)

def Info.size? (i : Info) : Option String.Pos.Raw := do
  let pos ← i.pos?
  let tailPos ← i.tailPos?
  return tailPos.unoffsetBy pos

-- `Info` without position information are considered to have "infinite" size
def Info.isSmaller (i₁ i₂ : Info) : Bool :=
  match i₁.size?, i₂.size? with
  | some sz₁, some sz₂ => sz₁ < sz₂
  | some _, none => true
  | _, _ => false

def Info.occursInside? (i : Info) (hoverPos : String.Pos.Raw) : Option String.Pos.Raw := do
  let headPos ← i.pos?
  let tailPos ← i.tailPos?
  guard (headPos ≤ hoverPos && hoverPos < tailPos)
  return hoverPos.unoffsetBy headPos

def Info.occursInOrOnBoundary (i : Info) (hoverPos : String.Pos.Raw) : Bool := Id.run do
  let some headPos := i.pos?
    | return false
  let some tailPos := i.tailPos?
    | return false
  return headPos <= hoverPos && hoverPos <= tailPos

def InfoTree.smallestInfo? (p : Info → Bool) (t : InfoTree) : Option (ContextInfo × Info) :=
  let ts := t.deepestNodes fun ctx i _ => if p i then some (ctx, i) else none

  let infos := ts.filterMap fun (ci, i) => do
    let diff := (← i.pos?).byteDistance (← i.tailPos?)
    return (diff, ci, i)

  infos.toArray.getMax? (fun a b => a.1 > b.1) |>.map fun (_, ci, i) => (ci, i)

structure HoverableInfoPrio where
  -- Prefer results directly *after* the hover position (only matters for `includeStop = true`; see #767)
  isHoverPosOnStop : Bool
  -- Relying on the info tree structure is _not_ sufficient for choosing the smallest surrounding node:
  -- `⟨x⟩` expands to an application of a canonical syntax with the span of the anonymous constructor to `x`,
  -- i.e. there are two info tree siblings whose spans are not disjoint and we should choose the smaller node
  -- surrounding the cursor.
  size : Nat
  -- Prefer results for constants over variables (which overlap at declaration names)
  isVariableInfo : Bool
  -- Prefer non-partial infos over partial infos
  isPartialTermInfo : Bool
  deriving BEq

instance : Ord HoverableInfoPrio where
  compare i1 i2 := Id.run do
    if i1.isHoverPosOnStop && ! i2.isHoverPosOnStop then
      return .lt
    if ! i1.isHoverPosOnStop && i2.isHoverPosOnStop then
      return .gt
    if i1.size > i2.size then
      return .lt
    if i1.size < i2.size then
      return .gt
    if i1.isVariableInfo && ! i2.isVariableInfo then
      return .lt
    if ! i1.isVariableInfo && i2.isVariableInfo then
      return .gt
    if i1.isPartialTermInfo && ! i2.isPartialTermInfo then
      return .lt
    if ! i1.isPartialTermInfo && i2.isPartialTermInfo then
      return .gt
    return .eq

instance : LE HoverableInfoPrio := leOfOrd
instance : Max HoverableInfoPrio := maxOfLe

/-- Find an info node, if any, which should be shown on hover/cursor at position `hoverPos`. -/
partial def InfoTree.hoverableInfoAtM? [Monad m] (t : InfoTree) (hoverPos : String.Pos.Raw) (includeStop := false)
    (filter : (ctx : ContextInfo) → (info : Info) → (children : PersistentArray InfoTree) →
      (results : List (HoverableInfoPrio × InfoWithCtx)) →
      m (List (HoverableInfoPrio × InfoWithCtx)) := fun _ _ _ results => pure results) :
    m (Option InfoWithCtx) := do
  let postNode ctx info children results : m (Option (HoverableInfoPrio × InfoWithCtx)) := do
    let results ← results.filterMap (fun r? => r?.join) |> filter ctx info children
    let maxPrio? := results.map (·.1) |>.max?
    let bestResult? := results.find? (·.1 == maxPrio?)
    if let some (prio, i) := bestResult? then
      -- Prefer innermost results
      return (prio, i)
    -- We skip `info` nodes associated with the `nullKind` and `withAnnotateState` because they are used by tactics (e.g. `rewrite`) to control
    -- which goal is displayed in the info views. See issue #1403.
    let isAuxInfo := info.stx.isOfKind nullKind
      || info.toElabInfo?.any (·.elaborator == `Lean.Elab.Tactic.evalWithAnnotateState)
    if isAuxInfo then
      return none
    let isEligibleInfoKind := info matches .ofFieldInfo _ | .ofOptionInfo _ | .ofErrorNameInfo _ || info.toElabInfo?.isSome
    let some r := info.stx.getRange? (canonicalOnly := true)
      | return none
    if ! r.contains hoverPos includeStop || ! isEligibleInfoKind then
      return none
    let priority : HoverableInfoPrio := {
      isHoverPosOnStop := r.stop == hoverPos
      size := r.start.byteDistance r.stop
      isVariableInfo := info matches .ofTermInfo { expr := .fvar .., .. }
      isPartialTermInfo := info matches .ofPartialTermInfo ..
    }
    let result := { ctx, info, children }
    return some (priority, result)
  let results ← t.visitM (m := m) (postNode := postNode)
  let bestResult? : Option InfoWithCtx := results.join.map (·.2)
  if let some bestResult := bestResult? then
    if let .ofTermInfo ti := bestResult.info then
      if ti.expr.isSyntheticSorry then
        return none
  return bestResult?

def Info.type? (i : Info) : MetaM (Option Expr) :=
  match i with
  | Info.ofTermInfo ti      => Meta.inferType ti.expr
  | Info.ofFieldInfo fi     => Meta.inferType fi.val
  | Info.ofDelabTermInfo ti => Meta.inferType ti.expr
  | _ => return none

def Info.docString? (i : Info) : MetaM (Option String) := do
  let env ← getEnv
  match i with
  | .ofDelabTermInfo { docString? := some s, .. } => return s
  | .ofTermInfo ti
  | .ofDelabTermInfo ti =>
    if let some n := ti.expr.constName? then
      return (← findDocString? env n)
  | .ofFieldInfo fi => return ← findDocString? env fi.projName
  | .ofOptionInfo oi =>
    if let some doc ← findDocString? env oi.declName then
      return doc
    if let some decl := (← getOptionDecls).find? oi.optionName then
      return decl.descr
    return none
  | .ofErrorNameInfo eni => do
    let some errorExplanation := getErrorExplanationRaw? (← getEnv) eni.errorName | return none
    return errorExplanation.summaryWithSeverity
  | .ofDocInfo di =>
    return (← findDocString? env di.stx.getKind)
  | .ofDocElabInfo dei =>
    return (← findDocString? env dei.name)
  | _ => pure ()
  if let some ei := i.toElabInfo? then
    return ← findDocString? env ei.stx.getKind <||> findDocString? env ei.elaborator
  return none

/-- Construct a hover popup, if any, from an info node in a context.-/
def Info.fmtHover? (ci : ContextInfo) (i : Info) : IO (Option FormatWithInfos) := do
  ci.runMetaM i.lctx do
    let mut fmts := #[]
    let mut infos := ∅
    let modFmt ← try
      let (termFmt, modFmt) ← fmtTermAndModule?
      if let some f := termFmt then
        fmts := fmts.push f.fmt
        infos := f.infos
      pure modFmt
    catch _ => pure none
    if let some m ← i.docString? then
      fmts := fmts.push m
    if let some f := modFmt then
      fmts := fmts.push f
    if fmts.isEmpty then
      return none
    else
      return some ⟨f!"\n***\n".joinSep fmts.toList, infos⟩

where
  fmtModule? (decl : Name) : MetaM (Option Format) := do
    let some mod ← findModuleOf? decl | return none
    return some f!"*import {mod}*"

  fmtTermAndModule? : MetaM (Option FormatWithInfos × Option Format) := do
    match i with
    | Info.ofTermInfo ti =>
      let e ← instantiateMVars ti.expr
      if e.isSort then
        -- Types of sorts are funny to look at in widgets, but ultimately not very helpful
        return (none, none)
      let tp ← instantiateMVars (← Meta.inferType e)
      let tpFmt ← Meta.ppExpr tp
      if let .const c _ := e then
        let eFmt ← PrettyPrinter.ppSignature c
        return (some { eFmt with fmt := f!"```lean\n{eFmt.fmt}\n```" }, ← fmtModule? c)
      let eFmt ← Meta.ppExpr e
      let lctx ← getLCtx
      -- Try not to show too scary internals
      let showTerm :=
        if ti.isDisplayableTerm then
          true
        else
          if let .fvar _ := e then
            if let some ldecl := lctx.findFVar? e then
              !ldecl.userName.hasMacroScopes
            else
              false
          else
            isAtomicFormat eFmt
      let fmt := if showTerm then f!"{eFmt} : {tpFmt}" else tpFmt
      return (some f!"```lean\n{fmt}\n```", none)
    | Info.ofFieldInfo fi =>
      let tp ← Meta.inferType fi.val
      let tpFmt ← Meta.ppExpr tp
      return (some f!"```lean\n{fi.fieldName} : {tpFmt}\n```", none)
    | _ => return (none, none)

  isAtomicFormat : Format → Bool
    | Std.Format.text _    => true
    | Std.Format.group f _ => isAtomicFormat f
    | Std.Format.nest _ f  => isAtomicFormat f
    | Std.Format.tag _ f   => isAtomicFormat f
    | _                    => false

structure GoalsAtResult where
  ctxInfo    : ContextInfo
  tacticInfo : TacticInfo
  useAfter   : Bool
  /-- Whether the tactic info is further indented than the hover position. -/
  indented   : Bool
  -- for overlapping goals, only keep those of the highest reported priority
  priority   : Nat

/--
  Try to retrieve `TacticInfo` for `hoverPos`.
  We retrieve all `TacticInfo` nodes s.t. `hoverPos` is inside the node's range plus trailing whitespace.
  We usually prefer the innermost such nodes so that for composite tactics such as `induction`, we show the nested proofs' states.
  However, if `hoverPos` is after the tactic, we prefer nodes that are not indented relative to it, meaning that e.g. at `|` in
  ```lean
  have := by
    exact foo
  |
  ```
  we show the (final, see below) state of `have`, not `exact`.

  Moreover, we instruct the LSP server to use the state after tactic execution if
  - the hover position is after the info's start position *and*
  - there is no nested tactic info after the hover position (tactic combinators should decide for themselves
    where to show intermediate states by calling `withTacticInfoContext`) -/
partial def InfoTree.goalsAt? (text : FileMap) (t : InfoTree) (hoverPos : String.Pos.Raw) : List GoalsAtResult :=
  let gs := t.collectNodesBottomUp fun ctx i cs gs => Id.run do
    let Info.ofTacticInfo ti := i
      | return gs
    let (some pos, some tailPos) := (i.pos?, i.tailPos?)
      | return gs
    let trailSize := i.stx.getTrailingSize
    -- show info at EOF even if strictly outside token + trail
    let atEOF := tailPos.byteIdx + trailSize == text.source.rawEndPos.byteIdx
    -- include at least one trailing character (see also `priority` below)
    if pos ≤ hoverPos ∧ (hoverPos.byteIdx < tailPos.byteIdx + max 1 trailSize || atEOF) then
      -- overwrite bottom-up results according to "innermost" heuristics documented above
      if gs.isEmpty || hoverPos ≥ tailPos && gs.all (·.indented) then
        return [{
          ctxInfo := ctx
          tacticInfo := ti
          useAfter := hoverPos > pos && !cs.any (hasNestedTactic pos tailPos)
          -- consider every position unindented after an empty `by` to support "hanging" `by` uses
          indented := (text.toPosition pos).column > (text.toPosition hoverPos).column && !isEmptyBy ti.stx
          -- use goals just before cursor as fall-back only
          -- thus for `(by foo)`, placing the cursor after `foo` shows its state as long
          -- as there is no state on `)`
          priority := if hoverPos.byteIdx == tailPos.byteIdx + trailSize then 0 else 1
        }]
    return gs
  let maxPrio? := gs.map (·.priority) |>.max?
  gs.filter (some ·.priority == maxPrio?)
where
  hasNestedTactic (pos tailPos) : InfoTree → Bool
    | InfoTree.node i@(Info.ofTacticInfo _) cs => Id.run do
      if let `(by $_) := i.stx then
        return false  -- ignore term-nested proofs such as in `simp [show p by ...]`
      if let (some pos', some tailPos') := (i.pos?, i.tailPos?) then
        -- ignore preceding nested infos
        -- ignore nested infos of the same tactic, e.g. from expansion
        if tailPos' > hoverPos && (pos', tailPos') != (pos, tailPos) then
          return true
      cs.any (hasNestedTactic pos tailPos)
    | InfoTree.node (Info.ofMacroExpansionInfo _) cs =>
      cs.any (hasNestedTactic pos tailPos)
    | _ => false
  isEmptyBy (stx : Syntax) : Bool :=
    -- there are multiple `by` kinds with the same structure
    stx.getNumArgs == 2 && stx[0].isToken "by" && stx[1].getNumArgs == 1 && stx[1][0].isMissing


partial def InfoTree.termGoalAt? (t : InfoTree) (hoverPos : String.Pos.Raw) : Option InfoWithCtx :=
  -- In the case `f a b`, where `f` is an identifier, the term goal at `f` should be the goal for the full application `f a b`.
  let filter ctx info children results :=
    if info.stx.isOfKind ``Parser.Term.app && info.stx[0].isIdent then
      results.filter (·.2.info.stx != info.stx[0])
    else
      results
  hoverableInfoAtM? (m := Id) t hoverPos (includeStop := true) (filter := filter)

end Lean.Elab
