{-# LANGUAGE Safe #-}
module Data.UTC.Class.IsDate
  ( IsDate (..)
  ) where

import Control.Monad.Catch

import Data.UTC.Internal
import Data.UTC.Class.Epoch

-- | This class captures the behaviour of the __Proleptic Gregorian Calendar__.
--
-- Without any exception the following holds:
--
--   * A regular year has 365 days and the corresponding February has 28 days.
--   * A leap year has 366 days and the corresponding February has 29 days.
--   * A year that is a multiple of 400 is a leap year.
--   * A year that is a multiple of 100 but not of 400 is not a leap year.
--   * A year that is a multiple of 4 but not of 100 is a leap year.
class Epoch t => IsDate t where
  -- | > year  "2014-⁠12-⁠24" == 2014
  -- For negative years the function assumes astronomical year
  -- numbering (year 1 ~ 1 AD, year 0 ~ 1 BC, year -1 ~ 2 BC etc).
  -- Note that 1 BC and 5 BC are therefore leap years.
  year                  :: t -> Integer
  -- | > month "2014-⁠12-⁠24" == 12
  -- The function only returns values ranging from 1 to 12.
  month                 :: t -> Integer
  -- | > day   "2014-⁠12-⁠24" == 24
  -- The function only returns values ranging from 1 to 31.
  day                   :: t -> Integer

  -- | Sets the year and fails if the result would be invalid.
  --
  -- > setYear 2005 "2004-02-28" :: Maybe Date
  -- > > Just 2005-02-28
  -- > setYear 2005 "2004-02-29" :: Maybe Date
  -- > > Nothing
  setYear               :: (MonadThrow m) => Integer  -> t -> m t
  -- | Sets the month of year and fails if the result would be invalid.
  --
  -- The function only accepts input ranging from 1 to 12.
  setMonth              :: (MonadThrow m) => Integer  -> t -> m t
  -- | Sets the day of month and fails if the result would be invalid.
  --
  -- The function only accepts input ranging from 1 to 31 (or less depending on month and year).
  setDay                :: (MonadThrow m) => Integer  -> t -> m t

  -- | A /year/ is a relative amount of time.
  -- The function's semantic is a follows:
  --
  --   * The years (positive or negative) are added.
  --   * If the target date is invalid then days are subtracted until the date gets valid.
  --   * If the resulting date is out of the instance type's range, the function fails
  --     (cannot happen for 'Data.UTC.Date' and 'Data.UTC.DateTime' as they use 
  --     multiprecision integers).
  --
  -- > addYears 4 "2000-02-29" :: Maybe Date
  -- > > Just 2004-02-29
  -- > addYears 1 "2000-02-29" :: Maybe Date
  -- > > Just 2001-02-28
  addYears              :: (MonadThrow m) => Integer  -> t -> m t
  addYears ys t
    = if isValidDate (year t + ys, month t, day t)
        then setYear (year t + ys) t
        else setYear (year t + ys) =<< setDay (day t - 1) t

  -- | A /month/ is a relative amount of time.
  -- The function's semantic is equivalent to that of 'addYears'.
  --
  -- The function fails if the resulting date is out of the instance type's range
  -- (cannot happen for 'Data.UTC.Date' and 'Data.UTC.DateTime' as they use 
  --  multiprecision integers).
  --
  -- > addMonths (-13) "1970-01-01" :: Maybe Date
  -- > > Just 1968-12-01
  addMonths             :: (MonadThrow m) => Integer  -> t -> m t
  addMonths ms t
    = setDay 1 t >>= setYear y >>= setMonth m >>= setDay d
    where
      ym = (year t * monthsPerYear)
         + (month t - 1)
         + ms
      y  = ym `div` monthsPerYear
      m  = (ym `mod` monthsPerYear) + 1
      d' = day t
      d  | isValidDate (y, m, d')     = d'
         | isValidDate (y, m, d' - 1) = d' - 1
         | isValidDate (y, m, d' - 2) = d' - 2
         | otherwise                  = d' - 3 -- was 31, now 28

  -- | A /day/ is an absolute amount of time. There is no surprise to expect.
  --
  -- The function fails if the resulting date is out of the instance type's range
  -- (cannot happen for 'Data.UTC.Date' and 'Data.UTC.DateTime' as they use 
  --  multiprecision integers).
  --
  -- > addDays 365 "1970-01-01" :: Maybe Date
  -- > > Just 1971-01-01
  -- > addDays 365 "2000-01-01" :: Maybe Date
  -- > > Just 2000-12-31
  addDays               :: (MonadThrow m) => Integer  -> t -> m t
  addDays ds t
    -- setDay 1 to avoid intermediate generation of invalid dates!
    = setDay 1 t >>= setYear y >>= setMonth m >>= setDay d
    where
      ds'       = yearMonthDayToDays (year t, month t, day t)
      (y, m, d) = daysToYearMonthDay (ds' + ds)