---------------------------------------------------------------
-- Copyright (c) 2013, Enzo Haussecker. All rights reserved. --
---------------------------------------------------------------

{-# LANGUAGE DeriveDataTypeable     #-}
{-# LANGUAGE DeriveGeneric          #-}
{-# LANGUAGE LambdaCase             #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE NamedFieldPuns         #-}
{-# LANGUAGE RecordWildCards        #-}
{-# OPTIONS -Wall                   #-}
{-# OPTIONS -fno-warn-type-defaults #-}

module Data.Time.Exts.Zone where

import Control.Arrow (first)
import Data.Aeson (FromJSON, ToJSON)
import Data.Convertible (Convertible(..))
import Data.Ord (comparing)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import System.Random (Random(..))

data City =
     Aden         -- ^ Yemeni Republic
   | Amman        -- ^ Hashemite Kingdom of Jordan
   | Anchorage    -- ^ United States of America
   | Auckland     -- ^ New Zealand
   | Baghdad      -- ^ Republic of Iraq
   | Berlin       -- ^ Federal Republic of Germany
   | Brussels     -- ^ Kingdom of Belgium
   | Bujumbura    -- ^ Republic of Burundi
   | Cairo        -- ^ Arab Republic of Egypt
   | Chicago      -- ^ United States of America
   | Damascus     -- ^ Syrian Arab Republic
   | Denver       -- ^ United States of America
   | Doha         -- ^ State of Qatar
   | Gaborone     -- ^ Republic of Botswana
   | Hong_Kong    -- ^ People's Republic of China
   | Honolulu     -- ^ United States of America
   | Johannesburg -- ^ Republic of South Africa
   | Kabul        -- ^ Islamic Republic of Afghanistan
   | Karachi      -- ^ Islamic Republic of Pakistan
   | Kinshasa     -- ^ Democratic Republic of the Congo
   | Kolkata      -- ^ Republic of India
   | Kuwait_City  -- ^ State of Kuwait
   | London       -- ^ United Kingdom of Great Britain and Northern Ireland
   | Los_Angeles  -- ^ United States of America
   | Luanda       -- ^ Republic of Angola
   | Manama       -- ^ Kingdom of Bahrain
   | Minsk        -- ^ Republic of Belarus
   | Mogadishu    -- ^ Federal Republic of Somalia
   | Moscow       -- ^ Russian Federation
   | New_York     -- ^ United States of America
   | Oslo         -- ^ Kingdom of Norway
   | Ouagadougou  -- ^ Burkina Faso
   | Paris        -- ^ French Republic
   | Pyongyang    -- ^ Democratic People's Republic of Korea
   | Riyadh       -- ^ Kingdom of Saudi Arabia
   | Sao_Paulo    -- ^ Federative Republic of Brazil
   | Sarajevo     -- ^ Bosnia and Herzegovina
   | Seoul        -- ^ Republic of Korea
   | Shanghai     -- ^ People's Republic of China
   | Singapore    -- ^ Republic of Singapore
   | Sofia        -- ^ Republic of Bulgaria
   | Stockholm    -- ^ Kingdom of Sweden
   | Tehran       -- ^ Islamic Republic of Iran
   | Tel_Aviv     -- ^ State of Israel
   | Tirana       -- ^ Republic of Albania
   | Tokyo        -- ^ Japan
   | Toronto      -- ^ Canada
   | Universal    -- ^ Tnternational Territory
   | Vienna       -- ^ Republic of Austria
   | Zurich       -- ^ Swiss Confederation
   deriving (Eq,Enum,Generic,Ord,Show,Typeable)

data TimeZone =
     Afghanistan_Time
   | Alaska_Daylight_Time
   | Alaska_Hawaii_Daylight_Time
   | Alaska_Hawaii_Standard_Time
   | Alaska_Standard_Time
   | Arabia_Daylight_Time
   | Arabia_Standard_Time
   | Brasilia_Summer_Time
   | Brasilia_Time
   | British_Summer_Time
   | Central_Africa_Time
   | Central_Daylight_Time
   | Central_European_Summer_Time
   | Central_European_Time
   | Central_Standard_Time
   | China_Daylight_Time
   | China_Standard_Time
   | Coordinated_Universal_Time
   | East_Africa_Time
   | Eastern_Daylight_Time
   | Eastern_European_Summer_Time
   | Eastern_European_Time
   | Eastern_Standard_Time
   | Further_Eastern_European_Time
   | Greenwich_Mean_Time
   | Gulf_Standard_Time
   | Hawaii_Aleutian_Standard_Time
   | Hong_Kong_Summer_Time
   | Hong_Kong_Time
   | India_Standard_Time
   | Iran_Daylight_Time
   | Iran_Standard_Time
   | Israel_Daylight_Time
   | Israel_Standard_Time
   | Japan_Standard_Time
   | Karachi_Time
   | Korea_Daylight_Time
   | Korea_Standard_Time
   | Moscow_Daylight_Time
   | Moscow_Standard_Time
   | Mountain_Daylight_Time
   | Mountain_Standard_Time
   | New_Zealand_Daylight_Time
   | New_Zealand_Standard_Time
   | Pacific_Daylight_Time
   | Pacific_Standard_Time
   | Pakistan_Standard_Time
   | Pakistan_Summer_Time
   | Singapore_Time
   | South_Africa_Standard_Time
   | West_Africa_Time
   | Yukon_Standard_Time
   deriving (Eq,Enum,Generic,Show,Typeable)

data TimeZoneAbbr = TimeZoneAbbr {
     abbr_city :: City   -- ^ Reference location
   , abbr_str  :: String -- ^ Time zone abbreviation string
   } deriving (Eq,Generic,Typeable)

instance Bounded City where
   minBound = Aden
   maxBound = Zurich

instance Bounded TimeZone where
   minBound = Afghanistan_Time
   maxBound = Yukon_Standard_Time

instance Convertible TimeZone TimeZoneAbbr where
   safeConvert = Right . \ case
     Afghanistan_Time              -> TimeZoneAbbr Kabul        "AFT"
     Alaska_Daylight_Time          -> TimeZoneAbbr Anchorage    "AKDT"
     Alaska_Hawaii_Daylight_Time   -> TimeZoneAbbr Anchorage    "AHDT"
     Alaska_Hawaii_Standard_Time   -> TimeZoneAbbr Anchorage    "AHST"
     Alaska_Standard_Time          -> TimeZoneAbbr Anchorage    "AKST"
     Arabia_Daylight_Time          -> TimeZoneAbbr Baghdad      "ADT"
     Arabia_Standard_Time          -> TimeZoneAbbr Riyadh       "AST"
     Brasilia_Summer_Time          -> TimeZoneAbbr Sao_Paulo    "BRST"
     Brasilia_Time                 -> TimeZoneAbbr Sao_Paulo    "BRT"
     British_Summer_Time           -> TimeZoneAbbr London       "BST"
     Central_Africa_Time           -> TimeZoneAbbr Gaborone     "CAT"
     Central_Daylight_Time         -> TimeZoneAbbr Chicago      "CDT"
     Central_European_Summer_Time  -> TimeZoneAbbr Paris        "CEST"
     Central_European_Time         -> TimeZoneAbbr Paris        "CET"
     Central_Standard_Time         -> TimeZoneAbbr Chicago      "CST"
     China_Daylight_Time           -> TimeZoneAbbr Shanghai     "CDT"
     China_Standard_Time           -> TimeZoneAbbr Shanghai     "CST"
     Coordinated_Universal_Time    -> TimeZoneAbbr Universal    "UTC"
     East_Africa_Time              -> TimeZoneAbbr Mogadishu    "EAT"
     Eastern_Daylight_Time         -> TimeZoneAbbr New_York     "EDT"
     Eastern_European_Summer_Time  -> TimeZoneAbbr Sofia        "EEST"
     Eastern_European_Time         -> TimeZoneAbbr Sofia        "EET"
     Eastern_Standard_Time         -> TimeZoneAbbr New_York     "EST"
     Further_Eastern_European_Time -> TimeZoneAbbr Minsk        "FET"
     Greenwich_Mean_Time           -> TimeZoneAbbr London       "GMT"
     Gulf_Standard_Time            -> TimeZoneAbbr Manama       "GST"
     Hawaii_Aleutian_Standard_Time -> TimeZoneAbbr Honolulu     "HST"
     Hong_Kong_Time                -> TimeZoneAbbr Hong_Kong    "HKT"
     Hong_Kong_Summer_Time         -> TimeZoneAbbr Hong_Kong    "HKST"
     India_Standard_Time           -> TimeZoneAbbr Kolkata      "IST"
     Iran_Daylight_Time            -> TimeZoneAbbr Tehran       "IRDT"
     Iran_Standard_Time            -> TimeZoneAbbr Tehran       "IRST"
     Israel_Daylight_Time          -> TimeZoneAbbr Tel_Aviv     "IDT"
     Israel_Standard_Time          -> TimeZoneAbbr Tel_Aviv     "IST"
     Japan_Standard_Time           -> TimeZoneAbbr Tokyo        "JST"
     Karachi_Time                  -> TimeZoneAbbr Karachi      "KART"
     Korea_Daylight_Time           -> TimeZoneAbbr Seoul        "KDT"
     Korea_Standard_Time           -> TimeZoneAbbr Seoul        "KST"
     Moscow_Daylight_Time          -> TimeZoneAbbr Moscow       "MSD"
     Moscow_Standard_Time          -> TimeZoneAbbr Moscow       "MSK"
     Mountain_Daylight_Time        -> TimeZoneAbbr Denver       "MDT"
     Mountain_Standard_Time        -> TimeZoneAbbr Denver       "MST"
     New_Zealand_Daylight_Time     -> TimeZoneAbbr Auckland     "NZDT"
     New_Zealand_Standard_Time     -> TimeZoneAbbr Auckland     "NZST"
     Pacific_Daylight_Time         -> TimeZoneAbbr Los_Angeles  "PDT"
     Pacific_Standard_Time         -> TimeZoneAbbr Los_Angeles  "PST"
     Pakistan_Standard_Time        -> TimeZoneAbbr Karachi      "PKT"
     Pakistan_Summer_Time          -> TimeZoneAbbr Karachi      "PKST"
     Singapore_Time                -> TimeZoneAbbr Singapore    "SGT"
     South_Africa_Standard_Time    -> TimeZoneAbbr Johannesburg "SAST"
     West_Africa_Time              -> TimeZoneAbbr Luanda       "WAT"
     Yukon_Standard_Time           -> TimeZoneAbbr Anchorage    "YST"

instance Convertible TimeZoneAbbr TimeZone where
   safeConvert = Right . \ TimeZoneAbbr{..} ->
     case abbr_str of
       "AFT"  -> Afghanistan_Time
       "AHDT" -> Alaska_Hawaii_Daylight_Time
       "AHST" -> Alaska_Hawaii_Standard_Time
       "AKDT" -> Alaska_Daylight_Time
       "AKST" -> Alaska_Standard_Time
       "ADT"  -> Arabia_Daylight_Time
       "AST"  -> Arabia_Standard_Time
       "BRST" -> Brasilia_Summer_Time
       "BRT"  -> Brasilia_Time
       "BST"  -> British_Summer_Time
       "CAT"  -> Central_Africa_Time
       "CDT"  -> case abbr_city of
                      Chicago   -> Central_Daylight_Time
                      Shanghai  -> China_Daylight_Time
                      _         -> missing abbr_city
       "CEST" -> Central_European_Summer_Time
       "CET"  -> Central_European_Time
       "CST"  -> case abbr_city of
                      Chicago   -> Central_Standard_Time
                      Shanghai  -> China_Standard_Time
                      _         -> missing abbr_city
       "EAT"  -> East_Africa_Time
       "EDT"  -> Eastern_Daylight_Time
       "EEST" -> Eastern_European_Summer_Time
       "EET"  -> Eastern_European_Time
       "EST"  -> Eastern_Standard_Time
       "FET"  -> Further_Eastern_European_Time
       "GMT"  -> Greenwich_Mean_Time
       "GST"  -> Gulf_Standard_Time
       "HST"  -> Hawaii_Aleutian_Standard_Time
       "HKST" -> Hong_Kong_Summer_Time
       "HKT"  -> Hong_Kong_Time
       "IDT"  -> Israel_Daylight_Time
       "IRDT" -> Iran_Daylight_Time
       "IRST" -> Iran_Standard_Time
       "IST"  -> case abbr_city of
                      Kolkata   -> India_Standard_Time
                      Tel_Aviv  -> Israel_Standard_Time
                      _         -> missing abbr_city
       "JST"  -> Japan_Standard_Time
       "KART" -> Karachi_Time
       "KDT"  -> Korea_Daylight_Time
       "KST"  -> Korea_Standard_Time
       "MDT"  -> Mountain_Daylight_Time
       "MSD"  -> Moscow_Daylight_Time
       "MSK"  -> Moscow_Standard_Time
       "MST"  -> Mountain_Standard_Time
       "NZDT" -> New_Zealand_Daylight_Time
       "NZST" -> New_Zealand_Standard_Time
       "PDT"  -> Pacific_Daylight_Time
       "PKST" -> Pakistan_Summer_Time
       "PKT"  -> Pakistan_Standard_Time
       "PST"  -> Pacific_Standard_Time
       "SAST" -> South_Africa_Standard_Time
       "SGT"  -> Singapore_Time
       "UTC"  -> Coordinated_Universal_Time
       "WAT"  -> West_Africa_Time
       "YST"  -> Yukon_Standard_Time
       _      ->            error $ "safeConvert: missing time zone abbreviation `" ++ abbr_str  ++ "'"
       where missing city = error $ "safeConvert: missing reference location `"     ++ show city ++ "'"

instance FromJSON City
instance FromJSON TimeZone
instance FromJSON TimeZoneAbbr

instance Ord TimeZone where
   compare = comparing offset

instance Random City where
   random        = first toEnum . randomR (0, 49)
   randomR (a,b) = first toEnum . randomR (fromEnum a, fromEnum b)

instance Random TimeZone where
   random        = first toEnum . randomR (0, 51)
   randomR (a,b) = first toEnum . randomR (fromEnum a, fromEnum b)

instance Show TimeZoneAbbr where
   show TimeZoneAbbr{abbr_str} = abbr_str

instance ToJSON City
instance ToJSON TimeZone
instance ToJSON TimeZoneAbbr

-- | A list of cities in alphabetical order.
cities :: [City]
cities =
  [ Aden
  , Amman
  , Anchorage
  , Auckland
  , Baghdad
  , Berlin
  , Brussels
  , Bujumbura
  , Cairo
  , Chicago
  , Damascus
  , Denver
  , Doha
  , Gaborone
  , Hong_Kong
  , Honolulu
  , Johannesburg
  , Kabul
  , Karachi
  , Kinshasa
  , Kolkata
  , Kuwait_City
  , London
  , Los_Angeles
  , Luanda
  , Manama
  , Minsk
  , Mogadishu
  , Moscow
  , New_York
  , Oslo
  , Ouagadougou
  , Paris
  , Pyongyang
  , Riyadh
  , Sao_Paulo
  , Sarajevo
  , Seoul
  , Shanghai
  , Singapore
  , Sofia
  , Stockholm
  , Tehran
  , Tel_Aviv
  , Tirana
  , Tokyo
  , Toronto
  , Universal
  , Vienna
  , Zurich
  ]

-- | Returns the Olson file associated with the given city.
getOlsonFile :: City -> FilePath
getOlsonFile = \ case
   Aden         -> "/usr/share/zoneinfo/Asia/Aden"
   Amman        -> "/usr/share/zoneinfo/Asia/Amman"
   Anchorage    -> "/usr/share/zoneinfo/America/Anchorage"
   Auckland     -> "/usr/share/zoneinfo/Pacific/Auckland"
   Baghdad      -> "/usr/share/zoneinfo/Asia/Baghdad"
   Berlin       -> "/usr/share/zoneinfo/Europe/Berlin"
   Brussels     -> "/usr/share/zoneinfo/Europe/Brussels"
   Bujumbura    -> "/usr/share/zoneinfo/Africa/Bujumbura"
   Cairo        -> "/usr/share/zoneinfo/Africa/Cairo"
   Chicago      -> "/usr/share/zoneinfo/America/Chicago"
   Damascus     -> "/usr/share/zoneinfo/Asia/Damascus"
   Denver       -> "/usr/share/zoneinfo/America/Denver"
   Doha         -> "/usr/share/zoneinfo/Asia/Qatar"
   Gaborone     -> "/usr/share/zoneinfo/Africa/Gaborone"
   Hong_Kong    -> "/usr/share/zoneinfo/Asia/Hong_Kong"
   Honolulu     -> "/usr/share/zoneinfo/Pacific/Honolulu"
   Johannesburg -> "/usr/share/zoneinfo/Africa/Johannesburg"
   Kabul        -> "/usr/share/zoneinfo/Asia/Kabul"
   Karachi      -> "/usr/share/zoneinfo/Asia/Karachi"
   Kinshasa     -> "/usr/share/zoneinfo/Africa/Kinshasa"
   Kuwait_City  -> "/usr/share/zoneinfo/Asia/Kuwait"
   Kolkata      -> "/usr/share/zoneinfo/Asia/Kolkata"
   London       -> "/usr/share/zoneinfo/Europe/London"
   Los_Angeles  -> "/usr/share/zoneinfo/America/Los_Angeles"
   Luanda       -> "/usr/share/zoneinfo/Africa/Luanda"
   Manama       -> "/usr/share/zoneinfo/Asia/Bahrain"
   Minsk        -> "/usr/share/zoneinfo/Europe/Minsk"
   Mogadishu    -> "/usr/share/zoneinfo/Africa/Mogadishu"
   Moscow       -> "/usr/share/zoneinfo/Europe/Moscow"
   New_York     -> "/usr/share/zoneinfo/America/New_York"
   Oslo         -> "/usr/share/zoneinfo/Europe/Oslo"
   Ouagadougou  -> "/usr/share/zoneinfo/Africa/Ouagadougou"
   Paris        -> "/usr/share/zoneinfo/Europe/Paris"
   Pyongyang    -> "/usr/share/zoneinfo/Asia/Pyongyang"
   Riyadh       -> "/usr/share/zoneinfo/Asia/Riyadh"
   Sao_Paulo    -> "/usr/share/zoneinfo/America/Sao_Paulo"
   Sarajevo     -> "/usr/share/zoneinfo/Europe/Sarajevo"
   Seoul        -> "/usr/share/zoneinfo/Asia/Seoul"
   Shanghai     -> "/usr/share/zoneinfo/Asia/Shanghai"
   Singapore    -> "/usr/share/zoneinfo/Asia/Singapore"
   Sofia        -> "/usr/share/zoneinfo/Europe/Sofia"
   Stockholm    -> "/usr/share/zoneinfo/Europe/Stockholm"
   Tehran       -> "/usr/share/zoneinfo/Asia/Tehran"
   Tel_Aviv     -> "/usr/share/zoneinfo/Asia/Tel_Aviv"
   Tirana       -> "/usr/share/zoneinfo/Europe/Tirane"
   Tokyo        -> "/usr/share/zoneinfo/Asia/Tokyo"
   Toronto      -> "/usr/share/zoneinfo/America/Toronto"
   Universal    -> "/usr/share/zoneinfo/Universal"
   Vienna       -> "/usr/share/zoneinfo/Europe/Vienna"
   Zurich       -> "/usr/share/zoneinfo/Europe/Zurich"

-- | Returns the offset (in minutes) for the given time zone.
offset :: Num a => TimeZone -> a
offset = \ case
   Afghanistan_Time              ->  270
   Alaska_Daylight_Time          -> -480
   Alaska_Hawaii_Daylight_Time   -> -540
   Alaska_Hawaii_Standard_Time   -> -600
   Alaska_Standard_Time          -> -540
   Arabia_Daylight_Time          ->  240
   Arabia_Standard_Time          ->  180
   Brasilia_Summer_Time          -> -120
   Brasilia_Time                 -> -180
   British_Summer_Time           ->  060
   Central_Africa_Time           ->  120
   Central_Daylight_Time         -> -300
   Central_European_Summer_Time  ->  120
   Central_European_Time         ->  060
   Central_Standard_Time         -> -360
   China_Daylight_Time           ->  540
   China_Standard_Time           ->  480
   Coordinated_Universal_Time    ->  000
   East_Africa_Time              ->  180
   Eastern_Daylight_Time         -> -240
   Eastern_European_Summer_Time  ->  180
   Eastern_European_Time         ->  120
   Eastern_Standard_Time         -> -300
   Further_Eastern_European_Time ->  180
   Greenwich_Mean_Time           ->  000
   Gulf_Standard_Time            ->  240
   Hawaii_Aleutian_Standard_Time -> -600
   Hong_Kong_Summer_Time         ->  540
   Hong_Kong_Time                ->  480
   India_Standard_Time           ->  330
   Iran_Daylight_Time            ->  270
   Iran_Standard_Time            ->  210
   Israel_Daylight_Time          ->  180
   Israel_Standard_Time          ->  120
   Japan_Standard_Time           ->  540
   Karachi_Time                  ->  300
   Korea_Daylight_Time           ->  600
   Korea_Standard_Time           ->  540
   Moscow_Daylight_Time          ->  240
   Moscow_Standard_Time          ->  240
   Mountain_Daylight_Time        -> -360
   Mountain_Standard_Time        -> -420
   New_Zealand_Daylight_Time     ->  780
   New_Zealand_Standard_Time     ->  720
   Pacific_Daylight_Time         -> -420
   Pacific_Standard_Time         -> -480
   Pakistan_Standard_Time        ->  300
   Pakistan_Summer_Time          ->  360
   Singapore_Time                ->  480
   South_Africa_Standard_Time    ->  120
   West_Africa_Time              ->  060
   Yukon_Standard_Time           -> -540