{-|
Module      : Css.Selector.Utils
Description : A set of utility methods to encode and decode strings.
Maintainer  : hapytexeu+gh@gmail.com
Stability   : experimental
Portability : POSIX

A module to encode and decode css selector strings. These are used in the parser and renderer to parse and render css selector strings.
-}
module Css.Selector.Utils (
    -- * Identifiers
      readIdentifier, encodeIdentifier
    , isValidIdentifier, toIdentifier
    -- * Css strings
    , readCssString, encodeString, encodeText
  ) where

import Control.Arrow(first)

import Data.Char(chr, digitToInt, intToDigit, isAsciiLower, isAsciiUpper, isHexDigit, ord)
import Data.Text(Text, cons, pack, singleton, snoc)
import qualified Data.Text as T

_initLast :: [a] -> Maybe ([a], a)
_initLast [] = Nothing
_initLast (a:as) = Just (go as a)
    where go [] x = ([], x)
          go (y:ys) x = first (x:) (go ys y)

_isQuote :: Char -> Bool
_isQuote '"' = True
_isQuote '\'' = True
_isQuote _ = False

-- | Parses a css string literal to a string that ontains the content of that
-- string literal.
readCssString :: String  -- ^ The string that contains the string literal in the css selector.
    -> String -- ^ A string that contains the content of the string literal.
readCssString (c:xs) | _isQuote c = f
    where f | Just (vs, c') <- _initLast xs = g c' vs
            | otherwise = "The string literal should contain at least two quotation marks."
              where  g c' vs | c == c' = _readCssString c vs
                             | otherwise = "The start and end quotation mark should be the same."
readCssString _ = error "The string should start with an \" or ' and end with the same quotation."

_readCssString :: Char -> String -> String
_readCssString c' = go
    where go [] = []
          go ('\\':'\n':xs) = go xs
          go ('\\':ca@(c:xs)) | c == c' = c : go xs
                              | otherwise = let ~(y,ys) = _parseEscape ca in y : go ys
          go (x:xs) | x == c' = error "The string can not contain a " ++ show x ++ ", you should escape it."
                    | otherwise = x : go xs

-- | Parse a given css identifier to the content of the identifier.
readIdentifier :: String -- ^ The given css identifier to parse.
    -> String -- ^ The result of the parsing: the content of the identifier.
readIdentifier = _readCssString '\\'

_notEncode :: Char -> Bool
_notEncode x = isAsciiLower x || isAsciiUpper x

-- | Convert a string to a css selector string literal. This is done by putting
-- quotes around the content, and escaping certain characters.
encodeString :: Char -- ^ The type of quotes that should be put around the content (should be @'@ or @"@).
    -> String -- ^ The string that should be converted to a css selector string literal.
    -> String -- ^ The corresponding css selector string literal.
encodeString c' = (c' :) . go
    where go [] = [c']
          go (c:cs) | _notEncode c = c : go cs
                    | otherwise = '\\' : _showHex (ord c) (go cs)

-- | Convert a string to a css selector string literal. This is done by putting
-- quotes around the content, and escaping certain characters.
encodeText :: Char -- ^ The type of quotes that should be put around the content (should be @'@ or @"@).
    -> Text -- ^ The string that should be converted to a css selector string literal.
    -> Text -- ^ The corresponding css selector string literal.
encodeText c' t = cons c' (snoc (T.concatMap _encodeCharacter t) c')

_encodeCharacter :: Char -> Text
_encodeCharacter c
    | _notEncode c = singleton c
    | otherwise = cons '\\' (pack (_showHex (ord c) ""))

-- | Encode a given identifier to its css selector equivalent by escaping
-- certain characters.
encodeIdentifier :: Text -- ^ The identifier to encode.
    -> Text -- ^ The encoded identifier.
encodeIdentifier = T.concatMap _encodeCharacter

_showHex :: Int -> ShowS
_showHex = go (6 :: Int)
    where go 0 _ s = s
          go k n rs = go (k-1) q (intToDigit r : rs)
              where ~(q, r) = quotRem n 16

_parseEscape :: String -> (Char, String)
_parseEscape = go (6 :: Int) 0
    where go 0 n cs = yield n cs
          go _ n "" = yield n ""
          go i n ca@(c:cs) | isHexDigit c = go (i-1) (16*n+digitToInt c) cs
                           | otherwise = yield n ca
          yield n s = (chr n, s)

-- | Check if the given identifier is a valid css selector identifier.
isValidIdentifier :: String  -- ^ The given identifier to check.
    -> Bool -- ^ 'True' if the given identifier is valid, 'False' otherwise.
isValidIdentifier = not . null

-- | Convert the given string to a given object by first checking if it is a
-- valid identifier, and if not raising an error. If it is a valid identifier,
-- the string is packed, and wrapped in the given function.
toIdentifier :: (Text -> a) -- ^ The given function to wrap the 'Text' identifier to an object.
    -> String -- ^ The string to validate, and wrap into the given function.
    -> a -- ^ The identifier object to return if the identifier is valid.
toIdentifier f ident | isValidIdentifier ident = f (pack ident)
                     | otherwise = error ("The identifier " <> show ident <> " is not a valid identifier.")