{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, GeneralizedNewtypeDeriving, RankNTypes, RecordWildCards, OverloadedStrings #-}
module Main where

import Control.Concurrent         (killThread)
import Control.Concurrent.Chan    (Chan)
import Data.ByteString            (ByteString)
import qualified Data.ByteString.Char8 as C
import Data.Set                   (Set, insert)
import Network                    (HostName, PortID(PortNumber), connectTo)
import Network.IRC                (Message)
import Network.IRC.Bot.BotMonad   (BotMonad(..))
import Network.IRC.Bot.Core       (BotConf(..), User(..), nullBotConf, simpleBot)
import Network.IRC.Bot.Log        (LogLevel(..), nullLogger, stdoutLogger)
import Network.IRC.Bot.Part.Dice  (dicePart)
import Network.IRC.Bot.Part.Hello (helloPart)
import Network.IRC.Bot.Part.Ping  (pingPart)
import Network.IRC.Bot.Part.NickUser (nickUserPart)
import Network.IRC.Bot.Part.Channels (initChannelsPart)
import System.Console.GetOpt
import System.Environment         (getArgs, getProgName)
import System.Exit                (exitFailure)
import System.IO                  (stdout)

data Flag
    = BotConfOpt { unBotConfOpt :: (BotConf -> BotConf) }

botOpts :: [OptDescr Flag]
botOpts =
    [ Option [] ["irc-server"] (ReqArg setIrcServer "hostname or IP") "irc server to connect to"
    , Option [] ["port"]       (ReqArg setPort      "port")           "port to connect to on server"
    , Option [] ["nick"]       (ReqArg setNick      "name")           "irc nick"
    , Option [] ["username"]   (ReqArg setUsername  "username")       "ident username"
    , Option [] ["hostname"]   (ReqArg setHostname  "hostname")       "hostname of machine bot is connecting from"
    , Option [] ["realname"]   (ReqArg setRealname  "name")           "bot's real name"
    , Option [] ["cmd-prefix"] (ReqArg setCmdPrefix "prefix")         "prefix to bot commands (e.g., ?, @, bot: )"
    , Option [] ["channel"]    (ReqArg addChannel   "channel name")   "channel to join after connecting. (can be specified more than once to join multiple channels)"
    , Option [] ["log-level"]  (ReqArg setLogLevel  "debug, normal, important, quiet") "set the logging level"
    , Option [] ["limit"]      (ReqArg setLimit     "int,int")        "enable rate limiter. burst length, delay in microseconds"
    ]
    where
      setIrcServer n = BotConfOpt $ \c -> c { host = n, user = (user c) { servername = n } }
      setPort str    = BotConfOpt $ \c -> c { port = PortNumber (fromIntegral $ read str) }
      setNick n      = BotConfOpt $ \c -> c { nick = C.pack n }
      setUsername n  = BotConfOpt $ \c -> c { user = (user c) { username = C.pack n } }
      setHostname n  = BotConfOpt $ \c -> c { user = (user c) { hostname = n } }
      setRealname n  = BotConfOpt $ \c -> c { user = (user c) { realname = (C.pack n) } }
      setCmdPrefix p = BotConfOpt $ \c -> c { commandPrefix = p }
      addChannel ch  = BotConfOpt $ \c -> c { channels = insert (C.pack ch) (channels c) }
      setLogLevel l  = BotConfOpt $ \c ->
        case l of
          "debug"     -> c { logger = stdoutLogger Debug }
          "normal"    -> c { logger = stdoutLogger Normal }
          "important" -> c { logger = stdoutLogger Important }
          "quiet"     -> c { logger = nullLogger }
          _           -> error $ "unknown log-level: " ++ l
      setLimit s    = BotConfOpt $ \c ->
        case break (== ',') s of
          (burstStr, delayStr) ->
              case reads burstStr of
                [(burstLen,[])] ->
                    case reads (drop 1 $ delayStr) of
                      [(delay,[])] ->
                          c { limits = Just (burstLen, delay) }
                      _ -> error $ "unabled to parse delay: " ++ delayStr
                _ -> error $ "unabled to parse burst length: " ++ burstStr

getBotConf :: Maybe (Chan Message -> IO ()) -> IO BotConf
getBotConf mLogger =
    do args <- getArgs
       case getOpt Permute botOpts args of
         (f,_,[])   ->
             do let conf = (foldr ($) nullBotConf (map unBotConfOpt f)) { channelLogger = mLogger }
                checkConf conf
                return conf
         (_,_,errs) ->
             do progName <- getProgName
                putStr (helpMessage progName)
                exitFailure

exitHelp msg =
    do progName <- getProgName
       putStrLn msg
       putStr (helpMessage progName)
       exitFailure

checkConf :: BotConf -> IO ()
checkConf BotConf{..}
    | null   host            = exitHelp "must specify --irc-server"
    | C.null nick            = exitHelp "must specify --nick"
    | C.null (username user) = exitHelp "must specify --username"
    |   null (hostname user) = exitHelp "must specify --hostname"
    | C.null (realname user) = exitHelp "must specify --realname"
    | otherwise            = return ()

helpMessage progName = usageInfo header botOpts
  where
  header = "Usage: "++progName++" [OPTION...]\n" ++ "e.g.\n" ++
           progName ++ " --irc-server irc.freenode.net --nick stepbot --username stepbot --hostname happstack.com --realname \"happstack bot\" --channel \"#stepbot\""

main :: IO ()
main =
    do botConf <- getBotConf Nothing
       ircParts <- initParts (channels botConf)
       (tids, reconnect) <- simpleBot botConf ircParts
       (logger botConf) Important  "Press enter to force reconnect."
       getLine
       reconnect
       (logger botConf) Important  "Press enter to quit."
       getLine

       mapM_ killThread tids

initParts :: (BotMonad m) =>
             Set ByteString  -- ^ set of channels to join
          -> IO [m ()]
initParts chans =
    do (_, channelsPart) <- initChannelsPart chans
       return [ pingPart
              , nickUserPart
              , channelsPart
              , dicePart
              , helloPart
              ]
