-- | Code for parsing extensions headers.
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
module Network.WebSockets.Extensions.Description
    ( ExtensionParam
    , ExtensionDescription (..)
    , ExtensionDescriptions

    , parseExtensionDescriptions
    , encodeExtensionDescriptions
    ) where

import           Control.Applicative              ((*>), (<*))
import qualified Data.Attoparsec.ByteString       as A
import qualified Data.Attoparsec.ByteString.Char8 as AC8
import qualified Data.ByteString                  as B
import           Data.Monoid                      (mconcat, mappend)
import           Prelude

type ExtensionParam = (B.ByteString, Maybe B.ByteString)

data ExtensionDescription = ExtensionDescription
    { extName   :: !B.ByteString
    , extParams :: ![ExtensionParam]
    } deriving (Eq, Show)

parseExtensionDescription :: A.Parser ExtensionDescription
parseExtensionDescription = do
    extName   <- parseIdentifier
    extParams <- A.many' (token ';' *> parseParam)
    return ExtensionDescription {..}
  where
    parseIdentifier = AC8.takeWhile isIdentifierChar <* AC8.skipSpace

    token c = AC8.char8 c <* AC8.skipSpace

    isIdentifierChar c =
        (c >= 'a' && c <= 'z') ||
        (c >= 'A' && c <= 'Z') ||
        (c >= '0' && c <= '9') ||
        c == '-' || c == '_'

    parseParam :: A.Parser ExtensionParam
    parseParam = do
        name <- parseIdentifier
        val  <- A.option Nothing $ fmap Just $ token '=' *> parseIdentifier
        return (name, val)

encodeExtensionDescription :: ExtensionDescription -> B.ByteString
encodeExtensionDescription ExtensionDescription {..} =
    mconcat (extName : map encodeParam extParams)
  where
    encodeParam (key, Nothing)  = ";" `mappend` key
    encodeParam (key, Just val) = ";" `mappend` key `mappend` "=" `mappend` val

type ExtensionDescriptions = [ExtensionDescription]

parseExtensionDescriptions :: B.ByteString -> Either String ExtensionDescriptions
parseExtensionDescriptions = A.parseOnly $
    AC8.skipSpace *>
    A.sepBy parseExtensionDescription (AC8.char8 ',' <* AC8.skipSpace) <*
    A.endOfInput

encodeExtensionDescriptions :: ExtensionDescriptions -> B.ByteString
encodeExtensionDescriptions = B.intercalate "," . map encodeExtensionDescription