```-----------------------------------------------------------------------------
-- |
-- Module      :  Graphics.Rendering.Chart.Axis.LocalTime
-- Copyright   :  (c) Tim Docker 2010, 2014
--
-- Calculate and render time axes

module Graphics.Rendering.Chart.Axis.LocalTime(
TimeSeq,
TimeLabelFn,
TimeLabelAlignment(..),

timeAxis,
autoTimeAxis,

days, months, years,

-- * Utilities
doubleFromLocalTime

) where

import Data.Default.Class
import Data.Time
import Data.Fixed
import System.Locale (defaultTimeLocale)
import Control.Lens

import Graphics.Rendering.Chart.Axis.Types

instance PlotValue LocalTime where
toValue    = doubleFromLocalTime
fromValue  = localTimeFromDouble
autoAxis   = autoTimeAxis

----------------------------------------------------------------------

-- | Map a LocalTime value to a plot coordinate.
doubleFromLocalTime :: LocalTime -> Double
doubleFromLocalTime lt = fromIntegral (toModifiedJulianDay (localDay lt))
+ fromRational (timeOfDayToDayFraction (localTimeOfDay lt))

-- | Map a plot coordinate to a LocalTime.
localTimeFromDouble :: Double -> LocalTime
localTimeFromDouble v =
LocalTime (ModifiedJulianDay i) (dayFractionToTimeOfDay (toRational d))
where
(i,d) = properFraction v

-- | TimeSeq is a (potentially infinite) set of times. When passed
--   a reference time, the function returns a a pair of lists. The first
--   contains all times in the set less than the reference time in
--   decreasing order. The second contains all times in the set greater
--   than or equal to the reference time, in increasing order.
type TimeSeq = LocalTime-> ([LocalTime],[LocalTime])

coverTS :: TimeSeq -> LocalTime -> LocalTime -> [LocalTime]
coverTS tseq minT maxT = min' ++ enumerateTS tseq minT maxT ++ max'
where
min' =  if elemTS minT tseq then [] else take 1 (fst (tseq minT))
max' =  if elemTS maxT tseq then [] else take 1 (snd (tseq maxT))

enumerateTS :: TimeSeq -> LocalTime -> LocalTime -> [LocalTime]
enumerateTS tseq minT maxT =
reverse (takeWhile (>=minT) ts1)  ++ takeWhile (<=maxT) ts2
where
(ts1,ts2) = tseq minT

elemTS :: LocalTime -> TimeSeq -> Bool
elemTS t tseq = case tseq t of
(_,t0:_) | t == t0 -> True
_                  -> False

-- | How to display a time
type TimeLabelFn = LocalTime -> String

data TimeLabelAlignment = UnderTicks
| BetweenTicks
deriving (Show)

-- | Create an 'AxisFn' to for a time axis.
--
--   The values to be plotted against this axis can be created with
--   'doubleFromLocalTime'.
timeAxis ::
TimeSeq
-- ^ Set the minor ticks, and the final range will be aligned to its
--   elements.
-> TimeSeq
-- ^ Set the labels and grid.
-> TimeLabelFn
-> TimeLabelAlignment
-> TimeSeq
-- ^ Set the second line of labels.
-> TimeLabelFn
-- ^ Format `LocalTime` for labels.
-> TimeLabelAlignment
-> AxisFn LocalTime
timeAxis tseq lseq labelf lal cseq contextf clal pts = AxisData {
_axis_visibility = def,
_axis_viewport = vmap(min', max'),
_axis_tropweiv = invmap(min', max'),
_axis_ticks    = [ (t,2) | t <- times] ++ [ (t,5) | t <- ltimes, visible t],
_axis_labels   = [ [ (t,l) | (t,l) <- labels labelf   ltimes lal, visible t]
, [ (t,l) | (t,l) <- labels contextf ctimes clal, visible t]
],
_axis_grid     = [ t     | t <- ltimes, visible t]
}
where
(minT,maxT)  = case pts of
[] -> (refLocalTime,refLocalTime)
ps -> (minimum ps, maximum ps)
refLocalTime = LocalTime (ModifiedJulianDay 0) midnight
times        = coverTS tseq minT maxT
ltimes       = coverTS lseq minT maxT
ctimes       = coverTS cseq minT maxT
min'         = minimum times
max'         = maximum times
visible t    = min' <= t && t <= max'
labels f ts lal' =
[ (align lal' m1' m2', f m1)
| (m1,m2) <- zip ts (tail ts)
, let m1' = if m1<min' then min' else m1
, let m2' = if m2>max' then max' else m2 ]

align BetweenTicks m1 m2 = avg m1 m2
align UnderTicks   m1 _  = m1

avg m1 m2    = localTimeFromDouble \$ m1' + (m2' - m1')/2
where
m1' = doubleFromLocalTime m1
m2' = doubleFromLocalTime m2

normalizeTimeOfDay :: LocalTime -> LocalTime
normalizeTimeOfDay t@(LocalTime day (TimeOfDay h m s))
| s <  0    = normalizeTimeOfDay (LocalTime day (TimeOfDay h (m-1) (s+60)))
| m <  0    = normalizeTimeOfDay (LocalTime day (TimeOfDay (h-1) (m+60) s))
| h <  0    = normalizeTimeOfDay (LocalTime (addDays (-1) day) (TimeOfDay (h+24) m s))
| s >= 60   = normalizeTimeOfDay (LocalTime day (TimeOfDay h (m+s`div'`60)
(s`mod'`60)))
| m >= 60   = normalizeTimeOfDay (LocalTime day (TimeOfDay (h+m`div`60)
(m`mod`60) s))
| h >= 24   = LocalTime (addDays (fromIntegral (h`div`24)) day)
(TimeOfDay (h`mod`24) m s)
| otherwise = t

addTod :: Int -> Int -> Pico -> LocalTime -> LocalTime
addTod dh dm ds (LocalTime day (TimeOfDay h m s)) = normalizeTimeOfDay t'
where t' = LocalTime day (TimeOfDay (h+dh) (m+dm) (s+ds))

truncateTo :: (HasResolution a) => Fixed a -> Fixed a -> Fixed a
truncateTo t step = t - t `mod'` step

secondSeq :: Pico -> TimeSeq
secondSeq step t = (iterate rev t1, tail (iterate fwd t1))
where h0       = todHour (localTimeOfDay t)
m0       = todMin  (localTimeOfDay t)
s0       = todSec  (localTimeOfDay t) `truncateTo` (1 / 1000)
t0       = LocalTime (localDay t) (TimeOfDay h0 m0 s0)
t1       = if t0 < t then t0 else rev t0
rev      = addTod 0 0 (negate step)
fwd      = addTod 0 0 step

millis1, millis10, millis100, seconds, fiveSeconds  :: TimeSeq
millis1 = secondSeq (1 / 1000)
millis10 = secondSeq (1 / 100)
millis100 = secondSeq (1 / 10)
seconds = secondSeq 1
fiveSeconds = secondSeq 5

minuteSeq :: Int -> TimeSeq
minuteSeq step t = (iterate rev t1, tail (iterate fwd t1))
where h0       = todHour (localTimeOfDay t)
m0       = todMin  (localTimeOfDay t)
t0       = LocalTime (localDay t) (TimeOfDay h0 m0 0)
t1       = if t0 < t then t0 else rev t0
rev      = addTod 0 (negate step) 0
fwd      = addTod 0 step 0

minutes, fiveMinutes :: TimeSeq
minutes = minuteSeq 1
fiveMinutes = minuteSeq 5

-- | A 'TimeSeq' for hours.
hours :: TimeSeq
hours t = (iterate rev t1, tail (iterate fwd t1))
where h0       = todHour (localTimeOfDay t)
t0       = LocalTime (localDay t) (TimeOfDay h0 0 0)
t1       = if t0 < t then t0 else rev t0
rev      = addTod (-1) 0 0
fwd      = addTod 1    0 0

-- | A 'TimeSeq' for calendar days.
days :: TimeSeq
days t = (map toTime \$ iterate rev t1, map toTime \$ tail (iterate fwd t1))
where t0       = localDay t
t1       = if toTime t0 < t then t0 else rev t0
rev      = pred
fwd      = succ
toTime d = LocalTime d midnight

-- | A 'TimeSeq' for calendar months.
months :: TimeSeq
months t = (map toTime \$ iterate rev t1, map toTime \$ tail (iterate fwd t1))
where t0       = let (y,m,_) = toGregorian \$ localDay t in fromGregorian y m 1
t1       = if toTime t0 < t then t0 else rev t0
toTime d = LocalTime d midnight

-- | A 'TimeSeq' for calendar years.
years :: TimeSeq
years t = (map toTime \$ iterate rev t1, map toTime \$ tail (iterate fwd t1))
where t0       = toGregorian (localDay t) ^. _1
t1       = if toTime t0 < t then t0 else rev t0
rev      = pred
fwd      = succ
toTime y = LocalTime (fromGregorian y 1 1) midnight

-- | A 'TimeSeq' for no sequence at all.
noTime :: TimeSeq
noTime _ = ([],[])

-- | Automatically choose a suitable time axis, based upon the time range
--   of data.  The values to be plotted against this axis can be created
--   with 'doubleFromLocalTime'.
autoTimeAxis :: AxisFn LocalTime
autoTimeAxis pts
| null pts              = timeAxis days    days    (ft "%d-%b-%y") UnderTicks
noTime  (ft "") UnderTicks []
| tdiff==0 && 100*dsec<1= timeAxis millis1   millis1  (ft "%S%Q") UnderTicks
noTime (ft "%S%Q") UnderTicks pts
| tdiff==0 && 10*dsec<1 = timeAxis millis10  millis10  (ft "%S%Q") UnderTicks
noTime (ft "%S%Q") UnderTicks pts
| tdiff==0 && dsec<1    = timeAxis millis10  millis100 (ft "%S%Q") UnderTicks
seconds (ft "%M:%S") BetweenTicks pts
| tdiff==0 && dsec<5    = timeAxis millis100 seconds (ft "%M:%S%Q") UnderTicks
seconds (ft "%M:%S") BetweenTicks pts
| tdiff==0 && dsec<32   = timeAxis seconds seconds (ft "%Ss") UnderTicks
minutes (ft "%d-%b-%y %H:%M") BetweenTicks pts
| tdiff==0 && dsec<120  = timeAxis seconds fiveSeconds (ft "%Ss") UnderTicks
minutes (ft "%d-%b-%y %H:%M") BetweenTicks pts
| tdiff==0 && dmin<7    = timeAxis fiveSeconds minutes (ft "%Mm") UnderTicks
hours   (ft "%d-%b-%y %H:00") BetweenTicks pts
| tdiff==0 && dmin<32   = timeAxis minutes minutes (ft "%Mm") UnderTicks
hours   (ft "%d-%b-%y %H:00") BetweenTicks pts
| tdiff==0 && dmin<90   = timeAxis minutes fiveMinutes (ft "%Mm") UnderTicks
hours   (ft "%d-%b-%y %H:00") BetweenTicks pts
| tdiff < 2 && dhour<4  = timeAxis fiveMinutes hours (ft "%H:%M") UnderTicks
days  (ft "%d-%b-%y") BetweenTicks pts
| tdiff < 2 && dhour<32 = timeAxis hours  hours  (ft "%H:%M") UnderTicks
days   (ft "%d-%b-%y") BetweenTicks pts
| tdiff < 4             = timeAxis hours  days   (ft "%d-%b-%y") BetweenTicks
noTime (ft "") BetweenTicks pts
| tdiff < 12            = timeAxis days   days   (ft "%d-%b") BetweenTicks
years  (ft "%Y") BetweenTicks pts
| tdiff < 45            = timeAxis days   days   (ft "%d") BetweenTicks
months (ft "%b-%y") BetweenTicks pts
| tdiff < 95            = timeAxis days   months (ft "%b-%y") BetweenTicks
noTime (ft "") BetweenTicks pts
| tdiff < 450           = timeAxis months months (ft "%b-%y") BetweenTicks
noTime (ft "") BetweenTicks pts
| tdiff < 735           = timeAxis months months (ft "%b") BetweenTicks
years  (ft "%Y") BetweenTicks pts
| tdiff < 1800          = timeAxis months years (ft "%Y") BetweenTicks
noTime (ft "") BetweenTicks pts
| otherwise             = timeAxis years  years (ft "%Y") BetweenTicks
noTime (ft "") BetweenTicks pts
where
tdiff = diffDays (localDay t1) (localDay t0)
dhour = if tdiff==0 then h1-h0 else 24*fromIntegral tdiff +h1-h0
dmin  = 60*dhour+(m1-m0)
dsec  = fromIntegral (60*dmin) + (s1-s0)
(TimeOfDay h0 m0 s0) = localTimeOfDay t0
(TimeOfDay h1 m1 s1) = localTimeOfDay t1
t1    = maximum pts
t0    = minimum pts
ft    = formatTime defaultTimeLocale

```