{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}
module Graphics.SvgTree.CssTypes
  ( CssSelector( .. )
  , CssSelectorRule
  , CssRule( .. )
  , CssDescriptor( .. )
  , CssDeclaration( .. )
  , CssElement( .. )
  , CssMatcheable( .. )
  , CssContext
  , Dpi
  , Number( .. )
  , serializeNumber
  , findMatchingDeclarations
  , toUserUnit
  , mapNumber
  , tserialize
  ) where
#if !MIN_VERSION_base(4,8,0)
import           Data.Monoid            (mconcat)
#endif
import           Data.List              (intersperse)
import           Data.Monoid            ((<>))
import qualified Data.Text              as T
import qualified Data.Text.Lazy.Builder as TB
import           Text.Printf
import           Codec.Picture          (PixelRGBA8 (..))
import           Graphics.SvgTree.Misc
type Dpi = Int
class TextBuildable a where
    
    tserialize :: a -> TB.Builder
data CssDescriptor
  = OfClass T.Text    
  | OfName  T.Text    
  | OfId    T.Text    
  | OfPseudoClass T.Text     
  | AnyElem                  
  | WithAttrib T.Text T.Text 
  deriving (Eq, Show)
instance TextBuildable CssDescriptor where
  tserialize d = case d of
      OfClass c       -> si '.' <> ft c
      OfName  n       -> ft n
      OfId    i       -> si '#' <> ft i
      OfPseudoClass c -> si '#' <> ft c
      AnyElem         -> si '*'
      WithAttrib a b  -> mconcat [si '[', ft a, si '=', ft b, si ']']
     where
      ft = TB.fromText
      si = TB.singleton
data CssSelector
  = Nearby          
  | DirectChildren  
  | AllOf [CssDescriptor] 
                          
  deriving (Eq, Show)
instance TextBuildable CssSelector where
  tserialize s = case s of
      Nearby         -> si '+'
      DirectChildren -> si '>'
      AllOf lst      -> mconcat $ map tserialize lst
    where
      si = TB.singleton
type CssSelectorRule = [CssSelector]
data CssRule = CssRule
    { 
      
      
      cssRuleSelector :: ![CssSelectorRule]
      
    , cssDeclarations :: ![CssDeclaration]
    }
    deriving (Eq, Show)
instance TextBuildable CssRule where
  tserialize (CssRule selectors decl) =
      mconcat tselectors
                 <> ft " {\n"
                 <> mconcat (fmap tserializeDecl decl)
                 <> ft "}\n"
     where
      ft = TB.fromText
      tserializeDecl d = ft "  " <> tserialize d <> ft ";\n"
      tselector =
          mconcat . intersperse (ft " ") . fmap tserialize
      tselectors =
          intersperse (ft ",\n") $ fmap tselector selectors
class CssMatcheable a where
  
  cssIdOf     :: a -> Maybe T.Text
  
  cssClassOf  :: a -> [T.Text]
  
  cssNameOf   :: a -> T.Text
  
  cssAttribOf :: a -> T.Text -> Maybe T.Text
type CssContext a = [[a]]
isDescribedBy :: CssMatcheable a
              => a -> [CssDescriptor] -> Bool
isDescribedBy e = all tryMatch
  where
    tryMatch (OfClass t)       = t `elem` cssClassOf e
    tryMatch (OfId    i)       = cssIdOf e == Just i
    tryMatch (OfName  n)       = cssNameOf e == n
    tryMatch (OfPseudoClass _) = False
    tryMatch (WithAttrib a v)  = cssAttribOf e a == Just v
    tryMatch AnyElem           = True
isMatching :: CssMatcheable a
           => CssContext a -> [CssSelector] -> Bool
isMatching = go where
  go  _ [] = True
  go []  _ = False
  go ((_ : near):upper) (Nearby : rest) = go (near:upper) rest
  go ((e:_):upper) (DirectChildren:AllOf descr:rest)
    | isDescribedBy e descr = go upper rest
  go _ (DirectChildren:_) = False
  go ((e:_):upper) selectors@(AllOf descr : rest)
    | isDescribedBy e descr = go upper rest
    | otherwise = go upper selectors
  go (_:upper) selector = go upper selector
findMatchingDeclarations :: CssMatcheable a
                         => [CssRule] -> CssContext a -> [CssDeclaration]
findMatchingDeclarations rules context =
    concat [cssDeclarations rule
                    | rule <- rules
                    , selector <- cssRuleSelector rule
                    , isMatching context $ reverse selector ]
data CssDeclaration = CssDeclaration
    { 
      _cssDeclarationProperty :: T.Text
      
    , _cssDecarationlValues   :: [[CssElement]]
    }
    deriving (Eq, Show)
instance TextBuildable CssDeclaration where
  tserialize (CssDeclaration n elems) =
      mconcat $ ft n : ft ": " : intersperse (si ' ') finalElems
     where
      finalElems = map tserialize (concat elems)
      ft = TB.fromText
      si = TB.singleton
data Number
  = Num Double       
  | Px Double        
  | Em Double        
  | Percent Double   
  | Pc Double
  | Mm Double        
  | Cm Double        
  | Point Double     
  | Inches Double    
  deriving (Eq, Show)
mapNumber :: (Double -> Double) -> Number -> Number
mapNumber f nu = case nu of
  Num n     -> Num $ f n
  Px n      -> Px $ f n
  Em n      -> Em $ f n
  Percent n -> Percent $ f n
  Pc n      -> Pc $ f n
  Mm n      -> Mm $ f n
  Cm n      -> Cm $ f n
  Point n   -> Point $ f n
  Inches n  -> Inches $ f n
serializeNumber :: Number -> String
serializeNumber n = case n of
    Num c     -> ppD c
    Px c      -> printf "%spx" (ppD c)
    Em cc     -> printf "%sem" (ppD cc)
    Percent p -> printf "%d%%" (floor $ 100 * p :: Int)
    Pc p      -> printf "%spc" (ppD p)
    Mm m      -> printf "%smm" (ppD m)
    Cm c      -> printf "%scm" (ppD c)
    Point p   -> printf "%spt" (ppD p)
    Inches i  -> printf "%sin" (ppD i)
instance TextBuildable Number where
   tserialize = TB.fromText . T.pack . serializeNumber
data CssElement
    = CssIdent     !T.Text
    | CssString    !T.Text
    | CssReference !T.Text
    | CssNumber    !Number
    | CssColor     !PixelRGBA8
    | CssFunction  !T.Text ![CssElement]
    | CssOpComa
    | CssOpSlash
    deriving (Eq, Show)
instance TextBuildable CssElement where
  tserialize e = case e of
    CssIdent    n -> ft n
    CssString   s -> si '"' <> ft s <> si '"'
    CssReference r -> si '#' <> ft r
    CssNumber   n -> tserialize n
    CssColor  (PixelRGBA8 r g b _) ->
      ft . T.pack $ printf  "#%02X%02X%02X" r g b
    CssFunction t els -> mconcat $ ft t : si '(' : args ++ [si ')']
        where args = intersperse (ft ", ") (map tserialize els)
    CssOpComa -> si ','
    CssOpSlash -> si '/'
    where
      ft = TB.fromText
      si = TB.singleton
toUserUnit :: Dpi -> Number -> Number
toUserUnit dpi = go where
  go nu = case nu of
    Num _     -> nu
    Px p      -> go $ Num p
    Em _      -> nu
    Percent _ -> nu
    Pc n      -> go . Inches $ (12 * n) / 72
    Inches n  -> Num $ n * fromIntegral dpi
    Mm n      -> go . Inches $ n / 25.4
    Cm n      -> go . Inches $ n / 2.54
    Point n   -> go . Inches $ n / 72