-- | Discretized floating point numbers, where the scaling factor is kept
-- as two phantom types denoting the rational number used for scaling.

module Numeric.Discretized where

import Control.Applicative
import Control.DeepSeq (NFData(..))
import Data.Aeson (FromJSON,ToJSON)
import Data.Binary (Binary)
import Data.Hashable (Hashable)
import Data.Proxy
import Data.Ratio
import Data.Serialize (Serialize)
import Data.Vector.Unboxed.Deriving
import Debug.Trace
import GHC.Generics
import GHC.Real (Ratio(..))
import GHC.TypeLits

import Algebra.Structure.Semiring
import Numeric.Limits



-- | Some discretizations are of the type @ln 2 / 2@ (@PAM@ matrices in Blast
-- for example). Using this type, we can annotate as follows: @Discretized
-- (RTyLn 2 :% RTyId 2)@.
--
-- One may use @Unknown@ if the scale is not known. For example, the blast
-- matrices use different scales internally and one needs to read the header to
-- get the scale.

data RatioTy a = RTyExp a | RTyId a | RTyLn a | RTyPlus (RatioTy a) (RatioTy a) | RTyTimes (RatioTy a) (RatioTy a) | Unknown

class RatioTyConstant a where
  ratioTyConstant  Proxy a  Ratio Integer

instance (KnownNat k)  RatioTyConstant (RTyExp (kNat)) where
  {-# Inline ratioTyConstant #-}
  ratioTyConstant Proxy = let n = natVal @k Proxy in toRational (exp $ fromInteger n)

instance (KnownNat k)  RatioTyConstant (RTyId (kNat)) where
  {-# Inline ratioTyConstant #-}
  ratioTyConstant Proxy = let n = natVal @k Proxy in toRational n

instance (KnownNat k)  RatioTyConstant (RTyLn (kNat)) where
  {-# Inline ratioTyConstant #-}
  ratioTyConstant Proxy = let n = natVal @k Proxy in toRational (log $ fromInteger n)

instance (RatioTyConstant a, RatioTyConstant b)  RatioTyConstant (RTyPlus (aRatioTy k) (bRatioTy k)) where
  {-# Inline ratioTyConstant #-}
  ratioTyConstant Proxy = ratioTyConstant @a Proxy + ratioTyConstant @b Proxy

instance (RatioTyConstant a, RatioTyConstant b)  RatioTyConstant (RTyTimes (aRatioTy k) (bRatioTy k)) where
  {-# Inline ratioTyConstant #-}
  ratioTyConstant Proxy = ratioTyConstant @a Proxy * ratioTyConstant @b Proxy

-- | A discretized value takes a floating point number @n@ and produces a
-- discretized value. The actual discretization formula is given on the type
-- level, freeing us from having to carry around some scaling function.
--
-- Typically, one might use types likes @100@, @(100 :% 1)@, or @(RTyLn 2 :%
-- RTyId 2)@.
--
-- The main use of a 'Discretized' value is to enable calculations with 'Int'
-- while somewhat pretending to use floating point values.
--
-- Be careful with certain operations like @(*)@ as they will easily cause the
-- numbers to arbitrarily wrong. @(+)@ and @(-)@ are fine, however.
--
-- NOTE Export and import of data is in the form of floating points, which can
-- lead to additional loss of precision if one is careless!
--
-- TODO fast 'Show' methods required!
--
-- TODO blaze stuff?
--
-- TODO We might want to discretize @LogDomain@ style values. This requires
-- some thought on in which direction to wrap. Maybe, we want to log-domain
-- Discretized values, which probably just works.

newtype Discretized (b  k) = Discretized { getDiscretized  Int }
  deriving (Eq,Ord,Generic,Show,Read)

derivingUnbox "Discretized"
  [t| forall t . Discretized t  Int |]  [| getDiscretized |]  [| Discretized |]

instance NFData (Discretized t) where
  rnf (Discretized k) = rnf k
  {-# Inline rnf #-}

instance Binary    (Discretized t)
instance Serialize (Discretized t)
instance FromJSON  (Discretized t)
instance ToJSON    (Discretized t)
instance Hashable  (Discretized t)

instance Num (Discretized Unknown) where
  Discretized x + Discretized y = Discretized $ x+y
  Discretized x - Discretized y = Discretized $ x-y
  (*) = error "Discretized Unknown does not admit (*)"
  abs (Discretized x) = Discretized $ abs x
  signum (Discretized x) = Discretized $ signum x
  fromInteger = error "Discretized Unknown does not admit fromInteger"
  {-# Inline (+) #-}
  {-# Inline (-) #-}
  {-# Inline abs #-}
  {-# Inline signum #-}

instance (KnownNat u, KnownNat l)  Num (Discretized ((uNat) :% (lNat))) where
  {-# Inline (+) #-}
  Discretized x + Discretized y = Discretized (x+y)
  {-# Inline (-) #-}
  Discretized x - Discretized y = Discretized (x-y)
  -- TODO it should be possible to generalize this over arbitrary value, or
  -- replace @KnownNat@ with the above @ratioTyConstant@.
  {-# Inline (*) #-}
  Discretized x * Discretized y =
    let u = fromInteger $ natVal @u Proxy
        l = fromInteger $ natVal @l Proxy
    in  Discretized $ (x*y*u) `div` l
  {-# Inline abs #-}
  abs (Discretized x) = Discretized (abs x)
  {-# Inline signum #-}
  signum (Discretized x) = Discretized $ signum x
  {-# Inline fromInteger #-}
  fromInteger x =
    let u = fromInteger $ natVal @u Proxy
        l = fromInteger $ natVal @l Proxy
    in  Discretized $ (fromInteger x*u) `div` l

instance Enum (Discretized b) where
  toEnum = Discretized
  {-# Inline toEnum #-}
  fromEnum = getDiscretized
  {-# Inline fromEnum #-}

-- instance (Enum (Discretized b), KnownNat u, KnownNat l) ⇒ Integral (Discretized u l) where

instance (KnownNat u, KnownNat l)  Fractional (Discretized ((uNat) :% (lNat))) where
  {-# Inline (/) #-}
  Discretized x / Discretized y =
    let u = fromInteger $ natVal @u Proxy
        l = fromInteger $ natVal @l Proxy
    in  Discretized $ (x * l) `div` (y * u)
  {-# Inline recip #-}
  recip (Discretized x) =
    let u = fromInteger $ natVal @u Proxy
        l = fromInteger $ natVal @l Proxy
    in  error "need to find approximately ok transformation"
  {-# Inline fromRational #-}
  fromRational (a :% b) =
    let u = natVal @u Proxy
        l = natVal @l Proxy
    in  Discretized . fromInteger $ (a * l) `div` (b * u)

instance (KnownNat u, KnownNat l)  Real (Discretized ((uNat) :% (lNat))) where
  {-# Inline toRational #-}
  toRational (Discretized d) =
    let u = natVal @u Proxy
        l = natVal @l Proxy
    in  (fromIntegral d * u) % l

instance (Num (Discretized k))  Semiring (Discretized k) where
  plus = (+)
  times = (*)
  zero = 0
  one = 1
  {-# Inline plus  #-}
  {-# Inline times #-}
  {-# Inline zero  #-}
  {-# Inline one   #-}

instance (NumericLimits (Discretized t)) where
  minFinite = Discretized minFinite
  {-# Inline minFinite #-}
  maxFinite = Discretized maxFinite
  {-# Inline maxFinite #-}

-- | Discretizes any @Real a@ into the @Discretized@ value. This conversion
-- is /lossy/ and uses a type-level rational of @u :% l@!

discretizeRatio  forall a u l . (Real a, KnownNat u, KnownNat l)  a  Discretized ((uNat) :% (lNat))
{-# Inline discretizeRatio #-}
discretizeRatio a =
  let u = natVal @u Proxy
      l = natVal @l Proxy
      k = toRational a
  in  Discretized . fromIntegral $ numerator k * l `div` (denominator k * u)