{- |
   Module      :  System.Win32.Utils
   Copyright   :  2009 Balazs Komuves, 2013 shelarcy
   License     :  BSD-style

   Maintainer  :  shelarcy@gmail.com
   Stability   :  Provisional
   Portability :  Non-portable (Win32 API)

   Utilities for calling Win32 API
-}
module System.Win32.Utils
  ( try, tryWithoutNull, trySized, try'
  -- * Maybe values
  , maybePtr, ptrToMaybe, maybeNum, numToMaybe
  , peekMaybe, withMaybe
  -- * Format picture translation
  , fromDateFormatPicture
  , fromTimeFormatPicture
  ) where

import Control.Monad               ( unless )
import Foreign.C.Types             ( CInt )
import Foreign.Marshal.Array       ( allocaArray, peekArray )
import Foreign.Marshal.Utils       ( with )
import Foreign.Ptr                 ( Ptr, nullPtr )
import Foreign.Storable            ( Storable(..) )
import Text.ParserCombinators.ReadP ( ReadP, (<++), between, char, count
                                    , readP_to_S, satisfy )


import System.Win32.String         ( LPTSTR, peekTString, peekTStringLen
                                   , withTStringBufferLen )
import System.Win32.Types          ( BOOL, UINT, eRROR_INSUFFICIENT_BUFFER
                                   , failIfZero, failWith, getLastError
                                   , maybeNum, maybePtr, numToMaybe
                                   , ptrToMaybe )
import qualified System.Win32.Types ( try )
import System.Win32.Word           ( DWORD, PDWORD )

-- | Support for API calls that are passed a fixed-size buffer and tell
-- you via the return value if the buffer was too small.  In that
-- case, we extend the buffer size and try again.
try :: String -> (LPTSTR -> UINT -> IO UINT) -> UINT -> IO String
try = System.Win32.Types.try
{-# INLINE try #-}

tryWithoutNull :: String -> (LPTSTR -> UINT -> IO UINT) -> UINT -> IO String
tryWithoutNull loc f n = do
   e <- allocaArray (fromIntegral n) $ \lptstr -> do
          r <- failIfZero loc $ f lptstr n
          if r > n then return (Left r) else do
            str <- peekTString lptstr
            return (Right str)
   case e of
        Left r'   -> tryWithoutNull loc f r'
        Right str -> return str

try' :: Storable a => String -> (Ptr a -> PDWORD -> IO BOOL) -> DWORD -> IO [a]
try' loc f n =
   with n $ \n' -> do
   e <- allocaArray (fromIntegral n) $ \lptstr -> do
          flg <- f lptstr n'
          unless flg $ do
            err_code <- getLastError
            unless (err_code == eRROR_INSUFFICIENT_BUFFER)
              $ failWith loc err_code
          r   <- peek n'
          if r > n then return (Left r) else do
            str <- peekArray (fromIntegral r) lptstr
            return (Right str)
   case e of
        Left r'   -> try' loc f r'
        Right str -> return str

-- | Support for API calls that return the required size, in characters
-- including a null character, of the buffer when passed a buffer size of zero.
trySized :: String -> (LPTSTR -> CInt -> IO CInt) -> IO String
trySized wh f = do
    c_len <- failIfZero wh $ f nullPtr 0
    let len = fromIntegral c_len
    withTStringBufferLen len $ \(buf', len') -> do
        let c_len' = fromIntegral len'
        c_len'' <- failIfZero wh $ f buf' c_len'
        let len'' = fromIntegral c_len''
        peekTStringLen (buf', len'' - 1) -- Drop final null character

-- | See also: 'Foreign.Marshal.Utils.maybePeek' function.
peekMaybe :: Storable a => Ptr a -> IO (Maybe a)
peekMaybe p =
  if p == nullPtr
    then return Nothing
    else Just `fmap` peek p

-- | See also: 'Foreign.Marshal.Utils.maybeWith' function.
withMaybe :: Storable a => Maybe a -> (Ptr a -> IO b) -> IO b
withMaybe Nothing  action = action nullPtr
withMaybe (Just x) action = with x action

-- | Type representing components of a Windows API day, month, year and era
-- format picture.
data DateFormatPicture
  = Day
  | Day0 -- Padded with zeros
  | DayShort
  | DayLong
  | Month
  | Month0 -- Padded with zeros
  | MonthShort
  | MonthLong
  | YearVeryShort -- Year represented only by the last digit
  | YearShort
  | Year
  | Era
  | DateOther String
  deriving (Eq, Show)

fromDFP :: DateFormatPicture -> String
fromDFP Day = "%-e" -- No padding
fromDFP Day0 = "%d" -- Padded with zeros
fromDFP DayShort = "%a" -- eg Tue
fromDFP DayLong = "%A" -- eg Tuesday
fromDFP Month = "%-m" -- No padding
fromDFP Month0 = "%m" -- Padded with zeros
fromDFP MonthShort = "%b" -- eg Jan
fromDFP MonthLong = "%B" -- eg January
fromDFP YearVeryShort = "%-y" -- No direct equivalent of a one digit year, so
                              -- do not distinguish from a short year without
                              -- padding
fromDFP YearShort = "%y"
fromDFP Year = "%Y"
fromDFP Era = "" -- No equivalent
fromDFP (DateOther cs) = escape cs

escape :: String -> String
escape [] = []
escape (c:cs) = escape' c ++ escape cs
 where
  escape' '%' = "%%"
  escape' '\t' = "%t"
  escape' '\n' = "%n"
  escape' c' = [c']

d :: ReadP Char
d = char 'd'

day :: ReadP DateFormatPicture
day = do
  _ <- d
  return Day

day0 :: ReadP DateFormatPicture
day0 = do
  _ <- count 2 d
  return Day0

dayShort :: ReadP DateFormatPicture
dayShort = do
  _ <- count 3 d
  return DayShort

dayLong :: ReadP DateFormatPicture
dayLong = do
  _ <- count 4 d
  return DayLong

days :: ReadP DateFormatPicture
days = dayLong <++ dayShort <++ day0 <++ day

bigM :: ReadP Char
bigM = char 'M'

month :: ReadP DateFormatPicture
month = do
  _ <- bigM
  return Month

month0 :: ReadP DateFormatPicture
month0 = do
  _ <- count 2 bigM
  return Month0

monthShort :: ReadP DateFormatPicture
monthShort = do
  _ <- count 3 bigM
  return MonthShort

monthLong :: ReadP DateFormatPicture
monthLong = do
  _ <- count 4 bigM
  return MonthLong

months :: ReadP DateFormatPicture
months = monthLong <++ monthShort <++ month0 <++ month

y :: ReadP Char
y = char 'y'

yearVeryShort :: ReadP DateFormatPicture
yearVeryShort = do
  _ <- y
  return YearVeryShort

yearShort :: ReadP DateFormatPicture
yearShort = do
  _ <- count 2 y
  return YearShort

year :: ReadP DateFormatPicture
year = do
  _ <- count 5 y <++ count 4 y
  return Year

years :: ReadP DateFormatPicture
years = year <++ yearShort <++ yearVeryShort

g :: ReadP Char
g = char 'g'

era :: ReadP DateFormatPicture
era = do
  _ <- count 2 g <++ count 1 g
  return Era

quote :: ReadP Char
quote = char '\''

notQuote :: ReadP Char
notQuote = satisfy (/= '\'')

escQuote :: ReadP Char
escQuote = do
  _ <- count 2 quote
  return '\''

quotedChars :: ReadP String
quotedChars = between quote quote $ greedy (escQuote <++ notQuote)

-- | Although not documented at
-- https://docs.microsoft.com/en-us/windows/win32/intl/day--month--year--and-era-format-pictures
-- the format pictures used by Windows do not require all such characters to be
-- enclosed in single quotation marks.
nonDateSpecial :: ReadP Char
nonDateSpecial = satisfy (\c -> c `notElem` ['d', 'M', 'y', 'g', '\''])

nonDateSpecials :: ReadP String
nonDateSpecials = greedy1 nonDateSpecial

dateOther :: ReadP DateFormatPicture
dateOther = do
  chars <- greedy1 (nonDateSpecials <++ quotedChars)
  return $ DateOther $ concat chars

datePicture :: ReadP [DateFormatPicture]
datePicture = greedy (days <++ months <++ years <++ era <++ dateOther)

-- | Type representing components of a Windows API hours, minute, and second
-- format picture.
data TimeFormatPicture
  = Hours12
  | Hours012 -- Padded with zeros
  | Hours24
  | Hours024 -- Padded with zeros
  | Minutes
  | Minutes0 -- Padded with zeros
  | Seconds
  | Seconds0 -- Padded with zeros
  | TimeMarkerShort -- One-character time marker string, eg "A" and "P"
  | TimeMarker -- Multi-character time marker string, eg "AM" and "PM"
  | TimeOther String
  deriving (Eq, Show)

fromTFP :: TimeFormatPicture -> String
fromTFP Hours12 = "%-l" -- No padding
fromTFP Hours012 = "%I" -- Padded with zeros
fromTFP Hours24 = "%-k" -- No padding
fromTFP Hours024 = "%H" -- Padded with zeros
fromTFP Minutes = "%-M" -- No padding
fromTFP Minutes0 = "%M" -- Padded with zeros
fromTFP Seconds = "%-S" -- No padding
fromTFP Seconds0 = "%S" -- Padded with zeros
fromTFP TimeMarkerShort = "%p" -- No direct equivalent, so do not distinguish
                               -- from TimeMarker
fromTFP TimeMarker = "%p"
fromTFP (TimeOther cs) = escape cs

h :: ReadP Char
h = char 'h'

hours12 :: ReadP TimeFormatPicture
hours12 = do
  _ <- h
  return Hours12

hours012 :: ReadP TimeFormatPicture
hours012 = do
  _ <- count 2 h
  return Hours012

bigH :: ReadP Char
bigH = char 'H'

hours24 :: ReadP TimeFormatPicture
hours24 = do
  _ <- bigH
  return Hours24

hours024 :: ReadP TimeFormatPicture
hours024 = do
  _ <- count 2 bigH
  return Hours024

hours :: ReadP TimeFormatPicture
hours = hours012 <++ hours12 <++ hours024 <++ hours24

m :: ReadP Char
m = char 'm'

minute :: ReadP TimeFormatPicture
minute = do
  _ <- m
  return Minutes

minute0 :: ReadP TimeFormatPicture
minute0 = do
  _ <- count 2 m
  return Minutes0

minutes :: ReadP TimeFormatPicture
minutes = minute0 <++ minute

s :: ReadP Char
s = char 's'

second :: ReadP TimeFormatPicture
second = do
  _ <- s
  return Seconds

second0 :: ReadP TimeFormatPicture
second0 = do
  _ <- count 2 s
  return Seconds0

seconds :: ReadP TimeFormatPicture
seconds = second0 <++ second

t :: ReadP Char
t = char 't'

timeMarkerShort :: ReadP TimeFormatPicture
timeMarkerShort = do
  _ <- t
  return TimeMarkerShort

timeMarker :: ReadP TimeFormatPicture
timeMarker = do
  _ <- count 2 t
  return TimeMarker

timeMarkers :: ReadP TimeFormatPicture
timeMarkers = timeMarker <++ timeMarkerShort

-- | Although not documented at
-- https://docs.microsoft.com/en-us/windows/win32/intl/hour--minute--and-second-format-pictures
-- the format pictures used by Windows do not require all such characters to be
-- enclosed in single quotation marks.
nonTimeSpecial :: ReadP Char
nonTimeSpecial = satisfy (\c -> c `notElem` ['h', 'H', 'm', 's', 't', '\''])

nonTimeSpecials :: ReadP String
nonTimeSpecials = greedy1 nonTimeSpecial

timeOther :: ReadP TimeFormatPicture
timeOther = do
  chars <- greedy1 (nonTimeSpecials <++ quotedChars)
  return $ TimeOther $ concat chars

timePicture :: ReadP [TimeFormatPicture]
timePicture = greedy (hours <++ minutes <++ seconds <++ timeMarkers <++
                     timeOther)

greedy :: ReadP a -> ReadP [a]
greedy p = greedy1 p <++ return []

greedy1 :: ReadP a -> ReadP [a]
greedy1 p = do
  first <- p
  rest <- greedy p
  return (first : rest)

parseMaybe :: ReadP a -> String -> Maybe a
parseMaybe parser input =
  case readP_to_S parser input of
    [] -> Nothing
    ((result, _):_) -> Just result

-- | Translate from a Windows API day, month, year, and era format picture to
-- the closest corresponding format string used by
-- 'Data.Time.Format.formatTime'.
fromDateFormatPicture :: String -> Maybe String
fromDateFormatPicture dfp =
  fmap (concatMap fromDFP) $ parseMaybe datePicture dfp

-- | Translate from a Windows API hours, minute, and second format picture to
-- the closest corresponding format string used by
-- 'Data.Time.Format.formatTime'.
fromTimeFormatPicture :: String -> Maybe String
fromTimeFormatPicture tfp =
  fmap (concatMap fromTFP) $ parseMaybe timePicture tfp
