{-# LANGUAGE MagicHash, MultiParamTypeClasses, TypeOperators, GeneralizedNewtypeDeriving #-}

-- |
-- Module      : Data.PolyDict 
-- Copyright   : (c) Taku Terao, 2017 
-- License     : BSD3 
-- Maintainer  : autotaker@gmail.com 
-- Stability  : experimental 
-- Portability : GHC
-- Type-safe, polymorphic dictionary. 

module Data.PolyDict( DictValue, Assoc, Dict, Key, lookup, insert, access, access', empty) where

import Prelude hiding(lookup)

import Data.Aeson
import Data.Hashable
import qualified Data.HashMap.Strict as H
import Data.Kind(Constraint)
import Data.List(intersperse)
import Data.Proxy
import Data.Text (pack)

import Data.Type.Equality

import GHC.TypeLits
import GHC.Prim(Proxy#, proxy#)
import GHC.OverloadedLabels

import Lens.Micro
import Unsafe.Coerce

-- | 'DictValue' is the constraint for values can be inserted into 'Dict'
type family DictValue v :: Constraint where
    DictValue v = (Eq v, Show v, ToJSON v)

-- | 'Assoc' n k defines the type of value associated with key k.
--   Parameter n defines the namespace for dictionary fields. For example:
--   
-- > data Log
-- > type instance Assoc Log "argments" = [String]
-- > type instance Assoc Log "count" = Int
-- Then 'Dict' Log is a dictionary type with (at least) two fields "arguments" and "count".
--
-- One can access the fields by using 'insert' and 'lookup'.
--
-- >>> insert #count 0 (empty :: Dict Log)
-- {"count": 0}
-- >>> lookup #count (insert #count 0 (empty :: Dict Log))
-- Just 0
--
-- Or by using lenses:
--
-- >>> import Lens.Micro
-- >>> (empty :: Dict Log) & (access #count ?~ 1) . (access #arguments ?~ ["a","b","c"])
-- {"arguments": ["a","b","c"], "count": 1}
--
type family Assoc n (k :: Symbol)

-- | A polymorphic, type-safe dictinary type where the parameter 'n' represents the namespace of dictionary fields.
newtype Dict n = Dict (H.HashMap (Hashed String) (Entry n))
  deriving(Eq)

instance ToJSON (Dict n) where
    toJSON (Dict dict) = 
        object [ pack (symbolVal' k) .= toJSON v | (Entry k v) <- H.elems dict ]

instance Eq (Entry n) where
    Entry k1 v1 == Entry k2 v2 =
        case f k1 k2 of
            Just Refl -> v1 == v2
            Nothing -> False
        where
        f :: (KnownSymbol k1, KnownSymbol k2) => Proxy# k1 -> Proxy# k2 -> Maybe (k1 :~: k2)
        f _ _ = sameSymbol Proxy Proxy
    {-# INLINE (==) #-}


instance Show (Dict n) where
    showsPrec _ (Dict d) = 
        showChar '{' . 
           foldl1 (.) (intersperse (showString ", ") 
               [ shows (symbolVal' k) . showString ": " . shows v | (Entry k v) <- H.elems d ])
        . showChar '}'

data Entry n where
    Entry :: (KnownSymbol k, DictValue v, Assoc n k ~ v) => Proxy# k -> v -> Entry n

-- | The type of keys. With the OverloadedLabels extenstion, #foo is the key for field "foo"
--   
--
newtype Key (k :: Symbol) = Key (Proxy k)

instance k ~ k' => IsLabel k (Key k') where
    fromLabel _ = Key Proxy
    {-# INLINE fromLabel #-}

-- | Return the value associated with the key.
lookup :: (KnownSymbol k, DictValue v, Assoc n k ~ v) => Key k -> Dict n -> Maybe v
lookup key dict = dict ^. access key
{-# INLINE lookup #-}

-- | Insert the value at the specified key of the dictionary
insert :: (KnownSymbol k, DictValue v, Assoc n k ~ v) => Key k -> v -> Dict n -> Dict n
insert key value = access key ?~ value
{-# INLINE insert #-}

-- | Return the empty dictionary.
empty :: Dict n
empty = Dict H.empty
{-# INLINE empty #-}

-- | Give the lens accessing to the value associated with the key.
access :: forall n k v. (KnownSymbol k, DictValue v, Assoc n k ~ v) => Key k -> Lens' (Dict n) (Maybe v)
access key = lens getter setter
    where
    k = hashed (symbolVal key)
    getter (Dict dict) = case H.lookup k dict of
        Just (Entry _ v) -> Just (unsafeCoerce v)
        Nothing -> Nothing
    setter (Dict dict) Nothing = Dict $ H.delete k dict
    setter (Dict dict) (Just v) = Dict $ H.insert k (Entry (proxy# :: Proxy# k) v) dict
{-# INLINE access #-}

-- | Same as 'access' but requires the default value.
access' :: forall n k v. (KnownSymbol k, DictValue v, Assoc n k ~ v) => Key k -> v -> Lens' (Dict n) v
access' key def = access key . non def
{-# INLINE access' #-}