{-# LANGUAGE OverloadedStrings, FlexibleContexts, DeriveDataTypeable, ScopedTypeVariables #-}
module Network.FTP.Commands where

{-| Implement all ftp commands in FTP Monad.
 -}

import qualified Prelude as P
import BasicPrelude

import Control.Monad.Trans.State (get, gets, put, modify)
import Control.Exception (throw)
import qualified Control.Exception.Lifted as Lifted

import qualified Data.ByteString.Char8 as S
import qualified Data.CaseInsensitive as CI
import Data.Typeable (Typeable)
import Data.Maybe (isJust)
import Data.Conduit ( ($$) )
import Data.Conduit.Network (Application, sinkSocket, sourceSocket, appSource, appSink)
import Data.Conduit.Network.Internal (AppData(..))

import Network.FTP.Monad
import qualified Network.FTP.Socket as NS
import Network.FTP.Backend (FTPBackend(..))
import Network.FTP.Utils (encode, decode)

type Command m = ByteString -> FTP m ()

data ApplicationQuit = ApplicationQuit
    deriving (Show, Typeable)

instance Exception ApplicationQuit

{-
 - check auth state before run command.
 -}
login_required :: FTPBackend m => Command m -> Command m
login_required cmd arg = do
    b <- isJust <$> FTP (gets ftpUser)
    if b
      then cmd arg
      else reply "530" "Command not possible in non-authenticated state."

cmd_user :: FTPBackend m => Command m
cmd_user name = do
    b <- isJust <$> FTP (gets ftpUser)
    if b
      then reply "530" "Command not possible in authenticated state."
      else do
        reply "331" "User name accepted; send password."
        pass <- wait (expect "PASS")
        muser <- lift (authenticate name pass)
        FTP $ modify $ \st -> st { ftpUser = muser }
        if isJust muser
           then reply "230" "login successful."
           else reply "530" "incorrect password."

cmd_cwd, cmd_cdup, cmd_pwd :: FTPBackend m => Command m

cmd_cwd dir = do
    cwd (decode dir)
    dir' <- pwd
    reply "250" $ "New directory now " ++ encode dir'

cmd_cdup _ = cmd_cwd ".."
cmd_pwd _ = do
    d <- pwd
    reply "257" $ "\"" ++ encode d ++ "\" is the current working directory."

cmd_type :: FTPBackend m => Command m
cmd_type tp = do
    let modType newtp = do
            st <- FTP get
            reply "200" $ S.pack $
                "Type changed from " ++ P.show (ftpDataType st) ++
                " to " ++ P.show newtp
            FTP $ put st{ ftpDataType = newtp }
    case tp of
        "I"     ->  modType Binary
        "L 8"   ->  modType Binary
        "A"     ->  modType ASCII
        "AN"    ->  modType ASCII
        "AT"    ->  modType ASCII
        _       ->  reply "504" $ "Type \"" ++ tp ++ "\" not supported."

cmd_pasv :: FTPBackend m => Command m
cmd_pasv _ = do
    local <- FTP (gets ftpLocal)
    sock <- liftIO $ NS.startPasvServer local
    FTP $ modify $ \st -> st { ftpChannel = PasvChannel sock }
    portstr <- liftIO $ NS.getSocketName sock >>= NS.toPortString
    reply "227" $ S.pack $ "Entering passive mode (" ++ portstr ++ ")"

cmd_port :: FTPBackend m => Command m
cmd_port port = do
    local <- FTP (gets ftpLocal)
    addr <- liftIO $ NS.fromPortString (S.unpack port)
    case validate local addr of
        Right _  -> doit addr
        Left err -> reply "501" err
  where
    validate (NS.SockAddrInet _ h1) (NS.SockAddrInet _ h2) =
                if h1==h2
                   then Right ()
                   else Left "Will only connect to same client as command channel."
    validate (NS.SockAddrInet _ _) _ = Left "Require IPv4 in specified address"
    validate _ _                     = Left "Require IPv4 on client"

    doit addr = do
        FTP $ modify $ \st -> st { ftpChannel = PortChannel addr }
        reply "200" $ S.pack $ "OK, later I will connect to " ++ P.show addr

runTransfer :: FTPBackend m => Application m -> FTP m ()
runTransfer app = do
    reply "150" "I'm going to open the data channel now."
    chan <- FTP (gets ftpChannel)
    FTP $ modify $ \st -> st{ ftpChannel = NoChannel }
    case chan of
        NoChannel -> fail "Can't connect when no data channel exists"

        PasvChannel sock ->
            Lifted.finally
              (do (csock, addr) <- liftIO $ NS.accept sock
                  lift $ Lifted.finally
                           (app AppData
                               { appSource    = sourceSocket csock
                               , appSink      = sinkSocket csock
                               , appSockAddr  = addr
                               , appLocalAddr = Nothing
                               })
                           (liftIO (NS.sClose csock))
              )
              (liftIO (NS.sClose sock))

        PortChannel addr -> do
            sock <- liftIO $ do
                proto <- NS.getProtocolNumber "tcp"
                sock <- NS.socket NS.AF_INET NS.Stream proto
                NS.connect sock addr
                return sock
            lift $ Lifted.finally
                     (app AppData
                         { appSource    = sourceSocket sock
                         , appSink      = sinkSocket sock
                         , appSockAddr  = addr
                         , appLocalAddr = Nothing
                         })
                     (liftIO (NS.sClose sock))

    reply "226" "Closing data connection; transfer complete."

cmd_list :: FTPBackend m => Command m
cmd_list dir = do
    dir' <- ftpAbsolute (decode dir)
    runTransfer $ \ad -> list dir' $$ (appSink ad)

cmd_nlst :: FTPBackend m => Command m
cmd_nlst dir = do
    dir' <- ftpAbsolute (decode dir)
    runTransfer $ \ad -> nlst dir' $$ (appSink ad)

cmd_noop :: FTPBackend m => Command m
cmd_noop _ = reply "200" "OK"

cmd_dele :: FTPBackend m => Command m
cmd_dele "" = reply "501" "Filename required"
cmd_dele name = do
    ftpAbsolute (decode name) >>= lift . dele
    reply "250" $ "File " ++ name ++ " deleted."

cmd_retr :: FTPBackend m => Command m
cmd_retr "" = reply "501" "Filename required"
cmd_retr name = do
    name' <- ftpAbsolute (decode name)
    runTransfer $ \ad -> download name' $$ (appSink ad)

cmd_stor :: FTPBackend m => Command m
cmd_stor ""   = reply "501" "Filename required"
cmd_stor name = do
    name' <- ftpAbsolute (decode name)
    runTransfer $ \ad -> (appSource ad) $$ upload name'

cmd_syst :: FTPBackend m => Command m
cmd_syst _ = reply "215" "UNIX Type: L8"

cmd_mkd :: FTPBackend m => Command m
cmd_mkd ""  = reply "501" "Directory name required"
cmd_mkd dir = do
    dir' <- ftpAbsolute (decode dir)
    lift $ mkd dir'
    reply "257" $ "\"" ++ encode dir' ++ "\" created."

cmd_rnfr, cmd_rnto :: FTPBackend m => Command m
cmd_rnfr ""   = reply "501" "Filename required"
cmd_rnfr name = do
    FTP $ modify $ \st -> st { ftpRename = Just (decode name) }
    reply "350" $ "Noted rename from "++name++"; please send RNTO."

cmd_rnto ""   = reply "501" "Filename required"
cmd_rnto name = do
    mfromname <- FTP (gets ftpRename)
    case mfromname of
        Nothing ->
            reply "503" "RNFR required before RNTO"
        Just fromname -> do
            FTP $ modify $ \st -> st { ftpRename = Nothing }
            fname <- ftpAbsolute fromname
            tname <- ftpAbsolute (decode name)
            lift (rename fname tname)
            reply "250" $ "File "++encode fromname++" renamed to "++name

cmd_rmd :: FTPBackend m => Command m
cmd_rmd ""   = reply "501" "Filename required"
cmd_rmd dir = do
    ftpAbsolute (decode dir) >>= lift . rmd
    reply "250" $ "Directory " ++ dir ++ " removed."

cmd_stat :: FTPBackend m => Command m
cmd_stat _ = do
    st <- FTP get
    reply "211" $ S.pack $ P.unlines
         [" *** Sever statistics and information"
         ," *** Please type HELP for more details"
         ,""
         ,"Server Software     : haskell-ftp, https://github.com/yihuang/haskell-ftp"
         ,"Connected From      : ", P.show (ftpRemote st)
         ,"Connected To        : ", P.show (ftpLocal st)
         ,"Data Transfer Type  : ", P.show (ftpDataType st)
         ,"Auth Status         : ", P.show (ftpUser st)
         ,"End of status."
         ]

cmd_mode, cmd_stru :: FTPBackend m => Command m
cmd_mode m =
    case m of
        "S" -> reply "200" "Mode is Stream."
        _   -> reply "504" $ "Mode \"" ++ m ++ "\" not supported."

cmd_stru s =
    case s of
        "F" -> reply "200" "Structure is File."
        _   -> reply "504" $ "Structure \"" ++ s ++ "\" not supported."

commands :: FTPBackend m => [(CI.CI ByteString, Command m)]
commands =
    [("USER", cmd_user)
    ,("PASS", const $ reply "530" "Out of sequence PASS command")
    ,("QUIT", const $ reply "221" "OK, Goodbye." >> throw ApplicationQuit)
    --,(Command "HELP" (help,             help_help))
    ,("CWD",  login_required cmd_cwd)
    ,("PWD",  login_required cmd_pwd)
    ,("CDUP", login_required cmd_cdup)
    ,("PASV", login_required cmd_pasv)
    ,("PORT", login_required cmd_port)
    ,("LIST", login_required cmd_list)
    ,("TYPE", login_required cmd_type)
    ,("MKD",  login_required cmd_mkd)
    ,("NOOP", login_required cmd_noop)
    ,("RNFR", login_required cmd_rnfr)
    ,("RNTO", login_required cmd_rnto)
    ,("DELE", login_required cmd_dele)
    ,("RMD",  login_required cmd_rmd)
    ,("RETR", login_required cmd_retr)
    ,("STOR", login_required cmd_stor)
    ,("STAT", login_required cmd_stat)
    ,("SYST", login_required cmd_syst)
    ,("NLST", login_required cmd_nlst)
    ,("MODE", login_required cmd_mode)
    ,("STRU", login_required cmd_stru)
    ]

commandLoop :: FTPBackend m => FTP m ()
commandLoop = do
    (cmd, arg) <- wait getCommand
    lift (ftplog $ CI.original cmd++" "++arg)
    case lookup cmd commands of
        Just cmd' -> do
            continue <- (cmd' arg >> return True)
                           `Lifted.catch` (\(_::ApplicationQuit) -> return False)
                           `Lifted.catch` (\(ex::SomeException) -> do
                                               reply "502" (S.pack $ P.show ex)
                                               return True
                                          )
            when continue commandLoop
        Nothing -> do
            reply "502" $ "Unrecognized command " ++ CI.original cmd
            commandLoop
