{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiWayIf #-}
{-# OPTIONS_HADDOCK hide #-}
module Text.HTML.Scalpel.Internal.Select.Combinators (
    (//)
,   (@:)
,   (@=)
,   (@=~)
,   atDepth
,   hasClass
,   match
,   notP
) where

import Text.HTML.Scalpel.Internal.Select.Types

import qualified Data.Text as T
import qualified Text.Regex.Base.RegexLike as RE
import qualified Text.StringLike as TagSoup


-- | The '@:' operator creates a 'Selector' by combining a 'TagName' with a list
-- of 'AttributePredicate's.
(@:) :: TagName -> [AttributePredicate] -> Selector
(@:) tag attrs = MkSelector [(toSelectNode tag attrs, defaultSelectSettings)]
infixl 9 @:

-- | The '@=' operator creates an 'AttributePredicate' that will match
-- attributes with the given name and value.
--
-- If you are attempting to match a specific class of a tag with potentially
-- multiple classes, you should use the 'hasClass' utility function.
(@=) :: AttributeName -> String -> AttributePredicate
(@=) key value = anyAttrPredicate $ \(attrKey, attrValue) ->
                                      matchKey key attrKey
                                      && TagSoup.fromString value == attrValue
infixl 6 @=

-- | The '@=~' operator creates an 'AttributePredicate' that will match
-- attributes with the given name and whose value matches the given regular
-- expression.
(@=~) :: RE.RegexLike re String
      => AttributeName -> re -> AttributePredicate
(@=~) key re = anyAttrPredicate $ \(attrKey, attrValue) ->
       matchKey key attrKey
    && RE.matchTest re (TagSoup.toString attrValue)
infixl 6 @=~

-- | The 'atDepth' operator constrains a 'Selector' to only match when it is at
-- @depth@ below the previous selector.
--
-- For example, @"div" // "a" `atDepth` 1@ creates a 'Selector' that matches
-- anchor tags that are direct children of a div tag.
atDepth :: Selector -> Int -> Selector
atDepth (MkSelector xs) depth = MkSelector (addDepth xs)
  where addDepth []                 = []
        addDepth [(node, settings)] = [
            (node, settings { selectSettingsDepth = Just depth })
          ]
        addDepth (x : xs)           = x : addDepth xs
infixl 6 `atDepth`

-- | The '//' operator creates an 'Selector' by nesting one 'Selector' in
-- another. For example, @"div" // "a"@ will create a 'Selector' that matches
-- anchor tags that are nested arbitrarily deep within a div tag.
(//) :: Selector -> Selector -> Selector
(//) a b = MkSelector (as ++ bs)
    where (MkSelector as) = a
          (MkSelector bs) = b
infixl 5 //

-- | The classes of a tag are defined in HTML as a space separated list given by
-- the @class@ attribute. The 'hasClass' function will match a @class@ attribute
-- if the given class appears anywhere in the space separated list of classes.
hasClass :: String -> AttributePredicate
hasClass clazz = anyAttrPredicate hasClass'
    where
        hasClass' (attrName, classes)
            | "class" == TagSoup.toString attrName = textClass `elem` classList
            | otherwise                            = False
            where textClass   = TagSoup.castString clazz
                  textClasses = TagSoup.castString classes
                  classList   = T.split (== ' ') textClasses

-- | Negates an 'AttributePredicate'.
notP :: AttributePredicate -> AttributePredicate
notP (MkAttributePredicate p) = MkAttributePredicate $ not . p

-- | The 'match' function allows for the creation of arbitrary
-- 'AttributePredicate's. The argument is a function that takes the attribute
-- key followed by the attribute value and returns a boolean indicating if the
-- attribute satisfies the predicate.
match :: (String -> String -> Bool) -> AttributePredicate
match f = anyAttrPredicate $ \(attrKey, attrValue) ->
              f (TagSoup.toString attrKey) (TagSoup.toString attrValue)