-- Copyright: 2009 Dino Morelli
-- License: BSD3 (see LICENSE)
-- Author: Dino Morelli <dino@ui3.info>

{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Error
import Control.Monad.Reader
import Data.List ( intercalate, isPrefixOf )
import Data.Map hiding ( map, null )
import Fez.Data.Conf ( ConfMap, parseToMap )
import Prelude hiding ( lookup )
import System.Environment ( getArgs )
import System.Exit ( ExitCode (..), exitWith )
import System.IO ( BufferMode (..), hSetBuffering, stdout )
import System.Process ( runCommand, waitForProcess )
import Text.Printf

import Multiplicity.Common ( ecTerminated )
import Multiplicity.Docs ( usage, sampleConfig )


-- Data type to hold environment for Reader monad
data Env = Env
   { config :: ConfMap
   , rawArgs :: [String]
   }


{- Custom monad stack for this application:
   ErrorT wrapped around Reader
-}

type Mult a = ErrorT String (Reader Env) a

runMult :: ErrorT e (Reader r) a -> r -> Either e a
runMult ev env = runReader (runErrorT ev) env


-- Type to carry results of parseArgs action
type ParseResult = (String, String)


{- Lookup in the config (which is a Map String String) as an
   (ErrorT String) action with meaningful error message.
-}
lookupE :: (MonadError String m) => String -> ConfMap -> m String
lookupE key mp = maybe (throwError $ "Key " ++ key ++ " not found")
   return $ lookup key mp


{- parseArgs transforms the config file plus passed args into the proper
   list of args for duplicity. Not as easy as it sounds, these arg
   lists are dependent on the duplicity action to be taken

   The real work of this program is all in here.
-}

parseArgs :: String -> Mult ParseResult

parseArgs "full"                  = parseType1
parseArgs "incremental"           = parseType1
parseArgs "incr"                  = parseType1
   -- duplicity accepts this abbreviation
parseArgs ""                      = parseType1

parseArgs "restore"               = parseType2
parseArgs "verify"                = parseType2

parseArgs "collection-status"     = parseType3
parseArgs "list-current-files"    = parseType3
parseArgs "cleanup"               = parseType3
parseArgs "remove-older-than"     = parseType3
parseArgs "remove-all-but-n-full" = parseType3

-- Anything else is crazy and unexpected, throw an error
parseArgs action = throwError $ "Unknown action: " ++ action


parseType1 :: Mult ParseResult
parseType1 = do
   args <- liftM (intercalate " ") $ asks rawArgs
   conf <- asks config

   pw <- lookupE "passphrase" conf
   cs <- lookupE "common-args" conf
   filters <- lookupE "filters" conf
   srcDir <- lookupE "src-dir" conf
   targetUrl <- lookupE "target-url" conf

   return ( pw
      , printf "%s %s %s %s %s" cs args filters srcDir targetUrl )


parseType2 :: Mult ParseResult
parseType2 = do
   arglist <- asks rawArgs
   conf <- asks config

   pw <- lookupE "passphrase" conf
   cs <- lookupE "common-args" conf
   srcUrl <- lookupE "target-url" conf
   let args = intercalate " " $ init arglist
   let targetDir = last arglist

   return ( pw
      , printf "%s %s %s %s" cs args srcUrl targetDir )


parseType3 :: Mult ParseResult
parseType3 = do
   args <- liftM (intercalate " ") $ asks rawArgs
   conf <- asks config

   pw <- lookupE "passphrase" conf
   cs <- lookupE "common-args" conf
   targetUrl <- lookupE "target-url" conf

   return ( pw
      , printf "%s %s %s" args cs targetUrl )


{- These two functions are the handlers for failure and success of 
   parseArgs
-}

endBadly :: String -> IO ExitCode
endBadly err = do
   putStrLn err
   return ecTerminated


invokeDuplicity :: String -> ParseResult -> IO ExitCode
invokeDuplicity action (pw, args) = do
   let displayCommand = buildCmdString "**HIDDEN**" action args 
   let realCommand = buildCmdString pw action args

   hSetBuffering stdout NoBuffering

   printf "\nCommand used to invoke duplicity:\n%s\n\n"
      (displayCommand :: String)

   waitForProcess =<< runCommand realCommand

   where
      buildCmdString = printf "PASSPHRASE=\"%s\" duplicity %s %s"


{- Make sense of the varied shapes of command-line we accept,
   parse it into conf path, action and duplicity arg list.
   Or fail with Nothing
-}
parseCommandLine :: [String] -> Maybe (String, String, [String])
parseCommandLine (confPath:[])
   | "--" `isPrefixOf` confPath = Nothing
parseCommandLine (confPath:[]) = Just (confPath, "", [])
parseCommandLine (confPath:everythingElse)
   | beginsWithSwitch everythingElse =
      Just (confPath, "", everythingElse)
   where
      beginsWithSwitch (first:_) = "--" `isPrefixOf` first
      beginsWithSwitch _         = False
parseCommandLine (confPath:action:switches) =
   Just (confPath, action, switches)
parseCommandLine _ = Nothing


main :: IO ()
main = do
   allArgs <- getArgs

   -- Special case where user asked for sample config
   when ("--sample-config" `elem` allArgs) sampleConfig

   case (parseCommandLine allArgs) of
      Nothing -> usage
      Just (confPath, action, rest) -> do
         conf <- liftM parseToMap $ readFile confPath

         -- Transform those raw args into complete args, or figure 
         -- out why we can't.
         let parseResult = runMult (parseArgs action) (Env conf rest)

         exitWith =<< either endBadly (invokeDuplicity action) parseResult
