------------------------------------------------------------------------------
-- | Defined to allow the constructor of 'MediaType' to be exposed to tests.
module Network.HTTP.Media.MediaType.Internal
    ( MediaType (..)
    , Parameters
    ) where

------------------------------------------------------------------------------
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive  as CI
import qualified Data.Map              as Map

------------------------------------------------------------------------------
import Control.Monad        (foldM, guard)
import Data.ByteString      (ByteString)
import Data.CaseInsensitive (CI, original)
import Data.Map             (Map)
import Data.Maybe           (fromMaybe)
import Data.Monoid          ((<>))
import Data.String          (IsString (..))

------------------------------------------------------------------------------
import Network.HTTP.Media.Accept       (Accept (..))
import Network.HTTP.Media.RenderHeader (RenderHeader (..))
import Network.HTTP.Media.Utils        (breakChar, trimBS)


------------------------------------------------------------------------------
-- | An HTTP media type, consisting of the type, subtype, and parameters.
data MediaType = MediaType
    { mainType   :: CI ByteString  -- ^ The main type of the MediaType
    , subType    :: CI ByteString  -- ^ The sub type of the MediaType
    , parameters :: Parameters     -- ^ The parameters of the MediaType
    } deriving (Eq, Ord)

instance Show MediaType where
    show = BS.unpack . renderHeader

instance IsString MediaType where
    fromString str = flip fromMaybe (parseAccept $ BS.pack str) $
        error $ "Invalid media type literal " ++ str

instance Accept MediaType where
    parseAccept bs = do
        (s, ps) <- uncons (map trimBS (BS.split ';' bs))
        (a, b)  <- breakChar '/' s
        guard $ not (BS.null a || BS.null b) && (a /= "*" || b == "*")
        ps' <- foldM insert Map.empty ps
        return $ MediaType (CI.mk a) (CI.mk b) ps'
      where
        uncons []      = Nothing
        uncons (a : b) = Just (a, b)
        both f (a, b) = (f a, f b)
        insert ps =
          fmap (flip (uncurry Map.insert) ps . both CI.mk) . breakChar '='

    matches a b
        | mainType b == "*" = params
        | subType b == "*"  = mainType a == mainType b && params
        | otherwise         = main && sub && params
      where
        main = mainType a == mainType b
        sub = subType a == subType b
        params = Map.null (parameters b) || parameters a == parameters b

    moreSpecificThan a b = (a `matches` b &&) $
        mainType a == "*" && anyB && params ||
        subType a == "*" && (anyB || subB && params) ||
        anyB || subB || params
      where
        anyB = mainType b == "*"
        subB = subType b == "*"
        params = not (Map.null $ parameters a) && Map.null (parameters b)

    hasExtensionParameters _ = True

instance RenderHeader MediaType where
    renderHeader (MediaType a b p) =
        Map.foldrWithKey f (original a <> "/" <> original b) p
      where
        f k v = (<> ";" <> original k <> "=" <> original v)


------------------------------------------------------------------------------
-- | 'MediaType' parameters.
type Parameters = Map (CI ByteString) (CI ByteString)