{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-|
Module      : Angle.Types.Scope
Description : Defines functions for working with GenScopes.
Copyright   : Copyright (C) 2015 Ben Moon
License     : GNU GPL, version 3
Maintainer  : GuiltyDolphin@gmail.com
Stability   : alpha

Each variable in Angle belongs to a scope.

Each scope contains a reference to its parent scope and the binding
environment that maps variables to their respective values.

Each variable can have a function definition and a value definition,
which are resolved independently and at distinguishable times.

A variable is considered in-scope if it exists in the current scope
or any of the parent scopes.

The outer-most scope is called the global scope and is accessible
from all other parts of a program.
-}
module Angle.Types.Scope
    ( GenScope(..)
    , BindEnv(..)
    , VarVal(..)
    , bindEnvFromList
    , deleteLitFromScope
    , emptyScope
    , emptyVar
    , isDefinedIn
    , isOutermostScope
    , mergeScope
    , resolve
    , setVarFunInScope
    , setVarLitInScope
    , modifyOuterScope
    , modifyGlobalScope
    , globalScope
    ) where


import Control.Monad
import qualified Data.Map as M
import Data.Maybe (isJust)
import Data.Function (on)


-- | Convert a list to a binding environment.
bindEnvFromList :: (Ord n) => [(n, VarVal a)] -> BindEnv n a
bindEnvFromList = BindEnv . M.fromList


emptyBindEnv :: BindEnv n a
emptyBindEnv = BindEnv M.empty


-- | Binding environment.
newtype BindEnv n a = BindEnv
    { unBindEnv :: M.Map n (VarVal a) }
    deriving (Show, Eq)


type BindMap n a = M.Map n (VarVal a)


-- | Contains variable-value bindings, along with a reference
-- to a parent scope.
data GenScope n v f = Scope
    { outerScope :: Maybe (GenScope n v f) -- ^ Parent scope, if any.
    , valueBindings :: BindEnv n v
    , lambdaBindings :: BindEnv n f
    } deriving (Show, Eq)


-- type GenScope n v f = (Ord n) => GScope n v f


-- | True if the given scope has no parent scopes.
isOutermostScope :: GenScope n v f -> Bool
isOutermostScope s = case outerScope s of
                    Nothing -> True
                    Just _  -> False


-- | The outermost scope of the given scope stack.
globalScope :: GenScope n v f -> GenScope n v f
globalScope sc@(Scope { outerScope = Nothing }) = sc
globalScope (Scope { outerScope = Just sc }) = globalScope sc


-- | True if the scope contains a defition for the given
-- identifier.
isDefinedIn :: (Ord n) => (GenScope n v f -> BindEnv n a) -> n -> GenScope n v f -> Bool
isDefinedIn binds name scope = isJust $ lookupBind name $ binds scope


onBind :: (BindMap n a -> b) -> BindEnv n a -> b
onBind f = f . unBindEnv


withBind :: (BindMap n a -> BindMap n a) -> BindEnv n a -> BindEnv n a
withBind f = toBind . onBind f


toBind :: BindMap n a -> BindEnv n a
toBind = BindEnv


onBinds :: (BindMap n a -> BindMap n a -> BindMap n a) -> BindEnv n a -> BindEnv n a -> BindEnv n a
onBinds f x = toBind . (f `on` unBindEnv) x


-- | Runs a function in the outer scope of that provided.
--
-- Returns `Nothing' if no outer scope exists.
withOuterScope :: GenScope n v f -> (GenScope n v f -> a) -> Maybe a
withOuterScope sc f = liftM f (outerScope sc)


-- | Allows the modification of a parent scope without modifying
-- the current scope (bar the parent changing).
modifyOuterScope :: GenScope n v f -> (GenScope n v f -> GenScope n v f) -> GenScope n v f
modifyOuterScope sc parF =
    case outerScope sc of
        Nothing -> sc
        Just parS -> let newPar = parF parS in sc { outerScope = Just newPar }


-- | Modifies the global scope while not affecting non-globals.
modifyGlobalScope :: GenScope n v f -> (GenScope n v f -> GenScope n v f) -> GenScope n v f
modifyGlobalScope sc globF =
    if isOutermostScope sc
    then globF sc
    else let (Just outer) = outerScope sc
         in sc { outerScope = Just $ modifyGlobalScope outer globF }


-- | Finds the local-most GenScope that contains a definition
-- for the specified identifier.
innerScopeDefining :: (Ord n) => (GenScope n v f -> BindEnv n a) -> n -> GenScope n v f -> Maybe (GenScope n v f)
innerScopeDefining binds name scope
    = if isDefinedIn binds name scope
      then Just scope
      else join $ withOuterScope scope (innerScopeDefining binds name)


-- | Retrieves the variable's definition from the local-most
-- scope in which it is defined.
--
-- Returns Nothing if no definition is found.
resolve :: (Ord n) => (GenScope n v f -> BindEnv n a) -> n -> GenScope n v f -> Maybe (VarVal a)
resolve binds name scope =
    case innerScopeDefining binds name scope of
             Nothing     -> Nothing
             Just scope' -> fromCurrentScope binds scope'
  where
    fromCurrentScope b = lookupBind name . b


-- | A scope with no parent or bindings.
emptyScope :: GenScope n v f
emptyScope = Scope {
               outerScope = Nothing
             , valueBindings = emptyBindEnv
             , lambdaBindings = emptyBindEnv
             }


-- | Run a function over the bindings of a scope.
onLitBindings
  :: (BindEnv n v -> BindEnv n v) -> GenScope n v f -> GenScope n v f
onLitBindings f scope = scope { valueBindings = f $ valueBindings scope }


onFunBindings
  :: (BindEnv n f -> BindEnv n f) -> GenScope n v f -> GenScope n v f
onFunBindings f scope = scope { lambdaBindings = f $ lambdaBindings scope }


insertVar :: (Ord n) => n -> VarVal a -> BindEnv n a -> BindEnv n a
insertVar = insertBind


-- | Set the value definition for the given variable in the given
-- scope.
setVarLitInScope :: (Ord n) => n -> VarVal v -> GenScope n v f -> GenScope n v f
setVarLitInScope name val = onLitBindings (insertVar name val)


-- | Set the lambda definition for the given variable in the given
-- scope.
setVarFunInScope :: (Ord n) => n -> VarVal f -> GenScope n v f -> GenScope n v f
setVarFunInScope name val = onFunBindings (insertVar name val)



-- | Merge the binding values of the scopes, favouring the first
-- when a definition exists in both, but always favouring a
-- definition over no definition.
mergeScope :: (Ord n) => GenScope n v f -> GenScope n v f -> GenScope n v f
mergeScope sc1 sc2
    = let nLits = mergeBinds `on` valueBindings
          nFuns = mergeBinds `on` lambdaBindings
      in sc1 { valueBindings = nLits sc1 sc2
             , lambdaBindings = nFuns sc1 sc2
             }


-- | Remove the value binding of a variable from the given scope.
deleteLitFromScope :: (Ord n) => n -> GenScope n v f -> GenScope n v f
deleteLitFromScope =  onLitBindings . deleteBind



deleteBind :: (Ord n) => n -> BindEnv n a -> BindEnv n a
deleteBind = withBind . M.delete


mergeBinds :: (Ord n) => BindEnv n a -> BindEnv n a -> BindEnv n a
mergeBinds = onBinds M.union


lookupBind :: (Ord n) => n -> BindEnv n a -> Maybe (VarVal a)
lookupBind = onBind . M.lookup


insertBind :: (Ord n) => n -> VarVal a -> BindEnv n a -> BindEnv n a
insertBind n = withBind . M.insert n


-- | Represents a variable definition.
data VarVal a = VarVal
    { varDef :: Maybe a
    , varBuiltin :: Bool
    } deriving (Show, Eq, Ord)


-- | Variable with no definitions.
emptyVar :: VarVal a
emptyVar = VarVal { varDef = Nothing
                  , varBuiltin = False }
