{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE NoImplicitPrelude          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TypeApplications           #-}

module Bricks.BuiltinFunctions where

import Bricks.Term
import Bricks.Type

-- Bricks internal
import           Bricks.Internal.Prelude
import           Bricks.Internal.Text    (Text)
import qualified Bricks.Internal.Text    as Text

-- Containers
import qualified Data.Map as Map
import           Data.Set (Set)

-- Base
import Data.Dynamic  (fromDynamic, toDyn)
import Data.Typeable (Typeable)
import Prelude       (Integer, Num (..))
import Text.Read     (readMaybe)

term'data :: forall a. Typeable a => Type a -> a -> Term
term'data (Type n) = Term'Data n . toDyn @a

fn'pure'parametric'arity1 :: (Term -> Term) -> Term
fn'pure'parametric'arity1 f =
  Term'Function $ \x -> pure $
  f x

fn'pure'parametric'arity2 :: (Term -> Term -> Term) -> Term
fn'pure'parametric'arity2 f =
  Term'Function $ \x -> pure $
  fn'pure'parametric'arity1 $ f x

fn'pure'parametric'arity3 :: (Term -> Term -> Term -> Term) -> Term
fn'pure'parametric'arity3 f =
  Term'Function $ \x -> pure $
  fn'pure'parametric'arity2 $ f x

fn'id :: Term
fn'id = fn'pure'parametric'arity1 id

fn'const :: Term
fn'const = fn'pure'parametric'arity2 const

{- | Function composition, in the traditional "backwards" order.

Read @f `fn'comp` g@ as "/f/ after /g/." -}

fn'comp :: Term
fn'comp = fn'pure'parametric'arity3 $ \f g x -> f /@\ (g /@\ x)

fn'flip :: Term
fn'flip = fn'pure'parametric'arity3 $ \f x y -> f /@@\ (y, x)

fn'dict'lookup :: Term
fn'dict'lookup =
  Term'Function $ \x -> do
    map <- reduce'dict'keys x
    pure $ Term'Function $ \y -> do
      key <- cast'data type'string y
      case Map.lookup key map of
        Nothing -> bottom . Bottom $ "Key " <> key <> " not found in dict"
        Just a -> pure a

fn'or :: Term
fn'or =
  Term'Function $ \x -> cast'data type'boolean x <&>
  \case
    True -> fn'const /@\ term'data type'boolean True
    False -> assert'type type'boolean

fn'and :: Term
fn'and =
  Term'Function $ \x -> cast'data type'boolean x <&>
  \case
    False -> fn'const /@\ term'data type'boolean False
    True -> assert'type type'boolean

fn'string'append :: Term
fn'string'append =
  Term'Function $ \x -> cast'data type'string x <&> \x' ->
  Term'Function $ \y -> cast'data type'string y <&> \y' ->
  term'data type'string (Text.append x' y')

fn'dict'disallowExtraKeys :: Set Text -> Term
fn'dict'disallowExtraKeys _allowedKeys =
  Term'Function $ undefined

fn'dict'merge'preferLeft :: Term
fn'dict'merge'preferLeft =
  Term'Function $ \x -> reduce'dict'keys x <&> \x' ->
  Term'Function $ \y -> reduce'dict'keys y <&> \y' ->
  Term'Dict'ReducedKeys $ Map.union x' y'

fn'dict'merge'preferRight :: Term
fn'dict'merge'preferRight = fn'flip /@\ fn'dict'merge'preferLeft

cast'data :: (MonadEval m, Typeable a) => Type a -> Term -> m a
cast'data = req fst

-- | Like 'fn'id', but fails if the argument is not of the given type.
assert'type :: Typeable a => Type a -> Term
assert'type t = Term'Function $ req snd t

req :: forall a b m. (MonadEval m, Typeable a)
  => ((a, Term) -> b)
  -> Type a
  -> Term
  -> m b
req s (Type n) = reduce'term >=> \case
  t@(Term'Data n' x) ->
    case fromDynamic @a x of
      Nothing -> bottom . Bottom $ "Expected " <> n <> ", got " <> n'
      Just a -> pure (s (a, t))
  x ->
    termTypeName x >>= \n' ->
      bottom . Bottom $ "Expected " <> n <> ", got " <> n'

fn'int'add :: Term
fn'int'add =
  Term'Function $ \x -> cast'data type'integer x <&> \x' ->
  Term'Function $ \y -> cast'data type'integer y <&> \y' ->
  term'data type'integer (x' + y')

fn'int'constructor :: Term
fn'int'constructor =
  Term'Function $ cast'data type'string >=> (
    Text.unpack >>> readMaybe @Integer >>>
    maybe (bottom . Bottom $ "invalid integer")
          (pure . term'data type'integer)
  )

standard'library :: Term
standard'library =
  Term'Dict'ReducedKeys . Map.fromList $
    [ ("add", fn'int'add)
    , ("integer", fn'int'constructor)
    , ("and", fn'and)
    , ("or", fn'or)
    , ("id", fn'id)
    , ("const", fn'const)
    ]