{-# LANGUAGE RankNTypes #-}
{-|
Module      : Angle.Exec.Operations
Description : Definitions for builtin operators.
Copyright   : Copyright (C) 2015 Ben Moon
License     : GNU GPL, version 3
Maintainer  : GuiltyDolphin@gmail.com
Stability   : alpha

Angle provides many builtin logical and arithmetical operators,
of which many are overloaded to work on multiple types. Definitions
for these operators can be found in this module.
-}
module Angle.Exec.Operations
    ( addLit
    , andLit
    , concatLit
    , divLit
    , eqLit
    , expLit
    , greaterEqLit
    , greaterLit
    , lessEqLit
    , lessLit
    , multLit
    , negLit
    , notLit
    , orLit
    , subLit
    ) where

import Control.Monad

import Angle.Types.Lang
import Angle.Exec.Error


-- | Addition operator.
--
-- On list followed by arbitrary types: appends the tail arguments to the list.
--
-- On numeric types: performs arithmetic addition.
--
-- On strings: performs concatenation.
addLit :: MultiOperator
addLit (LitList x:xs@(_:_)) = return $ LitList (x++xs)
addLit (LitStr x:xs) | allType LTStr xs = return $ LitStr $ x ++ concatMap (\(LitStr y) -> y) xs
addLit xs             = onlyNumOp addLitNum xs
    where addLitNum = onNum (+) (+)


-- | Logical and operator.
--
-- On booleans: performs logical and of the values.
andLit :: MultiOperator
andLit []                 = return $ LitBool True
andLit [LitList xs]       = andLit xs
andLit (x@(LitBool _):xs) = foldM andLitBool x xs
    where andLitBool = onLitBool (&&)
andLit (x:_) = throwExecError $ typeNotValidErr x


-- | Concatenation operator
--
-- On lists: concatenates the lists.
--
-- On lists with other types: concatenates the lists
-- and appends the other elements in the appropriate places.
--
-- On other types: creates a list of the values.
concatLit :: MultiOperator
concatLit (z:zs) = foldM concatLit' z zs
  where concatLit' (LitList xs) (LitList ys) = return . LitList $ xs ++ ys
        concatLit' (LitList xs) y = return . LitList $ xs ++ [y]
        concatLit' x (LitList ys) = return . LitList $ x : ys
        concatLit' x y = return . LitList $ [x, y]
concatLit _ = throwExecError $ malformedSignatureErr "++"


-- | Division operator.
--
-- On numerics: performs arithmetic division.
divLit :: MultiOperator
divLit (_:xs) | LitInt 0 `elem` xs = throwExecError divideByZeroErr
              | LitFloat 0 `elem` xs = throwExecError divideByZeroErr
divLit xs = onlyNumOp divLitNum xs
    where divLitNum = onNum div (/)


-- | Equality operator
--
-- On any types: true if all of the values are equal
eqLit :: MultiOperator
eqLit [] = return $ LitBool True
eqLit [LitList xs] = eqLit xs
eqLit (x:xs) = return . LitBool . all (==x) $ xs


-- | Exponentation operator
--
-- On numerics: raises the first item to the power produced
-- by raising each successive element to the next power.
expLit :: MultiOperator
expLit = onlyNumOp expLitNum
  where
    expLitNum = onNum (^) (**)


-- | Greater than or equal to operator.
--
-- On strings: compares the strings lexiographically.
--
-- On numerics: compares the values numerically.
greaterEqLit :: MultiOperator
greaterEqLit = compOp (>=)


-- | Greater than operator.
--
-- On strings: compares the strings lexiographically.
--
-- On numerics: compares the values numerically.
greaterLit :: MultiOperator
greaterLit = compOp (>)


-- | Less than or equal to operator.
--
-- On strings: compares the strings lexiographically.
--
-- On numerics: compares the values numerically.
lessEqLit :: MultiOperator
lessEqLit = compOp (<=)


-- | Less than comparison operator.
--
-- On strings: compares the strings lexiographically.
--
-- On numerics: compares the values numerically.
lessLit :: MultiOperator
lessLit = compOp (<)


-- | Multiplication operator.
--
-- On numerics: performs arithmetical multiplication on the values.
multLit :: MultiOperator
multLit = onlyNumOp multLitNum
    where multLitNum = onNum (*) (*)


-- | Negation operator.
--
-- On lists: returns the reversed list.
--
-- On numerics: returns the negative value.
negLit :: UnaryOperator
negLit (LitList xs)  = return $ LitList (reverse xs)
negLit (LitInt x)    = return $ LitInt (-x)
negLit (LitFloat x)  = return $ LitFloat (-x)
negLit x@(LitBool _) = notLit x
negLit x             = throwExecError $ typeNotValidErr x


-- | Logical not operator.
--
-- On boolean: performs logical negation.
notLit :: UnaryOperator
notLit (LitBool x) = return . LitBool $ not x
notLit x = throwExecError $ typeNotValidErr x


-- | Logical or operator.
--
-- On booleans: performs logical OR.
orLit :: MultiOperator
orLit []                 = return $ LitBool False
orLit [LitList xs]       = orLit xs
orLit (x@(LitBool _):xs) = foldM orLitBool x xs
    where orLitBool = onLitBool (||)
orLit (x:_)              = throwExecError $ typeNotValidErr x


-- | Subtraction operator.
--
-- On list followed by integers: treats the integers as indices to remove from the list.
--
-- On numerics: subtracts all tailing numerics from the first numeric.
subLit :: MultiOperator
subLit (x@(LitList _):ys@(_:_))
    | allType LTInt ys
    = foldM (flip $ \(LitInt i) -> langListDrop i ) x ys
      where langListDrop n (LitList zs)
                | n >= length zs = throwExecError $ indexOutOfBoundsErr n
                | otherwise = return (LitList res)
              where res = f++s
                    (f,_:s) = splitAt n zs
            langListDrop _ _ = undefined
subLit xs = onlyNumOp subLitNum xs
    where subLitNum = onNum (-) (-)


----------------------
-- END OF OPERATORS --
----------------------


-- | Lift a binary operator across boolean values to
-- work on `LangLit's.
onLitBool :: Binary Bool Bool -> BinaryOperator
onLitBool f (LitBool x) (LitBool y) = return . LitBool $ f x y
onLitBool _ x y = throwExecError $ typeMismatchOpErr x y


-- | Takes binary functions that act on Ints and Floats and
-- converts them to functions that act on `LangLit's.
onNum :: Binary Int Int -> Binary Double Double -> BinaryOperator
onNum i f = numOpLit i f LitInt LitFloat


-- | @numOp i f@ produces a function that will act upon
-- literal numerical values, using @i@ for integers, and
-- @f@ for floats. Integers will be casted to floats
-- if required.
numOp :: Binary Int LangLit -> Binary Double LangLit -> BinaryOperator
numOp i _ (LitInt x) (LitInt y)       = return $ i x y
numOp _ f (LitFloat x) (LitFloat y)   = return $ f x y
numOp _ f (LitInt x) (LitFloat y)     = return $ f (fromIntegral x) y
numOp _ f (LitFloat x) (LitInt y) =     return $ f x (fromIntegral y)
numOp _ _ x y                         = throwExecError $ typeMismatchOpErr x y


-- | Synonym for a function that performs a comparison
-- between its arguments.
type CompFunc       = forall a. (Ord a) => a -> a -> Bool

-- | Synonym for standard operators that act on a list of values.
type MultiOperator  = (CanErrorWithPos m) => [LangLit] -> m LangLit


-- | Synonym for an operator that acts upon only two values.
type BinaryOperator = (CanErrorWithPos m) => LangLit -> LangLit -> m LangLit


-- | Synonym for an operator that acts upon one value.
type UnaryOperator  = (CanErrorWithPos m) => LangLit -> m LangLit


-- | Synonym for a binary function.
type Binary a b = a -> a -> b


-- | Given a comparison function, produces an operator
-- that produces a comparison between `LangLit's.
--
-- When the operand is a single list, then the list will be
-- flattened until either a non-list value is found, or there
-- is more than one operand.
compOp :: CompFunc -> MultiOperator
compOp f [LitList xs]      = compOp f xs
compOp f xs = liftM (LitBool . and) $ mapM (uncurry $ compLit f) (group2 xs)


-- | Lift a comparison function to work on 'LangLit's.
compLit :: (CanErrorWithPos m) => (forall a. (Ord a) => a -> a -> Bool) -> LangLit -> LangLit -> m Bool
compLit f (LitInt x) (LitInt y) = return $ f (toRational x) (toRational y)
compLit f (LitInt x) (LitFloat y) = return $ f (toRational x) (toRational y)
compLit f x@(LitFloat _) y@(LitInt _) = compLit f y x
compLit f (LitFloat x) (LitFloat y) = return $ f (toRational x) (toRational y)
compLit f (LitStr x) (LitStr y) = return $ f x y
compLit _ x y = throwExecError $ typeMismatchOpErr x y


-- | Group the list into successive doubles.
--
-- e.g,
--
-- > group2 [1, 2, 3]
--
-- [(1, 2), (2, 3)]
group2 :: [a] -> [(a, a)]
group2 [] = []
group2 [x] = []
group2 xs = (fst', snd') : group2 (tail xs)
  where
    [fst', snd'] = take 2 xs


-- | Convenience function for producing `LangLit' values from
-- functions to be supplied to `numOp'.
numOpLit :: Binary Int a -> Binary Double b -> (a -> LangLit) -> (b -> LangLit) -> BinaryOperator
numOpLit i f t1 t2 = numOp i' f'
    where i' x y = t1 $ i x y
          f' x y = t2 $ f x y


-- | Like `onNum', but given functions must result in boolean
-- values.
--
-- Used for implementing comparison operators.
onNumBool :: Binary Int Bool -> Binary Double Bool -> BinaryOperator
onNumBool i f = numOpLit i f LitBool LitBool


-- | Operator (or remaining cases of a) that can only
-- act upon numeric types.
--
-- When the operand is a single list, then the list will be
-- flattened until either a non-list value is found, or there
-- is more than one operand.
--
-- Throws a `TypeNotValidError' if invalid literals are passed.
onlyNumOp :: (CanErrorWithPos m) => (LangLit -> LangLit -> m LangLit) -> [LangLit] -> m LangLit
onlyNumOp f [LitList xs]        = onlyNumOp f xs
onlyNumOp f (x@(LitInt _):xs)   = foldM f x xs
onlyNumOp f (x@(LitFloat _):xs) = foldM f x xs
onlyNumOp _ (x:_)               = throwExecError $ typeNotValidErr x
onlyNumOp _ [] = throwImplementationErr "onlyNumOp - got empty list"
