{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternGuards     #-}
module Graphics.SvgTree.CssParser
  ( CssElement( .. )
  , complexNumber
  , declaration
  , ruleSet
  , styleString
  , dashArray
  , numberList
  , num
  , cssRulesOfText
  ) where

#if !MIN_VERSION_base(4,8,0)
import           Control.Applicative        (pure, (*>), (<$), (<$>), (<*),
                                             (<*>))
#endif

import           Control.Applicative        (many, (<|>))
import           Data.Attoparsec.Text       (Parser, char, digit, double,
                                             letter, notChar, parseOnly, sepBy1,
                                             skipMany, skipSpace, string, (<?>))
import qualified Data.Attoparsec.Text       as AT

import           Data.Attoparsec.Combinator (many1, option, sepBy)

import           Codec.Picture              (PixelRGBA8 (..))
import qualified Data.Map                   as M
import qualified Data.Text                  as T
import           Graphics.SvgTree.ColorParser   (colorParser)
import           Graphics.SvgTree.CssTypes
import           Graphics.SvgTree.NamedColors   (svgNamedColors)

num :: Parser Double
num = realToFrac <$> (skipSpace *> plusMinus <* skipSpace)
  where doubleNumber = char '.' *> (scale <$> double)
                    <|> double

        scalingCoeff n = 10 ^ digitCount
          where digitCount :: Int
                digitCount = ceiling . logBase 10 $ abs n

        scale n = n / scalingCoeff n

        plusMinus = negate <$ string "-" <*> doubleNumber
                 <|> string "+" *> doubleNumber
                 <|> doubleNumber


ident :: Parser T.Text
ident =
  (\f c -> f . T.cons c . T.pack)
        <$> trailingSub
        <*> nmstart <*> nmchar
  where
    trailingSub = option id $ T.cons '-' <$ char '-'
    underscore = char '_'
    nmstart = letter <|> underscore
    nmchar = many (letter <|> digit <|> underscore <|> char '-')

str :: Parser T.Text
str = char '"' *> AT.takeWhile (/= '"') <* char '"' <* skipSpace
   <?> "str"

between :: Char -> Char -> Parser a -> Parser a
between o e p =
  (skipSpace *>
      char o *> skipSpace *> p
           <* skipSpace <* char e <* skipSpace)
           <?> ("between " ++ [o, e])

bracket :: Parser a -> Parser a
bracket = between '[' ']'


comment :: Parser ()
comment = string "/*" *> toStar *> skipSpace
  where
    toStar = skipMany (notChar '*') *> char '*' *> testEnd
    testEnd = (() <$ char '/') <|> toStar

cleanSpace :: Parser ()
cleanSpace = skipSpace <* many comment

-- | combinator: '+' S* | '>' S*
combinator :: Parser CssSelector
combinator = parse <* cleanSpace where
  parse = Nearby <$ char '+'
       <|> DirectChildren <$ char '>'
       <?> "combinator"

-- unary_operator : '-' | '+' ;

commaWsp :: Parser Char
commaWsp = skipSpace *> option ',' (char ',') <* skipSpace

ruleSet :: Parser CssRule
ruleSet = cleanSpace *> rule where
  rule = CssRule
      <$> selector `sepBy1` commaWsp
      <*> (between '{' '}' styleString)
      <?> "cssrule"

styleString :: Parser [CssDeclaration]
styleString = ((cleanSpace *> declaration) `sepBy` semiWsp) <* mayWsp
           <?> "styleString"
  where semiWsp = skipSpace *> char ';' <* skipSpace
        mayWsp = option ';' semiWsp

selector :: Parser [CssSelector]
selector = (:)
        <$> (AllOf <$> simpleSelector <* skipSpace <?> "firstpart:(")
        <*> ((next <|> return []) <?> "secondpart")
        <?> "selector"
  where
    combOpt :: Parser ([CssSelector] -> [CssSelector])

    combOpt = cleanSpace *> option id ((:) <$> combinator)
    next :: Parser [CssSelector]
    next = id <$> combOpt <*> selector

simpleSelector :: Parser [CssDescriptor]
simpleSelector = (:) <$> elementName <*> many whole
              <|> (many1 whole <?> "inmany")
              <?> "simple selector"
 where
  whole = pseudo <|> hash <|> classParser <|> attrib
       <?> "whole"
  pseudo = char ':' *> (OfPseudoClass <$> ident)
        <?> "pseudo"
  hash = char '#' *> (OfId <$> ident)
      <?> "hash"
  classParser = char '.' *> (OfClass <$> ident)
              <?> "classParser"

  elementName = el <* skipSpace <?> "elementName"
    where el = (OfName <$> ident)
            <|> AnyElem <$ char '*'

  attrib = bracket
    (WithAttrib <$> ident <*> (char '=' *> skipSpace *> (ident <|> str))
           <?> "attrib")

declaration :: Parser CssDeclaration
declaration =
  CssDeclaration <$> property
                 <*> (char ':'
                      *> cleanSpace
                      *> many1 expr
                      <* prio
                      )
                 <?> "declaration"
  where
    property = (ident <* cleanSpace) <?> "property"
    prio = option "" $ string "!important"

operator :: Parser CssElement
operator = skipSpace *> op <* skipSpace
  where
    op = CssOpSlash <$ char '/'
      <|> CssOpComa <$ char ','
      <?> "operator"

expr :: Parser [CssElement]
expr = ((:) <$> term <*> (concat <$> many termOp))
    <?> "expr"
  where
    op = option (:[]) $ (\a b -> [a, b]) <$> operator
    termOp = ($) <$> op <*> term

dashArray :: Parser [Number]
dashArray = skipSpace *> (complexNumber `sepBy1` commaWsp)

numberList :: Parser [Double]
numberList = skipSpace *> (num `sepBy1` commaWsp)

complexNumber :: Parser Number
complexNumber = do
    n <- num
    (Percent (n / 100) <$ char '%')
        <|> (Em n <$ string "em")
        <|> (Mm n <$ string "mm")
        <|> (Cm n <$ string "cm")
        <|> (Point n <$ string "pt")
        <|> (Pc n <$ string "pc")
        <|> (Px n <$ string "px")
        <|> (Inches n <$ string "in")
        <|> pure (Num n)

term :: Parser CssElement
term = checkRgb <$> function
    <|> (CssNumber <$> complexNumber)
    <|> (CssString <$> str)
    <|> (checkNamedColor <$> ident)
    <|> (CssColor <$> colorParser)
  where
    comma = skipSpace *> char ',' <* skipSpace
    checkNamedColor n
        | Just c <- M.lookup n svgNamedColors = CssColor c
        | otherwise = CssIdent n

    ref = char '#' *> ident

    checkRgb (CssFunction "rgb"
                [CssNumber r, CssNumber g, CssNumber b]) =
        CssColor $ PixelRGBA8 (to r) (to g) (to b) 255
       where clamp = max 0 . min 255
             to (Num n)     = floor $ clamp n
             to (Px n)      = floor $ clamp n
             to (Percent p) = floor . clamp $ p * 255
             to (Em c)      = floor $ clamp c
             to (Pc n)      = floor $ clamp n
             to (Mm n)      = floor $ clamp n
             to (Cm n)      = floor $ clamp n
             to (Point n)   = floor $ clamp n
             to (Inches n)  = floor $ clamp n

    checkRgb a = a
    functionParam = (CssReference <$> ref) <|> term

    function = CssFunction
       <$> ident <* char '('
       <*> (functionParam `sepBy` comma) <* char ')' <* skipSpace

-- | Parse CSS text into rules.
cssRulesOfText :: T.Text -> [CssRule]
cssRulesOfText txt = case parseOnly (many1 ruleSet) $ txt of
    Left _      -> []
    Right rules -> rules