--------------------------------------------------------------------------------
-- |
-- Module : Data.DotNet.TimeSpan.Internal
-- Copyright : (C) 2016 Yorick Laupa
-- License : (see the file LICENSE)
--
-- Maintainer : Yorick Laupa <yo.eight@gmail.com>
-- Stability : provisional
-- Portability : non-portable
--
-- .NET TimeSpan implemented in Haskell.
--------------------------------------------------------------------------------
module Data.DotNet.TimeSpan.Internal where

--------------------------------------------------------------------------------
import Data.Int
import Data.Monoid
import Prelude

--------------------------------------------------------------------------------
-- | .NET TimeSpan: Represents a time interval.
newtype TimeSpan = TimeSpan Int64 deriving (Eq, Ord)

--------------------------------------------------------------------------------
instance Show TimeSpan where
    show = timeSpanString

--------------------------------------------------------------------------------
millisPerSecond :: Int64
millisPerSecond = 1000

--------------------------------------------------------------------------------
millisPerMinute :: Int64
millisPerMinute = millisPerSecond * 60

--------------------------------------------------------------------------------
millisPerHour :: Int64
millisPerHour = millisPerMinute * 60

--------------------------------------------------------------------------------
millisPerDay :: Int64
millisPerDay = millisPerHour * 24

--------------------------------------------------------------------------------
ticksPerMillisecond :: Int64
ticksPerMillisecond = 10000

--------------------------------------------------------------------------------
ticksPerSecond :: Int64
ticksPerSecond = ticksPerMillisecond * 1000

--------------------------------------------------------------------------------
ticksPerMinute :: Int64
ticksPerMinute = ticksPerSecond * 60

--------------------------------------------------------------------------------
ticksPerHour :: Int64
ticksPerHour = ticksPerMinute * 60

--------------------------------------------------------------------------------
ticksPerDay :: Int64
ticksPerDay = ticksPerHour * 24

--------------------------------------------------------------------------------
daysPerTick :: Double
daysPerTick = 1 / (realToFrac ticksPerDay)

--------------------------------------------------------------------------------
hoursPerTick :: Double
hoursPerTick = 1 / (realToFrac ticksPerHour)

--------------------------------------------------------------------------------
minutesPerTick :: Double
minutesPerTick = 1 / (realToFrac ticksPerMinute)

--------------------------------------------------------------------------------
secondsPerTick :: Double
secondsPerTick = 1 / (realToFrac ticksPerSecond)

--------------------------------------------------------------------------------
millisPerTick :: Double
millisPerTick = 1 / (realToFrac ticksPerMillisecond)

--------------------------------------------------------------------------------
maxMillis :: Int64
maxMillis =
    truncate
    (((realToFrac (maxBound :: Int64) :: Double)
      / realToFrac ticksPerMillisecond) :: Double)

--------------------------------------------------------------------------------
minMillis :: Int64
minMillis =
    truncate
    (((realToFrac (minBound :: Int64) :: Double)
      / realToFrac ticksPerMillisecond) :: Double)

--------------------------------------------------------------------------------
-- | Initializes a new instance of the TimeSpan structure to the specified
--   number of ticks.
timeSpanTicks :: Int64 -> TimeSpan
timeSpanTicks = TimeSpan

--------------------------------------------------------------------------------
-- | Initializes a new instance of the TimeSpan structure to a specified number
--   of hours, minutes, and seconds.
timeSpanHoursMinsSecs :: Int64 -> Int64 -> Int64 -> TimeSpan
timeSpanHoursMinsSecs hh mm ss = TimeSpan $ totalSecs * ticksPerSecond
  where
    totalSecs = (hh * 3600) + (mm * 60) + ss

--------------------------------------------------------------------------------
-- | Initializes a new instance of the TimeSpan structure to a specified number
--   of days, hours, minutes, and seconds.
timeSpanDaysHoursMinsSecs :: Int64 -> Int64 -> Int64 -> Int64 -> TimeSpan
timeSpanDaysHoursMinsSecs dd hh mm ss =
    timeSpanDaysHoursMinsSecsMillis dd hh mm ss 0

--------------------------------------------------------------------------------
-- | Initializes a new instance of the TimeSpan structure to a specified number
--   of days, hours, minutes, seconds, and milliseconds.
timeSpanDaysHoursMinsSecsMillis :: Int64
                                -> Int64
                                -> Int64
                                -> Int64
                                -> Int64
                                -> TimeSpan
timeSpanDaysHoursMinsSecsMillis dd hh mm ss ms =
    TimeSpan $ _totalMillis * ticksPerMillisecond
  where
    _totalMillis = ((dd * 3600 * 24) +
                    (hh * 3600)       +
                    (mm * 60)         +
                    ss) * 1000 + ms

--------------------------------------------------------------------------------
-- | Gets the number of ticks that represent the value of the current 'TimeSpan'
--   structure.
ticks :: TimeSpan -> Int64
ticks (TimeSpan i) = i

--------------------------------------------------------------------------------
-- | Gets the days component of the time interval represented by the current
--   'TimeSpan' structure.
days :: TimeSpan -> Int64
days (TimeSpan i) = truncate $ (realToFrac i :: Double) /
                               (realToFrac ticksPerDay)

--------------------------------------------------------------------------------
-- | Gets the hours component of the time interval represented by the current
--   'TimeSpan' structure.
hours :: TimeSpan -> Int64
hours (TimeSpan i) = mod (truncate $
                          (realToFrac i :: Double) /
                          (realToFrac ticksPerHour)) 24

--------------------------------------------------------------------------------
-- | Gets the minutes component of the time interval represented by the current
--   'TimeSpan' structure.
minutes :: TimeSpan -> Int64
minutes (TimeSpan i) = mod (truncate $
                            (realToFrac i :: Double) /
                            (realToFrac ticksPerMinute)) 60

--------------------------------------------------------------------------------
-- | Gets the seconds component of the time interval represented by the current
--   'TimeSpan' structure.
seconds :: TimeSpan -> Int64
seconds (TimeSpan i) = mod (truncate $
                            (realToFrac i :: Double) /
                            (realToFrac ticksPerSecond)) 60

--------------------------------------------------------------------------------
-- | Gets the milliseconds component of the time interval represented by the
--   current 'TimeSpan' structure.
millis :: TimeSpan -> Int64
millis (TimeSpan i) = mod (truncate $
                           (realToFrac i :: Double) /
                           (realToFrac ticksPerMillisecond)) 1000

--------------------------------------------------------------------------------
-- | Returns a 'TimeSpan' that represents a specified number of seconds, where
--   the specification is accurate to the nearest millisecond.
fromSeconds :: Double -> TimeSpan
fromSeconds i = interval i millisPerSecond

--------------------------------------------------------------------------------
-- | Returns a 'TimeSpan' that represents a specified number of minutes, where
--   the specification is accurate to the nearest millisecond.
fromMinutes :: Double -> TimeSpan
fromMinutes i = interval i millisPerMinute

--------------------------------------------------------------------------------
-- | Returns a 'TimeSpan' that represents a specified number of hours, where the
--   specification is accurate to the nearest millisecond.
fromHours :: Double -> TimeSpan
fromHours i = interval i millisPerHour

--------------------------------------------------------------------------------
-- | Returns a 'TimeSpan' that represents a specified number of days, where the
--   specification is accurate to the nearest millisecond.
fromDays :: Double -> TimeSpan
fromDays i = interval i millisPerDay

--------------------------------------------------------------------------------
-- | Gets the value of the current 'TimeSpan' structure expressed in whole and
--   fractional days.
totalDays :: TimeSpan -> Double
totalDays (TimeSpan i) = (realToFrac i) * daysPerTick

--------------------------------------------------------------------------------
-- | Gets the value of the current 'TimeSpan' structure expressed in whole and
--   fractional hours.
totalHours :: TimeSpan -> Double
totalHours (TimeSpan i) = (realToFrac i) * hoursPerTick

--------------------------------------------------------------------------------
-- | Gets the value of the current 'TimeSpan' structure expressed in whole and
--   fractional minutes.
totalMinutes :: TimeSpan -> Double
totalMinutes (TimeSpan i) = (realToFrac i) * minutesPerTick

--------------------------------------------------------------------------------
-- | Gets the value of the current 'TimeSpan' structure expressed in whole and
--   fractional seconds.
totalSeconds :: TimeSpan -> Double
totalSeconds (TimeSpan i) = (realToFrac i) * secondsPerTick

--------------------------------------------------------------------------------
-- | Gets the value of the current 'TimeSpan' structure expressed in whole and
--   fractional milliseconds.
totalMillis :: TimeSpan -> Double
totalMillis (TimeSpan i) =
    let tmp = (realToFrac i) * millisPerTick in
    if tmp > (realToFrac maxMillis) then realToFrac maxMillis
    else if tmp < (realToFrac minMillis) then realToFrac minMillis
         else tmp

--------------------------------------------------------------------------------
data FormatLiteral = Positive | Negative

--------------------------------------------------------------------------------
padded :: Int -> a -> [a] -> [a]
padded n p xs = replicate diff p ++ xs
  where
    len_xs = length xs
    diff   = n - len_xs

--------------------------------------------------------------------------------
timeSpanString :: TimeSpan -> String
timeSpanString (TimeSpan _ticks) =
    start    <>
    genDay   <>
    genHours <>
    genMins  <>
    genSecs  <>
    genFract

  where
    ticksPerHourD   = realToFrac ticksPerHour   :: Double
    ticksPerDayD    = realToFrac ticksPerDay    :: Double
    ticksPerMinuteD = realToFrac ticksPerMinute :: Double
    ticksPerSecondD = realToFrac ticksPerSecond :: Double

    day :: Int64
    day = truncate $ realToFrac _ticks / ticksPerDayD

    time = _ticks `mod` ticksPerDay

    cday  = if _ticks < 0 then negate day else day
    ctime = if _ticks < 0 then negate time else time

    _hours :: Int64
    _hours = mod (truncate (realToFrac ctime / ticksPerHourD)) 24

    mins :: Int64
    mins = mod (truncate (realToFrac ctime / ticksPerMinuteD)) 60

    secs :: Int64
    secs = mod (truncate (realToFrac ctime / ticksPerSecondD)) 60

    fraction :: Int64
    fraction = ctime `mod` ticksPerSecond

    literal = if _ticks < 0 then Negative else Positive

    start =
        case literal of
            Positive -> ""
            Negative -> "-"

    genDay =
        if cday /= 0
        then show cday <> "."
        else mempty

    genHours = padded 2 '0' (show _hours) <> ":"
    genMins  = padded 2 '0' (show mins)  <> ":"
    genSecs  = padded 2 '0' (show secs)

    genFract =
        if fraction /= 0
        then "." <> padded 7 '0' (show fraction)
        else mempty

--------------------------------------------------------------------------------
interval :: Double -> Int64 -> TimeSpan
interval value scale =
    let tmp     = value * (realToFrac scale)
        _millis = tmp + (if value >= 0 then 0.5 else (-0.5))
        res     = truncate (_millis * (realToFrac ticksPerMillisecond)) in
    TimeSpan res