module FsAutoComplete.Core.InlayHints

open System
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax
open FsToolkit.ErrorHandling
open FsAutoComplete
open FSharp.Compiler.Symbols
open FSharp.UMX
open System.Linq
open System.Collections.Immutable
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text.Range
open FsAutoComplete.Logging

let logger = LogProvider.getLoggerByName "InlayHints"

/// `traversePat`from `SyntaxTraversal.Traverse`
///
/// Reason for extra function:
/// * can be used to traverse when traversal isn't available via `defaultTraverse` (for example: in `VisitExpr`, and want traverse a `SynPat`)
/// * visits `SynPat.Record(fieldPats)`
///
/// Note: doesn't visit `SynPat.Typed(targetType)`: requires traversal into `SynType` (`SynPat.Typed(pat)` gets visited!)
let rec private traversePat (visitor: SyntaxVisitorBase<_>) origPath pat =
  let defaultTraverse = defaultTraversePat visitor origPath
  visitor.VisitPat(origPath, defaultTraverse, pat)

and private defaultTraversePat visitor origPath pat =
  let path = SyntaxNode.SynPat pat :: origPath

  match pat with
  | SynPat.Paren(p, _) -> traversePat visitor path p
  | SynPat.As(p1, p2, _)
  | SynPat.Or(p1, p2, _, _) -> [ p1; p2 ] |> List.tryPick (traversePat visitor path)
  | SynPat.Ands(ps, _)
  | SynPat.Tuple(elementPats = ps)
  | SynPat.ArrayOrList(_, ps, _) -> ps |> List.tryPick (traversePat visitor path)
  | SynPat.Attrib(p, _, _) -> traversePat visitor path p
  | SynPat.LongIdent(argPats = args) ->
    match args with
    | SynArgPats.Pats ps -> ps |> List.tryPick (traversePat visitor path)
    | SynArgPats.NamePatPairs(ps, _, _) ->
      ps
      |> List.map (fun (NamePatPairField(pat = innerPat)) -> innerPat)
      |> List.tryPick (traversePat visitor path)
  | SynPat.Typed(p, _ty, _) -> traversePat visitor path p
  // no access to `traverseSynType` -> no traversing into `ty`
  | SynPat.Record(fieldPats = fieldPats) ->
    fieldPats
    |> List.map (fun (NamePatPairField(pat = innerPat)) -> innerPat)
    |> List.tryPick (traversePat visitor path)
  | _ -> None

type HintKind =
  | Parameter
  | Type

type HintInsertion = { Pos: Position; Text: string }

type Hint =
  { IdentRange: Range
    Kind: HintKind
    Pos: Position
    Text: string
    Insertions: HintInsertion[] option
    //ENHANCEMENT: allow xml doc
    Tooltip: string option }

let private isSignatureFile (f: string<LocalPath>) = System.IO.Path.GetExtension(UMX.untag f) = ".fsi"

let private getFirstPositionAfterParen (str: string) startPos =
  match str with
  | null -> -1
  | str when startPos > str.Length -> -1
  | str -> str.IndexOf('(', startPos) + 1

[<Literal>]
let maxHintLength = 30

let inline shouldTruncate (s: string) = s.Length > maxHintLength

let inline tryTruncate (s: string) =
  if shouldTruncate s then
    s.Substring(0, maxHintLength) + "..." |> Some
  else
    None

let truncated (s: string) = tryTruncate s |> Option.defaultValue s

let private createParamHint (identRange: Range) (paramName: string) =
  let (truncated, tooltip) =
    match tryTruncate paramName with
    | None -> (paramName, None)
    | Some truncated -> (truncated, Some paramName)

  { IdentRange = identRange
    Pos = identRange.Start
    Kind = Parameter
    Text = truncated + " ="
    Insertions = None
    Tooltip = tooltip }

module private ShouldCreate =
  let private isNotWellKnownName =
    let names = Set.ofList [ "value"; "x" ]

    fun (p: FSharpParameter) ->
      match p.Name with
      | None -> true
      | Some n -> not (Set.contains n names)


  [<return: Struct>]
  let private (|StartsWith|_|) (v: string) (fullName: string) =
    if fullName.StartsWith(v, StringComparison.Ordinal) then
      ValueSome()
    else
      ValueNone
  // doesn't differentiate between modules, types, namespaces
  // -> is just for documentation in code
  [<return: Struct>]
  let private (|Module|_|) = (|StartsWith|_|)

  [<return: Struct>]
  let private (|Type|_|) = (|StartsWith|_|)

  [<return: Struct>]
  let private (|Namespace|_|) = (|StartsWith|_|)

  let private commonCollectionParams =
    Set.ofList
      [ "mapping"
        "projection"
        "chooser"
        "value"
        "predicate"
        "folder"
        "state"
        "initializer"
        "action"

        "list"
        "array"
        "source"
        "lists"
        "arrays"
        "sources" ]

  let private isWellKnownParameterOrFunction (func: FSharpMemberOrFunctionOrValue) (param: FSharpParameter) =
    match func.FullName with
    | Module "Microsoft.FSharp.Core.Option" ->
      // don't show param named `option`, but other params for Option
      match param.Name with
      | Some "option" -> true
      | _ -> false
    | Module "Microsoft.FSharp.Core.ValueOption" ->
      match param.Name with
      | Some "voption" -> true
      | _ -> false
    | Module "Microsoft.FSharp.Core.ExtraTopLevelOperators" // only printf-members have `format`
    | Module "Microsoft.FSharp.Core.Printf" ->
      // don't show param named `format`
      match param.Name with
      | Some "format" -> true
      | _ -> false
    | Namespace "Microsoft.FSharp.Collections" ->
      match param.Name with
      | Some name -> commonCollectionParams |> Set.contains name
      | _ -> false
    | _ -> false

  let inline private hasName (p: FSharpParameter) = not (String.IsNullOrEmpty p.DisplayName) && p.DisplayName <> "````"

  let inline private isMeaningfulName (p: FSharpParameter) = p.DisplayName.Length > 2

  let inline private isOperator (func: FSharpMemberOrFunctionOrValue) =
    func.CompiledName.StartsWith("op_", StringComparison.Ordinal)

  /// Doesn't consider lower/upper cases:
  /// * `areSame "foo" "FOO" = true`
  /// * `areSame "Foo" "Foo" = true`
  let inline private areSame (a: ReadOnlySpan<char>) (b: ReadOnlySpan<char>) =
    a.Equals(b, StringComparison.OrdinalIgnoreCase)

  /// Boundary checks:
  /// * word boundary (-> upper case letter)
  ///   `"foo" |> isPrefixOf "fooBar"`
  /// Doesn't consider capitalization, except for word boundary after prefix:
  /// * `foo` prefix of `fooBar`
  /// * `foo` not prefix of `foobar`
  let inline private isPrefixOf (root: ReadOnlySpan<char>) (check: ReadOnlySpan<char>) =
    root.StartsWith(check, StringComparison.OrdinalIgnoreCase)
    && (
    // same
    root.Length <= check.Length
    ||
    // rest must start with upper case -> new word
    Char.IsUpper root[check.Length])

  /// Boundary checks:
  /// * word boundary (-> upper case letter)
  ///   `"bar" |> isPostfixOf "fooBar"`
  /// * `.` boundary (-> property access)
  ///   `"bar" |> isPostfixOf "data.bar"`
  ///
  /// Doesn't consider capitalization, except for word boundary at start of postfix:
  /// * `bar` postfix of `fooBar`
  /// * `bar` not postfix of `foobar`
  let inline private isPostfixOf (root: ReadOnlySpan<char>) (check: ReadOnlySpan<char>) =
    root.EndsWith(check, StringComparison.OrdinalIgnoreCase)
    && (root.Length <= check.Length
        ||
        // postfix must start with upper case -> word boundary
        Char.IsUpper root[root.Length - check.Length])

  let inline private removeLeadingUnderscore (name: ReadOnlySpan<char>) = name.TrimStart '_'
  let inline private removeTrailingTick (name: ReadOnlySpan<char>) = name.TrimEnd '\''

  let inline private extractLastIdentifier (name: ReadOnlySpan<char>) =
    // exclude backticks for now: might contain `.` -> difficult to split
    if name.StartsWith "``" || name.EndsWith "``" then
      name
    else
      match name.LastIndexOf '.' with
      | -1 -> name
      | i -> name.Slice(i + 1)

  /// Note: when in parens: might not be an identifier, but expression!
  ///
  /// Note: might result in invalid expression (because no matching parens `string (2)` -> `string (2`)
  let inline private trimParensAndSpace (name: ReadOnlySpan<char>) = name.TrimStart("( ").TrimEnd(" )")

  /// Note: including `.`
  let inline private isLongIdentifier (name: ReadOnlySpan<char>) =
    // name |> Seq.forall PrettyNaming.IsLongIdentifierPartCharacter
    let mutable valid = true
    let mutable i = 0

    while valid && i < name.Length do
      if PrettyNaming.IsLongIdentifierPartCharacter name[i] then
        i <- i + 1
      else
        valid <- false

    valid

  let private areSimilar (paramName: string) (argumentText: string) =
    // no pipe with span ...
    let paramName = removeTrailingTick (removeLeadingUnderscore (paramName.AsSpan()))

    let argumentName =
      let argumentText = argumentText.AsSpan()
      let argTextNoParens = trimParensAndSpace argumentText

      if isLongIdentifier argTextNoParens then
        removeTrailingTick (extractLastIdentifier argTextNoParens)
      else
        argumentText

    // special case: argumentText is empty string. Happens for unit (`()`)
    if argumentName.IsWhiteSpace() then
      false
    else
      // // covered by each isPre/PostfixOf
      // areSame paramName argumentName
      // ||
      isPrefixOf argumentName paramName
      || isPostfixOf argumentName paramName
      || isPrefixOf paramName argumentName
      || isPostfixOf paramName argumentName

  let inline private doesNotMatchArgumentText (parameterName: string) (userArgumentText: string) =
    parameterName <> userArgumentText
    && not (userArgumentText.StartsWith(parameterName, StringComparison.Ordinal))

  let private isParamNamePostfixOfFuncName (func: FSharpMemberOrFunctionOrValue) (paramName: string) =
    let funcName = func.DisplayName.AsSpan()
    let paramName = removeLeadingUnderscore (paramName.AsSpan())

    isPostfixOf funcName paramName

  /// <summary>
  /// We filter out parameters that generate lots of noise in hints.
  /// * parameter has no name
  /// * parameter has length > 2
  /// * parameter is one of a set of 'known' names that clutter (like printfn formats)
  /// * param &amp; function is "well known"/commonly used
  /// * parameter does match or is a pre/postfix of user-entered text
  /// * user-entered text does match or is a pre/postfix of parameter
  /// * parameter is postfix of function name
  /// </summary>
  let paramHint (func: FSharpMemberOrFunctionOrValue) (p: FSharpParameter) (argumentText: string) =
    hasName p
    && isMeaningfulName p
    && isNotWellKnownName p
    && (not (isWellKnownParameterOrFunction func p))
    && (not (isOperator func))
    && (not (areSimilar p.DisplayName argumentText))
    && (not (isParamNamePostfixOfFuncName func p.DisplayName))


type TypeName = string
type TypeNameForAnnotation = TypeName

type SpecialRule =
  /// For Optional: `?v` -> `?v: int`, NOT `v: int option`
  /// And parens must include optional, not just `v`
  | RemoveOptionFromType

type SpecialRules = SpecialRule list

[<RequireQualifiedAccess>]
type Parens =
  | Forbidden
  /// Technically `Optional` too: Usually additional parens are ok
  ///
  /// Note: `additionalParens` are inside of existing parens:
  /// `(|ident|)`
  /// * `()`: existing parens
  /// * `||`: additional parens location
  | Exist of additionalParens: Range
  | Optional of Range
  | Required of Range

type MissingExplicitType =
  { Ident: Range
    InsertAt: Position
    Parens: Parens
    SpecialRules: SpecialRules }

type MissingExplicitType with

  /// <returns>
  /// * type name
  /// * type name formatted with `SpecialRules`
  ///   -> to use as type annotation
  /// </returns>
  member x.FormatType(ty: FSharpType, displayContext: FSharpDisplayContext) : TypeName * TypeNameForAnnotation =
    let typeName = ty.Format displayContext

    let anno =
      if x.SpecialRules |> List.contains RemoveOptionFromType then
        // Optional parameter:
        // `static member F(?a) =` -> `: int`, NOT `: int option`
        if typeName.EndsWith(" option", StringComparison.Ordinal) then
          typeName.Substring(0, typeName.Length - " option".Length)
        else
          typeName
      else
        typeName

    (typeName, anno)

  member x.CreateEdits(typeForAnnotation) =
    [| match x.Parens with
       | Parens.Required range -> { Pos = range.Start; Text = "(" }
       | _ -> ()

       { Pos = x.InsertAt; Text = ": " }
       { Pos = x.InsertAt
         Text = typeForAnnotation }

       match x.Parens with
       | Parens.Required range -> { Pos = range.End; Text = ")" }
       | _ -> () |]

  member x.TypeAndEdits(ty: FSharpType, displayContext: FSharpDisplayContext) =
    let (ty, tyForAnnotation) = x.FormatType(ty, displayContext)
    let edits = x.CreateEdits(tyForAnnotation)
    (ty, edits)

  /// Note: No validation of `mfv`!
  member x.TypeAndEdits(mfv: FSharpMemberOrFunctionOrValue, displayContext: FSharpDisplayContext) =
    x.TypeAndEdits(mfv.FullType, displayContext)


/// Note: Missing considers only directly typed, not parently (or ancestorly) typed:
/// ```fsharp
/// let (value: int, _) = (1,2)
/// //   ^^^^^ directly typed -> Exists
/// let (value,_): int*int = (1,2)
/// //             ^^^ parently typed -> Missing
/// ```
[<RequireQualifiedAccess>]
type ExplicitType =
  /// in for loop (only indent allowed -- nothing else (neither type nor parens))
  | Invalid
  | Exists
  | Missing of MissingExplicitType
  | Debug of string

type ExplicitType with

  member x.TryGetTypeAndEdits(ty: FSharpType, displayContext: FSharpDisplayContext) =
    match x with
    | ExplicitType.Missing data -> data.TypeAndEdits(ty, displayContext) |> Some
    | _ -> None

/// Type Annotation must be directly for identifier, not somewhere up the line:
/// `v: int` -> directly typed
/// `(v,_): int*int` -> parently typed
///
/// Still considered directly typed:
/// * Parentheses: `(v): int`
/// * Attributes: `([<Attr>]v): int`
let rec private isDirectlyTyped (identStart: Position) (path: SyntaxVisitorPath) =
  //ENHANCEMENT: handle SynExpr.Typed? -> not at binding, but usage
  match path with
  | [] -> false
  | SyntaxNode.SynPat(SynPat.Typed(pat = pat)) :: _ when rangeContainsPos pat.Range identStart -> true
  | SyntaxNode.SynPat(SynPat.Paren _) :: path -> isDirectlyTyped identStart path
  | SyntaxNode.SynPat(SynPat.Attrib(pat = pat)) :: path when rangeContainsPos pat.Range identStart ->
    isDirectlyTyped identStart path
  | SyntaxNode.SynBinding(SynBinding(headPat = headPat; returnInfo = Some(SynBindingReturnInfo _))) :: _ when
    rangeContainsPos headPat.Range identStart
    ->
    true
  | SyntaxNode.SynExpr(SynExpr.Paren _) :: path -> isDirectlyTyped identStart path
  | SyntaxNode.SynExpr(SynExpr.Typed(expr = expr)) :: _ when rangeContainsPos expr.Range identStart -> true
  | _ -> false

/// Note: FULL range of pattern -> everything in parens
///   For `SynPat.Named`: Neither `range` nor `ident.idRange` span complete range: Neither includes Accessibility:
///   `let private (a: int)` is not valid, must include private: `let (private a: int)`
let rec private getParensForPatternWithIdent (patternRange: Range) (identStart: Position) (path: SyntaxVisitorPath) =
  match path with
  | SyntaxNode.SynPat(SynPat.Paren _) :: _ ->
    // (x)
    Parens.Exist patternRange
  | SyntaxNode.SynBinding(SynBinding(headPat = headPat)) :: _ when rangeContainsPos headPat.Range identStart ->
    // let x =
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.Tuple(isStruct = true)) :: _ ->
    // struct (x,y)
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.Tuple _) :: SyntaxNode.SynPat(SynPat.Paren _) :: _ ->
    // (x,y)
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.Tuple _) :: _ ->
    // x,y
    Parens.Required patternRange
  | SyntaxNode.SynPat(SynPat.ArrayOrList _) :: _ ->
    // [x;y;z]
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.As _) :: SyntaxNode.SynPat(SynPat.Paren _) :: _ -> Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.As(rhsPat = pat)) :: SyntaxNode.SynBinding(SynBinding(headPat = headPat)) :: _ when
    rangeContainsPos pat.Range identStart
    && rangeContainsPos headPat.Range identStart
    ->
    // let _ as value =
    // ->
    // let _ as value: int =
    // (new `: int` belongs to let binding, NOT as pattern)
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.As(lhsPat = pat)) :: SyntaxNode.SynBinding(SynBinding(headPat = headPat)) :: _ when
    rangeContainsPos pat.Range identStart
    && rangeContainsPos headPat.Range identStart
    ->
    // let value as _ =
    // ->
    // let (value: int) as _ =
    // (`: int` belongs to as pattern, but let bindings tries to parse type annotation eagerly -> without parens let binding finished after `: int` -> as not pattern)
    Parens.Required patternRange
  | SyntaxNode.SynPat(SynPat.As(rhsPat = pat)) :: _ when rangeContainsPos pat.Range identStart ->
    // _ as (value: int)
    Parens.Required patternRange
  | SyntaxNode.SynPat(SynPat.As(lhsPat = pat)) :: _ when rangeContainsPos pat.Range identStart ->
    // value: int as _
    // ^^^^^^^^^^ unlike rhs this here doesn't require parens...
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.Record _) :: _ ->
    // { Value=value }
    Parens.Optional patternRange
  | SyntaxNode.SynPat(SynPat.LongIdent(argPats = SynArgPats.NamePatPairs(range = range))) :: _ when
    rangeContainsPos range identStart
    ->
    // U (Value=value)
    //   ^           ^
    //   must exist to be valid
    Parens.Optional patternRange
  | SyntaxNode.SynExpr(SynExpr.ForEach _) :: _ ->
    // for i in [1..4] do
    Parens.Optional patternRange
  | []
  | _ -> Parens.Required patternRange

/// Gets range of `SynPat.Named`
///
/// Issue with range of `SynPat.Named`:
/// `pat.range` only covers ident (-> `= ident.idRange`),
/// not `accessibility`.
///
/// Note: doesn't handle when accessibility is on prev line
let private rangeOfNamedPat (text: IFSACSourceText) (pat: SynPat) =
  match pat with
  | SynPat.Named(accessibility = None) -> pat.Range
  | SynPat.Named(ident = SynIdent(ident = ident); accessibility = Some(access)) ->
    option {
      let start = ident.idRange.Start
      let! line = text.GetLine start

      let access = access.ToString().ToLowerInvariant().AsSpan()
      // word before ident must be access
      let pre = line.AsSpan(0, start.Column)

      match pre.LastIndexOf(access) with
      | -1 -> return! None
      | c ->
        // must be directly before ident
        let word = pre.Slice(c).TrimEnd()

        if word.Length = access.Length then
          let start = Position.mkPos start.Line c

          let range =
            let range = ident.idRange
            Range.mkRange range.FileName start range.End

          return range
        else
          return! None
    }
    |> Option.defaultValue pat.Range
  | _ -> failwith "Pattern must be Named!"

/// Note: (deliberately) fails when `pat` is neither `Named` nor `OptionalVal`
let rec private getParensForIdentPat (text: IFSACSourceText) (pat: SynPat) (path: SyntaxVisitorPath) =
  match pat with
  | SynPat.Named(ident = SynIdent(ident = ident)) ->
    // neither `range`, not `pat.Range` includes `accessibility`...
    // `let private (a: int)` is not valid, must include private: `let (private a: int)`
    let patternRange = rangeOfNamedPat text pat
    let identStart = ident.idRange.Start
    getParensForPatternWithIdent patternRange identStart path
  | SynPat.OptionalVal(ident = ident) ->
    let patternRange = pat.Range
    let identStart = ident.idRange.Start
    getParensForPatternWithIdent patternRange identStart path
  | _ -> failwith "Pattern must be Named or OptionalVal!"

[<return: Struct>]
let rec private (|Typed|_|) pat =
  match pat with
  | SynPat.Typed _ -> ValueSome()
  | SynPat.Attrib(pat = pat) -> (|Typed|_|) pat
  | _ -> ValueNone

[<return: Struct>]
let rec (|NamedPat|_|) pat =
  match pat with
  | SynPat.Named(ident = SynIdent(ident = ident)) -> ValueSome ident
  | SynPat.Attrib(pat = pat) -> (|NamedPat|_|) pat
  | _ -> ValueNone

let tryGetExplicitTypeInfo (text: IFSACSourceText, ast: ParsedInput) (pos: Position) : ExplicitType option =
  SyntaxTraversal.Traverse(
    pos,
    ast,
    { new SyntaxVisitorBase<_>() with
        member x.VisitExpr(path, traverseSynExpr, defaultTraverse, expr) =
          match expr with
          // special case:
          // for loop:
          // for i = 1 to 3 do
          //     ^ -> just Ident (neither SynPat nor SynSimplePat)
          //     -> no type allowed (not even parens)...
          | SynExpr.For(ident = ident) when rangeContainsPos ident.idRange pos -> ExplicitType.Invalid |> Some
          | SynExpr.Lambda(parsedData = Some(args, body)) ->
            // original visitor walks down `SynExpr.Lambda(args; body)`
            // Issue:
            //  `args` are `SynSimplePats` -> no complex pattern
            //  When pattern: is in body. In `args` then generated Identifier:
            //  * `let f1 = fun v -> v + 1`
            //    -> `v` is in `args` (-> SynSimplePat)
            //  * `let f2 = fun (Value v) -> v + 1`
            //    -> compiler generated `_arg1` in `args`,
            //    and `v` is inside match expression in `body` & `parsedData` (-> `SynPat` )
            // -> unify by looking into `parsedData` (-> args & body):
            //    -> `parsedData |> fst` contains `args` as `SynPat`
            //TODO: always correct?
            let arg = args |> List.tryFind (fun pat -> rangeContainsPos pat.Range pos)

            if arg |> Option.isSome then
              let pat = arg.Value
              traversePat x (SyntaxNode.SynExpr(expr) :: path) pat
            elif rangeContainsPos body.Range pos then
              traverseSynExpr body
            else
              None
          | _ -> defaultTraverse expr

        member visitor.VisitPat(path, defaultTraverse, pat) =
          let invalidPositionForTypeAnnotation (path: SyntaxNode list) =
            match path with
            | SyntaxNode.SynExpr(SynExpr.LetOrUse(isUse = true)) :: _ ->
              // use! value =
              true
            | _ -> false

          //ENHANCEMENT: differentiate between directly typed and parently typed?
          //        (maybe even further ancestorly typed?)
          // ```fsharp
          // let (a: int,b) = (1,2)
          // //      ^^^ directly typed
          // let (a,b): int*int = (1,2)
          // //         ^^^ parently typed
          // ```
          // currently: only directly typed is typed
          match pat with
          // no simple way out: Range for `SynPat.LongIdent` doesn't cover full pats (just ident)
          // see dotnet/fsharp#13115
          // | _ when not (rangeContainsPos pat.Range pos) -> None
          | SynPat.Named(ident = SynIdent(ident = ident)) when
            rangeContainsPos ident.idRange pos && invalidPositionForTypeAnnotation path
            ->
            ExplicitType.Invalid |> Some
          | SynPat.Named(ident = SynIdent(ident = ident); isThisVal = false) when rangeContainsPos ident.idRange pos ->
            let typed = isDirectlyTyped ident.idRange.Start path

            if typed then
              ExplicitType.Exists |> Some
            else
              let parens = getParensForIdentPat text pat path

              ExplicitType.Missing
                { Ident = ident.idRange
                  InsertAt = ident.idRange.End
                  Parens = parens
                  SpecialRules = [] }
              |> Some
          | SynPat.OptionalVal(ident = ident) when rangeContainsPos ident.idRange pos ->
            let typed = isDirectlyTyped ident.idRange.Start path

            if typed then
              ExplicitType.Exists |> Some
            else
              let parens = getParensForIdentPat text pat path

              ExplicitType.Missing
                { Ident = ident.idRange
                  InsertAt = ident.idRange.End
                  Parens = parens
                  SpecialRules = [ RemoveOptionFromType ]
                //              ^^^^^^^^^^^^^^^^^^^^
                //              `?v: int`, NOT `?v: int option`
                }
              |> Some
          | _ -> defaultTraversePat visitor path pat

        member _.VisitSimplePats(path, pat) =
          let (|ImplicitCtorPath|_|) (path: SyntaxNode) =
            match path with
            // normal ctor in type: `type A(v) = ...`
            | SyntaxNode.SynMemberDefn(SynMemberDefn.ImplicitCtor _) -> Some()
            //TODO: when? example?
            | SyntaxNode.SynTypeDefn(SynTypeDefn(
                typeRepr = SynTypeDefnRepr.Simple(
                  simpleRepr = SynTypeDefnSimpleRepr.General(implicitCtorSynPats = Some(ctorPats))))) when
              rangeContainsPos ctorPats.Range pos
              ->
              Some()
            | _ -> None

          let (|SpecificPat|_|) (pats: SynPat list) =
            pats
            |> List.tryPick (fun pat -> if rangeContainsPos pat.Range pos then Some pat else None)

          match pat with
          | Typed
          | SynPat.Paren(pat = Typed) -> Some ExplicitType.Exists
          | SynPat.Paren(pat = innerPat) ->
            match path with
            | ImplicitCtorPath :: _ ->
              // ok, deal with constructor parameters
              // * named pats
              match innerPat with
              // single ctor arg
              | NamedPat ident ->
                ExplicitType.Missing
                  { Ident = ident.idRange
                    InsertAt = ident.idRange.End
                    Parens = Parens.Forbidden
                    SpecialRules = [] }
                |> Some
              // multiple ctor args
              // * arg in question already has a type
              | SynPat.Tuple(elementPats = SpecificPat Typed) -> Some ExplicitType.Exists
              // * arg in question doesn't have a type
              | SynPat.Tuple(elementPats = SpecificPat(NamedPat ident)) ->
                ExplicitType.Missing
                  { Ident = ident.idRange
                    InsertAt = ident.idRange.End
                    Parens = Parens.Forbidden
                    SpecialRules = [] }
                |> Some
              | _ -> None

            | _ -> None
          | _ -> None }
  )

///<returns>
/// List of all curried params.
///
/// For each curried param:
/// * its range (including params)
/// * range of tupled params
///   * Note: one range when no tuple
///
/// ```fsharp
/// f alpha (beta, gamma)
/// ```
/// ->
/// ```fsharp
/// [
///   (2:7, [2:7])
///   (8:21, [9:13; 15:20])
/// ]
/// ```
///</returns>
let private getArgRangesOfFunctionApplication (ast: ParsedInput) pos =
  (pos, ast)
  ||> ParsedInput.tryPick (fun _path node ->
    let rec (|IgnoreParens|) =
      function
      | SynExpr.Paren(expr = expr)
      | expr -> expr

    let rec (|Arg|) =
      function
      | IgnoreParens(SynExpr.Tuple(isStruct = false; exprs = exprs)) as arg ->
        arg.Range, exprs |> List.map (fun e -> e.Range)
      | expr -> expr.Range, [ expr.Range ]

    let rec (|Args|) =
      function
      | IgnoreParens(SynExpr.App(funcExpr = Args args; argExpr = Arg arg)) -> arg :: args
      | _ -> []

    match node with
    | SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.App(isInfix = true; argExpr = Args args); range = m))
    | SyntaxNode.SynExpr(SynExpr.App(funcExpr = SynExpr.App(isInfix = true; funcExpr = Args args); range = m))
    | SyntaxNode.SynExpr(SynExpr.App(isInfix = false; range = m) & Args args) when m.Start = pos -> Some(List.rev args)
    | _ -> None)

/// Note: No exhausting check. Doesn't check for:
/// * is already typed (-> done by getting `ExplicitType`)
/// * Filters like excluding functions (vs. lambda functions)
/// * `mfv.IsFromDefinition`
///
/// `allowFunctionValues`: `let f = fun a b -> a + b`
/// -> enabled: `f` is target
/// Note: NOT actual functions with direct parameters:
/// `let f a b = a + b` -> `f` isn't target
/// Note: can be parameters too:
/// `let map f v = f v` -> `f` is target
let isPotentialTargetForTypeAnnotation
  (allowFunctionValues: bool)
  (_symbolUse: FSharpSymbolUse, mfv: FSharpMemberOrFunctionOrValue)
  =
  //ENHANCEMENT: extract settings
  (mfv.IsValue || (allowFunctionValues && mfv.IsFunction))
  && not (
    mfv.IsMember
    || mfv.IsMemberThisValue
    || mfv.IsConstructorThisValue
    || PrettyNaming.IsOperatorDisplayName mfv.DisplayName
  )

let tryGetDetailedExplicitTypeInfo
  (isValidTarget: FSharpSymbolUse * FSharpMemberOrFunctionOrValue -> bool)
  (text: IFSACSourceText, parseAndCheck: ParseAndCheckResults)
  (pos: Position)
  =
  option {
    let! line = text.GetLine pos
    let! symbolUse = parseAndCheck.TryGetSymbolUse pos line

    match symbolUse.Symbol with
    | :? FSharpMemberOrFunctionOrValue as mfv when isValidTarget (symbolUse, mfv) ->
      let! explTy = tryGetExplicitTypeInfo (text, parseAndCheck.GetAST) pos
      return (symbolUse, mfv, explTy)
    | _ -> return! None
  }

let private tryCreateTypeHint (explicitType: ExplicitType) (ty: FSharpType) (displayContext: FSharpDisplayContext) =
  match explicitType with
  | ExplicitType.Missing data ->
    let (ty, tyForAnno) = data.FormatType(ty, displayContext)

    let (truncated, tooltip) =
      match tryTruncate ty with
      | None -> (ty, None)
      | Some truncated -> (truncated, Some ty)

    { IdentRange = data.Ident
      Pos = data.InsertAt
      Kind = Type
      // TODO: or use tyForAnno?: `?value: int`, but type is `int option`
      Text = ": " + truncated
      Insertions = Some <| data.CreateEdits tyForAnno
      Tooltip = tooltip }
    |> Some
  | _ -> None

type HintConfig =
  { ShowTypeHints: bool
    ShowParameterHints: bool }

let provideHints
  (text: IFSACSourceText, parseAndCheck: ParseAndCheckResults, range: Range, hintConfig)
  : Async<Hint[]> =
  asyncResult {
    let! cancellationToken = Async.CancellationToken

    let symbolUses =
      parseAndCheck.GetCheckResults.GetAllUsesOfAllSymbolsInFile(cancellationToken)
      |> Seq.filter (fun su -> rangeContainsRange range su.Range)

    let typeHints = ImmutableArray.CreateBuilder()
    let parameterHints = ImmutableArray.CreateBuilder()

    for symbolUse in symbolUses do
      match symbolUse.Symbol with
      | :? FSharpMemberOrFunctionOrValue as mfv when
        hintConfig.ShowTypeHints
        && symbolUse.IsFromDefinition
        && isPotentialTargetForTypeAnnotation false (symbolUse, mfv)
        ->
        tryGetExplicitTypeInfo (text, parseAndCheck.GetAST) symbolUse.Range.Start
        |> Option.bind (fun explTy -> tryCreateTypeHint explTy mfv.FullType symbolUse.DisplayContext)
        |> Option.iter typeHints.Add

      | :? FSharpMemberOrFunctionOrValue as func when
        hintConfig.ShowParameterHints
        && func.IsFunction
        && not symbolUse.IsFromDefinition
        ->
        let curriedParamGroups = func.CurriedParameterGroups

        let appliedArgRanges =
          getArgRangesOfFunctionApplication parseAndCheck.GetAST symbolUse.Range.Start
          |> Option.defaultValue []

        for (def, (appliedArgRange, tupleRanges)) in Seq.zip curriedParamGroups appliedArgRanges do
          assert (def.Count > 0)

          match tupleRanges with
          | _ when def.Count = 1 ->
            // single param at def
            let p = def[0]
            let! appliedArgText = text[appliedArgRange]

            if ShouldCreate.paramHint func p appliedArgText then
              let defArgName = p.DisplayName
              let hint = createParamHint appliedArgRange defArgName
              parameterHints.Add hint
          | [ _ ] ->
            // single param at app (but tuple at def)
            let! appliedArgText = text[appliedArgRange]
            // only show param hint when at least one of the tuple params should be shown
            if def |> Seq.exists (fun p -> ShouldCreate.paramHint func p appliedArgText) then
              let defArgName =
                let names = def |> Seq.map (fun p -> p.DisplayName) |> String.concat ","

                "(" + names + ")"

              let hint = createParamHint appliedArgRange defArgName
              parameterHints.Add hint
          | _ ->
            // both tuple
            for (p, eleRange) in Seq.zip def tupleRanges do
              let! appliedArgText = text[eleRange]

              if ShouldCreate.paramHint func p appliedArgText then
                let defArgName = p.DisplayName
                let hint = createParamHint eleRange defArgName
                parameterHints.Add hint

      | :? FSharpMemberOrFunctionOrValue as methodOrConstructor when
        hintConfig.ShowParameterHints
        && (methodOrConstructor.IsConstructor || methodOrConstructor.IsMethod)
        -> // TODO: support methods when this API comes into FCS
        let endPosForMethod = symbolUse.Range.End
        let line, _ = Position.toZ endPosForMethod

        let afterParenPosInLine =
          getFirstPositionAfterParen (text.GetLineString(line)) (endPosForMethod.Column)

        let tupledParamInfos =
          parseAndCheck.GetParseResults.FindParameterLocations(Position.fromZ line afterParenPosInLine)

        let appliedArgRanges =
          parseAndCheck.GetParseResults.GetAllArgumentsForFunctionApplicationAtPosition symbolUse.Range.Start

        match tupledParamInfos, appliedArgRanges with
        | None, None -> ()

        // Prefer looking at the "tupled" view if it exists, even if the other ranges exist.
        // M(1, 2) can give results for both, but in that case we want the "tupled" view.
        | Some tupledParamInfos, _ ->
          let parameters =
            methodOrConstructor.CurriedParameterGroups |> Seq.concat |> Array.ofSeq // TODO: need ArgumentLocations to be surfaced

          if parameters.Length <> tupledParamInfos.ArgumentLocations.Length then
            // safety - if the number of parameters doesn't match the number of argument locations, then we can't
            // reliably create hints, so skip it
            logger.info (
              Log.setMessage
                "Parameter hints for {memberName} may fail because the number of parameters in the definition ({memberParameters}) doesn't match the number of argument locations ({providedParameters})"
              >> Log.addContext
                "memberName"
                $"{methodOrConstructor.DeclaringEntity
                   |> Option.map (fun e -> e.FullName)
                   |> Option.defaultValue String.Empty}::{methodOrConstructor.DisplayName}"
              >> Log.addContext "memberParameters" parameters.Length
              >> Log.addContext "providedParameters" tupledParamInfos.ArgumentLocations.Length
            )

          // iterate over the _provided_ parameters, because otherwise we might index into optional parameters
          // from the method's definition that the user didn't have to provide.
          // thought/note: what about `param array` parameters?
          tupledParamInfos.ArgumentLocations
          |> Array.iteri (fun idx paramLocationInfo ->
            if parameters.Length <= idx then
              // safety - if the number of parameters doesn't match the number of argument locations, then we can't
              // reliably create hints, so skip it
              ()
            else
              let param = parameters.[idx]
              let paramName = param.DisplayName
              // PLI.IsNamedArgument is true if the user has provided a name here. There's no since in providing a hint
              // for a named argument, so skip it
              if paramLocationInfo.IsNamedArgument then
                ()
              // otherwise apply our 'should we make a hint' logic to the argument text
              else if ShouldCreate.paramHint methodOrConstructor param "" then
                let hint = createParamHint paramLocationInfo.ArgumentRange paramName
                parameterHints.Add(hint))

        // This will only happen for curried methods defined in F#.
        | _, Some appliedArgRanges ->
          let parameters = methodOrConstructor.CurriedParameterGroups |> Seq.concat

          let definitionArgs = parameters |> Array.ofSeq

          let parms =
            appliedArgRanges
            |> List.indexed
            |> List.choose (fun (i, v) ->
              if i < definitionArgs.Length then
                Some(definitionArgs.[i], v)
              else
                None)

          for (definitionArg, appliedArgRange) in parms do
            let! appliedArgText = text[appliedArgRange]

            let shouldCreate =
              ShouldCreate.paramHint methodOrConstructor definitionArg appliedArgText

            if shouldCreate then
              let hint = createParamHint appliedArgRange definitionArg.DisplayName
              parameterHints.Add(hint)

      | _ -> ()

    let typeHints = typeHints.ToImmutableArray()
    let parameterHints = parameterHints.ToImmutableArray()

    return typeHints.AddRange(parameterHints).ToArray()
  }
  |> AsyncResult.foldResult id (fun _ -> [||])
