{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- |Debug utilities
module Ide.Plugin.Eval.Util (
    asS,
    timed,
    isLiterate,
    handleMaybe,
    handleMaybeM,
    response,
    response',
    gStrictTry,
    logWith,
) where

import           Control.Monad.Extra        (maybeM)
import           Control.Monad.IO.Class     (MonadIO (liftIO))
import           Control.Monad.Trans.Class  (lift)
import           Control.Monad.Trans.Except (ExceptT (..), runExceptT, throwE)
import           Data.Aeson                 (Value (Null))
import           Data.Bifunctor             (first)
import           Data.String                (IsString (fromString))
import qualified Data.Text                  as T
import           Development.IDE            (IdeState, Priority (..), ideLogger,
                                             logPriority)
import           Exception                  (ExceptionMonad, SomeException (..),
                                             evaluate, gcatch)
import           GHC.Exts                   (toList)
import           GHC.Stack                  (HasCallStack, callStack,
                                             srcLocFile, srcLocStartCol,
                                             srcLocStartLine)
import           Language.LSP.Server
import           Language.LSP.Types
import           Outputable                 (Outputable (ppr), ppr,
                                             showSDocUnsafe)
import           System.FilePath            (takeExtension)
import           System.Time.Extra          (duration, showDuration)
import           UnliftIO.Exception         (catchAny)

asS :: Outputable a => a -> String
asS = showSDocUnsafe . ppr

timed :: MonadIO m => (t -> String -> m a) -> t -> m b -> m b
timed out name op = do
    (secs, r) <- duration op
    _ <- out name (showDuration secs)
    return r

-- |Log using hie logger, reports source position of logging statement
logWith :: (HasCallStack, MonadIO m, Show a1, Show a2) => IdeState -> a1 -> a2 -> m ()
logWith state key val =
    liftIO . logPriority (ideLogger state) logLevel $
        T.unwords
            [T.pack logWithPos, asT key, asT val]
  where
    logWithPos =
        let stk = toList callStack
            pr pos = concat [srcLocFile pos, ":", show . srcLocStartLine $ pos, ":", show . srcLocStartCol $ pos]
         in if null stk then "" else pr . snd . head $ stk

    asT :: Show a => a -> T.Text
    asT = T.pack . show

-- | Set to Info to see extensive debug info in hie log, set to Debug in production
logLevel :: Priority
logLevel = Debug -- Info

isLiterate :: FilePath -> Bool
isLiterate x = takeExtension x `elem` [".lhs", ".lhs-boot"]

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

response :: Functor f => ExceptT String f c -> f (Either ResponseError c)
response =
    fmap (first (\msg -> ResponseError InternalError (fromString msg) Nothing))
        . runExceptT

response' :: ExceptT String (LspM c) WorkspaceEdit -> LspM c (Either ResponseError Value)
response' act = do
    res <- runExceptT act
             `catchAny` showErr
    case res of
      Left e ->
          return $ Left (ResponseError InternalError (fromString e) Nothing)
      Right a -> do
        _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing a) (\_ -> pure ())
        return $ Right Null

gStrictTry :: ExceptionMonad m => m b -> m (Either String b)
gStrictTry op =
    gcatch
        (op >>= fmap Right . gevaluate)
        showErr

gevaluate :: MonadIO m => a -> m a
gevaluate = liftIO . evaluate

showErr :: Monad m => SomeException -> m (Either String b)
showErr = return . Left . show
