{-# LANGUAGE FlexibleInstances, TemplateHaskell #-}
-- | Commandline options tools. Designed to be imported as @qualified@.
module Hbro.Options (
    CliOptions(),
    OptionsReader(..),
    startURI,
    socketPath,
    help,
    quiet,
    verbose,
    version,
    vanilla,
    recompile,
    denyReconf,
    forceReconf,
    dyreDebug,
    usage,
    get,
    getStartURI,
    getSocketURI,
    getUIFile)
where

-- {{{ Imports
import Hbro.Util

import Control.Conditional
import Control.Lens as L  hiding((??))
import Control.Monad.Base
import Control.Monad.Reader

import Data.Default
import Data.Functor
import Data.List
import Data.Maybe

import Network.URI as N

import Prelude hiding(log)

import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Environment.XDG.BaseDir
import System.FilePath
import System.Posix.Process
-- }}}

-- {{{ Types
-- | Available commandline options (cf @hbro -h@).
data CliOptions = CliOptions {
    _startURI      :: Maybe String,
    _socketPath    :: Maybe FilePath,
    _UIFile        :: Maybe FilePath,
    _help          :: Bool,
    _quiet         :: Bool,
    _verbose       :: Bool,
    _version       :: Bool,
    _vanilla       :: Bool,
    _recompile     :: Bool,
    _denyReconf    :: Bool,
    _forceReconf   :: Bool,
    _dyreDebug     :: Bool}
    deriving(Eq)

makeLenses ''CliOptions

instance Show CliOptions where
    show opts = intercalate " " $ catMaybes [
        return . ("URI=" ++)     =<< view startURI opts,
        return . ("SOCKET=" ++)  =<< view socketPath opts,
        return . ("UI_FILE=" ++) =<< view uIFile opts,
        view help        opts ? Just "HELP" ?? Nothing,
        view quiet       opts ? Just "QUIET" ?? Nothing,
        view verbose     opts ? Just "VERBOSE" ?? Nothing,
        view version     opts ? Just "VERSION" ?? Nothing,
        view vanilla     opts ? Just "VANILLA" ?? Nothing,
        view recompile   opts ? Just "RECOMPILE" ?? Nothing,
        view denyReconf  opts ? Just "DENY_RECONFIGURATION" ?? Nothing,
        view forceReconf opts ? Just "FORCE_RECONFIGURATION" ?? Nothing,
        view dyreDebug   opts ? Just "DYRE_DEBUG" ?? Nothing]

instance Default CliOptions where
    def = CliOptions {
        _startURI     = Nothing,
        _socketPath   = Nothing,
        _UIFile       = Nothing,
        _help         = False,
        _quiet        = False,
        _verbose      = False,
        _version      = False,
        _vanilla      = False,
        _recompile    = False,
        _denyReconf   = False,
        _forceReconf  = False,
        _dyreDebug    = False}

-- | 'MonadReader' for 'CliOptions'
class OptionsReader m where
    readOptions :: Simple Lens CliOptions a -> m a

instance (Monad m) => OptionsReader (ReaderT CliOptions m) where
    readOptions l = return . view l =<< ask

instance OptionsReader ((->) CliOptions) where
    readOptions l = view l
-- }}}


description :: [OptDescr (CliOptions -> CliOptions)]
description = [
    Option ['h']     ["help"]               (NoArg (set help True))                         "Print this help",
    Option ['q']     ["quiet"]              (NoArg (set quiet True))                        "Do not print any log",
    Option ['v']     ["verbose"]            (NoArg (set verbose True))                      "Print detailed logs",
    Option ['V']     ["version"]            (NoArg (set version True))                      "Print version",
    Option ['1']     ["vanilla"]            (NoArg (set vanilla True))                      "Do not read custom configuration file",
    Option ['r']     ["recompile"]          (NoArg (set recompile True))                    "Only recompile configuration",
    Option ['s']     ["socket"]             (ReqArg (\v -> set socketPath (Just v)) "PATH") "Where to open IPC socket",
    Option ['u']     ["ui"]                 (ReqArg (\v -> set uIFile (Just v)) "PATH")     "Path to UI descriptor (XML file)",
    Option []        ["force-reconf"]       (NoArg id)                                      "Recompile configuration before starting the program",
    Option []        ["deny-reconf"]        (NoArg id)                                      "Do not recompile configuration even if it has changed",
    Option []        ["dyre-debug"]         (NoArg id)                                      "Use './cache/' as the cache directory and ./ as the configuration directory. Useful to debug the program"]

-- | Usage text (cf @hbro -h@)
usage :: String
usage = usageInfo "Usage: hbro [OPTIONS] [URI]" description

-- | Get and parse commandline options
get :: (MonadBase IO m) => m CliOptions
get = io $ do
    options <- getOpt' Permute description <$> getArgs
    case options of
        (opts, input, _, []) -> return $ set startURI ((null $ concat input) ? Nothing ?? Just (concat input)) (foldl (flip id) def opts)
        (_, _, _, _)         -> return def

-- | Get URI passed in commandline, check whether it is a file path or an internet URI
-- and return the corresponding normalized URI (that is: prefixed with "file://" or "http://")
getStartURI :: (MonadBase IO m, OptionsReader m) => m (Maybe URI)
getStartURI = do
    theURI <- readOptions startURI
    case theURI of
      Just uri -> do
          fileURI <- io $ doesFileExist uri
          case fileURI of
              True -> io getCurrentDirectory >>= return . N.parseURIReference . ("file://" ++) . (</> uri)
              _    -> return $ N.parseURIReference uri
      _ -> return Nothing


-- | Return socket URI used by this instance
getSocketURI :: (MonadBase IO m, OptionsReader m) => m String
getSocketURI = maybe getDefaultSocketURI (return . ("ipc://" ++)) =<< readOptions socketPath
  where
    getDefaultSocketURI = do
      dir <- io getTemporaryDirectory
      pid <- io getProcessID
      return $ "ipc://" ++ dir </> "hbro." ++ show pid

-- | Return UI descriptor (XML file) used to build the GUI
getUIFile :: (MonadBase IO m, OptionsReader m) => m FilePath
getUIFile = maybe getDefaultUIFile return =<< readOptions uIFile
  where
    getDefaultUIFile = io (getUserConfigDir "hbro" >/> "ui.xml")
