{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE RankNTypes          #-}

-- | Custom SYB traversals
module Wingman.Judgements.SYB where

import           Data.Foldable (foldl')
import           Data.Generics hiding (typeRep)
import qualified Data.Text as T
import           Development.IDE.GHC.Compat
import           Development.IDE.GHC.Compat.Util (unpackFS)
import           GHC.Exts (Any)
import           Type.Reflection
import           Unsafe.Coerce (unsafeCoerce)
import           Wingman.StaticPlugin (pattern WingmanMetaprogram)


------------------------------------------------------------------------------
-- | Like 'everything', but only looks inside 'Located' terms that contain the
-- given 'SrcSpan'.
everythingContaining
    :: forall r
     . Monoid r
    => SrcSpan
    -> GenericQ r
    -> GenericQ r
everythingContaining dst f = go
  where
    go :: GenericQ r
    go x =
      case genericIsSubspan dst x of
        Just False -> mempty
        _ -> foldl' (<>) (f x) (gmapQ go x)


------------------------------------------------------------------------------
-- | Helper function for implementing 'everythingWithin'
--
-- NOTE(sandy): Subtly broken. In an ideal world, this function shuld return
-- @Just False@ for nodes of /any type/ which do not contain the span. But if
-- this functionality exists anywhere within the SYB machinery, I have yet to
-- find it.
genericIsSubspan
    :: SrcSpan
    -> GenericQ (Maybe Bool)
genericIsSubspan dst = mkQ1 (L noSrcSpan ()) Nothing $ \case
  L span _ -> Just $ dst `isSubspanOf` span


------------------------------------------------------------------------------
-- | Like 'mkQ', but allows for polymorphic instantiation of its specific case.
-- This instantation matches whenever the dynamic value has the same
-- constructor as the proxy @f ()@ value.
mkQ1 :: forall a r f
      . (Data a, Data (f ()))
     => f ()                  -- ^ Polymorphic constructor to match on
     -> r                     -- ^ Default value
     -> (forall b. f b -> r)  -- ^ Polymorphic match
     -> a
     -> r
mkQ1 proxy r br a =
    case l_con == a_con && sameTypeModuloLastApp @a @(f ()) of
      -- We have proven that the two values share the same constructor, and
      -- that they have the same type if you ignore the final application.
      -- Therefore, it is safe to coerce @a@ to @f b@, since @br@ is universal
      -- over @b@ and can't inspect it.
      True  -> br $ unsafeCoerce @_ @(f Any) a
      False -> r
  where
    l_con = toConstr proxy
    a_con = toConstr a


------------------------------------------------------------------------------
-- | Given @a ~ f1 a1@ and @b ~ f2 b2@, returns true if @f1 ~ f2@.
sameTypeModuloLastApp :: forall a b. (Typeable a, Typeable b) => Bool
sameTypeModuloLastApp =
  let tyrep1 = typeRep @a
      tyrep2 = typeRep @b
   in case (tyrep1 , tyrep2) of
        (App a _, App b _) ->
          case eqTypeRep a b of
            Just HRefl -> True
            Nothing    -> False
        _ -> False


metaprogramAtQ :: SrcSpan -> GenericQ [(SrcSpan, T.Text)]
metaprogramAtQ ss = everythingContaining ss $ mkQ mempty $ \case
  L new_span (WingmanMetaprogram program) -> pure (new_span, T.pack $ unpackFS program)
  (_ :: LHsExpr GhcTc) -> mempty


metaprogramQ :: GenericQ [(SrcSpan, T.Text)]
metaprogramQ = everything (<>) $ mkQ mempty $ \case
  L new_span (WingmanMetaprogram program) -> pure (new_span, T.pack $ unpackFS program)
  (_ :: LHsExpr GhcTc) -> mempty

