{-# LANGUAGE RankNTypes #-}

-- | A data type for monetary values, with associated operations and
--   only sensible instances.
module Data.Money
  (
    Money(Money)
  -- * Optics
  , money
  -- * Operators
  , ($+$)
  , ($-$)
  , (*$)
  , ($*)
  , ($/)
  , ($/$)
  , ($^)
  , ($^^)
  , ($**)
  ) where

import Data.Functor (Functor (fmap))
import Data.Foldable (Foldable (foldMap))
import Data.Monoid (Monoid, mempty, mappend)
import Data.Profunctor (Profunctor, dimap)
import Data.Semigroup (Semigroup, (<>))
import Data.Traversable (Traversable (traverse))

-- | A newtype for monetary values represented as type @num@.
--
--   The 'Semigroup' instance allows amounts of money to be added together.
--
--   Any 'Num' instances present are hidden, as operations like multiplying
--   money by money don't make any sense.
newtype Money num =
  Money num
  deriving (Eq, Ord)

instance Show num => Show (Money num) where
  show (Money m) = '$': show m

instance Num a => Semigroup (Money a) where
  Money m <> Money n = Money (m + n)

instance Num a => Monoid (Money a) where
  mappend = (<>)
  mempty = Money 0

instance Functor Money where
  fmap f (Money n) = Money (f n)

instance Foldable Money where
  foldMap f (Money n) = f n

instance Traversable Money where
  traverse f (Money n) = fmap Money (f n)

type Iso s t a b = forall p f. (Profunctor p, Functor f) => p a (f b) -> p s (f t)

-- | The raw numeric value inside monetary value
money :: Iso (Money a) (Money b) a b
money = dimap (\(Money a) -> a) (fmap Money)

-- | Add money to money. A synonym for @<>@.
infixl 6 $+$
($+$) :: Num a => Money a -> Money a -> Money a
($+$) = (<>)

-- | Subtract money from money
infixl 6 $-$
($-$) :: Num a => Money a -> Money a -> Money a
($-$) (Money m) (Money n) = Money (m - n)

-- | Multiply a scalar by money
infixr 7 *$
(*$) :: Num a => a -> Money a -> Money a
(*$) x (Money m) = Money (x * m)

-- | Multiply money by a scalar
infixl 7 $*
($*) :: Num a => Money a -> a -> Money a
($*) = flip (*$)

-- | Divide money by a scalar
infixl 7 $/
($/) :: Fractional a => Money a -> a -> Money a
($/) (Money m) x = Money (m/x)

-- | Divide money by money
infixl 7 $/$
($/$) :: Fractional a => Money a -> Money a -> a
($/$) (Money n) (Money m) = n / m

-- | Raise money to a non-negative integral power
infixr 8 $^
($^) :: (Num a, Integral b) => Money a -> b -> Money a
($^) (Money m) x = Money (m ^ x)

-- | Raise money to an integral power
infixr 8 $^^
($^^) :: (Fractional a, Integral b) => Money a -> b -> Money a
($^^) (Money m) x = Money (m ^^ x)

-- | Raise money to a floating-point power
infixr 8 $**
($**) :: Floating a => Money a -> a -> Money a
($**) (Money m) x = Money (m ** x)