{-# Language TemplateHaskell, OverloadedStrings, BangPatterns
    , ScopedTypeVariables, TypeApplications #-}
module Client.State.DCC
  (
    DCCState(..)
  , dsOffers
  , dsTransfers
  , emptyDCCState
  
  , DCCOffer(..)
  , dccNetwork
  , dccFromInfo
  , dccFromIP
  , dccPort
  , dccFileName
  , dccSize
  , dccOffset
  , dccStatus
  
  , DCCTransfer(..)
  , dtThread
  , dtProgress
  , ConnectionStatus(..)
  
  , DCCUpdate(..)
  
  , supervisedDownload
  
  , parseSEND
  , parseACCEPT
  
  , resumeMsg
  , acceptUpdate
  
  , getFileOffset
  , insertAsNewMax
  , ctcpToTuple
  , statusAtKey
  , reportStopWithStatus
  , isSend
  ) where
import           Control.Applicative (Alternative(..))
import           Control.Concurrent.Async
import           Control.Concurrent.STM
import           Control.Exception (bracket, IOException)
import qualified Control.Exception as E
import           Control.Lens hiding (from)
import           Control.Monad (unless, when)
import           Data.Attoparsec.Text
import qualified Data.ByteString as B
import           Data.ByteString.Builder (word32BE, toLazyByteString)
import           Data.ByteString.Lazy (toStrict)
import           Data.IntMap (Key, IntMap)
import qualified Data.IntMap as I hiding (size, empty)
import           Data.List (find)
import           Data.Text (Text)
import qualified Data.Text as Text
import           Data.Word (Word32, Word64)
import           Hookup
import           Irc.Identifier (Identifier, idText)
import           Irc.Message (IrcMsg(..))
import           Irc.UserInfo (UserInfo(..), uiNick)
import           Network.Socket ( HostName, PortNumber, Family(..)
                                , hostAddressToTuple )
import           System.FilePath ((</>), takeFileName)
import           System.IO (withFile, IOMode(..), openFile, hClose, hFileSize)
data DCCOffer = DCCOffer
  { _dccNetwork  :: !Text
  , _dccFromInfo :: !UserInfo
  , _dccFromIP   ::  HostName 
  , _dccPort     :: !PortNumber
  , _dccFileName ::  FilePath 
  , _dccSize     :: !Word32 
                            
  , _dccOffset   :: !Word32 
  , _dccStatus   :: !ConnectionStatus
  } deriving (Show, Eq)
data ConnectionStatus
  = CorrectlyFinished | UserKilled | LostConnection | Downloading | Pending
  | NotExist
  deriving (Eq, Show)
data DCCTransfer = DCCTransfer
  { _dtThread   :: !(Maybe (Async ())) 
                                       
  , _dtProgress :: !Word32 
  }
data DCCState = DCCState
  { _dsOffers    :: !(IntMap DCCOffer)
  , _dsTransfers :: !(IntMap DCCTransfer)
  }
data DCCUpdate
  = PercentUpdate !Key !Word32
  | Finished !Key
  | SocketInterrupted !Key
  | UserInterrupted !Key
  | Accept !Key !PortNumber !Word32 
  deriving (Show, Eq)               
makeLenses ''DCCOffer
makeLenses ''DCCTransfer
makeLenses ''DCCState
emptyDCCState :: DCCState
emptyDCCState = DCCState mempty mempty
dccOffer :: Text -> UserInfo -> HostName -> PortNumber
         -> FilePath -> Word32 -> DCCOffer
dccOffer network userFrom hostaddr port filename filesize =
  DCCOffer network userFrom hostaddr port filename filesize 0 Pending
supervisedDownload ::
  FilePath        ->
  Key             ->
  TChan DCCUpdate ->
  DCCState        ->
  IO DCCState
supervisedDownload dir key updChan state = do
  let Just offer = view (dsOffers . at key) state 
  supervisorThread <- async $
      withAsync (startDownload dir key updChan offer) $ \realTransferThread ->
        do upd <- E.catches (Finished key <$ wait realTransferThread)
                    [ E.Handler (\(_ :: IOException) ->
                                   return (SocketInterrupted key))
                    
                    , E.Handler (\(_ :: AsyncCancelled) ->
                                   return (UserInterrupted key))
                    ]
           atomically (writeTChan updChan upd)
  let startPercent = percent (_dccOffset offer) (_dccSize offer)
      newTransfer  = DCCTransfer (Just supervisorThread) startPercent
      newOffer     = offer { _dccStatus = Downloading }
      newState     = set (dsOffers . at key) (Just newOffer)
                   $ set (dsTransfers . at key) (Just newTransfer) state
  return newState
startDownload :: FilePath -> Key -> TChan DCCUpdate -> DCCOffer -> IO ()
startDownload dir key updChan offer@(DCCOffer _ _ from port name totalSize offset _) = do
  let openMode = if offset > 0 then AppendMode else WriteMode
      filepath = dir </> name
  bracket (connect param) close $ \conn ->
    bracket (openFile filepath openMode) hClose $ \hdl ->
      do 
         
         recvChan1 <- atomically newTChan
         recvChan2 <- atomically (dupTChan recvChan1)
         
         
         
         
         
         
         
         
         withAsync (sendStream totalSize conn recvChan1)
           $ \outThread -> withAsync (report offer key recvChan2 updChan)
             $ \_reportThread -> do recvSendLoop offset recvChan1 conn hdl
                                    wait outThread
  where
    param = ConnectionParams
              { cpFamily = AF_INET
              , cpHost   = from
              , cpPort   = port
              , cpSocks  = Nothing
              , cpTls    = Nothing }
    buffSize = 4 * 1024 * 1024
    recvSendLoop size chan conn hdl =
      do bytes <- recv conn buffSize
         unless (B.null bytes) $
           do B.hPut hdl bytes
              let newSize = size + fromIntegral (B.length bytes)
              atomically (writeTChan chan newSize)
              recvSendLoop newSize chan conn hdl
sendStream :: Word32 -> Connection -> TChan Word32 -> IO ()
sendStream maxSize conn chan =
  do val <- atomically (readTChan chan)
     let valBE = toStrict (toLazyByteString (word32BE val))
     send conn valBE
     unless (val >= maxSize) (sendStream maxSize conn chan)
report :: DCCOffer -> Key -> TChan Word32 -> TChan DCCUpdate -> IO ()
report offer key input output = compareAndUpdate (percent offset totalsize)
  where
    offset    = _dccOffset offer
    totalsize = _dccSize   offer
    compareAndUpdate :: Word32 -> IO ()
    compareAndUpdate prevPercent =
      do curSize <- atomically $ readTChan input
         let curPercent = percent curSize totalsize
             updateEv   = PercentUpdate key curPercent
         if curPercent == 100
           then atomically (writeTChan output updateEv)
           else do when (curPercent > prevPercent)
                        (atomically (writeTChan output updateEv))
                   compareAndUpdate curPercent
percent :: Word32 -> Word32 -> Word32
percent a total = fromIntegral (fromIntegral a * 100 `div` fromIntegral total :: Word64)
reportStopWithStatus :: Key -> ConnectionStatus -> DCCState -> DCCState
reportStopWithStatus key newstatus
  = set (dsOffers    . ix key . dccStatus) newstatus
  . set (dsTransfers . ix key . dtThread ) Nothing
parseSEND :: Text -> UserInfo -> Text -> Either String DCCOffer
parseSEND network userFrom = parseOnly (sendFormat network userFrom)
sendFormat :: Text -> UserInfo -> Parser DCCOffer
sendFormat network userFrom =
  do name      <- string "SEND" *> space *> nameFormat
     addr      <- ipv4Dotted <$ space <*> decimal
     port      <- space *> decimal
     totalsize <- space *> decimal
     return (dccOffer network userFrom addr port name totalsize)
parseACCEPT :: DCCState -> UserInfo -> Text -> Maybe DCCUpdate
parseACCEPT state userFrom text =
  case parseOnly acceptFormat text of
    Left _ -> Nothing
    Right (fileName, port, offset) ->
      do (key, _) <- find (predicate fileName) offerList
         return (Accept key port offset)
  where
    offerList = I.toDescList (_dsOffers state)
    predicate fileName (key, offer) =
      view dccFileName offer == fileName &&
      view dccFromInfo offer == userFrom &&
      statusAtKey key state  == Pending
acceptFormat :: Parser (FilePath, PortNumber, Word32)
acceptFormat =
  do filepath <- string "ACCEPT" *> space *> nameFormat
     port     <- space *> decimal
     offset   <- space *> decimal
     return (filepath, port, offset)
nameFormat :: Parser FilePath
nameFormat = do textPath <- try quotedName <|> noSpaceName
                return (takeFileName (Text.unpack textPath))
  where
    quotedName = char '\"' *> takeWhile1 ('\"' /=) <* char '\"'
    noSpaceName = takeWhile1 (' ' /=)
ipv4Dotted :: Word32 -> HostName
ipv4Dotted addr = ipv4Format (bigToLittleEndian (hostAddressToTuple addr))
  where
    bigToLittleEndian (a, b, c, d) = (d, c, b, a)
    ipv4Format (d,c,b,a) =
      show d <> "." <> show c <> "." <> show b <> "." <> show a
getFileOffset :: FilePath -> IO (Maybe Word32)
getFileOffset path =
  do res <- E.try (withFile path ReadMode hFileSize)
     return $! case res :: Either IOError Integer of
                 Right n | n > 0 -> Just $! fromIntegral n
                 _               -> Nothing
insertAsNewMax :: DCCOffer -> DCCState -> DCCState
insertAsNewMax newoffer (DCCState offers transfers) =
  let newmax    = if I.null offers then 1 else 1 + fst (I.findMax offers)
      newOffers = I.insert newmax newoffer offers
  in DCCState newOffers transfers
ctcpToTuple :: IrcMsg -> Maybe (UserInfo, Identifier, Text, Text)
ctcpToTuple (Ctcp fromU target command txt) =
  Just (fromU, target, command, txt)
ctcpToTuple (CtcpNotice fromU target command txt) =
  Just (fromU, target, command, txt)
ctcpToTuple _ = Nothing
statusAtKey :: Key -> DCCState -> ConnectionStatus
statusAtKey key (DCCState offers _) =
  case I.lookup key offers of
    Nothing -> NotExist
    Just d  -> view dccStatus d
resumeMsg ::
  Word32            ->
  DCCOffer          ->
  (String, String) 
resumeMsg sizeoffset offer = (target, txt)
  where
    filename = _dccFileName offer
    port = show (_dccPort offer)
    sizeoffset' = show sizeoffset
    quoting = if ' ' `elem` filename then "\"" else ""
    txt = concat ["RESUME ", quoting, filename, quoting,
                  " ", port, " ", sizeoffset' ]
    target = views (dccFromInfo . uiNick) (Text.unpack . idText) offer
acceptUpdate :: DCCUpdate -> DCCState -> DCCState
acceptUpdate (Accept k port offset) state =
  case view (dsOffers . at k) state of
    Nothing       -> state 
    Just oldOffer -> set (dsOffers . at k) (Just newOffer) state
      where
        newOffer = oldOffer { _dccPort = port, _dccOffset = offset }
acceptUpdate _ state = state
isSend :: Text -> Bool
isSend txt
  | "SEND":_ <- Text.splitOn " " txt = True
  | otherwise                        = False