module Development.IDE.Plugin.Plugins.FillTypeWildcard
  ( suggestFillTypeWildcard
  ) where

import           Data.Char
import qualified Data.Text                   as T
import           Language.LSP.Protocol.Types (Diagnostic (..),
                                              TextEdit (TextEdit))

suggestFillTypeWildcard :: Diagnostic -> [(T.Text, TextEdit)]
suggestFillTypeWildcard Diagnostic{_range=_range,..}
-- Foo.hs:3:8: error:
--     * Found type wildcard `_' standing for `p -> p1 -> p'
    | "Found type wildcard" `T.isInfixOf` _message
    , " standing for " `T.isInfixOf` _message
    , typeSignature <- extractWildCardTypeSignature _message
        =  [("Use type signature: ‘" <> typeSignature <> "’", TextEdit _range typeSignature)]
    | otherwise = []

-- | Extract the type and surround it in parentheses except in obviously safe cases.
--
-- Inferring when parentheses are actually needed around the type signature would
-- require understanding both the precedence of the context of the hole and of
-- the signature itself. Inserting them (almost) unconditionally is ugly but safe.
extractWildCardTypeSignature :: T.Text -> T.Text
extractWildCardTypeSignature msg
  | enclosed || not isApp || isToplevelSig = sig
  | otherwise                              = "(" <> sig <> ")"
  where
    msgSigPart      = snd $ T.breakOnEnd "standing for " msg
    (sig, rest)     = T.span (/='’') . T.dropWhile (=='‘') . T.dropWhile (/='‘') $ msgSigPart
    -- If we're completing something like ‘foo :: _’ parens can be safely omitted.
    isToplevelSig   = errorMessageRefersToToplevelHole rest
    -- Parenthesize type applications, e.g. (Maybe Char).
    isApp           = T.any isSpace sig
    -- Do not add extra parentheses to lists, tuples and already parenthesized types.
    enclosed        =
      case T.uncons sig of
        Nothing -> error "GHC provided invalid type"
        Just (firstChr, _) -> not (T.null sig) && (firstChr, T.last sig) `elem` [('(', ')'), ('[', ']')]

-- | Detect whether user wrote something like @foo :: _@ or @foo :: (_, Int)@.
-- The former is considered toplevel case for which the function returns 'True',
-- the latter is not toplevel and the returned value is 'False'.
--
-- When type hole is at toplevel then there’s a line starting with
-- "• In the type signature" which ends with " :: _" like in the
-- following snippet:
--
-- source/library/Language/Haskell/Brittany/Internal.hs:131:13: error:
--     • Found type wildcard ‘_’ standing for ‘HsDecl GhcPs’
--       To use the inferred type, enable PartialTypeSignatures
--     • In the type signature: decl :: _
--       In an equation for ‘splitAnnots’:
--           splitAnnots m@HsModule {hsmodAnn, hsmodDecls}
--             = undefined
--             where
--                 ann :: SrcSpanAnnA
--                 decl :: _
--                 L ann decl = head hsmodDecls
--     • Relevant bindings include
--       [REDACTED]
--
-- When type hole is not at toplevel there’s a stack of where
-- the hole was located ending with "In the type signature":
--
-- source/library/Language/Haskell/Brittany/Internal.hs:130:20: error:
--     • Found type wildcard ‘_’ standing for ‘GhcPs’
--       To use the inferred type, enable PartialTypeSignatures
--     • In the first argument of ‘HsDecl’, namely ‘_’
--       In the type ‘HsDecl _’
--       In the type signature: decl :: HsDecl _
--     • Relevant bindings include
--       [REDACTED]
errorMessageRefersToToplevelHole :: T.Text -> Bool
errorMessageRefersToToplevelHole msg =
  not (T.null prefix) && " :: _" `T.isSuffixOf` T.takeWhile (/= '\n') rest
  where
    (prefix, rest) = T.breakOn "• In the type signature:" msg
