{-# LANGUAGE DeriveFunctor     #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}

-- |Internal IRC conduit types and utilities.
module Network.IRC.Conduit.Internal.Messages where

import Control.Applicative ((<$>))
import Data.ByteString     (ByteString, singleton, unpack)
import Data.Char           (ord)
import Data.Maybe          (listToMaybe, isJust)
import Data.Monoid         ((<>))
import Data.String         (fromString)
import Network.IRC.CTCP    (CTCPByteString, getUnderlyingByteString, orCTCP)
import Text.Read           (readMaybe)

import qualified Data.ByteString       as B
import qualified Data.ByteString.Char8 as B8
import qualified Network.IRC           as I

-- *Type synonyms
type ChannelName a = a
type NickName    a = a
type ServerName  a = a
type Reason      a = Maybe a
type IsModeSet     = Bool
type ModeFlag    a = a
type ModeArg     a = a
type NumericArg  a = a

-- |The target of a message. Will be a nick or channel name.
type Target      a = a

type IrcEvent   = Event ByteString
type IrcSource  = Source ByteString
type IrcMessage = Message ByteString

-- *Messages

-- |A decoded IRC message + source.
data Event a = Event
    { _raw     :: ByteString
    -- ^The message as a bytestring.
    , _source  :: Source a
    -- ^The source of the message (user, channel, or server).
    , _message :: Message a
    -- ^The decoded message. This will never be a 'RawMsg'.
    }
    deriving (Eq, Functor, Show)

-- |The source of an IRC message.
data Source a = User (NickName a)
              -- ^The message comes directly from a user.
              | Channel (ChannelName a) (NickName a)
              -- ^The message comes from a user in a channel.
              | Server (ServerName a)
              -- ^The message comes directly from the server.
              deriving (Eq, Functor, Show)

-- |A decoded IRC message.
data Message a = Privmsg (Target a) (Either CTCPByteString a)
               -- ^A message, either from a user or to a channel the
               -- client is in. CTCPs are distinguished by starting
               -- and ending with a \\001 (SOH).
               | Notice (Target a) (Either CTCPByteString a)
               -- ^Like a privmsg, but should not provoke an automatic
               -- response.
               | Nick (NickName a)
               -- ^Someone has updated their nick.
               | Join (ChannelName a)
               -- ^Someone has joined a channel.
               | Part (ChannelName a) (Reason a)
               -- ^Someone has left a channel.
               | Quit (Reason a)
               -- ^Someone has left the network.
               | Mode (Target a) IsModeSet [ModeFlag a] [ModeArg a]
               -- ^Someone has set some channel modes or user modes.
               | Topic (ChannelName a) a
               -- ^Someone has set the topic of a channel.
               | Invite (ChannelName a) (NickName a)
               -- ^The client has been invited to a channel.
               | Kick (ChannelName a) (NickName a) (Reason a)
               -- ^Someone has been kicked from a channel.
               | Ping (ServerName a) (Maybe (ServerName a))
               -- ^The client has received a server ping, and should
               -- send a pong asap.
               | Pong (ServerName a)
               -- ^A pong sent to the named server.
               | Numeric Int [NumericArg a]
               -- ^One of the many server numeric responses.
               | RawMsg a
               -- ^Never produced by decoding, but can be used to send
               -- arbitrary bytestrings to the IRC server. Naturally,
               -- this should only be used when you are confident that
               -- the produced bytestring will be a valid IRC message.
               deriving (Eq, Functor, Show)

-- *Decoding messages

fromByteString :: ByteString -> Either ByteString IrcEvent
fromByteString bs = maybe (Left bs) Right $ uncurry (Event bs) <$> attemptDecode bs

-- |Attempt to decode a ByteString into a message, returning a Nothing
-- if either the source or the message can't be determined.
attemptDecode :: ByteString -> Maybe (IrcSource, IrcMessage)
attemptDecode bs = I.decode bs >>= decode'
  where
    decode' msg = case msg of
      -- Disambiguate PRIVMSG and NOTICE source by checking the first
      -- character of the target
      I.Message (Just (I.NickName n _ _)) "PRIVMSG" [t, m] | isChan t  -> Just (Channel t n, privmsg t m)
                                                           | otherwise -> Just (User n,      privmsg t m)

      I.Message (Just (I.NickName n _ _)) "NOTICE"  [t, m] | isChan t  -> Just (Channel t n, notice t m)
                                                           | otherwise -> Just (User n,      notice t m)

      I.Message (Just (I.NickName n _ _)) "NICK"   [n']    -> Just (User n,      Nick n')
      I.Message (Just (I.NickName n _ _)) "JOIN"   [c]     -> Just (Channel c n, Join c)
      I.Message (Just (I.NickName n _ _)) "PART"   (c:r)   -> Just (Channel c n, Part c   $ listToMaybe r)
      I.Message (Just (I.NickName n _ _)) "QUIT"   r       -> Just (User n,      Quit     $ listToMaybe r)
      I.Message (Just (I.NickName n _ _)) "KICK"   (c:u:r) -> Just (Channel c n, Kick c u $ listToMaybe r)
      I.Message (Just (I.NickName n _ _)) "INVITE" [_, c]  -> Just (User n,      Invite c n)
      I.Message (Just (I.NickName n _ _)) "TOPIC"  [c, t]  -> Just (Channel c n, Topic  c t)

      I.Message (Just (I.NickName n _ _)) "MODE" (t:fs:as) | n == t    -> (User n,)      <$> mode t fs as
                                                           | otherwise -> (Channel t n,) <$> mode t fs as

      I.Message (Just (I.Server s)) "PING" (s1:s2) -> Just (Server s,  Ping s1 $ listToMaybe s2)
      I.Message Nothing             "PING" (s1:s2) -> Just (Server s1, Ping s1 $ listToMaybe s2)

      I.Message (Just (I.Server s)) n args | isNumeric n -> (Server s,) <$> numeric n args

      _ -> Nothing

    -- An IRC channel name can start with '#', '&', '+', or '!', all
    -- of which have different meanings. However, most servers only
    -- support '#'.
    isChan t = B.take 1 t `elem` ["#", "&", "+", "!"]

    -- Check if the message looks like a ctcp or not, and produce the appropriate message type.
    privmsg t = Privmsg t . (Right `orCTCP` Left)
    notice  t = Notice  t . (Right `orCTCP` Left)

    -- Decode a set of mode changes
    mode t fs as = case unpack fs of
      (f:fs') | f == fromIntegral (ord '+') -> Just $ Mode t True  (map singleton fs') as
              | f == fromIntegral (ord '-') -> Just $ Mode t False (map singleton fs') as
      _ -> Nothing

    -- Parse the number in a numeric response
    isNumeric = isJust . (readMaybe :: String -> Maybe Int) . B8.unpack
    numeric n args = flip Numeric args <$> readMaybe (B8.unpack n)

-- *Encoding messages

-- |Encode an IRC message into a single bytestring suitable for
-- sending to the server.
toByteString :: IrcMessage -> ByteString
toByteString (Privmsg t (Left ctcpbs))  = mkMessage "PRIVMSG" [t, getUnderlyingByteString ctcpbs]
toByteString (Privmsg t (Right bs))     = mkMessage "PRIVMSG" [t, bs]
toByteString (Notice  t (Left ctcpbs))  = mkMessage "NOTICE"  [t, getUnderlyingByteString ctcpbs]
toByteString (Notice  t (Right bs))     = mkMessage "NOTICE"  [t, bs]
toByteString (Nick n)                   = mkMessage "NICK"    [n]
toByteString (Join c)                   = mkMessage "JOIN"    [c]
toByteString (Part c (Just r))          = mkMessage "PART"    [c, r]
toByteString (Part c Nothing)           = mkMessage "PART"    [c]
toByteString (Quit (Just r))            = mkMessage "QUIT"    [r]
toByteString (Quit Nothing)             = mkMessage "QUIT"    []
toByteString (Mode t True  ms as)       = mkMessage "MODE"    $ t : ("+" <> B.concat ms) : as
toByteString (Mode t False ms as)       = mkMessage "MODE"    $ t : ("-" <> B.concat ms) : as
toByteString (Invite c n)               = mkMessage "INVITE"  [c, n]
toByteString (Topic c bs)               = mkMessage "TOPIC"   [c, bs]
toByteString (Kick c n (Just r))        = mkMessage "KICK"    [c, n, r]
toByteString (Kick c n Nothing)         = mkMessage "KICK"    [c, n]
toByteString (Ping s1 (Just s2))        = mkMessage "PING"    [s1, s2]
toByteString (Ping s1 Nothing)          = mkMessage "PING"    [s1]
toByteString (Pong s)                   = mkMessage "PONG"    [s]
toByteString (Numeric n as)             = mkMessage (fromString $ show n) as
toByteString (RawMsg bs)                = bs

mkMessage :: ByteString -> [ByteString] -> ByteString
mkMessage cmd = I.encode . I.Message Nothing cmd

-- |Construct a raw message.
rawMessage :: ByteString
           -- ^The command
           -> [ByteString]
           -- ^The arguments
           -> IrcMessage
rawMessage cmd = RawMsg . mkMessage cmd