{-# LANGUAGE DeriveDataTypeable #-}
{-| Read locales on unix systems and parse them into their corresponding 'TimeLocale' representation.
-}
module System.Locale.Read
  ( getLocale
  , getCurrentLocale
  , parseLocale
  , TimeLocale(..)
  , LocaleParseException(..)
  ) where

import           Control.Applicative
import           Control.Exception
import           Data.Attoparsec.Text
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Encoding.Error as Text
import qualified Data.ByteString as BS
import           Data.Time.Format (TimeLocale(..))
import           Data.Typeable
import           System.Process
import           System.Environment
import           System.Exit (ExitCode(..))
import           Data.List (isPrefixOf)

-- | Thrown when the locale cannot be parsed
data LocaleParseException
  = LocaleParseException String
  | LocaleUtf8Exception Text.UnicodeException
  | LocaleExitException ExitCode
  deriving (Show,Eq,Typeable)

instance Exception LocaleParseException

-- | 'Parser' for locales returned by the unix utility 'locale'
parseLocale :: Parser TimeLocale
parseLocale = do
  abDay <- parseSemicolonSeparatedLine
  day <- parseSemicolonSeparatedLine
  abMon <- parseSemicolonSeparatedLine
  mon <- parseSemicolonSeparatedLine
  [am, pm] <- parseSemicolonSeparatedLine
  dateTimeFmt' <- manyTill anyChar newline
  dateFmt' <- manyTill anyChar newline
  timeFmt' <- manyTill anyChar newline
  time12Fmt' <- manyTill anyChar newline
  pure
    (TimeLocale
       (zip day abDay)
       (zip mon abMon)
       (am, pm)
       dateTimeFmt'
       dateFmt'
       timeFmt'
       time12Fmt'
       [])

-- | Read a locale with 'LC_TIME' set according to the first argument.
--
-- Throws a 'LocaleParseException' if the output of calling 'locale'
-- cannot be parsed.
--
-- The 'knownTimeZones' field will always be empty.
--
-- > getLocale (Just "en_US.UTF-8")
getLocale :: Maybe String -> IO TimeLocale
getLocale localeName = do
  process <- getLocaleProcess localeName
  output <- withCreateProcess process $
    \_stdin (Just stdout) _stderr ph -> do
      bsOutput <- BS.hGetContents stdout
      exitCode <- waitForProcess ph
      case exitCode of
        ExitSuccess       -> return ()
        e@(ExitFailure _) -> throwIO $ LocaleExitException e
      either (throwIO . LocaleUtf8Exception) return $ Text.decodeUtf8' bsOutput
  case parseOnly (parseLocale <* endOfInput) output of
    Left err -> throwIO (LocaleParseException err)
    Right locale -> pure locale

-- | Get the current locale of the process.
--
-- Throws a 'LocaleParseException' if the output of calling 'locale'
-- cannot be parsed.
--
-- The 'knownTimeZones' field will always be empty.
getCurrentLocale :: IO TimeLocale
getCurrentLocale = getLocale Nothing

getLocaleProcess :: Maybe String -> IO CreateProcess
getLocaleProcess localeName = do
  oldEnv <- getEnvironment
  return $ (proc "locale"
             [ "abday"
             , "day"
             , "abmon"
             , "mon"
             , "am_pm"
             , "d_t_fmt"
             , "d_fmt"
             , "t_fmt"
             , "t_fmt_ampm"
             ]) { std_out = CreatePipe, env = toLangEnv oldEnv <$> localeName}

newline :: Parser Char
newline = char '\n'

parseSemicolonSeparatedLine :: Parser [String]
parseSemicolonSeparatedLine =
  sepBy (many (satisfy (not . finalChar))) (char ';') <* newline
  where
    finalChar c = c == ';' || c == '\n'

toLangEnv :: [(String, String)] -> String -> [(String,String)]
toLangEnv oldEnv s = ("LC_ALL", s) : [ e | e@(k, _) <- oldEnv, not $ "LC_" `isPrefixOf` k ]