module Codec.Archive.Zip.Util where

import           Control.Applicative ((<$>))
import           Data.Bits ((.&.), shiftR, shiftL)
import           Data.ByteString (ByteString)
import qualified Data.ByteString as B (length)
import           Data.Time (UTCTime(..), TimeOfDay(..), fromGregorian, picosecondsToDiffTime, secondsToDiffTime, timeToTimeOfDay, toGregorian)
import           Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import           Data.Word (Word16, Word32)
import           System.Time (ClockTime(..))

import           Data.Conduit (Sink)
import qualified Data.Conduit.List as CL (fold)
import           Data.Digest.CRC32 (crc32Update)
import           Data.Serialize.Get (Get, getWord32le, isEmpty, lookAhead, runGet, skip)


ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM cond conseq altern = do
    c <- cond
    if c then conseq else altern


many :: (Monad m, Functor m) => m (Maybe a) -> m [a]
many p = do
  r <- p
  case r of
       Just x  -> (x:) <$> many p
       Nothing -> return []


------------------------------------------------------------------------------
-- Serialize utils.
maybeEmpty :: Get a -> Get (Maybe a)
maybeEmpty p = do
    e <- isEmpty
    if e
      then return Nothing
      else Just <$> p


runGet' :: Get a -> ByteString -> a
runGet' g b =
    either error id $ runGet g b


signature :: Word32 -> Get ()
signature sig = do
    s <- lookAhead getWord32le
    if s == sig
      then skip 4
      else fail "Wrong signature."


------------------------------------------------------------------------------
-- Time utils.
data MSDOSDateTime = MSDOSDateTime
    { msDOSDate :: Word16
    , msDOSTime :: Word16
    } deriving (Show)


msDOSDateTimeToUTCTime :: MSDOSDateTime -> UTCTime
msDOSDateTimeToUTCTime dosDateTime =
    UTCTime { utctDay = fromGregorian year month day
            , utctDayTime =
                secondsToDiffTime $ hours * 60 * 60 + minutes * 60 + seconds
            }
  where
    seconds = fromIntegral $ 2 * (dosTime .&. 0x1F)
    minutes = fromIntegral $ (shiftR dosTime 5) .&. 0x3F
    hours   = fromIntegral $ shiftR dosTime 11

    day     = fromIntegral $ dosDate .&. 0x1F
    month   = fromIntegral $ (shiftR dosDate 5) .&. 0xF
    year    = 1980 + fromIntegral (shiftR dosDate 9)

    dosDate = msDOSDate dosDateTime
    dosTime = msDOSTime dosDateTime


utcTimeToMSDOSDateTime :: UTCTime -> MSDOSDateTime
utcTimeToMSDOSDateTime utcTime =
    MSDOSDateTime { msDOSDate = dosDate
                  , msDOSTime = dosTime
                  }
  where
    dosTime = fromIntegral $ seconds + shiftL minutes 5 + shiftL hours 11
    dosDate = fromIntegral $ day + shiftL month 5 + shiftL year 9

    seconds = fromEnum (todSec tod) `div` 2
    minutes = todMin tod
    hours   = todHour tod
    tod     = timeToTimeOfDay $ utctDayTime utcTime

    year    = fromIntegral year' - 1980
    (year', month, day) = toGregorian $ utctDay utcTime


clockTimeToUTCTime :: ClockTime -> UTCTime
clockTimeToUTCTime (TOD seconds picoseconds) =
    let utcTime = posixSecondsToUTCTime $ fromIntegral seconds in
    utcTime { utctDayTime = utctDayTime utcTime
                          + picosecondsToDiffTime picoseconds
            }


------------------------------------------------------------------------------
-- Conduit utils.
crc32Sink :: Monad m => Sink ByteString m Word32
crc32Sink =
    CL.fold crc32Update 0


sizeSink :: Monad m => Sink ByteString m Int
sizeSink =
    CL.fold (\acc input -> B.length input + acc) 0