{-# LANGUAGE CPP #-}

-- | Log message formatting and debuging functions.
module Control.Super.Plugin.Log
  ( pprToStr, sDocToStr
  , missingCaseError
  , smErrMsg, smDebugMsg, smObjMsg, smWarnMsg
  , formatGroupSrcSpans
  , formatConstraint, formatSpan
  -- * Debug Functions
  , printTrace, printObjTrace, trace
  -- * Debugging and priniting from within TcPluginM
  , printObj, printMsg, printErr, printWarn
  , pluginAssert, pluginFailSDoc
  ) where

import Data.List ( groupBy, intercalate )

import Debug.Trace ( trace )

import SrcLoc
  ( SrcSpan(..)
  , srcSpanFileName_maybe
  , srcSpanStartLine, srcSpanEndLine
  , srcSpanStartCol, srcSpanEndCol )
import Outputable ( Outputable, SDoc )
import FastString ( unpackFS )
import TcRnTypes
  ( Ct(..), CtFlavour(..)--, CtLoc(..)
  , ctFlavour, ctPred )
import TcPluginM ( TcPluginM, tcPluginIO, unsafeTcPluginTcM )
import IOEnv ( failWithM )

import Control.Super.Plugin.Debug ( pprToStr, sDocToStr )
import Control.Super.Plugin.Utils ( removeDup )
import Control.Super.Plugin.Constraint ( constraintSourceLocation )

-- | @prefixMsg prefix msg@ prefixes a message with the given string.
prefixMsg :: String -> String -> String
prefixMsg prefix = unlines . fmap ((pluginMsgPrefix ++ prefix) ++) . lines

-- | Message prefix of the plugin.
pluginMsgPrefix :: String
pluginMsgPrefix = "[SM]"

-- | Prefix a message with the error prefix.
smErrMsg :: String -> String
smErrMsg = prefixMsg $ " ERROR: "

-- | Prefix a message with the warning prefix.
smWarnMsg :: String -> String
smWarnMsg = prefixMsg $ " WARNING: "

-- | Prefix a message with the standard debug prefix.
smDebugMsg :: String -> String
smDebugMsg = prefixMsg $ " "

-- | Prefix a message with the debug prefix and a note that this is a
--   printed object.
smObjMsg :: String -> String
smObjMsg = prefixMsg $ "> "

-- | Used to emit an error with a message describing the missing case.
--   The string is the function that misses the case and the 'Outputable'
--   is the object being matched.
missingCaseError :: (Outputable o) => String -> Maybe o -> a
missingCaseError funName (Just val) = error $ "Missing case in '" ++ funName ++ "' for " ++ pprToStr val
missingCaseError funName Nothing    = error $ "Missing case in '" ++ funName ++ "'"

-- -----------------------------------------------------------------------------
-- Formatting
-- -----------------------------------------------------------------------------

-- | Format a list of source spans by grouping spans in the same file together.
formatGroupSrcSpans :: [SrcSpan] -> String
formatGroupSrcSpans spans = unwords $ fmap formatSpanGroup groupedSpans
  where
    formatSpanGroup :: [SrcSpan] -> String
    formatSpanGroup [] = ""
    formatSpanGroup ss@(s:_) =
      case srcSpanFileName_maybe s of
        Nothing -> intercalate ", " $ fmap formatSpan ss
        Just file -> unpackFS file ++ ": " ++ intercalate ", " (fmap formatSpan ss) ++ ";"

    groupedSpans = groupBy eqFileName $ removeDup spans
    eqFileName s1 s2 = srcSpanFileName_maybe s1 == srcSpanFileName_maybe s2

-- | Format a source span.
formatSpan :: SrcSpan -> String
formatSpan (UnhelpfulSpan str) = unpackFS str
formatSpan (RealSrcSpan s) =
  show (srcSpanStartLine s) ++ ":" ++
  show (srcSpanStartCol s) ++ "-" ++
  (if srcSpanStartLine s /= srcSpanEndLine s then show (srcSpanEndLine s) ++ ":" else "") ++
  show (srcSpanEndCol s)

-- | Format a constraint in a readable way, without displaying information
--   irrelevant to the plugin.
--
--   /Example:/
--
-- >>> [G] Supermonad m Identity m (129:12-131:41, CDictCan)
-- >>> [D] m_a1kdW ~ m (134:3-14, CNonCanonical)
formatConstraint :: Ct -> String
formatConstraint ct
  =  "["  ++ formatCtFlavour ct
  ++ "] " ++ formatCtType ct
  ++ " (" ++ formatSpan (constraintSourceLocation ct)
  ++ ", " ++ formatCtDataCon ct
  ++ ")"
  where
    formatCtDataCon :: Ct -> String
    formatCtDataCon c = case c of
      CDictCan      {} -> "CDictCan"
#if MIN_VERSION_GLASGOW_HASKELL(8,4,0,0)
      CIrredCan     {} -> "CIrredCan"
#else
      CIrredEvCan   {} -> "CIrredEvCan"
#endif
      CTyEqCan      {} -> "CTyEqCan"
      CFunEqCan     {} -> "CFunEqCan"
      CNonCanonical {} -> "CNonCanonical"
      CHoleCan      {} -> "CHoleCan"
    formatCtFlavour :: Ct -> String
    formatCtFlavour c = case ctFlavour c of
      Given      -> "G"
      -- Pattern matching this way is important, because up to GHC 8.0
      -- "Wanted" does not have arguments. Howeverm from GHC 8.2 onwards
      -- it does have arguments.
      Wanted {}  -> "W"
      Derived    -> "D"
    formatCtType :: Ct -> String
    formatCtType c = pprToStr $ ctPred c

-- -----------------------------------------------------------------------------
-- Debug Functions
-- -----------------------------------------------------------------------------

-- | Print the result of calling 'show' on the given object.
printTrace :: (Show a) => a -> a
printTrace x = trace (show x) x

-- | Print the result of the 'Outputable' instance of the given object.
printObjTrace :: (Outputable o) => o -> o
printObjTrace o = trace (pprToStr o) o

-- -----------------------------------------------------------------------------
-- Debugging and printing from within TcPluginM
-- -----------------------------------------------------------------------------

-- | Internal function for printing from within the monad.
internalPrint :: String -> TcPluginM ()
internalPrint = tcPluginIO . putStr

-- | Print a message using the plugin formatting.
printMsg :: String -> TcPluginM ()
printMsg = internalPrint . smDebugMsg

-- | Print an error message using the plugin formatting.
printErr :: String -> TcPluginM ()
printErr = internalPrint . smErrMsg

-- | Print a warning message using the plugin formatting.
printWarn :: String -> TcPluginM ()
printWarn = internalPrint . smWarnMsg

-- | Print an object using the plugin formatting.
printObj :: Outputable o => o -> TcPluginM ()
printObj = internalPrint . smObjMsg . pprToStr

-- | Throw a type checker error with the given message.
pluginFailSDoc :: SDoc -> TcPluginM a
pluginFailSDoc msg = do
  printMsg $ sDocToStr msg
  unsafeTcPluginTcM $ failWithM (sDocToStr msg)

-- | If the given condition is false this will fail the compiler with the given error message.
pluginAssert :: Bool -> SDoc -> TcPluginM ()
pluginAssert True _ = return ()
pluginAssert False msg = pluginFailSDoc msg