{-# LANGUAGE ExistentialQuantification #-}
-----------------------------------------------------------------------------
-- Copyright 2019, Ideas project team. This file is distributed under the
-- terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer  :  bastiaan.heeren@ou.nl
-- Stability   :  provisional
-- Portability :  portable (depends on ghc)
--
-- References, bindings, and heterogenous environments
--
-----------------------------------------------------------------------------

module Ideas.Common.Environment
   ( -- * Reference
     Ref, Reference(..), mapRef
     -- * Binding
   , Binding, makeBinding
   , fromBinding, showValue, getTermValue
     -- * Heterogeneous environment
   , Environment, makeEnvironment, singleBinding
   , HasEnvironment(..), HasRefs(..)
   , bindings, noBindings, (?)
   ) where

import Control.Monad
import Data.Function
import Data.List
import Data.Semigroup as Sem
import Ideas.Common.Id
import Ideas.Common.Rewriting.Term
import Ideas.Common.View
import Ideas.Utils.Prelude
import Ideas.Utils.Typeable
import qualified Data.Map as M

-----------------------------------------------------------
-- Reference

-- | A data type for references (without a value)
data Ref a = Ref
   { identifier :: Id                -- Identifier
   , printer    :: a -> String       -- Pretty-printer
   , parser     :: String -> Maybe a -- Parser
   , refView    :: View Term a       -- Conversion to/from term
   , refType    :: IsTypeable a
   }

instance Show (Ref a) where
   show = showId

instance Eq (Ref a) where
   (==) = (==) `on` getId

instance HasId (Ref a) where
   getId = identifier
   changeId f d = d {identifier = f (identifier d)}

instance HasTypeable Ref where
   getTypeable = Just . refType

-- | A type class for types as references
class (IsTerm a, Typeable a, Show a, Read a) => Reference a where
   makeRef     :: IsId n => n -> Ref a
   makeRefList :: IsId n => n -> Ref [a]
   -- default implementation
   makeRef n     = Ref (newId n) show readM termView typeable
   makeRefList n = Ref (newId n) show readM termView typeable

instance Reference Int

instance Reference Term

instance Reference Char where
   makeRefList n = Ref (newId n) id Just variableView typeable

instance Reference ShowString

instance Reference a => Reference [a] where
   makeRef = makeRefList

instance (Reference a, Reference b) => Reference (a, b)

mapRef :: Typeable b => Isomorphism a b -> Ref a -> Ref b
mapRef iso r = r
   { printer = printer r . to iso
   , parser  = fmap (from iso) . parser r
   , refView = refView r >>> toView iso
   , refType = typeable
   }

-----------------------------------------------------------
-- Binding

data Binding = forall a . Binding (Ref a) a

instance Show Binding where
   show a = showId a ++ "=" ++ showValue a

instance Eq Binding where
   (==) = let f (Binding r a) = (getId r, build (refView r) a)
          in (==) `on` f

instance HasId Binding where
   getId (Binding r _ ) = getId r
   changeId f (Binding r a) = Binding (changeId f r) a

makeBinding :: Ref a -> a -> Binding
makeBinding = Binding

fromBinding :: Typeable a => Binding -> Maybe (Ref a, a)
fromBinding (Binding r a) = liftM2 (,) (gcastFrom r r) (castFrom r a)

showValue :: Binding -> String
showValue (Binding r a) = printer r a

getTermValue :: Binding -> Term
getTermValue (Binding r a) = build (refView r) a

-----------------------------------------------------------
-- Heterogeneous environment

newtype Environment = Env { envMap :: M.Map Id Binding }
   deriving Eq

instance Show Environment where
   show = intercalate ", " . map show . bindings

instance Sem.Semigroup Environment where
   a <> b = Env (envMap a `mappend` envMap b) -- left has presedence

instance Monoid Environment where
   mempty  = Env mempty
   mappend = (<>)

instance HasRefs Environment where
   allRefs env = [ Some ref | Binding ref _ <- bindings env ]

makeEnvironment :: [Binding] -> Environment
makeEnvironment xs = Env $ M.fromList [ (getId a, a) | a <- xs ]

singleBinding :: Ref a -> a -> Environment
singleBinding r = makeEnvironment . return . Binding r

class HasEnvironment env where
   environment    :: env -> Environment
   setEnvironment :: Environment -> env -> env
   deleteRef      :: Ref a -> env -> env
   insertRef      :: Ref a -> a -> env -> env
   changeRef      :: Ref a -> (a -> a) -> env -> env
   -- default definitions
   deleteRef a = changeEnv (Env . M.delete (getId a) . envMap)
   insertRef r =
      let f b = Env . M.insert (getId b) b . envMap
      in changeEnv . f . Binding r
   changeRef r f env  =
      maybe id (insertRef r . f) (r ? env) env

-- local helper
changeEnv :: HasEnvironment env => (Environment -> Environment) -> env -> env
changeEnv f env = setEnvironment (f (environment env)) env

class HasRefs a where
   getRefs   :: a -> [Some Ref]
   allRefs   :: a -> [Some Ref] -- with duplicates
   getRefIds :: a -> [Id]
   -- default implementation
   getRefIds a = [ getId r | Some r <- getRefs a]
   getRefs = sortBy cmp . nubBy eq . allRefs
    where
      cmp :: Some Ref -> Some Ref -> Ordering
      cmp (Some x) (Some y) = compareId (getId x) (getId y)
      eq a b = cmp a b == EQ

instance HasEnvironment Environment where
   environment    = id
   setEnvironment = const

bindings :: HasEnvironment env => env -> [Binding]
bindings = sortBy compareId . M.elems . envMap . environment

noBindings :: HasEnvironment env => env -> Bool
noBindings = M.null . envMap . environment

(?) :: HasEnvironment env => Ref a -> env -> Maybe a
ref ? env = do
   let m = envMap (environment env)
   Binding r a <- M.lookup (getId ref) m
   msum [ castBetween r ref a                  -- typed value
        , castFrom r a >>= parser ref          -- value as string
        , castFrom r a >>= match (refView ref) -- value as term
        ]