{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
module Debug.Breakpoint.TypeChecker
  ( tcPlugin
  ) where

import           Data.Either
import           Data.Maybe
import           Data.Traversable (for)
import qualified GHC.Tc.Plugin as Plugin

import qualified Debug.Breakpoint.GhcFacade as Ghc

--------------------------------------------------------------------------------
-- Type Checker Plugin
--------------------------------------------------------------------------------

data TcPluginNames =
  MkTcPluginNames
    { showLevClassName :: !Ghc.Name
    , showLevNameTc :: !Ghc.Name
    , showClass :: !Ghc.Class
    , succeedClass :: !Ghc.Class
    , showWrapperTyCon :: !Ghc.TyCon
    }

tcPlugin :: Ghc.TcPlugin
tcPlugin = Ghc.TcPlugin
  { Ghc.tcPluginInit  = initTcPlugin
  , Ghc.tcPluginSolve = solver
  , Ghc.tcPluginStop = const $ pure ()
  , Ghc.tcPluginRewrite = mempty
  }

initTcPlugin :: Ghc.TcPluginM TcPluginNames
initTcPlugin = do
  breakpointMod <-
    Ghc.findImportedModule' (Ghc.mkModuleName "Debug.Breakpoint")

  showLevClassName <- Plugin.lookupOrig breakpointMod (Ghc.mkClsOcc "ShowLev")
  showLevNameTc <- Plugin.lookupOrig breakpointMod (Ghc.mkVarOcc "showLev")
  showClass <- Plugin.tcLookupClass Ghc.showClassName
  succeedClass <- Plugin.tcLookupClass =<< Plugin.lookupOrig breakpointMod (Ghc.mkClsOcc "Succeed")
  showWrapperTyCon <- Plugin.tcLookupTyCon =<< Plugin.lookupOrig breakpointMod (Ghc.mkClsOcc "ShowWrapper")

  pure MkTcPluginNames{..}

data FindWantedResult
  = FoundLifted Ghc.Type Ghc.Ct
  | FoundUnlifted Ghc.Type Ghc.Ct
  | NotFound

findShowLevWanted
  :: TcPluginNames
  -> Ghc.Ct
  -> FindWantedResult
findShowLevWanted names ct
  | Ghc.CDictCan' _ di_cls di_tys <- ct
  , showLevClassName names == Ghc.getName di_cls
  , [Ghc.TyConApp tyCon [], arg2] <- di_tys
  = if Ghc.getName tyCon == Ghc.liftedRepName
       then FoundLifted arg2 ct
       else FoundUnlifted arg2 ct
  | otherwise = NotFound

findShowWithSuperclass
  :: TcPluginNames
  -> Ghc.Ct
  -> Maybe (Ghc.Type, Ghc.Ct)
findShowWithSuperclass names ct
  | Ghc.CDictCan' di_ev di_cls di_tys <- ct
  , Ghc.getName (showClass names) == Ghc.getName di_cls
  , hasShowLevSuperclass . Ghc.ctLocOrigin $ Ghc.ctEvLoc di_ev
  , [arg] <- di_tys
  = Just (arg, ct)
  | otherwise = Nothing
  where
    hasShowLevSuperclass (Ghc.OccurrenceOf name)
      = name == showLevNameTc names
    hasShowLevSuperclass _ = False

solver :: TcPluginNames -> Ghc.TcPluginSolver
solver names _given _derived wanted = do
  instEnvs <- Plugin.getInstEnvs

  -- Check if wanted is ShowLev
  --   * Create a new wanted for Show
  --   * Use its EvBindId as the inner dict for ShowLev
  --   * Emit the new wanted
  (showLevDicts, mNewWanteds) <- fmap (unzip . catMaybes) $
    for (findShowLevWanted names <$> wanted) $ \case
      FoundUnlifted ty ct -> do
        unshowableDict <- Ghc.unsafeTcPluginTcM $ buildUnshowableDict ty
        pure $ Just ((unshowableDict, ct), Nothing)
      FoundLifted ty ct -> do
        (showDict, newWanted) <- buildShowLevDict names ct ty
        let (succInst, _) = fromRight (error "impossible: no Succeed instance") $
              Ghc.lookupUniqueInstEnv instEnvs (succeedClass names) [ty]
        pure $ Just
          ((liftDict succInst ty (getEvExprFromDict showDict), ct)
          , Just newWanted
          )
      NotFound -> pure Nothing

  -- Check if wanted is Show that arises from a use of showLev and create the
  -- missing Show dict if so.
  unshowableDicts <- for (findShowWithSuperclass names `mapMaybe` wanted) $
    \(ty, ct) -> do
        dict <- lookupUnshowableDict names ty
        pure (dict, ct)

  pure $ Ghc.TcPluginOk
           (showLevDicts ++ unshowableDicts)
           (catMaybes mNewWanteds)

buildShowLevDict
  :: TcPluginNames
  -> Ghc.Ct
  -> Ghc.Type
  -> Ghc.TcPluginM (Ghc.EvTerm, Ghc.Ct)
buildShowLevDict names showLevWanted ty = do
  showWantedEv <-
    Plugin.newWanted
      (Ghc.ctLoc showLevWanted)
      (Ghc.mkTyConApp (Ghc.classTyCon $ showClass names) [ty])
  let showCt = Ghc.mkNonCanonical showWantedEv
  pure (Ghc.ctEvTerm showWantedEv, showCt)

lookupUnshowableDict
  :: TcPluginNames
  -> Ghc.Type
  -> Ghc.TcPluginM Ghc.EvTerm
lookupUnshowableDict names ty = do
  instEnvs <- Plugin.getInstEnvs
  unshowableDict <- Ghc.unsafeTcPluginTcM $ buildUnshowableDict ty
  let (inst, _) = fromRight (error "impossible: no Show instance for ShowWrapper") $
        Ghc.lookupUniqueInstEnv
          instEnvs
          (showClass names)
          [Ghc.mkTyConApp (showWrapperTyCon names) [ty]]
  pure $ liftDict inst ty (getEvExprFromDict unshowableDict)

getEvExprFromDict :: Ghc.EvTerm -> Ghc.EvExpr
getEvExprFromDict = \case
  Ghc.EvExpr expr -> expr
  _ -> error "invalid argument to getEvExprFromDict"

buildUnshowableDict :: Ghc.Type -> Ghc.TcM Ghc.EvTerm
buildUnshowableDict ty = do
  let tyString = Ghc.showSDocOneLine' $ Ghc.pprTypeForUser' ty
  str <- Ghc.mkStringExpr $ "<" <> tyString <> ">"
  pure . Ghc.EvExpr $
    Ghc.mkCoreLams [Ghc.mkWildValBinder' ty] str

liftDict :: Ghc.ClsInst -> Ghc.Type -> Ghc.EvExpr -> Ghc.EvTerm
liftDict succ_inst ty dict = Ghc.evDFunApp (Ghc.is_dfun succ_inst) [ty] [dict]
