{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE LambdaCase                #-}
{-# LANGUAGE OverloadedStrings         #-}
module Ide.Plugin.Error (
      -- * Plugin Error Handling API
    PluginError(..),
    toErrorCode,
    toPriority,
    handleMaybe,
    handleMaybeM,
    getNormalizedFilePathE,
) where

import           Control.Monad.Extra         (maybeM)
import           Control.Monad.Trans.Class   (lift)
import           Control.Monad.Trans.Except  (ExceptT (..), throwE)
import qualified Data.Text                   as T
import           Ide.Logger
import           Language.LSP.Protocol.Types

-- ----------------------------------------------------------------------------
-- Plugin Error wrapping
-- ----------------------------------------------------------------------------

-- |Each PluginError corresponds to either a specific ResponseError we want to
-- return or a specific way we want to log the error. If the currently present
-- ones are insufficient for the needs of your plugin, please feel free to add
-- a new one.
--
-- Currently the PluginErrors we provide can be broken up into several groups.
-- First is PluginInternalError, which is the most serious of the errors, and
-- also the "default" error that is used for things such as uncaught exceptions.
-- Then we have PluginInvalidParams, which along with PluginInternalError map
-- to a corresponding ResponseError.
--
-- Next we have PluginRuleFailed and PluginInvalidUserState, with the only
-- difference being PluginRuleFailed is specific to Shake rules and
-- PluginInvalidUserState can be used for everything else. Both of these are
-- "non-errors", and happen whenever the user's code is in a state where the
-- plugin is unable to provide a answer to the users request. PluginStaleResolve
-- is similar to the above two Error types, but is specific to resolve plugins,
-- and is used only when the data provided by the resolve request is stale,
-- preventing the proper resolution of it.
--
-- Finally we have the outlier, PluginRequestRefused, where we allow a handler
-- to preform "pluginEnabled" checks inside the handler, and reject the request
-- after viewing it. The behavior of only one handler passing `pluginEnabled`
-- and then returning PluginRequestRefused should be the same as if no plugins
-- passed the `pluginEnabled` stage.
data PluginError
  = -- |PluginInternalError should be used if an error has occurred. This
    -- should only rarely be returned. As it's logged with Error, it will be
    -- shown by the client to the user via `showWindow`. All uncaught exceptions
    -- will be caught and converted to this error.
    --
    -- This error will be be converted into an InternalError response code. It
    -- will be logged with Error and takes the highest precedence (1) in being
    -- returned as a response to the client.
    PluginInternalError T.Text
    -- |PluginInvalidParams should be used if the parameters of the request are
    -- invalid. This error means that there is a bug in the client's code
    -- (otherwise they wouldn't be sending you requests with invalid
    -- parameters).
    --
    -- This error will be will be converted into a InvalidParams response code.
    -- It will be logged with Warning and takes medium precedence (2) in being
    -- returned as a response to the client.
  | PluginInvalidParams T.Text
    -- |PluginInvalidUserState should be thrown when a function that your plugin
    -- depends on fails. This should only be used when the function fails
    -- because the user's code is in an invalid state.
    --
    -- This error takes the name of the function that failed. Prefer to catch
    -- this error as close to the source as possible.
    --
    -- This error will be logged with Debug, and will be converted into a
    -- RequestFailed response. It takes a low precedence (3) in being returned
    -- as a response to the client.
  | PluginInvalidUserState T.Text
    -- |PluginRequestRefused allows your handler to inspect a request before
    -- rejecting it. In effect it allows your plugin to act make a secondary
    -- `pluginEnabled` decision after receiving the request. This should only be
    -- used if the decision to accept the request can not be made in
    -- `pluginEnabled`.
    --
    -- This error will be with Debug. If it's the only response to a request,
    -- HLS will respond as if no plugins passed the `pluginEnabled` stage.
  | PluginRequestRefused T.Text
    -- |PluginRuleFailed should be thrown when a Rule your response depends on
    -- fails.
    --
    -- This error takes the name of the Rule that failed.
    --
    -- This error will be logged with Debug, and will be converted into a
    -- RequestFailed response code. It takes a low precedence (3) in being
    -- returned as a response to the client.
  | PluginRuleFailed T.Text
    -- |PluginStaleResolve should be thrown when your resolve request is
    -- provided with data it can no longer resolve.
    --
    -- This error will be logged with Debug, and will be converted into a
    -- ContentModified response. It takes a low precedence (3) in being returned
    -- as a response to the client.
  | PluginStaleResolve

instance Pretty PluginError where
    pretty = \case
      PluginInternalError msg     -> "Internal Error:"     <+> pretty msg
      PluginStaleResolve          -> "Stale Resolve"
      PluginRuleFailed rule       -> "Rule Failed:"        <+> pretty rule
      PluginInvalidParams text    -> "Invalid Params:"     <+> pretty text
      PluginInvalidUserState text -> "Invalid User State:" <+> pretty text
      PluginRequestRefused msg    -> "Request Refused: "   <+> pretty msg

-- |Converts to ErrorCode used in LSP ResponseErrors
toErrorCode :: PluginError -> (LSPErrorCodes |? ErrorCodes)
toErrorCode (PluginInternalError _)    = InR ErrorCodes_InternalError
toErrorCode (PluginInvalidParams _)    = InR ErrorCodes_InvalidParams
toErrorCode (PluginInvalidUserState _) = InL LSPErrorCodes_RequestFailed
-- PluginRequestRefused should never be a argument to `toResponseError`, as
-- it should be dealt with in `extensiblePlugins`, but this is here to make
-- this function complete
toErrorCode (PluginRequestRefused _)   = InR ErrorCodes_MethodNotFound
toErrorCode (PluginRuleFailed _)       = InL LSPErrorCodes_RequestFailed
toErrorCode PluginStaleResolve         = InL LSPErrorCodes_ContentModified

-- |Converts to a logging priority. In addition to being used by the logger,
-- `combineResponses` currently uses this to  choose which response to return,
-- so care should be taken in changing it.
toPriority :: PluginError -> Priority
toPriority (PluginInternalError _)    = Error
toPriority (PluginInvalidParams _)    = Warning
toPriority (PluginInvalidUserState _) = Debug
toPriority (PluginRequestRefused _)   = Debug
toPriority (PluginRuleFailed _)       = Debug
toPriority PluginStaleResolve         = Debug

handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b
handleMaybe msg = maybe (throwE msg) return

handleMaybeM :: Monad m => e -> m (Maybe b) -> ExceptT e m b
handleMaybeM msg act = maybeM (throwE msg) return $ lift act

getNormalizedFilePathE :: Monad m => Uri -> ExceptT PluginError m NormalizedFilePath
getNormalizedFilePathE uri = handleMaybe (PluginInvalidParams (T.pack $ "uriToNormalizedFile failed. Uri:" <>  show uri))
        $ uriToNormalizedFilePath
        $ toNormalizedUri uri
