{-# language DeriveGeneric #-}
{-# language LambdaCase #-}
{-# language OverloadedStrings #-}

{-# options_ghc -Wall #-}

module Data.RelevantTime
  ( RelevantTime(..)
  , encodeRelevantTime
  , decodeRelevantTime
  , absolutizeRelevantTime
  , relativizeAbsoluteTime
  ) where

import Data.Text (Text)
import Data.Aeson (ToJSON(..),FromJSON(..))
import Data.Int (Int64)
import Chronos.Types (Time)
import Torsor (add,difference,invert,scale)
import GHC.Generics (Generic)
import qualified Chronos as CH
import qualified Data.Text as T
import qualified Data.Text.Read as TR
import qualified Data.Aeson as AE

data RelevantTime
  = RelevantTimeMinute {-# UNPACK #-} !Int64
  | RelevantTimeHour   {-# UNPACK #-} !Int64
  | RelevantTimeDay    {-# UNPACK #-} !Int64
  | RelevantTimeNow
  | RelevantTimeMidnight
  | RelevantTimeNoon
  deriving (Eq,Ord,Generic,Show)

instance ToJSON RelevantTime where
  toJSON = AE.String . encodeRelevantTime

instance FromJSON RelevantTime where
  parseJSON = AE.withText "RelevantTime"
    (maybe (fail "invalid RelevantTime") return . decodeRelevantTime)

encodeRelevantTime :: RelevantTime -> Text
encodeRelevantTime = \case
  RelevantTimeNow -> "now"
  RelevantTimeMidnight -> "midnight"
  RelevantTimeNoon -> "noon"
  RelevantTimeMinute x -> T.pack (show x ++ "m")
  RelevantTimeHour x -> T.pack (show x ++ "h")
  RelevantTimeDay x -> T.pack (show x ++ "d")

decodeRelevantTime :: Text -> Maybe RelevantTime
decodeRelevantTime t = if T.null t
  then Nothing
  else case t of
    "now" -> Just RelevantTimeNow
    "noon" -> Just RelevantTimeNoon
    "midnight" -> Just RelevantTimeMidnight
    _ -> case T.last t of
      'm' -> RelevantTimeMinute <$> readInt (T.init t)
      'h' -> RelevantTimeHour <$> readInt (T.init t)
      'd' -> RelevantTimeDay <$> readInt (T.init t)
      'w' -> RelevantTimeDay <$> (fmap ((*) 7 ) $ readInt (T.init t))
      'M' -> RelevantTimeDay <$> (fmap ((*) 30) $ readInt (T.init t))
      _ -> Nothing

absolutizeRelevantTime :: Time -> RelevantTime -> Time
absolutizeRelevantTime now = \case
  RelevantTimeNow -> now
  RelevantTimeNoon -> add (scale 12 CH.hour) (CH.dayToTimeMidnight (CH.timeToDayTruncate now))
  RelevantTimeMidnight -> CH.dayToTimeMidnight (CH.timeToDayTruncate now)
  RelevantTimeMinute n -> add (invert (scale n CH.minute)) now
  RelevantTimeHour n -> add (invert (scale n CH.hour)) now
  RelevantTimeDay n -> add (invert (scale n CH.day)) now

relativizeAbsoluteTime :: Time -> Time -> RelevantTime
relativizeAbsoluteTime now abst
  | absolutizeRelevantTime now RelevantTimeNoon == abst = RelevantTimeNoon
  | absolutizeRelevantTime now RelevantTimeMidnight == abst = RelevantTimeMidnight
  | diff <  (CH.getTimespan CH.minute) = RelevantTimeNow
  | diff <= (CH.getTimespan CH.minute) * 59 = RelevantTimeMinute $ round $ (toFrac diff) / (minute * nanoseconds)
  | diff <= (CH.getTimespan CH.hour) * 23 = RelevantTimeHour $ round $ (toFrac diff) / (hour * nanoseconds)
  | otherwise = RelevantTimeDay $ round $ (toFrac diff) / (day * nanoseconds)
  where
  diff :: Int64
  diff = (abs $ CH.getTimespan (now `difference` abst))
  toFrac :: Int64 -> Rational
  toFrac = fromIntegral . abs
  minute = 60
  hour = 60 * minute
  day = 24 * hour
  nanoseconds = 1000000000

readInt :: Text -> Maybe Int64
readInt x = case TR.signed TR.decimal x of
  Left _ -> Nothing
  Right (i,leftover) -> if T.null leftover
    then Just i
    else Nothing