{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE DeriveGeneric #-}

-- | A small DSL for creating mode strings. The documentation contains parts
-- from <https://modern.ircdocs.horse> only for quick reference. Please refer to
-- the source for up to date documentation on individual modes!

module Network.Yak.Modes
    ( ModeStr
    , emitModeStr
    , fetchModeStr
    , ModeType(..)
    , Mode(..)
    , OpaqueMode(..)
    , ModeParameter(..)
    , ModeOp(..)
    , modeOps
    -- * Building Mode Strings
    -- | Mode Strings can be built using the following three combinators. The
    -- types are polymorphic over different mode types and therefore do not
    -- fully reflect their use.
    --
    -- As an example consider
    --
    -- > foo :: ModeStr
    -- > foo = set ban "foo!bar@quux" <> unset moderated
    --
    -- which translates to
    --
    -- > +b+m foo!bar@quux
    , get
    , set
    , unset
    -- * Common Channel Modes
    , ban
    , exception
    , clientLimit
    , inviteOnly
    , inviteOnlyException
    , key
    , secret
    , moderated
    , protectedTopic
    , noExternal
    -- * Common User Modes
    , UserMode
    , pattern UserMode
    , invisible
    , oper
    , localOper
    , registered
    , wallops
    -- * Channel Member Prefix Modes
    , PrefixMode
    , pattern PrefixMode
    , founder
    , protected
    , operator
    , halfop
    , voice
    -- * Server Modes
    , ServerModes(..)
    , defaultModes
    , defaultPrefixModes
    , defaultUserModes
    -- * Types
    , HostMask
    ) where

import Control.Applicative
import Control.Monad (guard)
import Data.List.NonEmpty (NonEmpty)
import Data.Attoparsec.ByteString.Char8 (Parser)
import Data.ByteString.Char8 (ByteString)
import Data.Foldable
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Void
import GHC.Generics

import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B

data ModeType
    = TypeA
    | TypeB
    | TypeC
    | TypeD
    deriving (Eq, Show, Ord, Read, Generic)

-- | A mode is defined through a character and its type.
--
--  * 'TypeA': Modes that add or remove an address to or from a list. These
--  modes MUST always have a parameter when sent from the server to a client. A
--  client MAY issue this type of mode without an argument to obtain the current
--  contents of the list. The numerics used to retrieve contents of Type A modes
--  depends on the specific mode. Also see the EXTBAN parameter.
--
--  * 'TypeB': Modes that change a setting on a channel. These modes MUST always
--  have a parameter.
--
--  * 'TypeC': Modes that change a setting on a channel. These modes MUST have a
--  parameter when being set, and MUST NOT have a parameter when being unset.
--
--  * 'TypeD': Modes that change a setting on a channel. These modes MUST NOT
--  have a parameter.
--
--  The final type parameter to 'Mode' determines the parameter type of the
--  defined mode.
data Mode :: ModeType -> * -> * where
    ModeTypeA :: Char -> Mode 'TypeA a
    ModeTypeB :: Char -> Mode 'TypeB a
    ModeTypeC :: Char -> Mode 'TypeC a
    ModeTypeD :: Char -> Mode 'TypeD Void

modeChar :: Mode t a -> Char
modeChar (ModeTypeA c) = c
modeChar (ModeTypeB c) = c
modeChar (ModeTypeC c) = c
modeChar (ModeTypeD c) = c

-- | An existential wrapper around modes that hides the parameter type, such
-- that modes of a similar type can be grouped together in a simple container,
-- such as those in a 'ServerModes'.
data OpaqueMode (t :: ModeType) =
    forall a. ModeParameter a => OpaqueMode (Mode t a)

matchOpaque :: Char -> OpaqueMode t -> Bool
matchOpaque c (OpaqueMode m) = modeChar m == c

-- | 'ServerModes' lets you define a dictionary of modes to be passed to the
-- parser (see 'fetchModeStr'). Modes are very server specific and it is not
-- possible to achieve good coverage/compatibility here, so this needs to be
-- done in user code. See 'defaultModes' for a starting point. Note that
-- 'ServerModes' is a monoid, so you can easily augment the standardized modes
-- with custom server specific ones.
data ServerModes = ServerModes
    { typeAModes :: [OpaqueMode 'TypeA]
    , typeBModes :: [OpaqueMode 'TypeB]
    , typeCModes :: [OpaqueMode 'TypeC]
    , typeDModes :: [OpaqueMode 'TypeD]
    }

findAMode :: ServerModes -> Char -> Maybe (OpaqueMode 'TypeA)
findAMode s c = find (matchOpaque c) (typeAModes s)

findBMode :: ServerModes -> Char -> Maybe (OpaqueMode 'TypeB)
findBMode s c = find (matchOpaque c) (typeBModes s)

findCMode :: ServerModes -> Char -> Maybe (OpaqueMode 'TypeC)
findCMode s c = find (matchOpaque c) (typeCModes s)

findDMode :: ServerModes -> Char -> Maybe (OpaqueMode 'TypeD)
findDMode s c = find (matchOpaque c) (typeDModes s)

instance Semigroup ServerModes where
    ServerModes a1 b1 c1 d1 <> ServerModes a2 b2 c2 d2 =
        ServerModes (a1 <> a2) (b1 <> b2) (c1 <> c2) (d1 <> d2)

instance Monoid ServerModes where
    mempty = ServerModes [] [] [] []
    mappend = (<>)

data FetchSt
    = Setting Char
    | Unsetting Char
    | Getting Char

fetchStChar :: FetchSt -> Char
fetchStChar (Setting c) = c
fetchStChar (Unsetting c) = c
fetchStChar (Getting c) = c

matchMode :: ServerModes -> FetchSt -> Parser ModeStr
matchMode smodes f
    | Just (OpaqueMode m) <- findAMode smodes (fetchStChar f) =
        case f of
            Setting _ -> set m <$> seizeMode
            Unsetting _ -> unset m <$> seizeMode
            Getting _ -> pure $ get m
    | Just (OpaqueMode m) <- findBMode smodes (fetchStChar f) =
        case f of
            Setting _ -> set m <$> seizeMode
            Unsetting _ -> unset m <$> seizeMode
            Getting _ -> fail "invalid modestring"
    | Just (OpaqueMode m) <- findCMode smodes (fetchStChar f) =
        case f of
            Setting _ -> set m <$> seizeMode
            Unsetting _ -> pure $ unset m
            Getting _ -> fail "invalid modestring"
    | Just (OpaqueMode m@(ModeTypeD _)) <- findDMode smodes (fetchStChar f) =
        case f of
            Setting _ -> pure $ set m
            Unsetting _ -> pure $ unset m
            Getting _ -> fail "invalid modestring"
    | otherwise = empty

-- | Fetch a 'ModeStr' from a 'ByteString', given some defined collection of
-- modes in the form of a 'ServerModes'. Will return 'Nothing' if the parsing
-- fails.
fetchModeStr :: ServerModes -> ByteString -> Maybe ModeStr
fetchModeStr smodes = either (const Nothing) Just . A.parseOnly go
  where
    go = do
        ms <- modes
        A.skipSpace
        guard $ not . null $ ms
        foldl1 (<>) <$> sequence ms
    modes = do
        ms <- A.takeTill A.isSpace
        let cs = snd $
                B.foldl
                    (\(next, xs) c ->
                         case c of
                             '+' -> (Setting, xs)
                             '-' -> (Unsetting, xs)
                             c' -> (next, next c' : xs))
                    (Getting, [])
                    ms
        pure $ map (matchMode smodes) cs

-- | Emit a 'ModeStr' to a 'ByteString' for use in a message.
emitModeStr :: ModeStr -> ByteString
emitModeStr (ModeStr ms) =
    let cs = B.concat . toList . fmap opModeCmd $ ms
        ps = B.unwords . toList . fmap opModeParam $ ms
     in cs <> " " <> ps

-- | Mode Parameters are types that can be used as parameters to some mode.
class ModeParameter a where
    renderMode :: a -> ByteString
    seizeMode :: Parser a

instance ModeParameter Void where
    renderMode = absurd
    seizeMode = empty

instance ModeParameter Int where
    renderMode = B.pack . show
    seizeMode = A.decimal

instance ModeParameter Word where
    renderMode = B.pack . show
    seizeMode = A.decimal

instance ModeParameter Text where
    renderMode = encodeUtf8
    seizeMode =
        decodeUtf8 <$> do
            x <- A.takeTill (\x -> A.isSpace x || x == ',')
            if B.null x
                then empty
                else pure x

-- | Operations that can be performed with a mode.
data ModeOp where
    -- | Type A modes can be retrieved in list form.
    GetMode :: Mode 'TypeA a -> ModeOp
    SetMode :: ModeParameter a => Mode t a -> Maybe a -> ModeOp
    UnsetMode :: ModeParameter a => Mode t a -> Maybe a -> ModeOp

opModeCmd :: ModeOp -> ByteString
opModeCmd (GetMode m) = B.snoc "+" $ modeChar m
opModeCmd (SetMode m _) = B.snoc "+" $ modeChar m
opModeCmd (UnsetMode m _) = B.snoc "-" $ modeChar m

opModeParam :: ModeOp -> ByteString
opModeParam (GetMode _) = mempty
opModeParam (SetMode _ a) = maybe mempty renderMode a
opModeParam (UnsetMode _ a) = maybe mempty renderMode a

newtype ModeStr = ModeStr { modeOps :: NonEmpty ModeOp }
    deriving Semigroup

get :: Mode 'TypeA a -> ModeStr
get = ModeStr . pure . GetMode

class SetMode m where
    type ModeSetter m
    set :: m -> ModeSetter m

instance ModeParameter a => SetMode (Mode 'TypeA a) where
    type ModeSetter (Mode 'TypeA a) = a -> ModeStr
    set m a = ModeStr . pure $ SetMode m (Just a)

instance ModeParameter a => SetMode (Mode 'TypeB a) where
    type ModeSetter (Mode 'TypeB a) = a -> ModeStr
    set m a = ModeStr . pure $ SetMode m (Just a)

instance ModeParameter a => SetMode (Mode 'TypeC a) where
    type ModeSetter (Mode 'TypeC a) = a -> ModeStr
    set m a = ModeStr . pure $ SetMode m (Just a)

instance SetMode (Mode 'TypeD Void) where
    type ModeSetter (Mode 'TypeD Void) = ModeStr
    set m = ModeStr . pure $ SetMode m Nothing

class UnsetMode m where
    type ModeUnsetter m
    unset :: m -> ModeUnsetter m

instance ModeParameter a => UnsetMode (Mode 'TypeA a) where
    type ModeUnsetter (Mode 'TypeA a) = a -> ModeStr
    unset m a = ModeStr . pure $ UnsetMode m (Just a)

instance ModeParameter a => UnsetMode (Mode 'TypeB a) where
    type ModeUnsetter (Mode 'TypeB a) = a -> ModeStr
    unset m a = ModeStr . pure $ UnsetMode m (Just a)

instance ModeParameter a => UnsetMode (Mode 'TypeC a) where
    type ModeUnsetter (Mode 'TypeC a) = ModeStr
    unset m = ModeStr . pure $ UnsetMode m Nothing

instance UnsetMode (Mode 'TypeD Void) where
    type ModeUnsetter (Mode 'TypeD Void) = ModeStr
    unset m = ModeStr . pure $ UnsetMode m Nothing

type HostMask = Text

-- | This channel mode controls a list of client masks that are ‘banned’ from
-- joining or speaking in the channel. If this mode has values, each of these
-- values should be a client mask.
ban :: Mode 'TypeA HostMask
ban = ModeTypeA 'b'

-- | This channel mode controls a list of client masks that are exempt from the
-- ‘ban’ channel mode. If this mode has values, each of these values should be a
-- client mask.
exception :: Mode 'TypeA HostMask
exception = ModeTypeA 'e'

-- | This channel mode controls whether new users may join based on the number
-- of users who already exist in the channel. If this mode is set, its value is
-- an integer and defines the limit of how many clients may be joined to the
-- channel.
clientLimit :: Mode 'TypeC Int
clientLimit = ModeTypeC 'l'

-- | This channel mode controls whether new users need to be invited to the
-- channel before being able to join.
inviteOnly :: Mode 'TypeD Void
inviteOnly = ModeTypeD 'i'

-- | This channel mode controls a list of channel masks that are exempt from the
-- invite-only channel mode. If this mode has values, each of these values
-- should be a client mask.
inviteOnlyException :: Mode 'TypeA HostMask
inviteOnlyException = ModeTypeA 'I'

-- | This mode letter sets a ‘key’ that must be supplied in order to join this
-- channel. If this mode is set, its’ value is the key that is required.
key :: Mode 'TypeB Text
key = ModeTypeB 'k'

-- | This channel mode controls whether the channel is ‘secret’, and does not
-- have any value.
secret :: Mode 'TypeD Void
secret = ModeTypeD 's'

-- | This channel mode controls whether users may freely talk on the channel,
-- and does not have any value.
moderated :: Mode 'TypeD Void
moderated = ModeTypeD 'm'

-- | This channel mode controls whether channel privileges are required to set
-- the topic, and does not have any value.
protectedTopic :: Mode 'TypeD Void
protectedTopic = ModeTypeD 't'

-- | This channel mode controls whether users who are not joined to the channel
-- can send messages to it, and does not have any value.
noExternal :: Mode 'TypeD Void
noExternal = ModeTypeD 'n'

type UserMode = Mode 'TypeD Void

pattern UserMode :: Char -> UserMode
pattern UserMode c = ModeTypeD c

-- | If a user is set to ‘invisible’, they will not show up in commands such as
-- WHO unless they share a channel with the user that submitted the command. In
-- addition, the only channels that will show up in a WHOIS of an invisible user
-- will be those they share with the user that submitted the command.
invisible :: UserMode
invisible = UserMode 'i'

-- | If a user has this mode, this indicates that they are a network operator.
oper :: UserMode
oper = UserMode 'o'

-- | If a user has this mode, this indicates that they are a server operator. A
-- local operator has operator privileges for their server, and not for the rest
-- of the network.
localOper :: UserMode
localOper = UserMode 'O'

-- | If a user has this mode, this indicates that they have logged into a user
-- account.
registered :: UserMode
registered = UserMode 'r'

-- | If a user has this mode, this indicates that they will receive WALLOPS
-- messages from the server.
wallops :: UserMode
wallops = UserMode 'w'

type PrefixMode = Mode 'TypeB HostMask

pattern PrefixMode :: Char -> PrefixMode
pattern PrefixMode c = ModeTypeB c

founder :: PrefixMode
founder = PrefixMode 'q'

protected :: PrefixMode
protected = PrefixMode 'a'

operator :: PrefixMode
operator = PrefixMode 'o'

halfop :: PrefixMode
halfop = PrefixMode 'h'

voice :: PrefixMode
voice = PrefixMode 'v'

defaultModes :: ServerModes
defaultModes =
    ServerModes
    { typeAModes =
          [ OpaqueMode ban
          , OpaqueMode exception
          , OpaqueMode inviteOnlyException]
    , typeBModes = [OpaqueMode key]
    , typeCModes = [OpaqueMode clientLimit]
    , typeDModes =
          [ OpaqueMode moderated
          , OpaqueMode protectedTopic
          , OpaqueMode noExternal
          , OpaqueMode secret
          ]
    }

defaultUserModes :: ServerModes
defaultUserModes =
    ServerModes
    { typeAModes = []
    , typeBModes = []
    , typeCModes = []
    , typeDModes =
          [ OpaqueMode invisible
          , OpaqueMode oper
          , OpaqueMode localOper
          , OpaqueMode registered
          , OpaqueMode wallops
          ]
    }

defaultPrefixModes :: ServerModes
defaultPrefixModes =
    ServerModes
    { typeAModes = []
    , typeBModes =
          [ OpaqueMode founder
          , OpaqueMode protected
          , OpaqueMode operator
          , OpaqueMode halfop
          , OpaqueMode voice
          ]
    , typeCModes = []
    , typeDModes = []
    }