{-# Language OverloadedStrings #-}

{-|
Module      : Irc.Message
Description : High-level representation of IRC messages
Copyright   : (c) Eric Mertens, 2016
License     : ISC
Maintainer  : emertens@gmail.com

This module defines high-level IRC commands. Commands are interpreted
and their arguments are extracted into the appropriate types.

-}

module Irc.Message
  (
  -- * High-level messages
    IrcMsg(..)
  , CapCmd(..)
  , CapMore(..)
  , cookIrcMsg

  -- * Properties of messages
  , MessageTarget(..)
  , ircMsgText
  , msgTarget
  , msgActor

  -- * Helper functions
  , nickSplit
  , computeMaxMessageLength
  , capCmdText
  ) where

import           Control.Monad
import           Data.Function
import           Data.Maybe
import           Data.Text (Text)
import qualified Data.Text as Text
import           Data.Text.Read as Text
import           Irc.Identifier
import           Irc.RawIrcMsg
import           Irc.UserInfo
import           Irc.Codes
import           View

-- | High-level IRC message representation
data IrcMsg
  = UnknownMsg !RawIrcMsg -- ^ pass-through for unhandled messages
  | Reply !ReplyCode [Text] -- ^ code arguments
  | Nick !UserInfo !Identifier -- ^ old new
  | Join !UserInfo !Identifier !Text -- ^ user channel account(extended-join)
  | Part !UserInfo !Identifier (Maybe Text) -- ^ user channel reason
  | Quit !UserInfo (Maybe Text) -- ^ user reason
  | Kick !UserInfo !Identifier !Identifier !Text -- ^ kicker channel kickee comment
  | Topic !UserInfo !Identifier !Text -- ^ user channel topic
  | Privmsg !UserInfo !Identifier !Text -- ^ source target txt
  | Ctcp !UserInfo !Identifier !Text !Text -- ^ source target command txt
  | CtcpNotice !UserInfo !Identifier !Text !Text -- ^ source target command txt
  | Notice !UserInfo !Identifier !Text -- ^ source target txt
  | Mode !UserInfo !Identifier [Text] -- ^ source target txt
  | Authenticate !Text -- ^ parameters
  | Cap !CapCmd -- ^ cap command and parameters
  | Ping [Text] -- ^ parameters
  | Pong [Text] -- ^ parameters
  | Error !Text -- ^ message
  | BatchStart !Text !Text [Text] -- ^ reference-id type parameters
  | BatchEnd !Text -- ^ reference-id
  | Account !UserInfo !Text -- ^ user account name changed (account-notify extension)
  | Chghost !UserInfo !Text !Text -- ^ Target, new username and new hostname
  | Wallops  !UserInfo !Text -- ^ Braodcast message: Source, message
  deriving Show

data CapMore = CapMore | CapDone
  deriving (Show, Read, Eq, Ord)

-- | Sub-commands of the CAP command sent by server
data CapCmd
  = CapLs !CapMore [(Text, Maybe Text)] -- ^ list of supported caps
  | CapList [Text] -- ^ list of active caps
  | CapAck [Text] -- ^ request accepted
  | CapNak [Text] -- ^ request denied
  | CapNew [(Text, Maybe Text)] -- ^ new capability available (cap-notify extension)
  | CapDel [Text] -- ^ capability removed (cap-notify extension)
  deriving (Show, Read, Eq, Ord)

-- | Match command text to structured cap sub-command
cookCapCmd :: Text -> [Text] -> Maybe CapCmd
cookCapCmd cmd args =
  case (cmd, args) of
    ("LS"  , ["*", caps]) -> Just (CapLs CapMore (splitCapList caps))
    ("LS"  , [     caps]) -> Just (CapLs CapDone (splitCapList caps))
    ("LIST", [     caps]) -> Just (CapList (Text.words caps))
    ("ACK" , [     caps]) -> Just (CapAck (Text.words caps))
    ("NAK" , [     caps]) -> Just (CapNak (Text.words caps))
    ("NEW" , [     caps]) -> Just (CapNew (splitCapList caps))
    ("DEL" , [     caps]) -> Just (CapDel (Text.words caps))
    _                     -> Nothing

-- | Interpret a low-level 'RawIrcMsg' as a high-level 'IrcMsg'.
-- Messages that can't be understood are wrapped in 'UnknownMsg'.
cookIrcMsg :: RawIrcMsg -> IrcMsg
cookIrcMsg msg =
  case view msgCommand msg of
    cmd | Right (n,"") <- decimal cmd ->
        Reply (ReplyCode n) (view msgParams msg)
    "CAP" | _target:cmdTxt:rest <- view msgParams msg
          , Just cmd <- cookCapCmd cmdTxt rest -> Cap cmd

    "AUTHENTICATE" | x:_ <- view msgParams msg ->
        Authenticate x

    "PING" -> Ping (view msgParams msg)
    "PONG" -> Pong (view msgParams msg)

    "PRIVMSG" | Just user <- view msgPrefix msg
           , [chan,txt]   <- view msgParams msg ->

           case parseCtcp txt of
             Just (cmd,args) -> Ctcp user (mkId chan) (Text.toUpper cmd) args
             Nothing         -> Privmsg user (mkId chan) txt

    "NOTICE" | Just user <- view msgPrefix msg
           , [chan,txt]    <- view msgParams msg ->

           case parseCtcp txt of
             Just (cmd,args) -> CtcpNotice user (mkId chan) (Text.toUpper cmd) args
             Nothing         -> Notice user (mkId chan) txt

    "JOIN" | Just user <- view msgPrefix msg
           , chan:rest <- view msgParams msg ->

              Join user (mkId chan)
            $ case rest of
                ["*" , _real] -> ""
                [acct, _real] -> acct
                _             -> ""

    "QUIT" | Just user <- view msgPrefix msg
           , reasons   <- view msgParams msg ->
           Quit user (listToMaybe reasons)

    "PART" | Just user    <- view msgPrefix msg
           , chan:reasons <- view msgParams msg ->
           Part user (mkId chan) (listToMaybe reasons)

    "NICK"  | Just user <- view msgPrefix msg
            , newNick:_ <- view msgParams msg ->
           Nick user (mkId newNick)

    "KICK"  | Just user <- view msgPrefix msg
            , [chan,nick,reason] <- view msgParams msg ->
           Kick user (mkId chan) (mkId nick) reason

    "TOPIC" | Just user <- view msgPrefix msg
            , [chan,topic] <- view msgParams msg ->
            Topic user (mkId chan) topic

    "MODE"  | Just user <- view msgPrefix msg
            , target:modes <- view msgParams msg ->
            Mode user (mkId target) modes

    "ERROR" | [reason] <- view msgParams msg ->
            Error reason

    "BATCH" | refid : ty : params <- view msgParams msg
            , Just ('+',refid') <- Text.uncons refid ->
            BatchStart refid' ty params

    "BATCH" | [refid] <- view msgParams msg
            , Just ('-',refid') <- Text.uncons refid ->
            BatchEnd refid'

    "ACCOUNT" | Just user <- view msgPrefix msg
              , [acct] <- view msgParams msg ->
      Account user (if acct == "*" then "" else acct)

    "CHGHOST" | Just user <- view msgPrefix msg
              , [newuser, newhost] <- view msgParams msg ->
      Chghost user newuser newhost

    "WALLOPS" | Just user <- view msgPrefix msg
              , [txt] <- view msgParams msg ->
      Wallops user txt

    _      -> UnknownMsg msg

-- | Parse a CTCP encoded message:
--
-- @\^ACOMMAND arguments\^A@
parseCtcp :: Text -> Maybe (Text, Text)
parseCtcp txt =
  do txt1 <- Text.stripSuffix "\^A" =<< Text.stripPrefix "\^A" txt
     let (cmd,args) = Text.break (==' ') txt1
     guard (not (Text.null cmd))
     return (cmd, Text.drop 1 args)


-- | Targets used to direct a message to a window for display
data MessageTarget
  = TargetUser   !Identifier -- ^ Metadata update for a user
  | TargetWindow !Identifier -- ^ Directed message to channel or from user
  | TargetNetwork            -- ^ Network-level message
  | TargetHidden             -- ^ Completely hidden message
  deriving (Show)

-- | Target information for the window that could be appropriate to
-- display this message in.
msgTarget :: Identifier -> IrcMsg -> MessageTarget
msgTarget me msg =
  case msg of
    UnknownMsg{}             -> TargetNetwork
    Nick user _              -> TargetUser (userNick user)
    Mode _ tgt _ | tgt == me -> TargetNetwork
                 | otherwise -> TargetWindow tgt
    Join _ chan _            -> TargetWindow chan
    Part _ chan _            -> TargetWindow chan
    Quit user _              -> TargetUser (userNick user)
    Kick _ chan _ _          -> TargetWindow chan
    Topic _ chan _           -> TargetWindow chan
    Privmsg src tgt _        -> directed src tgt
    Ctcp src tgt _ _         -> directed src tgt
    CtcpNotice src tgt _ _   -> directed src tgt
    Notice  src tgt _        -> directed src tgt
    Authenticate{}           -> TargetHidden
    Ping{}                   -> TargetHidden
    Pong{}                   -> TargetHidden
    Error{}                  -> TargetNetwork
    Cap{}                    -> TargetNetwork
    Reply code args          -> replyTarget code args
    BatchStart{}             -> TargetHidden
    BatchEnd{}               -> TargetHidden
    Account user _           -> TargetUser (userNick user)
    Chghost user _ _         -> TargetUser (userNick user)
    Wallops src _            -> TargetWindow (userNick src)
  where
    directed src tgt
      | Text.null (userHost src) = TargetNetwork -- server message
      | tgt == me = TargetWindow (userNick src)
      | otherwise = TargetWindow tgt

    replyTarget RPL_TOPIC    (_:chan:_)   = TargetWindow (mkId chan)
    replyTarget RPL_INVITING (_:_:chan:_) = TargetWindow (mkId chan)
    replyTarget _                _        = TargetNetwork

-- | 'UserInfo' of the user responsible for a message.
msgActor :: IrcMsg -> Maybe UserInfo
msgActor msg =
  case msg of
    UnknownMsg{}  -> Nothing
    Reply{}       -> Nothing
    Nick x _      -> Just x
    Join x _ _    -> Just x
    Part x _ _    -> Just x
    Quit x _      -> Just x
    Kick x _ _ _  -> Just x
    Topic x _ _   -> Just x
    Privmsg x _ _ -> Just x
    Ctcp x _ _ _  -> Just x
    CtcpNotice x _ _ _ -> Just x
    Notice x _ _  -> Just x
    Mode x _ _    -> Just x
    Account x _   -> Just x
    Authenticate{}-> Nothing
    Ping{}        -> Nothing
    Pong{}        -> Nothing
    Error{}       -> Nothing
    Cap{}         -> Nothing
    BatchStart{}  -> Nothing
    BatchEnd{}    -> Nothing
    Chghost x _ _ -> Just x
    Wallops x _   -> Just x

-- | Text representation of an IRC message to be used for matching with
-- regular expressions.
ircMsgText :: IrcMsg -> Text
ircMsgText msg =
  case msg of
    UnknownMsg raw -> Text.unwords (view msgCommand raw : view msgParams raw)
    Reply (ReplyCode n) xs -> Text.unwords (Text.pack (show n) : xs)
    Nick x y       -> Text.unwords [renderUserInfo x, idText y]
    Join x _ _     -> renderUserInfo x
    Part x _ mb    -> Text.unwords (renderUserInfo x : maybeToList mb)
    Quit x mb      -> Text.unwords (renderUserInfo x : maybeToList mb)
    Kick x _ z r   -> Text.unwords [renderUserInfo x, idText z, r]
    Topic x _ t    -> Text.unwords [renderUserInfo x, t]
    Privmsg x _ t  -> Text.unwords [renderUserInfo x, t]
    Ctcp x _ c t   -> Text.unwords [renderUserInfo x, c, t]
    CtcpNotice x _ c t -> Text.unwords [renderUserInfo x, c, t]
    Notice x _ t   -> Text.unwords [renderUserInfo x, t]
    Mode x _ xs    -> Text.unwords (renderUserInfo x:"set mode":xs)
    Ping xs        -> Text.unwords xs
    Pong xs        -> Text.unwords xs
    Cap cmd        -> capCmdText cmd
    Error t        -> t
    Account x a    -> Text.unwords [renderUserInfo x, a]
    Authenticate{} -> ""
    BatchStart{}   -> ""
    BatchEnd{}     -> ""
    Chghost x a b  -> Text.unwords [renderUserInfo x, a, b]
    Wallops x t    -> Text.unwords [renderUserInfo x, t]

capCmdText :: CapCmd -> Text
capCmdText cmd =
  case cmd of
    CapLs more caps -> capMoreText more <> capUnsplitCaps caps
    CapNew     caps -> capUnsplitCaps caps
    CapList    caps -> Text.unwords caps
    CapAck     caps -> Text.unwords caps
    CapNak     caps -> Text.unwords caps
    CapDel     caps -> Text.unwords caps

capMoreText :: CapMore -> Text
capMoreText CapDone = ""
capMoreText CapMore = "* "

capUnsplitCaps :: [(Text, Maybe Text)] -> Text
capUnsplitCaps xs = Text.unwords [ k <> maybe "" ("=" <>) v | (k, v) <- xs ]

-- nickname   =  ( letter / special ) *8( letter / digit / special / "-" )
-- letter     =  %x41-5A / %x61-7A       ; A-Z / a-z
-- digit      =  %x30-39                 ; 0-9
-- special    =  %x5B-60 / %x7B-7D
isNickChar :: Char -> Bool
isNickChar x = '0' <= x && x <= '9'
              || 'A' <= x && x <= '}'
              || '-' == x

-- | Split a nick into text parts group by whether or not those parts are valid
-- nickname characters.
nickSplit :: Text -> [Text]
nickSplit = Text.groupBy ((==) `on` isNickChar)

-- | Maximum length computation for the message part for
-- privmsg and notice. Note that the need for the limit is because
-- the server will limit the length of the message sent out to each
-- client, not just the length of the messages it will recieve.
--
-- Note that the length is on the *encoded message* which is UTF-8
-- The calculation isn't using UTF-8 on the userinfo part because
-- I'm assuming that the channel name and userinfo are all ASCII
--
-- @
-- :my!user@info PRIVMSG #channel :messagebody\r\n
-- @
computeMaxMessageLength :: UserInfo -> Text -> Int
computeMaxMessageLength myUserInfo target
  = 512 -- max IRC command
  - Text.length (renderUserInfo myUserInfo)
  - length (": PRIVMSG  :\r\n"::String)
  - Text.length target

splitCapList :: Text -> [(Text, Maybe Text)]
splitCapList caps =
  [ (name, value)
    | kv <- Text.words caps
    , let (name, v) = Text.break ('=' ==) kv
    , let value | Text.null v = Nothing
                | otherwise   = Just $! Text.tail v
    ]