{-# Language OverloadedStrings #-}
{-|
Module      : Client.Commands.DCC
Description : DCC command implementations
Copyright   : (c) Eric Mertens, 2016-2020
License     : ISC
Maintainer  : emertens@gmail.com
-}

module Client.Commands.DCC (dccCommands) where

import           Client.Commands.Arguments.Spec
import           Client.Commands.Chat (cmdCtcp)
import           Client.Commands.TabCompletion
import           Client.Commands.Types
import           Client.Configuration
import           Client.State
import           Client.State.DCC
import           Client.State.Focus
import           Control.Applicative
import qualified Control.Concurrent.Async as Async
import           Control.Lens
import           System.Directory (doesDirectoryExist)
import           System.FilePath ((</>))

dccCommands :: CommandSection
dccCommands = CommandSection "DCC"

  [ Command
      (pure "dcc")
      (liftA2 (,) (optionalArg (simpleToken "[accept|cancel|clear|resume]"))
                               optionalNumberArg)
      "Main access to the DCC subsystem with the following subcommands:\n\n\
       \  /dcc           : Access to a list of pending offer and downloads\n\
       \  /dcc accept #n : start downloading the #n pending offer\n\
       \  /dcc resume #n : same as accept but appending to the file on `download-dir`\n\
       \  /dcc clear  #n : remove the #n offer from the list \n\
       \  /dcc cancel #n : cancel the download #n \n\n"
    $ ClientCommand cmdDcc noClientTab
  ]

-- | Implementation of @/dcc [(cancel|accept|resume)] [key]
cmdDcc :: ClientCommand (Maybe String, Maybe Int)
cmdDcc st (Nothing, Nothing) = commandSuccess (changeSubfocus FocusDCC st)
cmdDcc st (Just cmd, Just key) = checkAndBranch st cmd key
cmdDcc st _ = commandFailureMsg "Invalid syntax" st

checkAndBranch :: ClientState -> String -> Int -> IO CommandResult
checkAndBranch st cmd key
  | isCancel, NotExist <- curKeyStatus
      = commandFailureMsg "No such DCC entry" st
  | isCancel, curKeyStatus == Pending
      = commandSuccess
      $ set (clientDCC . dsOffers . ix key . dccStatus) UserKilled st
  | isCancel, curKeyStatus /= Downloading
      = commandFailureMsg "Transfer already stopped" st
  | isCancel = Async.cancel threadId *> commandSuccess st

  | isClear, NotExist <- curKeyStatus
      = commandFailureMsg "No such DCC entry" st
  | isClear, curKeyStatus `elem` [Downloading, Pending]
      = commandFailureMsg "Cancel the download first" st
  | isClear = commandSuccess
            $ set (clientDCC . dsOffers    . at key) Nothing
            $ set (clientDCC . dsTransfers . at key) Nothing st

  | isAcceptOrResume, curKeyStatus `elem` alreadyAcceptedSet
      = commandFailureMsg "Offer already accepted" st
  | isAcceptOrResume, NotExist <- curKeyStatus
      = commandFailureMsg "No such DCC entry" st
  | isAcceptOrResume
      = do isDirectory <- doesDirectoryExist downloadPath
           msize       <- getFileOffset downloadPath
           case (isDirectory, msize, cmd, mcs) of
             (True, _, _, _)      -> commandFailureMsg "DCC transfer would overwrite a directory" st
             (_, Nothing, _, _)   -> acceptOffer -- resume from 0 is accept
             (_, _, "accept", _)  -> acceptOffer -- overwrite file
             (_, Just size, "resume", Just cs) -> resumeOffer size cs
             _ -> commandFailureMsg "Unknown case" st

  | otherwise = commandFailureMsg "Invalid syntax" st
  where
    -- General
    isAcceptOrResume = cmd `elem` ["accept", "resume"]
    isCancel         = cmd == "cancel"
    isClear          = cmd == "clear"
    dccState         = view clientDCC st
    curKeyStatus     = statusAtKey key dccState
    alreadyAcceptedSet = [ CorrectlyFinished, UserKilled, LostConnection
                         , Downloading]

    -- For cancel, other cases handled on the guards
    threadId = st ^?! clientDCC . dsTransfers . ix key . dtThread . _Just

    -- Common values for resume or accept
    Just offer   = view (clientDCC . dsOffers . at key) st -- guarded exist
    updChan      = view clientDCCUpdates st
    downloadDir  = view (clientConfig . configDownloadDir) st
    downloadPath = downloadDir </> _dccFileName offer
    mcs          = preview (clientConnection (_dccNetwork offer)) st

    -- Actual workhorses for the commands
    acceptOffer =
        do newDCCState <- supervisedDownload downloadDir key updChan dccState
           commandSuccess (set clientDCC newDCCState st)

    resumeOffer size cs =
        let newOffer = offer { _dccOffset = size }
            (target, txt) = resumeMsg size newOffer
            st' = set (clientDCC . dsOffers . at key) (Just newOffer) st
        in cmdCtcp cs st' (target, "DCC", txt)