{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE TypeInType #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DeriveGeneric #-}

module Network.Yak.Types
(
    Prefix(..),
    Host(..),
    hostNick,
    hostUser,
    hostHost,
    Msg(..),
    build,
    buildPrefix,
    vacant,
    (<:>),
    castMsg,
    SomeMsg(..),
    Unused(..),
    Flag(..),
    SList(..),
    CList(..),
    Parameter(..),
    PList(..),
    phead,
    ptail,
    params,
    prefix,
    Channel(..),
    Message(..),
    Nickname,
    Username,
    Hostname,
    Mask,
    Modes(..),
    Token(..),
    UReply(..),
    ureplyNick,
    ureplyIsOp,
    ureplyIsAway,
    ureplyHostname,
    Member(..),
    memberPrefix,
    memberData
)
where

import Control.Applicative
import Control.Lens
import Data.List (intersperse)
import Data.List.NonEmpty (NonEmpty(..))
import Data.ByteString.Char8 (ByteString)
import Data.Attoparsec.ByteString.Char8
import Data.Text (Text)
import Data.Void
import Data.Maybe (fromMaybe)
import Data.Word (Word)
import Data.Proxy
import Data.Time.Clock.POSIX
import Data.Kind (Type)
import Data.Text.Encoding
import Data.String
import GHC.TypeLits
import GHC.Generics
import GHC.Exts (IsList (..))

import qualified Data.Text as T
import qualified Data.ByteString.Char8 as B

type Username = Text
type Nickname = Text
type Mask = Text
type Hostname = Text

declareLenses [d|
    data Host = Host
        { hostNick :: Nickname
        , hostUser :: Maybe Username
        , hostHost :: Maybe Hostname
        }
        deriving (Eq, Show, Read, Ord, Generic)
    |]

data Prefix
    = PrefixServer Text
    | PrefixUser Host
    deriving (Eq, Show, Read, Ord, Generic)

-- | Proxy type for holding IRC messages
data Msg command params = Msg
    { msgPrefix  :: Maybe Prefix
    , msgParams  :: PList params }

instance Eq (PList params) => Eq (Msg command params) where
    Msg a b == Msg c d = a == c && b == d

instance Show (PList params) => Show (Msg command params) where
    show (Msg a b) = "Msg (" ++ show a ++ ") (" ++ show b ++ ")"

vacant :: Msg c '[]
vacant = Msg Nothing PNil

(<:>) :: x -> Msg c xs -> Msg c (x ': xs)
x <:> (Msg p xs) = Msg p (PCons x xs)
infixr 5 <:>

-- | Safely cast one message to another. Equality of parameter lists is
-- statically enforced.
castMsg :: (KnownSymbol c1, KnownSymbol c2) => Msg c1 p -> Msg c2 p
castMsg (Msg c p) = Msg c p

data SomeMsg where
    SomeMsg :: (KnownSymbol c, Parameter (PList p)) => Msg c p -> SomeMsg

-- | Class for any kind of IRC parameter that can be rendered to 'ByteString'
-- and read from a 'ByteString'
class Parameter a where
    render :: a -> ByteString
    seize  :: Parser a

-- | Text is encoded as UTF-8. Pieces of Text are space normally separated. Text
-- parsing enforces the text to be non-empty, and commas are disallowed.
instance Parameter Text where
    render = encodeUtf8
    seize  = decodeUtf8 <$> do
                 x <- takeTill (\x -> isSpace x || x == ',')
                 if B.null x then empty else pure x

-- | Lists are comma separated
instance Parameter a => Parameter [a] where
    render = mconcat . intersperse "," . map render
    seize  = sepBy seize (char ',')

-- | Like lists but non-empty
instance Parameter a => Parameter (NonEmpty a) where
    render (x :| []) = render x
    render (x :| xs) = render (x : xs)

    seize = fromList <$> sepBy1 seize (char ',')

-- | A Maybe parameter can be totally absent
instance Parameter a => Parameter (Maybe a) where
    render (Just x) = render x
    render Nothing  = ""

    seize = optional seize

instance (Parameter x, Parameter (PList xs))
      => Parameter (PList (x ': xs)) where
    render (PCons x PNil) = render x
    render (PCons x xs) =
        case render xs of
            "" -> render x
            _  -> render x <> " " <> render xs

    seize = PCons <$> seize <*> (skipSpace *> seize)

instance Parameter (PList '[]) where
    render _ = ""
    seize  = pure PNil

-- | Proxy type for inserting special syntax
data Unused (a :: Symbol) = Unused deriving (Show, Eq, Ord, Read, Generic)

instance KnownSymbol a => Parameter (Unused (a :: Symbol)) where
    render _ = B.pack . symbolVal $ Proxy @a
    seize = let x = B.pack $ symbolVal (Proxy @a) in Unused <$ string x

-- | Type for expressing possibly missing flags.
data Flag a = Set | Unset deriving (Show, Eq, Ord, Read, Generic)

instance KnownSymbol a => Parameter (Flag (a :: Symbol)) where
    render Set   = B.pack . symbolVal $ Proxy @a
    render Unset = ""
    seize = let x = B.pack $ symbolVal (Proxy @a)
             in fromMaybe Unset <$> optional (Set <$ string x)

instance Parameter ByteString where
    render = id
    seize = takeByteString

instance Parameter Word where
    render = render . T.pack . show
    seize  = decimal

-- | Only positive
instance Parameter Int where
    render = render . T.pack . show
    seize  = decimal

instance Parameter Char where
    render = render . T.singleton
    seize = satisfy (not . isSpace)

instance Parameter Host where
    render h = render
             $ view hostNick h
            <> maybe "" (T.cons '!') (view hostUser h)
            <> maybe "" (T.cons '@') (view hostHost h)
    seize = do
        n <- takeTill (inClass " .!@\r\n")
        p <- peekChar
        case p of
            Just c | c == '.' -> empty
            _ -> Host (decodeUtf8 n)
             <$> optional (decodeUtf8 <$>
                              (char '!' *> takeTill (inClass " @\r\n")))
             <*> optional (decodeUtf8 <$>
                              (char '@' *> takeTill (inClass " \r\n")))

instance (Parameter a, Parameter b) => Parameter (a, b) where
    render (a,b) = render a <> " " <> render b
    seize = (,) <$> seize <*> (skipSpace *> seize)

instance (Parameter a, Parameter b) => Parameter (Either a b) where
    render (Left x)  = render x
    render (Right x) = render x

    seize = (Left <$> seize) <|> (Right <$> seize)

-- | For illegal parameters
instance TypeError ('Text "Illegal IRC Parameter") => Parameter Void where
    render = absurd
    seize  = empty

instance Parameter POSIXTime where
    render x = let x' = truncate x :: Int
                in B.pack . show $ x'
    seize  = do
        (x :: Int) <- decimal
        pure $ fromIntegral x

-- | Space separated lists. Use with caution, since spaces are also the
-- separator for 'PList'!
newtype SList a = SList { getSList :: [a] }
    deriving (Eq, Show, Ord, Read, Functor, Applicative, Monad, Foldable,
              Traversable, Semigroup, Monoid, Alternative, Generic)

instance Wrapped (SList a) where
    type Unwrapped (SList a) = [a]
    _Wrapped' = iso getSList SList

instance (t ~ SList b) => Rewrapped (SList a) t

-- | Syntactic sugar for construction
instance IsList (SList a) where
    type Item (SList a) = a
    fromList = SList
    toList = getSList

instance Parameter a => Parameter (SList a) where
    render = mconcat . intersperse " " . map render . getSList
    seize  = SList <$> sepBy seize space

-- | Space separated lists after colon. Used in some numeric replies
newtype CList a = CList { getCList :: [a] }
    deriving (Eq, Show, Ord, Read, Functor, Applicative, Monad, Foldable,
              Traversable, Semigroup, Monoid, Alternative, Generic)

instance Wrapped (CList a) where
    type Unwrapped (CList a) = [a]
    _Wrapped' = iso getCList CList

instance (t ~ CList b) => Rewrapped (CList a) t

-- | Syntactic sugar for construction
instance IsList (CList a) where
    type Item (CList a) = a
    fromList = CList
    toList = getCList

instance Parameter a => Parameter (CList a) where
    render = mconcat . (":" :) . intersperse " " . map render . getCList
    seize  = CList <$> (char ':' *> sepBy seize space)

-- | Heterogeneous list of parameters.
data PList a where
    PNil  :: PList '[]
    PCons :: forall x xs. x -> PList xs -> PList (x ': xs)

instance Eq (PList '[]) where
    PNil == PNil = True

instance (Eq x, Eq (PList xs)) => Eq (PList (x ': xs)) where
    (PCons x xs) == (PCons y ys) = x == y && xs == ys

instance Show (PList '[]) where
    show PNil = "PNil"

instance (Show x, Show (PList xs)) => Show (PList (x ': xs)) where
    show (PCons x xs) = show x ++ " `PCons` " ++ show xs

phead :: Parameter x' => Lens (PList (x ': xs)) (PList (x' ': xs)) x x'
phead = lens (\(PCons x _) -> x) (\(PCons _ xs) x -> PCons x xs)

ptail :: Lens (PList (x ': xs)) (PList (x ': xs')) (PList xs) (PList xs')
ptail = lens (\(PCons _ xs) -> xs) (\(PCons x _) xs -> PCons x xs)

instance (Parameter x, Parameter x')
      => Field1 (PList (x ': xs))
                (PList (x' ': xs)) x x'
      where
    _1 = phead

instance (Parameter x, Parameter x')
      => Field2 (PList (a ': x ': xs))
                (PList (a ': x' ': xs)) x x'
      where
    _2 = ptail . phead

instance (Parameter x, Parameter x')
      => Field3 (PList (a ': b ': x ': xs))
                (PList (a ': b ': x' ': xs)) x x'
      where
    _3 = ptail . ptail . phead

instance (Parameter x, Parameter x')
      => Field4 (PList (a ': b ': c ': x ': xs))
                (PList (a ': b ': c ': x' ': xs)) x x'
      where
    _4 = ptail . ptail . ptail . phead

instance (Parameter x, Parameter x')
      => Field5 (PList (a ': b ': c ': d ': x ': xs))
                (PList (a ': b ': c ': d ': x' ': xs)) x x'
      where
    _5 = ptail . ptail . ptail . ptail . phead

instance (Parameter x, Parameter x')
      => Field6 (PList (a ': b ': c ': d ': e ': x ': xs))
                (PList (a ': b ': c ': d ': e ': x' ': xs)) x x'
      where
    _6 = ptail . ptail . ptail . ptail . ptail . phead

instance (Parameter x, Parameter x')
      => Field7 (PList (a ': b ': c ': d ': e ': f ': x ': xs))
                (PList (a ': b ': c ': d ': e ': f ': x' ': xs)) x x'
      where
    _7 = ptail . ptail . ptail . ptail . ptail . ptail . phead

instance (Parameter x, Parameter x')
      => Field8 (PList (a ': b ': c ': d ': e ': f ': g ': x ': xs))
                (PList (a ': b ': c ': d ': e ': f ': g ': x' ': xs)) x x'
      where
    _8 = ptail . ptail . ptail . ptail . ptail . ptail . ptail . phead

instance (Parameter x, Parameter x')
      => Field9 (PList (a ': b ': c ': d ': e ': f ': g ': h ': x ': xs))
                (PList (a ': b ': c ': d ': e ': f ': g ': h ': x' ': xs)) x x'
      where
    _9 = ptail . ptail . ptail . ptail . ptail . ptail . ptail . ptail . phead

makeLensesFor [("msgParams", "params"), ("msgPrefix", "prefix")] ''Msg

-- The following bit of code for Build and Reverse is adapted from an example
-- for building HLists with variadic functions, courtesy of lyxia on #haskell.
type family Reverse' (acc :: [Type]) (xs :: [Type]) :: [Type]
type instance Reverse' acc '[] = acc
type instance Reverse' acc (x ': xs) = Reverse' (x ': acc) xs

class HReverse acc xs where
    hReverse' :: PList acc -> PList xs -> PList (Reverse' acc xs)

instance HReverse acc '[] where
    hReverse' = const

instance HReverse (x ': acc) xs => HReverse acc (x ': xs) where
    hReverse' acc (PCons x xs) = hReverse' (PCons x acc) xs

class Build (xs :: [Type]) f where
    build' :: Msg c xs -> f

instance (ys ~ Reverse' '[] xs, HReverse '[] xs) => Build xs (Msg c ys) where
    build' (Msg a b) = Msg a (hReverse' PNil b)

instance Build (x ': xs) g => Build xs (x -> g) where
    build' xs x = build' (x <:> xs)

-- | Generalized constructor function for the creation of 'Msg' values. The
-- types here may seem opaque, but essentially this is a variadic type-safe
-- constructor.
--
-- > build "hunter2" :: Pass
-- > build [Channel "#haskell"] (Message "hello world!") :: PrivMsg
--
-- The type annotations may or may not be necessary, depending on the
-- information available to the compiler at the use site.
build :: Build '[] f => f
build = build' vacant

-- | Like 'build' but takes a 'Prefix' that will be added to the message.
buildPrefix :: Build '[] f => Prefix -> f
buildPrefix p = build' (vacant & prefix .~ Just p)

newtype Channel = Channel { getChannel :: Text }
    deriving (Eq, Show, Ord, Read, IsString, Semigroup, Monoid, Generic)

makeWrapped ''Channel

instance Parameter Channel where
    render = render . getChannel
    seize  = do
        mark <- satisfy (inClass "&#")
        name <- many1 $ satisfy (notInClass " \7,\n")
        pure . Channel . T.pack $ mark : name

newtype Message = Message { getMessage :: Text }
    deriving (Eq, Show, Ord, Read, IsString, Semigroup, Monoid, Generic)

makeWrapped ''Message

instance Parameter Message where
    render = render . T.cons ':' . getMessage
    seize  = Message . decodeUtf8 <$> (char ':' *> takeTill (inClass "\n"))

newtype Modes = Modes { getModes :: [Char] } deriving (Generic)

makeWrapped ''Modes

instance Parameter Modes where
    render = B.pack . getModes
    seize  = Modes <$> many1 (satisfy isAlpha_ascii)

data Token
    = KeyValue Text Text
    | PositiveToken Text
    | NegativeToken Text
    deriving (Eq, Show, Ord, Read, Generic)

makePrisms ''Token

instance Parameter Token where
    render (PositiveToken t) = render . T.toUpper $ t
    render (NegativeToken t) = render . T.cons '-' . T.toUpper $ t
    render (KeyValue t v) = render (T.toUpper t <> "=" <> v)

    seize = kv <|> neg <|> pos
        where ident = T.pack <$> many1 (satisfy (inClass "A-Z"))
              kv = KeyValue <$> ident
                            <*> (char '=' *> (decodeUtf8 <$> takeTill isSpace))
              neg = NegativeToken <$> (char '-' *> ident)
              pos = PositiveToken <$> ident

-- | Replies to 'Userhost' queries.
declareLenses [d|
    data UReply = UReply
        { ureplyNick     :: Nickname
        , ureplyIsOp     :: Bool
        , ureplyIsAway   :: Bool
        , ureplyHostname :: Hostname
        }
        deriving (Eq, Show, Ord, Read, Generic)
    |]

instance Parameter UReply where
    render (UReply nick isop isaway host) =
        render $ nick <> if isop then "*" else "" <> "="
              <> if isaway then "-" else "+" <> host

    seize = UReply
        <$> seize
        <*> option False (True <$ char '*')
        <*> (char '=' *> ((True <$ char '-') <|> (False <$ char '+')))
        <*> seize

declareLenses [d|
    data Member a = Member
        { memberPrefix :: Maybe Char
        , memberData :: a
        }
        deriving (Eq, Show, Ord, Read, Generic)
    |]

instance Parameter a => Parameter (Member a) where
    render (Member p c) = maybe "" B.singleton p <> (render c)
    seize = Member <$> optional (satisfy (inClass "~&@%+")) <*> seize