{-# LANGUAGE FlexibleInstances #-}

module BizExpr (repl, eval, Context) where

import Data.Maybe (fromMaybe)
import Text.Read (readMaybe)
import qualified Data.Text.Lazy as L
import qualified Data.Map as M
import System.IO (isEOF)
import Control.Monad (when)

type Context = M.Map L.Text L.Text

repl :: IO ()
repl = do
  eof <- isEOF
  when (not eof) (
      getLine >>=
        putStrLn
        . maybe "Failed to evaluate expression" id
        . (eval M.empty :: String -> Maybe String)
      >> repl
    )

maybeHead (x:_) = Just x
maybeHead _ = Nothing

eval :: CoerceTo a => Context -> String -> Maybe a
eval c x = coerceTo =<< evalAst =<< maybeHead . fst =<< parseLevel c x

data Value = IntVal Integer | StrVal String | BoolVal Bool | FloatVal Float
  deriving (Show)

class CoerceTo a where
  coerceTo :: Value -> Maybe a
  coerceFrom :: a -> Value

instance CoerceTo Integer where
  coerceTo (FloatVal n) = Just $ floor n
  coerceTo (IntVal n) = Just n
  coerceTo v = coerceTo . FloatVal =<< coerceTo v
  coerceFrom = IntVal

instance CoerceTo Float where
  coerceTo (FloatVal n) = Just n
  coerceTo (IntVal n) = Just $ fromIntegral n
  coerceTo (StrVal s) = readMaybe s
  coerceTo (BoolVal True) = Just 1
  coerceTo (BoolVal False) = Just 0

  coerceFrom = FloatVal

instance CoerceTo String where
  coerceTo (BoolVal v) = Just $ show v
  coerceTo (IntVal v) = Just $ show v
  coerceTo (StrVal v) = Just v
  coerceTo (FloatVal v) = Just $ show v
  coerceFrom = StrVal

instance CoerceTo Bool where
  coerceTo (BoolVal v) = Just v
  coerceTo (IntVal v) = Just $ v /= 0
  coerceTo (StrVal "True") = Just True
  coerceTo (StrVal "true") = Just True
  coerceTo v = coerceTo . IntVal =<< coerceTo v

  coerceFrom = BoolVal

type Name = String

data Ast
  = Expr Name [Ast]
  | Val Value
  deriving (Show)

parseLevel :: Context -> String -> Maybe ([Ast], String)
parseLevel c = go "" (Just [])
  where
    go :: String -> Maybe [Ast] -> String -> Maybe ([Ast], String)
    go prev (Just l) ('(' : next) =
      case parseLevel c next of
        Nothing ->
          Nothing
        Just (arg, remnant) ->
          let r =
                if remnant /= "" && head remnant == ','
                  then tail remnant
                  else remnant
           in go "" (Just (l ++ [Expr prev arg])) r
    go prev (Just l) (')' : remnant) =
      let l0 = [Val $ newVal c prev | prev /= ""]
       in Just (l ++ l0, remnant)
    go prev (Just l) (',' : next) =
      let l0 = [Val $ newVal c prev]
       in go "" (Just (l ++ l0)) next
    go prev l (' ' : next) =
      go "" l next
    go "" (Just l) "" =
      Just (l, "")
    go prev (Just l) "" =
      Just (l ++ [Val $ newVal c prev], "")
    go prev l (x : xs) =
      go (prev ++ [x]) l xs

newVal :: Context -> String -> Value
newVal c x = case M.lookup (L.pack x) c of
  Nothing -> StrVal x
  Just n -> StrVal $ L.unpack n

evalAst :: Ast -> Maybe Value
evalAst (Expr name ast) = fromMaybe Nothing (getFunc name <*> evalLevel ast)
evalAst (Val val) = Just val

evalLevel :: [Ast] -> Maybe [Value]
evalLevel = mapM evalAst

getFunc :: String -> Maybe ([Value] -> Maybe Value)
getFunc "sum" = Just eSum
getFunc "any" = Just eAny
getFunc "all" = Just eAll
getFunc "average" = Just eAverage
getFunc _ = Nothing

eSum :: [Value] -> Maybe Value
eSum = eFold (+) (0.0 :: Float)

eAny :: [Value] -> Maybe Value
eAny = eFold (||) False

eAll :: [Value] -> Maybe Value
eAll = eFold (&&) True

eAverage :: [Value] -> Maybe Value
eAverage xs = do
  let n = coerceTo =<< eSum xs
      d = Just . fromIntegral $ length xs
   in FloatVal <$> ((/) <$> n <*> d)

eFold :: (CoerceTo a, CoerceTo b) => (b -> a -> b) -> b -> [Value] -> Maybe Value
eFold fn init [] = Just $ coerceFrom init
eFold fn init (x : xs) =
  let b = coerceTo =<< eFold fn init xs
      a = coerceTo x
   in coerceFrom <$> (fn <$> b <*> a)