{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

{- |

Conversion from 'Expression' (the AST produced by the parser) to 'Term'
(an augmented form of the lambda calculus used for evaluation).

-}
module Bricks.ExpressionToTerm where

-- Bricks
import Bricks.BuiltinFunctions
import Bricks.Expression
import Bricks.Term
import Bricks.Type

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

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

-- Base
import Control.Applicative (liftA2)
import System.IO           (IO)

expression'to'term :: Expression -> IO Term
expression'to'term =
  \case
    Expr'Var x -> var'to'term x
    Expr'Str x -> str'to'term x
    Expr'Str'Indented x -> str'to'term (inStr'to'strDynamic x)
    Expr'List x -> list'to'term x
    Expr'Dict x -> dict'to'term x
    Expr'Dot x -> dot'to'term x
    Expr'Lambda x -> lambda'to'term x
    Expr'Apply x -> apply'to'term x
    Expr'Let x -> let'to'term x

var'to'term :: Var -> IO Term
var'to'term = pure . Term'Var . var'text

apply'to'term :: Apply -> IO Term
apply'to'term x =
  do
    a <- expression'to'term (apply'func x)
    b <- expression'to'term (apply'arg x)
    pure $ a /@\ b

str'to'term :: Str'Dynamic -> IO Term
str'to'term x =
  case Seq.toList (strDynamic'toSeq x) of
    [] -> pure $ term'data type'string ""
    ys -> foldr1 (liftA2 f) $ fmap str'1'to'term ys
  where
    f a b = fn'string'append /@@\ (a, b)

str'1'to'term :: Str'1 -> IO Term
str'1'to'term = \case
  Str'1'Literal x -> pure $ term'data type'string (str'static'text x)
  Str'1'Antiquote x -> expression'to'term x

list'to'term :: List -> IO Term
list'to'term x =
  Term'List <$> traverse expression'to'term (list'expressions x)

dict'to'term :: Dict -> IO Term
dict'to'term = undefined

dot'to'term :: Dot -> IO Term
dot'to'term x =
  do
    a <- expression'to'term (dot'dict x)
    b <- expression'to'term (dot'key x)
    pure $ fn'dict'lookup /@@\ (a, b)

let'to'term :: Let -> IO Term
let'to'term x =
  Term'LetRec <$> bindings <*> body

  where
    bindings :: IO (Map Text Term)
    bindings =
      Map.fromList . List.concat <$>
      traverse letBinding'to'term (let'bindings x)

    body :: IO Term
    body =
      expression'to'term $ let'value x

letBinding'to'term :: LetBinding -> IO [(Text, Term)]
letBinding'to'term =
  \case
    LetBinding'Eq a b ->
      do
        b' <- expression'to'term b
        pure [(var'text a, b')]
    LetBinding'Inherit d xs ->
      do
        -- Use a pointer for the dict we're inheriting from, to avoid
        -- having to reduce it more than once.
        p <- newTermPtr =<< expression'to'term d

        pure
          $ fmap (\x ->
            ( var'text x
            , fn'dict'lookup /@@\ (p, term'data type'string (var'text x))
            ))
          $ Seq.toList xs


--------------------------------------------------------------------------------
--  Converting a lambda expression to a lambda term
--------------------------------------------------------------------------------

lambda'to'term :: Lambda -> IO Term
lambda'to'term x =
  do
    body <- expression'to'term (lambda'body x)
    case lambda'head x of
      Param'Name var       -> lambda'to'term'simple var body
      Param'DictPattern dp -> lambda'to'term'dictPattern dp body
      Param'Both var dp    -> lambda'to'term'both var dp body

lambda'to'term'simple :: Var -> Term -> IO Term
lambda'to'term'simple var body =
  -- For a simple named parameter, the AST translates directly into the
  -- lambda calculus.
  pure $ TermPattern'Simple (var'text var) |-> body

lambda'to'term'dictPattern :: DictPattern -> Term -> IO Term
lambda'to'term'dictPattern dp body = do
  -- For dict patterns, we have to do a few more things:
  let names = dictPattern'names dp

    -- 1. If there is no ellipsis, add a check to fail if there are
    --    extra keys in the argument.
  let h = if dictPattern'ellipsis dp then fn'id
          else fn'dict'disallowExtraKeys names

  defs <- dictPattern'defaults dp

    -- 2. Insert a dict-merging function to apply default arguments.
  let g = fn'dict'merge'preferLeft /@\ Term'Dict'ReducedKeys defs

  let f = TermPattern'Dict names |-> body

  pure $ fn'comp /@@\ (fn'comp /@@\ (f, g), h)

lambda'to'term'both :: Var -> DictPattern -> Term -> IO Term
lambda'to'term'both var dp body =
  -- For a named parameter /and/ a dict pattern, we nest the dict pattern
  -- lambda inside a regular lambda.
  lambda'to'term'simple var =<< lambda'to'term'dictPattern dp body

dictPattern'names :: DictPattern -> Set Text
dictPattern'names (DictPattern xs _) =
  Set.fromList . fmap f . Seq.toList $ xs
  where
    f = var'text . dictPattern'1'name

dictPattern'defaults :: DictPattern -> IO (Map Text Term)
dictPattern'defaults (DictPattern xs _) =
  Map.fromList . catMaybes <$> traverse f (Seq.toList xs)
  where
    f :: DictPattern'1 -> IO (Maybe (Text, Term))
    f x =
      case dictPattern'1'default x of
        Nothing -> pure Nothing
        Just d ->
          do
            a <- expression'to'term d
            pure $ Just (var'text (dictPattern'1'name x), a)