packages feed

ginger2-2.0.0.0: src/Language/Ginger/Value.hs

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleContexts #-}

module Language.Ginger.Value
where

import Control.Monad.Except (runExceptT, throwError, MonadError)
import Control.Monad.Reader (MonadReader (..))
import Control.Monad.Trans (MonadTrans, lift)
import Data.Aeson
          ( FromJSON (..)
          , FromJSONKey (..)
          , ToJSON (..)
          , FromJSONKeyFunction (..)
          , ToJSONKey (..)
          )
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString.Lazy as LBS
import Data.Foldable (fold)
import Data.Int
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Scientific (floatingOrInteger)
import Data.String (IsString (..))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.Lazy as LText
import Data.Vector (Vector)
import qualified Data.Vector as V
import Data.Word
import GHC.Float (float2Double)
import Test.Tasty.QuickCheck (Arbitrary (..))
import qualified Test.Tasty.QuickCheck as QC

import Language.Ginger.AST
import Language.Ginger.RuntimeError

data Env m =
  Env
    { envVars :: !(Map Identifier (Value m))
    , envRootMay :: Maybe (Env m)
    }
  deriving (Eq)

envRoot :: Env m -> Env m
envRoot e =
  fromMaybe e $ envRootMay e

emptyEnv :: Env m
emptyEnv = Env mempty Nothing

instance Semigroup (Env m) where
  a <> b = Env
            { envVars = envVars a <> envVars b
            , envRootMay = envRootMay a <> envRootMay b
            }

instance Monoid (Env m) where
  mempty = emptyEnv

type TemplateLoader m = Text -> m (Maybe Text)

type Encoder m = Text -> m Encoded

data Context m =
  Context
    { contextEncode :: Encoder m
    , contextLoadTemplateFile :: TemplateLoader m
    , contextVars :: !(Map Identifier (Value m))
    , contextOutput :: !OutputPolicy
    }

data OutputPolicy
  = Quiet
  | Output
  deriving (Show, Eq, Ord, Enum, Bounded)

instance Semigroup OutputPolicy where
  Quiet <> Quiet = Quiet
  _ <> _ = Output

instance Monoid OutputPolicy where
  mappend = (<>)
  mempty = Quiet

emptyContext :: Applicative m => Context m
emptyContext =
  Context
    { contextEncode = pure . Encoded
    , contextLoadTemplateFile = const $ pure Nothing
    , contextVars = mempty
    , contextOutput = Output
    }

data Scalar
  = NoneScalar
  | BoolScalar !Bool
  | StringScalar !Text
  | EncodedScalar !Encoded
  | BytesScalar !ByteString
  | IntScalar !Integer
  | FloatScalar !Double
  deriving (Show, Eq, Ord)

instance FromJSON Scalar where
  parseJSON JSON.Null =
    pure NoneScalar
  parseJSON (JSON.Bool b) =
    pure $ BoolScalar b
  parseJSON (JSON.String s) =
    pure $ StringScalar s
  parseJSON (JSON.Number i) =
    pure $ either FloatScalar IntScalar $ floatingOrInteger i
  parseJSON x = JSON.unexpected x

instance ToJSON Scalar where
  toJSON NoneScalar = JSON.Null
  toJSON (BoolScalar b) = JSON.Bool b
  toJSON (StringScalar s) = toJSON s
  toJSON (EncodedScalar (Encoded e)) = JSON.String e
  toJSON (BytesScalar bs) =
    JSON.object
      [ ("@type", JSON.String "bytes")
      , ("@data", toJSON (BS.unpack bs))
      ]
  toJSON (IntScalar i) = toJSON i
  toJSON (FloatScalar f) = toJSON f

instance FromJSONKey Scalar where
  fromJSONKey = FromJSONKeyText StringScalar

instance ToJSONKey Scalar where
  toJSONKey = JSON.toJSONKeyText scalarToText

scalarToText :: Scalar -> Text
scalarToText NoneScalar = ""
scalarToText (BoolScalar True) = "true"
scalarToText (BoolScalar False) = "false"
scalarToText (StringScalar str) = str
scalarToText (BytesScalar b) = decodeUtf8 . Base64.encode $ b
scalarToText (EncodedScalar (Encoded e)) = e
scalarToText (IntScalar i) = Text.show i
scalarToText (FloatScalar f) = Text.show f

newtype RefID = RefID { unRefID :: Int }
  deriving (Show, Ord, Eq, Bounded, Enum)

-- | A value, as using by the interpreter.
data Value m
  = ScalarV !Scalar
  | ListV !(Vector (Value m))
  | DictV !(Map Scalar (Value m))
  | NativeV !(NativeObject m)
  | ProcedureV !(Procedure m)
  | TestV !(Test m)
  | FilterV !(Filter m)
  | MutableRefV !RefID

instance FromJSON (Value m) where
  parseJSON v@JSON.Object {} = DictV <$> parseJSON v
  parseJSON v@JSON.Array {} = ListV <$> parseJSON v
  parseJSON v = ScalarV <$> parseJSON v

instance ToJSON (Value m) where
  toJSON (ScalarV s) = toJSON s
  toJSON (ListV xs) = toJSON xs
  toJSON (DictV d) = toJSON d
  toJSON x = toJSON (show x)

traverseValue :: Monoid a => (Value m -> a) -> Value m -> a
traverseValue p v@(ListV xs) =
  p v <> fold (fmap (traverseValue p) xs)
traverseValue p v@(DictV m) =
  p v <> mconcat (map (traverseValue p . snd) $ Map.toList m)
traverseValue p v = p v

instance Show (Value m) where
  show (ScalarV s) = show s
  show (ListV xs) = "ListV " ++ show xs
  show (DictV m) = "DictV " ++ show (Map.toAscList m)
  show (NativeV {}) = "<<native>>"
  show (ProcedureV {}) = "<<procedure>>"
  show (TestV {}) = "<<test>>"
  show (FilterV {}) = "<<filter>>"
  show (MutableRefV i) = show i

instance Eq (Value m) where
  ScalarV a == ScalarV b = a == b
  ListV a == ListV b = a == b
  DictV a == DictV b = a == b
  _ == _ = False

tagNameOf :: Value m -> Text
tagNameOf ScalarV {} = "scalar"
tagNameOf ListV {} = "list"
tagNameOf DictV {} = "dict"
tagNameOf NativeV {} = "native"
tagNameOf ProcedureV {} = "procedure"
tagNameOf TestV {} = "test"
tagNameOf FilterV {} = "filter"
tagNameOf MutableRefV {} = "mutref"

pattern NoneV :: Value m
pattern NoneV = ScalarV NoneScalar

pattern BoolV :: Bool -> Value m
pattern BoolV b = ScalarV (BoolScalar b)

pattern TrueV :: Value m
pattern TrueV = BoolV True

pattern FalseV :: Value m
pattern FalseV = BoolV False

pattern StringV :: Text -> Value m
pattern StringV v = ScalarV (StringScalar v)

pattern EncodedV :: Encoded -> Value m
pattern EncodedV v = ScalarV (EncodedScalar v)

pattern BytesV :: ByteString -> Value m
pattern BytesV v = ScalarV (BytesScalar v)

pattern IntV :: Integer -> Value m
pattern IntV v = ScalarV (IntScalar v)

pattern FloatV :: Double -> Value m
pattern FloatV v = ScalarV (FloatScalar v)

newtype ObjectID = ObjectID { unObjectID :: Text }
  deriving (Eq)

instance IsString ObjectID where
  fromString = ObjectID . Text.pack

data TypeDoc
  = TypeDocNone
  | TypeDocAny
  | TypeDocSingle !Text
  | TypeDocAlternatives !(Vector Text)
  deriving (Show)

instance ToValue TypeDoc m where
  toValue TypeDocNone = StringV "none"
  toValue TypeDocAny = StringV "any"
  toValue (TypeDocSingle t) = StringV t
  toValue (TypeDocAlternatives xs) =
    dictV ["oneof" .= xs]

data ArgumentDoc =
  ArgumentDoc
    { argumentDocName :: !Text
    , argumentDocType :: !(Maybe TypeDoc)
    , argumentDocDefault :: !(Maybe Text)
    , argumentDocDescription :: !Text
    }
    deriving (Show)

instance ToValue ArgumentDoc m where
  toValue a =
    dictV
      [ "name" .= argumentDocName a
      , "type" .= argumentDocType a
      , "default" .= argumentDocDefault a
      , "description" .= argumentDocDescription a
      ]

data ProcedureDoc =
  ProcedureDoc
    { procedureDocName :: !Text
    , procedureDocArgs :: !(Vector ArgumentDoc)
    , procedureDocReturnType :: !(Maybe TypeDoc)
    , procedureDocDescription :: !Text
    }
    deriving (Show)

instance ToValue ProcedureDoc m where
  toValue d =
    dictV
      [ "name" .= procedureDocName d
      , "args" .= procedureDocArgs d
      , "returnType" .= procedureDocReturnType d
      , "description" .= procedureDocDescription d
      ]

data Procedure m
  = NativeProcedure
        !ObjectID
        !(Maybe ProcedureDoc)
        !( [(Maybe Identifier, Value m)]
            -> Context m
            -> m (Either RuntimeError (Value m))
         )
  | GingerProcedure !(Env m) ![(Identifier, Maybe (Value m))] !Expr
  | NamespaceProcedure

instance Eq (Procedure m) where
  NativeProcedure a _ _ == NativeProcedure b _ _ =
    a == b
  GingerProcedure env1 argspec1 body1 == GingerProcedure env2 argspec2 body2 =
    (env1, argspec1, body1) == (env2, argspec2, body2)
  NamespaceProcedure == NamespaceProcedure = True
  _ == _ = False

pureNativeProcedure :: Applicative m
                    => ObjectID
                    -> Maybe ProcedureDoc
                    -> ([(Maybe Identifier, Value m)] -> Either RuntimeError (Value m))
                    -> Procedure m
pureNativeProcedure oid doc f =
  NativeProcedure oid doc $ \args _ -> pure (f args)

nativeFunc :: (Monad m)
           => ObjectID
           -> Maybe ProcedureDoc
           -> (Value m -> m (Either RuntimeError (Value m)))
           -> Procedure m
nativeFunc oid doc f =
  NativeProcedure oid doc $ \args _ -> case args of
    [] ->
      pure . Left $
        ArgumentError
          "<native function>"
          "<positional argument>"
          "value"
          "end of arguments"
    [(_, x)] ->
      f x
    (_:(name, x):_) ->
      pure . Left $
        ArgumentError
          "<native function>"
          (maybe "<positional argument>" identifierName $ name)
          "end of arguments"
          (tagNameOf $ x)

pureNativeFunc :: (Applicative m)
               => ObjectID
               -> Maybe ProcedureDoc
               -> (Value m -> Either RuntimeError (Value m))
               -> Procedure m
pureNativeFunc oid doc f =
  NativeProcedure oid doc $ \args _ -> case args of
    [] ->
      pure . Left $
        ArgumentError
          "<native function>"
          "<positional argument>"
          "value"
          "end of arguments"
    [(_, x)] ->
      pure $ f x
    (_:(name, x):_) ->
      pure . Left $
        ArgumentError
          "<native function>"
          (maybe "<positional argument>" identifierName $ name)
          "end of arguments"
          (tagNameOf $ x)

pureNativeFunc2 :: (Applicative m)
               => ObjectID
               -> Maybe ProcedureDoc
               -> (Value m -> Value m -> Either RuntimeError (Value m))
               -> Procedure m
pureNativeFunc2 oid doc f =
  NativeProcedure oid doc $ \args _ -> case args of
    [] ->
      pure . Left $
        ArgumentError
          "<native function>"
          "<positional argument>"
          "value"
          "end of arguments"
    [_] ->
      pure . Left $
        ArgumentError
          "<native function>"
          "<positional argument>"
          "value"
          "end of arguments"
    [(_, x), (_, y)] ->
      pure $ f x y
    (_:_:(name, x):_) ->
      pure . Left $
        ArgumentError
          "<native function>"
          (maybe "<positional argument>" identifierName $ name)
          "end of arguments"
          (tagNameOf $ x)

type MetaFunc m a =
     Expr
  -> [(Maybe Identifier, Value m)]
  -> Context m
  -> Env m
  -> m (Either RuntimeError a)

type TestFunc m = MetaFunc m Bool

type FilterFunc m = MetaFunc m (Value m)

data Test m =
  NativeTest  
    { testDoc :: !(Maybe ProcedureDoc)
    , runTest :: !(TestFunc m)
    }

data Filter m =
  NativeFilter
    { filterDoc :: !(Maybe ProcedureDoc)
    , runFilter :: !(FilterFunc m)
    }

data NativeObject m =
  NativeObject
    { nativeObjectGetFieldNames :: m [Scalar]
    , nativeObjectGetField :: Scalar -> m (Maybe (Value m))
    , nativeObjectGetAttribute :: Identifier -> m (Maybe (Value m))
    , nativeObjectStringified :: m Text
    , nativeObjectEncoded :: Context m -> m Encoded
    , nativeObjectAsList :: m (Maybe (Vector (Value m)))
    , nativeObjectCall :: Maybe
                            (NativeObject m
                              -> [(Maybe Identifier, Value m)]
                              -> m (Either RuntimeError (Value m))
                            )
    , nativeObjectEq :: NativeObject m
                     -> NativeObject m
                     -> m (Either RuntimeError Bool)
    }

nativeObjectAsDict :: Monad m
                   => NativeObject m
                   -> m (Maybe (Map Scalar (Value m)))
nativeObjectAsDict o = do
  fieldNames <- nativeObjectGetFieldNames o
  case fieldNames of
    [] -> pure Nothing
    keys -> do
      pairs <- mapM makePair keys
      pure . Just $ Map.fromList pairs
  where
    makePair k = (k,) . fromMaybe NoneV <$> nativeObjectGetField o k

(-->) :: obj -> (obj -> obj -> a) -> a
obj --> field = field obj obj

defNativeObject :: Monad m => NativeObject m
defNativeObject =
  NativeObject
    { nativeObjectGetFieldNames = pure []
    , nativeObjectGetField = \_ -> pure Nothing
    , nativeObjectGetAttribute = \_ -> pure Nothing
    , nativeObjectStringified = pure "<native object>"
    , nativeObjectEncoded = const $ pure (Encoded "[[native object]]")
    , nativeObjectAsList = pure Nothing
    , nativeObjectCall = Nothing
    , nativeObjectEq = \_self _other -> pure . Right $ False
    }

instance IsString Scalar where
  fromString = StringScalar . Text.pack

instance IsString (Value m) where
  fromString = ScalarV . fromString

class ToScalar a where
  toScalar :: a -> Scalar

instance ToScalar Scalar where
  toScalar = id

instance ToScalar () where
  toScalar () = NoneScalar

instance ToScalar Bool where
  toScalar = BoolScalar

instance ToScalar Integer where
  toScalar = IntScalar

toIntScalar :: Integral a => a -> Scalar
toIntScalar = IntScalar . fromIntegral

instance ToScalar Int where
  toScalar = toIntScalar

instance ToScalar Word where
  toScalar = toIntScalar

instance ToScalar Int8 where
  toScalar = toIntScalar

instance ToScalar Int16 where
  toScalar = toIntScalar

instance ToScalar Int32 where
  toScalar = toIntScalar

instance ToScalar Int64 where
  toScalar = toIntScalar

instance ToScalar Word8 where
  toScalar = toIntScalar

instance ToScalar Word16 where
  toScalar = toIntScalar

instance ToScalar Word32 where
  toScalar = toIntScalar

instance ToScalar Word64 where
  toScalar = toIntScalar

instance ToScalar Double where
  toScalar = FloatScalar

instance ToScalar Float where
  toScalar = FloatScalar . float2Double


instance ToScalar Text where
  toScalar = StringScalar

instance ToScalar LText.Text where
  toScalar = StringScalar . LText.toStrict

instance ToScalar ByteString where
  toScalar = BytesScalar

instance ToScalar LBS.ByteString where
  toScalar = BytesScalar . LBS.toStrict

instance ToScalar a => ToScalar (Maybe a) where
  toScalar Nothing = NoneScalar
  toScalar (Just x) = toScalar x

instance (ToScalar a, ToScalar b) => ToScalar (Either a b) where
  toScalar (Left x) = toScalar x
  toScalar (Right x) = toScalar x

instance ToScalar Identifier where
  toScalar = toScalar . identifierName

class FnArgValue a where
  fromArgValue :: Value m -> Either RuntimeError a

--------------------------------------------------------------------------------
-- FromValue
--------------------------------------------------------------------------------

class FromValue a m where
  fromValue :: Value m -> m (Either RuntimeError a)

--------------------------------------------------------------------------------
-- FromValue instances
--------------------------------------------------------------------------------

instance Applicative m => FromValue (Value m) m where
  fromValue = pure . Right

instance Applicative m => FromValue Scalar m where
  fromValue = pure . asScalarVal

instance Applicative m => FromValue Text m where
  fromValue = pure . asTextVal

instance Applicative m => FromValue Integer m where
  fromValue = pure . asIntVal

instance Applicative m => FromValue Int m where
  fromValue = fmap (fmap fromInteger) . pure . asIntVal

instance Applicative m => FromValue Double m where
  fromValue = pure . asFloatVal

instance Applicative m => FromValue Bool m where
  fromValue = pure . asBoolVal

instance Applicative m => FromValue () m where
  fromValue NoneV = pure $ Right ()
  fromValue x = pure . Left $ TagError "" "fromValue" (tagNameOf x)

instance (Applicative m, FromValue a m) => FromValue (Maybe a) m where
  fromValue NoneV = pure $ Right Nothing
  fromValue x = fmap (fmap Just) $ fromValue x

instance (Monad m, FromValue l m, FromValue r m) => FromValue (Either l r) m where
  fromValue v = do
    fromValue v >>= \case
      Right r -> pure . Right $ Right r
      _ -> do
        fromValue v >>= \case
          Left e -> pure $ Left e
          Right l -> pure . Right $ Left l

instance (Monad m, FromValue a m) => FromValue [a] m where
  fromValue x = runExceptT $ do
    items :: [Value m] <- eitherExceptM (asListVal x)
    mapM (eitherExceptM . fromValue) items

instance (Monad m, FromValue a m) => FromValue (Vector a) m where
  fromValue x = runExceptT $ do
    items :: Vector (Value m) <- eitherExceptM (asVectorVal x)
    V.mapM (eitherExceptM . fromValue) items

instance (Monad m, FromValue a m) => FromValue (Map Scalar a) m where
  fromValue x = runExceptT $ do
    items :: Map Scalar (Value m) <- eitherExceptM (asDictVal x)
    Map.fromList <$> mapM (\(k, v) -> (k,) <$> eitherExceptM (fromValue v)) (Map.toList items)

--------------------------------------------------------------------------------
-- ToValue
--------------------------------------------------------------------------------

class ToValue a m where
  toValue :: a -> Value m

instance ToValue (Value m) m where
  toValue = id

class FnToValue a m where
  fnToValue :: ObjectID -> Maybe ProcedureDoc -> a -> Value m

--------------------------------------------------------------------------------
-- ToValue Scalar instances
--------------------------------------------------------------------------------

instance ToValue Scalar a where
  toValue = ScalarV

instance ToValue () a where
  toValue = ScalarV . toScalar

instance ToValue Bool a where
  toValue = ScalarV . toScalar

instance ToValue Integer a where
  toValue = ScalarV . toScalar

instance ToValue Int a where
  toValue = ScalarV . toScalar

instance ToValue Int8 a where
  toValue = ScalarV . toScalar

instance ToValue Int16 a where
  toValue = ScalarV . toScalar

instance ToValue Int32 a where
  toValue = ScalarV . toScalar

instance ToValue Int64 a where
  toValue = ScalarV . toScalar

instance ToValue Word a where
  toValue = ScalarV . toScalar

instance ToValue Word8 a where
  toValue = ScalarV . toScalar

instance ToValue Word16 a where
  toValue = ScalarV . toScalar

instance ToValue Word32 a where
  toValue = ScalarV . toScalar

instance ToValue Word64 a where
  toValue = ScalarV . toScalar

instance ToValue Double a where
  toValue = ScalarV . toScalar

instance ToValue Float a where
  toValue = ScalarV . toScalar

instance ToValue Text a where
  toValue = ScalarV . toScalar

instance ToValue LText.Text a where
  toValue = ScalarV . toScalar

instance ToValue ByteString a where
  toValue = ScalarV . toScalar

instance ToValue LBS.ByteString a where
  toValue = ScalarV . toScalar

--------------------------------------------------------------------------------
-- Compound / derived instances
--------------------------------------------------------------------------------

instance ToValue a m => ToValue (Maybe a) m where
  toValue Nothing = ScalarV NoneScalar
  toValue (Just x) = toValue x

instance (ToValue a m, ToValue b m) => ToValue (Either a b) m where
  toValue (Left x) = toValue x
  toValue (Right x) = toValue x

instance ToValue a m => ToValue (Vector a) m where
  toValue = ListV . fmap toValue

instance ToValue a m => ToValue [a] m where
  toValue = ListV . V.fromList . map toValue

instance (ToValue a1 m, ToValue a2 m)
         => ToValue (a1, a2) m where
  toValue (x1, x2) =
    ListV $ V.fromList [toValue x1, toValue x2]

instance (ToValue a1 m, ToValue a2 m, ToValue a3 m)
         => ToValue (a1, a2, a3) m where
  toValue (x1, x2, x3) =
    ListV $ V.fromList [toValue x1, toValue x2, toValue x3]

instance (ToValue a1 m, ToValue a2 m, ToValue a3 m, ToValue a4 m)
         => ToValue (a1, a2, a3, a4) m where
  toValue (x1, x2, x3, x4) =
    ListV $ V.fromList [toValue x1, toValue x2, toValue x3, toValue x4]

instance (ToValue a1 m, ToValue a2 m, ToValue a3 m, ToValue a4 m, ToValue a5 m)
         => ToValue (a1, a2, a3, a4, a5) m where
  toValue (x1, x2, x3, x4, x5) =
    ListV $ V.fromList [toValue x1, toValue x2, toValue x3, toValue x4, toValue x5]

instance (ToValue a1 m, ToValue a2 m, ToValue a3 m, ToValue a4 m, ToValue a5 m, ToValue a6 m)
         => ToValue (a1, a2, a3, a4, a5, a6) m where
  toValue (x1, x2, x3, x4, x5, x6) =
    ListV $ V.fromList [toValue x1, toValue x2, toValue x3, toValue x4, toValue x5, toValue x6]

instance (ToValue a1 m, ToValue a2 m, ToValue a3 m, ToValue a4 m, ToValue a5 m, ToValue a6 m, ToValue a7 m)
         => ToValue (a1, a2, a3, a4, a5, a6, a7) m where
  toValue (x1, x2, x3, x4, x5, x6, x7) =
    ListV $ V.fromList [toValue x1, toValue x2, toValue x3, toValue x4, toValue x5, toValue x6, toValue x7]

instance (ToScalar k, ToValue v m) => ToValue (Map k v) m where
  toValue = DictV . Map.mapKeys toScalar . Map.map toValue

instance ToValue v m => ToValue (Map LText.Text v) m where
  toValue = toValue . Map.mapKeys LText.toStrict

instance ToValue v m => ToValue (Map String v) m where
  toValue = toValue . Map.mapKeys Text.pack

--------------------------------------------------------------------------------
-- Function instances
--------------------------------------------------------------------------------

class ToNativeProcedure m a where
  toNativeProcedure :: a -> [(Maybe Identifier, Value m)] -> Context m -> m (Either RuntimeError (Value m))

instance Applicative m => ToNativeProcedure m (Value m) where
  toNativeProcedure val [] _ =
    pure (Right val)
  toNativeProcedure _ _ _ =
    pure . Left $
      ArgumentError "<native function>" "<positional argument>" "end of arguments" "value"

instance Applicative m => ToNativeProcedure m (m (Value m)) where
  toNativeProcedure action [] _ =
    Right <$> action
  toNativeProcedure _ _ _ =
    pure . Left $
      ArgumentError "<native function>" "<positional argument>" "end of arguments" "value"

instance Applicative m => ToNativeProcedure m (m (Either RuntimeError (Value m))) where
  toNativeProcedure action [] _ =
    action
  toNativeProcedure _ _ _ =
    pure . Left $
      ArgumentError "<native function>" "<positional argument>" "end of arguments" "value"

instance (Applicative m, ToNativeProcedure m a) => ToNativeProcedure m (Value m -> a) where
  toNativeProcedure _ [] _ =
    pure . Left $
      ArgumentError "<native function>" "<positional argument>" "value" "end of arguments"
  toNativeProcedure _ ((Just _, _):_) _ =
    pure . Left $
      ArgumentError "<native function>" "<positional argument>" "positional argument" "named argument"
  toNativeProcedure f ((Nothing, v):xs) ctx =
    toNativeProcedure (f v) xs ctx


instance Applicative m => FnToValue (Value m -> Value m) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m -> Value m) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m -> Value m -> Value m) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f


instance Applicative m => FnToValue (Value m -> m (Value m)) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> m (Value m)) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> m (Value m)) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m -> m (Value m)) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m -> Value m -> m (Value m)) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f


instance Applicative m => FnToValue (Value m -> m (Either RuntimeError (Value m))) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> m (Either RuntimeError (Value m))) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> m (Either RuntimeError (Value m))) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m -> m (Either RuntimeError (Value m))) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

instance Applicative m => FnToValue (Value m -> Value m -> Value m -> Value m -> Value m -> m (Either RuntimeError (Value m))) m where
  fnToValue oid doc f = ProcedureV . NativeProcedure oid doc . toNativeProcedure $ f

--------------------------------------------------------------------------------
-- Procedure helpers
--------------------------------------------------------------------------------

eitherExcept :: (Monad m, MonadError e (t m))
             => Either e a -> t m a
eitherExcept = either throwError pure

eitherExceptM :: (Monad m, MonadError e (t m), MonadTrans t)
              => m (Either e a) -> t m a
eitherExceptM = (>>= eitherExcept) . lift

resolveArgs :: Text
            -> [(Identifier, Maybe (Value m))]
            -> [(Maybe Identifier, Value m)]
            -> Either RuntimeError (Map Identifier (Value m))
resolveArgs context specs args =
  let kwargs0 = Map.fromList [(k, v) | (Just k, v) <- args]
      varargs0 = [v | (Nothing, v) <- args]
  in go specs kwargs0 varargs0
  where
    go ((argName, defVal):xs) kwargs varargs =
      case Map.lookup argName kwargs of
        Nothing ->
          -- positional argument
          case varargs of
            [] ->
              -- No more arguments passed, look for default value
              maybe
                -- No default, argument required
                (Left $
                  ArgumentError
                    context
                    (identifierName argName)
                    "argument"
                    "end of arguments"
                )
                -- Default exists, use it.
                (\val ->
                  Map.insert argName val <$> go xs kwargs varargs
                )
                defVal
            (v:varargs') ->
              -- Argument passed, use it.
              Map.insert argName v <$> go xs kwargs varargs'
        Just v ->
          -- Keyword argument found.
          Map.insert argName v <$> go xs (Map.delete argName kwargs) varargs

    go [] kwargs varargs =
        -- Map remaining arguments to @varargs@ and @kwargs@.
        Right $ Map.fromList
          [ ("varargs", ListV $ V.fromList varargs)
          , ("kwargs", DictV (Map.mapKeys (toScalar . identifierName) kwargs))
          ]

leftNaN :: Double -> Either RuntimeError Double
leftNaN c | isNaN c = Left $ NumericError "<unknown>" "not a number"
leftNaN c | isInfinite c = Left $ NumericError "<unknown>" "infinity"
leftNaN c = Right c

numericFunc :: Monad m
             => (Integer -> Integer)
             -> (Double -> Double)
             -> Value m
             -> Either RuntimeError (Value m)
numericFunc f g =
  numericFuncCatch f' g'
  where
    f' x = Right (f x)
    g' x = Right (g x)

numericFuncCatch :: Monad m
                  => (Integer -> Either RuntimeError Integer)
                  -> (Double -> Either RuntimeError Double)
                  -> Value m
                  -> Either RuntimeError (Value m)
numericFuncCatch f _ (IntV a) = IntV <$> f a
numericFuncCatch _ f (FloatV a) = FloatV <$> (leftNaN =<< f a)
numericFuncCatch _ _ a = Left (TagError "<unknown>" "number" (tagNameOf a))

asOptionalVal :: (Value m -> Either RuntimeError a) -> Value m -> Either RuntimeError (Maybe a)
asOptionalVal _ NoneV = Right Nothing
asOptionalVal asVal x = Just <$> asVal x

asIntVal :: Value m -> Either RuntimeError Integer
asIntVal (IntV a) = Right a
asIntVal x = Left $ TagError "conversion to int" "int" (tagNameOf x)

asFloatVal :: Value m -> Either RuntimeError Double
asFloatVal (FloatV a) = Right a
asFloatVal (IntV a) = Right (fromInteger a)
asFloatVal x = Left $ TagError "conversion to float" "float" (tagNameOf x)

asBoolVal :: Value m -> Either RuntimeError Bool
asBoolVal (BoolV a) = Right a
asBoolVal NoneV = Right False
asBoolVal x = Left $ TagError "conversion to bool" "bool" (tagNameOf x)

asVectorVal :: Monad m => Value m -> m (Either RuntimeError (Vector (Value m)))
asVectorVal (ListV a) = pure . Right $ a
asVectorVal (NativeV n) =
  maybe
    (Left $ TagError "conversion to list" "list" "non-list native object")
    Right <$>
    nativeObjectAsList n
asVectorVal x = pure . Left $ TagError "conversion to list" "list" (tagNameOf x)

asListVal :: Monad m => Value m -> m (Either RuntimeError [Value m])
asListVal (ListV a) = pure . Right $ V.toList a
asListVal (NativeV n) =
  maybe
    (Left $ TagError "conversion to list" "list" "non-list native object")
    (Right . V.toList) <$>
    nativeObjectAsList n
asListVal x = pure . Left $ TagError "conversion to list" "list" (tagNameOf x)

asDictVal :: Monad m => Value m -> m (Either RuntimeError (Map Scalar (Value m)))
asDictVal (DictV a) = pure $ Right a
asDictVal (NativeV n) =
  maybe
    (Left $ TagError "conversion to dict" "dict" "non-dict native object")
    Right <$>
    nativeObjectAsDict n
asDictVal x = pure . Left $ TagError "conversion to dict" "dict" (tagNameOf x)

asTextVal :: Value m -> Either RuntimeError Text
asTextVal (StringV a) = Right a
asTextVal (EncodedV (Encoded a)) = Right a
asTextVal (IntV a) = Right (Text.show a)
asTextVal (FloatV a) = Right (Text.show a)
asTextVal NoneV = Right ""
asTextVal x = Left $ TagError "conversion to string" "string" (tagNameOf x)

asScalarVal :: Value m -> Either RuntimeError Scalar
asScalarVal (ScalarV a) = Right a
asScalarVal x = Left $ TagError "conversion to scalar" "scalar" (tagNameOf x)

intFunc :: (Monad m, ToValue a m)
         => (Integer -> Either RuntimeError a)
         -> Value m
         -> Either RuntimeError (Value m)
intFunc f a = toValue <$> (asIntVal a >>= f)

floatFunc :: (Monad m, ToValue a m)
         => (Double -> Either RuntimeError a)
         -> Value m
         -> Either RuntimeError (Value m)
floatFunc f a = toValue <$> (asFloatVal a >>= f)

boolFunc :: (Monad m, ToValue a m)
         => (Bool -> a)
         -> Value m
         -> Either RuntimeError (Value m)
boolFunc f (BoolV a) = pure . toValue $ f a
boolFunc _ a = Left (TagError "bool function" "bool" (tagNameOf a))

textFunc :: (Monad m, ToValue a m)
         => (Text -> Either RuntimeError a)
         -> Value m
         -> Either RuntimeError (Value m)
textFunc f (StringV a) = toValue <$> f a
textFunc f (EncodedV (Encoded a)) = toValue <$> f a
textFunc f (IntV a) = toValue <$> f (Text.show a)
textFunc f (FloatV a) = toValue <$> f (Text.show a)
textFunc f NoneV = toValue <$> f ""
textFunc _ a = Left (TagError "text function" "int" (tagNameOf a))

numericFunc2 :: Monad m
             => (Integer -> Integer -> Integer)
             -> (Double -> Double -> Double)
             -> Value m
             -> Value m
             -> Either RuntimeError (Value m)
numericFunc2 f g =
  numericFunc2Catch f' g'
  where
    f' x y = Right (f x y)
    g' x y = Right (g x y)

numericFunc2Catch :: Monad m
                  => (Integer -> Integer -> Either RuntimeError Integer)
                  -> (Double -> Double -> Either RuntimeError Double)
                  -> Value m
                  -> Value m
                  -> Either RuntimeError (Value m)
numericFunc2Catch f _ (IntV a) (IntV b) = IntV <$> (a `f` b)
numericFunc2Catch _ f (FloatV a) (FloatV b) = FloatV <$> (leftNaN =<< a `f` b)
numericFunc2Catch _ f (IntV a) (FloatV b) = FloatV <$> (leftNaN =<< fromInteger a `f` b)
numericFunc2Catch _ f (FloatV a) (IntV b) = FloatV <$> (leftNaN =<< a `f` fromInteger b)
numericFunc2Catch _ _ (FloatV _) b = Left (TagError "numeric function" "number" (tagNameOf b))
numericFunc2Catch _ _ (IntV _) b = Left (TagError "numeric function" "number" (tagNameOf b))
numericFunc2Catch _ _ b _ = Left (TagError "numeric function" "number" (tagNameOf b))

intFunc2 :: Monad m
         => (Integer -> Integer -> Either RuntimeError Integer)
         -> Value m
         -> Value m
         -> Either RuntimeError (Value m)
intFunc2 f a b = do
  x <- asIntVal a
  y <- asIntVal b
  IntV <$> f x y

floatFunc2 :: Monad m
         => (Double -> Double -> Either RuntimeError Double)
         -> Value m
         -> Value m
         -> Either RuntimeError (Value m)
floatFunc2 f (IntV a) b = floatFunc2 f (FloatV $ fromIntegral a) b
floatFunc2 f (FloatV a) (IntV b) = floatFunc2 f (FloatV a) (FloatV $ fromIntegral b)
floatFunc2 f (FloatV a) (FloatV b) = FloatV <$> (a `f` b)
floatFunc2 _ (FloatV _) b = Left (TagError "floating-point function" "float" (tagNameOf b))
floatFunc2 _ b _ = Left (TagError "floating-point function" "float" (tagNameOf b))

boolFunc2 :: Monad m
         => (Bool -> Bool -> Bool)
         -> Value m
         -> Value m
         -> Either RuntimeError (Value m)
boolFunc2 f a b = BoolV <$> (f <$> asBoolVal a <*> asBoolVal b)

native :: (Monad m, MonadTrans t, MonadError RuntimeError (t m))
       => m (Either RuntimeError a)
       -> t m a
native action =
  lift action >>= either throwError pure

encodeText :: ( Monad m
              , MonadError RuntimeError (t m)
              , MonadTrans t
              , MonadReader (Context m) (t m)
              )
           => Text
           -> t m Encoded
encodeText str = do
  ctx <- ask
  encodeTextWith ctx str

encodeTextWith :: ( Monad m
              , MonadError RuntimeError (t m)
              , MonadTrans t
              )
           => Context m
           -> Text
           -> t m Encoded
encodeTextWith ctx str = do
  let encoder = contextEncode ctx
  native (Right <$> encoder str)

encode :: ( Monad m
          , MonadError RuntimeError (t m)
          , MonadTrans t
          , MonadReader (Context m) (t m)
          )
       => Value m
       -> t m Encoded
encode v = do
  ctx <- ask
  encodeWith ctx v

encodeWith :: ( Monad m
          , MonadError RuntimeError (t m)
          , MonadTrans t
          )
       => Context m
       -> Value m
       -> t m Encoded
encodeWith _ (EncodedV e) = pure e
encodeWith ctx (NativeV n) = native (Right <$> nativeObjectEncoded n ctx)
encodeWith _ (ProcedureV _) = pure $ Encoded "[[procedure]]"
encodeWith ctx v = encodeTextWith ctx =<< stringify v

--------------------------------------------------------------------------------
-- String / Encoding helpers
--------------------------------------------------------------------------------

stringify :: ( Monad m
             , MonadError RuntimeError (t m)
             , MonadTrans t
             )
          => Value m
          -> t m Text
stringify NoneV = pure ""
stringify TrueV = pure "true"
stringify FalseV = pure ""
stringify (StringV str) = pure str
stringify (BytesV b) =
  pure . decodeUtf8 . Base64.encode $ b
stringify (EncodedV (Encoded e)) =
  pure e
stringify (IntV i) = pure $ Text.show i
stringify (FloatV f) = pure $ Text.show f
stringify (ScalarV s) = pure . Text.show $ s
stringify (ListV xs) = do
  elems <- V.mapM stringify xs
  pure $ Text.intercalate ", " $ V.toList elems
stringify (DictV m) = do
  elems <- mapM stringifyKV $ Map.toAscList m
  pure $ Text.intercalate ", " elems
stringify (NativeV n) =
  native (Right <$> nativeObjectStringified n)
stringify (ProcedureV (NativeProcedure oid _ _)) =
  pure $ "[[procedure " <> unObjectID oid <> "]]"
stringify (ProcedureV (GingerProcedure {})) =
  pure $ "[[procedure]]"
stringify (ProcedureV NamespaceProcedure) =
  pure $ "[[procedure namespace]]"
stringify (TestV _) =
  pure "[[test]]"
stringify (FilterV _) =
  pure "[[filter]]"
stringify (MutableRefV ref) =
  pure $ "[[ref#]]" <> Text.show (unRefID ref)

stringifyKV :: ( Monad m
               , MonadError RuntimeError (t m)
               , MonadTrans t
               )
            => (Scalar, Value m)
            -> t m Text
stringifyKV (k, v) = do
  kStr <- stringify (ScalarV k)
  vStr <- stringify v
  pure $ kStr <> ": " <> vStr


--------------------------------------------------------------------------------
-- Dictionary helpers
--------------------------------------------------------------------------------

dictV :: [(Scalar, Value m)] -> Value m
dictV items = DictV $ Map.fromList items

infixr 8 .=

(.=) :: (ToValue v m) => Scalar -> v -> (Scalar, Value m)
k .= v = (k, toValue v)

--------------------------------------------------------------------------------
-- Arbitrary instances
--------------------------------------------------------------------------------

instance Arbitrary Scalar where
  arbitrary =
    QC.oneof
      [ pure NoneScalar
      , BoolScalar <$> arbitrary
      , StringScalar . Text.pack <$> QC.listOf arbitrary
      , EncodedScalar . Encoded . Text.pack <$> QC.listOf arbitrary
      , BytesScalar . BS.pack <$> QC.listOf arbitrary
      , IntScalar <$> arbitrary
      , FloatScalar <$> arbitrary
      ]

  shrink = \case
    BoolScalar True -> [BoolScalar False]
    StringScalar str | Text.null str -> []
    StringScalar str -> [StringScalar $ Text.init str]
    EncodedScalar (Encoded str) | Text.null str -> []
    EncodedScalar (Encoded str) -> [EncodedScalar . Encoded $ Text.init str]
    BytesScalar str | BS.null str -> []
    BytesScalar str -> [BytesScalar $ BS.init str]
    IntScalar i -> NoneScalar : (IntScalar <$> shrink i)
    FloatScalar f -> NoneScalar : (FloatScalar <$> shrink f)
    NoneScalar -> []
    _ -> [NoneScalar]

instance Monad m => Arbitrary (Value m) where
  arbitrary =
    QC.oneof
      [ pure NoneV
      , ScalarV <$> arbitrary
      , ListV . V.fromList <$> fuelledList arbitrary
      , DictV . Map.fromList <$> fuelledList arbitrary
      , NativeV <$> arbitraryNative
      , ProcedureV <$> arbitraryNativeProcedure
      ]

arbitraryNativeProcedure :: Monad m => QC.Gen (Procedure m)
arbitraryNativeProcedure = do
  retval <- QC.scale (`div` 2) arbitrary
  oid <- ObjectID . ("arbitrary:" <>) . identifierName <$> arbitrary
  pure $ NativeProcedure oid Nothing (\_ _ -> pure (Right retval))

arbitraryNative :: Monad m => QC.Gen (NativeObject m)
arbitraryNative = do
  objectID <- arbitrary
  pure defNativeObject
    { nativeObjectGetFieldNames = pure ["id"]
    , nativeObjectGetField = \case
        "id" -> pure . Just . toValue . identifierName $ objectID
        _ -> pure Nothing
    , nativeObjectStringified = pure $ identifierName objectID
    , nativeObjectEq = \self other -> do
        otherID <- nativeObjectGetField other "id"
        selfID <- nativeObjectGetField self "id"
        pure . Right $ otherID == selfID
    }