{-# LANGUAGE FlexibleContexts,
             ExistentialQuantification,
             ScopedTypeVariables,
             RankNTypes,
             TypeSynonymInstances,
             DeriveDataTypeable
 #-}

module AOP.Internal.JoinpointModel (
  -- | Join points
  FunctionTag,
  defaultFunctionTag,
  Jp (..),
  newjp,
  compareFun,
  compareType,
  getJpArg,

  -- Pointcuts
  PC (..),
  runPC,
  RequirePC (..),

  -- Advice
  Advice,
  applyAdv,

  -- Aspects
  Aspect (..),
  aspect,
  deleteAsp,
  EAspect (..),
  AspectEnv,  

  -- Other modules
  module X,
) where

import Unsafe.Coerce
import Control.Monad
import Data.Unique
import System.IO.Unsafe

import AOP.Internal.LessGen as X
import AOP.Internal.Typeable1Monad as X
import AOP.Internal.StableNamesEq
import AOP.Internal.PolyTypeableUtils

-- JOIN POINTS

type FunctionTag = Integer

defaultFunctionTag = 343123

-- | Join points are function applications. We store the function and the argument, and the function type representation.
-- | We add a FunctionTag value to use for quantification.
data Jp m a b = (Typeable1Monad m, PolyTypeable (a -> m b)) => Jp (a -> m b) FunctionTag a TypeRep

-- | Creates a join point with given function, tag, and argument
newjp :: (Typeable1Monad m, PolyTypeable (a -> m b)) => (a -> m b) -> FunctionTag -> a -> Jp m a b
newjp f t a = Jp f t a (polyTypeOf f)

-- | Comparing identity of functions:
compareFun :: (Typeable1Monad m, PolyTypeable (a -> m b)) => t -> FunctionTag -> Jp m a b -> Bool
compareFun f ft (Jp g t _ _) = if t == defaultFunctionTag then stableNamesEq f g else ft == t

-- | Compare types to see if type representation t is less general 
-- | than the type of the function associated to the join point
compareType :: (Typeable1Monad m, PolyTypeable (a -> m b)) => TypeRep -> Jp m a b -> Bool
compareType  t (Jp _ _ _ ft) = isLessGeneral ft t

-- | Gets the argument bound to the join point
getJpArg :: Monad m => Jp m a b -> a
getJpArg (Jp _ _ x _) = x

-- POINTCUTS

-- | A pointcut is a predicate on the current join point. It is used to identify join points of interest.
data PC m a b = PC {mpcond :: forall a' b'. m (Jp m a' b' -> m Bool)}

-- | Extracts the computation resulting of applying a join point to the pointcut
runPC (PC mpcond) jp = do { pccond <- mpcond; pccond jp}

-- | A RequirePC is not a valid standalone pointcut, it reflects a type requirement and must be combined with a standard PC.
data RequirePC m a b = Typeable1Monad m => RequirePC {mpcond' ::  forall a' b'. m (Jp m a' b' -> m Bool)}

-- | Support for PolyTypeable

instance (Typeable1 m) => Typeable2 (Jp m) where
  typeOf2 _ = mkTyConApp (mkTyCon3 "EffectiveAspects" "AOP.Internal.JoinpointModel" "Jp")
              [typeOf1 (undefined :: m ())]

instance (Typeable1 m) => Typeable2 (PC m) where
  typeOf2 _ = mkTyConApp (mkTyCon3 "PC" "PC" "PC") 
              [typeOf1 (undefined :: m ())]

instance (Typeable1 m) => Typeable2 (RequirePC m) where
  typeOf2 _ = mkTyConApp (mkTyCon3 "RequirePC" "RequirePC" "RequirePC") 
              [typeOf1 (undefined :: m ())]

-- ADVICE

type Advice m a b = (a -> m b) -> a -> m b

-- | Coerces t2 to be compatible with the advice. It passes t1 as a the proceed argument of the advice.
-- This coercion is safe, as described in the paper.
applyAdv :: Advice m a b -> t2 -> t2
applyAdv = unsafeCoerce


-- ASPECTS

type AspectHandle = Unique

-- | Typed first-class aspect. An aspect is tagged with a Unique value, used for identity
data Aspect m a b c d = LessGen (a -> b) (c -> m d) => Aspect (PC m a b) (Advice m c d) AspectHandle

newAspectHandle :: AspectHandle
newAspectHandle = unsafePerformIO newUnique

-- | Constructs a well-typed aspect
aspect :: (Typeable1Monad m, LessGen (a1 -> b1) (a2 -> m b2)) => PC m a1 b1 -> Advice m a2 b2 -> Aspect m a1 b1 a2 b2
aspect pc adv = Aspect pc adv newAspectHandle

-- | Aspect with hidden types, to be used in the aspect environment
data EAspect m = forall a b c d. LessGen (a -> b) (c -> m d) => EAspect (Aspect m a b c d)

-- | Aspect environment
type AspectEnv m = [EAspect m]

instance Show AspectHandle where
         show handle = show $ hashUnique handle

instance Show (Aspect m a b c d) where
         show (Aspect pc adv handle) = show handle

instance Show (EAspect m) where
         show (EAspect (Aspect pc adv handle)) = show handle

-- | Deletes asp from the aspect environment, used in undeploy
deleteAsp :: Typeable1Monad m => EAspect m -> AspectEnv m -> AspectEnv m
deleteAsp asp = filter (\asp' -> asp /= asp')

-- | Support for PolyTypeable
instance PolyTypeable Unique where
  polyTypeOf _ = mkTyConApp (mkTyCon3 "GHC" "Unique" "") []

-- | Notion of aspect equality to delete aspects from the aspect environment
instance Typeable1Monad m => Eq (EAspect m) where
  EAspect (Aspect _ _ u1) == EAspect (Aspect _ _ u2) = u1 == u2