-- |Hascal is a simple, minimalistic, tiny calculator library and program.
--
-- * Hascal only understands single-character operators.
-- * Hascal only understands infix operators.
-- * Hascal does not understand parantheses.
-- * @_@ is hard-coded as single prefix for negating numbers.
-- * By default, Hascal understands the operators @+@, @-@, @*@, @/@, @^@, and
-- @?@ (logarithm).
-- * By default, Hascal understands the constants @pi@, @e@, and @i@.
-- * Using Hascal as a library, you can add new operators and new constants
-- using a configuration data type.
--
-- The hascal executable program is easy to use. In a shell, type:
-- 
-- >>> hascal 1+2-3*4/5^6?7
-- -1.7263530417152033
--
-- Given a configuration, the 'hascal' function similarly evaluates an
-- expression of type 'String'. In a Haskell interpreter like GHCI, type:
--
-- >>> hascal def "1+2-3*4/5^6?7"
-- Right (0.2736469582847967 :+ 0.0)
--
-- >>> hascal def "1++2"
-- Left "Error at \"\"."

--------------------------------------------------------------------------------

module Hascal
  ( Config(..)
  , Data.Default.Default(..)
  , hascal
  , showHascal
  , showNumber
  ) where


--------------------------------------------------------------------------------

import Data.Complex
import Data.Default
import Data.List
import Data.List.Split


--------------------------------------------------------------------------------
-- | The result of an evaluation of a string is either a String containing an
-- error message or a complex number.

type Result   t = Either String (Complex t)


--------------------------------------------------------------------------------
-- | An operator is a pair of
-- * a 'Char' containing its identifier; and
-- * a binary numeral function.

type Operator t = (Char, Complex t -> Complex t -> Complex t)


--------------------------------------------------------------------------------

-- | A constant is a pair of
-- * a 'String' containing its identifier; and
-- * a 'Complex' number containing its value.

type Constant t = (String, Complex t)


--------------------------------------------------------------------------------

-- | A configuration. 'def' is the default configuration.

data Config t = Config
    { operators :: [Operator t] -- ^ A list of operators. The order in this list also determines the order of evaluation.
    , constants :: [Constant t] -- ^ A list of constants.
    }


instance (Read t, RealFloat t) => Default (Config t) where
    def = Config
        [ ('+', (+))
        , ('-', (-))
        , ('*', (*))
        , ('/', (/))
        , ('^', (**))
        , ('?', flip logBase)
        ]
        [ ("pi", pi   :+0)
        , ("e" , exp 1:+0)
        , ("i" , 0    :+1)
        ]


--------------------------------------------------------------------------------

-- | Given a configuration and a 'String'-expression, returns a 'String'
-- containing the error message or the resulting complex number.

showHascal :: (Show t, Read t, RealFloat t)
           => Config t
           -> String
           -> String
showHascal conf s = either id showNumber (hascal conf s)


--------------------------------------------------------------------------------

-- | Given a configuration and a 'String'-expression, returns 'Either' a
-- 'String' containing an error message; or a 'Complex' number.

hascal :: (Read t, RealFloat t) => Config t -> String -> Result t
hascal conf = calc (operators conf) (constants conf)


--------------------------------------------------------------------------------

-- Given a list of operators, a list of constants, and a 'String'-expression,
-- returns 'Either' a 'String' containing an error message; or a 'Complex'
-- number.

calc :: (Read t, RealFloat t)
     => [Operator t]
     -> [Constant t]
     -> String
     -> Result t
calc []             cs s = readNumber cs s
calc ((c, f) : ops) cs s =
    foldl1 apply (map (calc ops cs) (splitOn [c] s))
  where
    apply x y = case (x, y) of
        (Right rx, Right ry) -> Right (f rx ry)
        (Left  rx, _       ) -> Left rx
        (_       , Left  ry) -> Left ry


--------------------------------------------------------------------------------

-- 'Either' returns a 'String' containing an error message or returns a
-- complex number represented in the given 'String' as a 'Constant' or as a
-- number. It’s a wrapper around 'findOrRead', adding the underscore prefix
-- operator for negation.

readNumber :: (Read t, RealFloat t)
           => [Constant t]
           -> String
           -> Result t
readNumber cs ('_':s) = fmap negate (findOrRead cs s)
readNumber cs      s  = findOrRead cs s


--------------------------------------------------------------------------------

-- 'Either' returns a 'String' containing an error message or returns a
-- complex number represented in the given 'String' as a 'Constant' or as a
-- number.

findOrRead :: (Read t, Floating t)
           => [Constant t]
           -> String
           -> Result t
findOrRead cs s = maybe (maybeRead s) (Right . snd) (find ((==s) . fst) cs)
  where
    maybeRead s
      | any (null . snd) (reads s :: [(Double,String)]) -- TODO: solve this better
      = Right (read s:+0)
      | otherwise
      = Left ("Error at \"" ++ s ++ "\".")


--------------------------------------------------------------------------------

-- | Show a 'Complex' number a little bit more human-readable by matching both
-- the real and the imaginery part against zero and one.

showNumber :: (Show t, RealFloat t)
           => Complex t
           -> String
showNumber (r:+0) = show r
showNumber (r:+1) = show r ++ " + i"
showNumber (0:+i) = show i ++ " * i"
showNumber (r:+i) = show r ++ " + " ++ show i ++ " * i"