{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Ratings.Glicko
  ( Glicko(..)
  , initialGlicko
  , HasGlicko(..)
  , updateGlicko
  ) where


import Ratings.Types


------------------------------------------------------------------------------
data Glicko = Glicko
  { _glickoRating   :: Rating
  , _glickoRD       :: RD
  } deriving (Eq,Show,Read)


------------------------------------------------------------------------------
-- | This type class forces the user to provide a static c value to be used
-- for their whole system.  Typically you'll provide a simple orphan instance
-- for Glicko like this:
--
-- > instance HasGlicko Glicko where
-- >   mkGlicko = Glicko
-- >   glickoRating = _glickoRating
-- >   glickoRD = _glickoRD
-- >   glickoC _ = 34.6
--
-- Alternatively, if you really want to avoid the orphan instance you can use
-- a newtype wrapper around Glicko.
class HasGlicko a where
    mkGlicko :: Rating -> RD -> a
    glickoRating :: a -> Rating
    glickoRD :: a -> RD
    glickoC :: a -> Double

q :: Double
q = log 10 / 400

g :: RD -> RD
g (RD rd) = RD $ 1 / sqrt (1 + 3 * q*q * rd*rd / (pi*pi))

e :: HasGlicko glicko => Rating -> glicko -> Double
e (Rating me) them =
    1 / (1 + 10 ** (negate $ _unRD (g $ glickoRD them) *
                    (me - _unRating (glickoRating them)) / 400))

d2 :: HasGlicko glicko => Rating -> [glicko] -> Double
d2 me opps = 1 / (q*q * sum (map f opps))
  where
    f opp = (_unRD $ g $ glickoRD opp) ^ (2::Int) * e me opp * (1 - e me opp)


------------------------------------------------------------------------------
-- | Initial rating to use when we have no prior knowledge of a player's
-- strength.
initialGlicko :: Glicko
initialGlicko = Glicko 1500 350


------------------------------------------------------------------------------
-- | Updates a player's glicko strength based on all outcomes in a single
-- rating period. 
updateGlicko
    :: HasGlicko glicko
    => Int
    -- ^ Number of rating periods since rating was last updated
    -> [(glicko, Score)]
    -- ^ Outcomes against each player
    -> glicko
    -- ^ Previous glicko strength estimate
    -> glicko
    -- ^ New glicko strength estimate
updateGlicko t opps me = mkGlicko r' rd'
  where
    r = glickoRating me
    rd0 = _unRD $ glickoRD me
    c = glickoC me
    rd = min (_unRD $ _glickoRD initialGlicko)
             (sqrt $ rd0*rd0 + c*c * fromIntegral t)
    d2Inv = 1.0 / d2 (glickoRating me) (map fst opps)
    rdInv = 1.0 / (rd*rd)
    r' = Rating $ _unRating r + (q * foo / (rdInv + d2Inv))
    rd' = RD $ sqrt $ 1 / (rdInv + d2Inv)
    foo = sum $ map f opps
    f (opp, Score s) = _unRD (g (glickoRD opp)) * (s - e r opp)


testOpps :: [(Glicko, Score)]
testOpps =
  [ (Glicko 1400 30, 1)
  , (Glicko 1550 100, 0)
  , (Glicko 1700 300, 0)
  ]

--instance HasGlicko Glicko where
--    mkGlicko = Glicko           
--    glickoRating = _glickoRating
--    glickoRD = _glickoRD
--    glickoC _ = 34.6
--
--testGlicko = updateGlicko 0 testOpps (Glicko 1500 200) == (Glicko 1464.1064627569112 151.39890244796933)