{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}

module Text.Time.Pretty.TimeAgo
  ( daysAgo
  , daysAgoToDays
  , DaysAgo(..)
  , timeAgo
  , timeAgoToDiffTime
  , TimeAgo(..)
  ) where

import Data.Time
import Data.Validity
import GHC.Generics (Generic)

import Text.Time.Pretty.Constants

data DaysAgo =
  DaysAgo
    { daysAgoSign :: Ordering
    , daysAgoYears :: Integer
    , daysAgoMonths :: Integer
    , daysAgoWeeks :: Integer
    , daysAgoDays :: Integer
    }
  deriving (Show, Eq, Generic)

instance Validity DaysAgo where
  validate da@DaysAgo {..} =
    mconcat
      [ genericValidate da
      , check
          (case daysAgoSign of
             EQ ->
               and
                 [ daysAgoDays == 0
                 , daysAgoWeeks == 0
                 , daysAgoMonths == 0
                 , daysAgoYears == 0
                 ]
             _ ->
               any
                 (> 0)
                 [daysAgoDays, daysAgoWeeks, daysAgoMonths, daysAgoYears])
          "the sign makes sense"
      , check (daysAgoYears >= 0) "years are positive"
      , check
          (daysAgoDays + daysPerWeek * daysAgoWeeks +
           approximateDaysPerMonth * daysAgoMonths <
           approximateDaysPerYear)
          "days, weeks and months do not sum to a year"
      , check (daysAgoMonths < 12) "months < 12"
      , check (daysAgoMonths >= 0) "months are positive"
      , check
          (daysAgoDays + daysPerWeek * daysAgoWeeks < approximateDaysPerMonth)
          "days and weeks do not sum to a month"
      , check (daysAgoWeeks < 5) "weeks < 5"
      , check (daysAgoWeeks >= 0) "weeks are positive"
      , check (daysAgoDays < 7) "days < 7"
      , check (daysAgoDays >= 0) "days are positive"
      ]

daysAgo :: Integer -> DaysAgo
daysAgo i = DaysAgo {..}
  where
    totalDays = abs i
    daysAgoSign = compare i 0
    daysAgoYears = totalDays `div` approximateDaysPerYear
    daysLeftAfterYears = totalDays - daysAgoYears * approximateDaysPerYear
    daysAgoMonths = daysLeftAfterYears `div` approximateDaysPerMonth
    daysLeftAfterMonths =
      daysLeftAfterYears - daysAgoMonths * approximateDaysPerMonth
    daysAgoWeeks = daysLeftAfterMonths `div` daysPerWeek
    daysLeftAfterWeeks = daysLeftAfterMonths - daysAgoWeeks * daysPerWeek
    daysAgoDays = daysLeftAfterWeeks

daysAgoToDays :: DaysAgo -> Integer
daysAgoToDays DaysAgo {..} =
  (case daysAgoSign of
     EQ -> const 0
     GT -> id
     LT -> negate) $
  daysAgoDays + daysPerWeek * daysAgoWeeks +
  approximateDaysPerMonth * daysAgoMonths +
  approximateDaysPerYear * daysAgoYears

data TimeAgo =
  TimeAgo
    { timeAgoSign :: Ordering
    , timeAgoDaysAgo :: DaysAgo
    , timeAgoHours :: Integer
    , timeAgoMinutes :: Integer
    , timeAgoSeconds :: Integer
    , timeAgoPicoSeconds :: Integer
    }
  deriving (Show, Eq, Generic)

instance Validity TimeAgo where
  validate ta@TimeAgo {..} =
    mconcat
      [ genericValidate ta
      , check
          (case timeAgoSign of
             EQ ->
               and
                 [ daysAgoToDays timeAgoDaysAgo == 0
                 , timeAgoHours == 0
                 , timeAgoMinutes == 0
                 , timeAgoSeconds == 0
                 , timeAgoPicoSeconds == 0
                 ]
             _ ->
               any
                 (> 0)
                 [ daysAgoToDays timeAgoDaysAgo
                 , timeAgoHours
                 , timeAgoMinutes
                 , timeAgoSeconds
                 , timeAgoPicoSeconds
                 ])
          "the sign makes sense"
      , check (daysAgoSign timeAgoDaysAgo /= LT) "The days ago are not negative"
      , check (timeAgoHours < hoursPerDay) "hours < 24"
      , check (timeAgoHours >= 0) "hours are positive"
      , check (timeAgoMinutes < minutesPerHour) "minutes < 60"
      , check (timeAgoMinutes >= 0) "minutes are positive"
      , check (timeAgoSeconds < secondsPerMinute) "seconds < 60"
      , check (timeAgoSeconds >= 0) "seconds are positive"
      , check (timeAgoPicoSeconds < picoSecondsPerSecond) "picoseconds < 1E12"
      , check (timeAgoPicoSeconds >= 0) "picoseconds are positive"
      ]

timeAgo :: NominalDiffTime -> TimeAgo
timeAgo dt = TimeAgo {..}
  where
    timeAgoSign = compare dt 0
    timeAgoPicoSeconds =
      totalPicoSecondsAgo - picoSecondsPerSecond * totalSecondsAgo
    timeAgoSeconds = totalSecondsAgo - secondsPerMinute * totalMinutesAgo
    timeAgoMinutes = totalMinutesAgo - minutesPerHour * totalHoursAgo
    timeAgoHours = totalHoursAgo - hoursPerDay * totalDaysAgo
    timeAgoDaysAgo = daysAgo totalDaysAgo
    totalPicoSecondsAgo =
      floor $ absDt * fromIntegral (picoSecondsPerSecond :: Integer)
    totalSecondsAgo = floor absDt :: Integer
    totalMinutesAgo = floor $ absDt / fromIntegral (secondsPerMinute :: Integer)
    totalHoursAgo =
      floor $
      absDt / fromIntegral (minutesPerHour * secondsPerMinute :: Integer)
    totalDaysAgo =
      floor $
      absDt /
      fromIntegral (hoursPerDay * minutesPerHour * secondsPerMinute :: Integer)
    absDt = abs dt

timeAgoToDiffTime :: TimeAgo -> NominalDiffTime
timeAgoToDiffTime TimeAgo {..} =
  (/ fromIntegral (picoSecondsPerSecond :: Integer)) $
  realToFrac $
  (case timeAgoSign of
     EQ -> const 0
     GT -> id
     LT -> negate)
    (timeAgoPicoSeconds +
     picoSecondsPerSecond *
     (timeAgoSeconds +
      secondsPerMinute *
      (timeAgoMinutes +
       minutesPerHour *
       (timeAgoHours + hoursPerDay * (daysAgoToDays timeAgoDaysAgo)))))