content: {-# LANGUAGE OverloadedStrings #-}
  -----------------------------------------------------------------------------
  -- |
  -- Module      : Hasmin.Types.Declaration
  -- Copyright   : (c) 2017 Cristian Adrián Ontivero
  -- License     : BSD3
  -- Stability   : experimental
  -- Portability : unknown
  --
  -----------------------------------------------------------------------------
  module Hasmin.Types.Declaration
      ( Declaration(..)
      , clean
      ) where
  
  import Control.Monad.Reader (Reader, ask)
  import Control.Arrow (first)
  import Control.Monad ((>=>))
  import Data.Map.Strict (Map)
  import Data.Monoid ((<>))
  import Data.Foldable (toList)
  import Data.Maybe (fromMaybe)
  import Data.Sequence (Seq, (|>))
  import Data.List (find, delete, minimumBy, (\\))
  import Data.List.NonEmpty (NonEmpty((:|)))
  import qualified Data.List.NonEmpty as NE
  import Data.Text (Text)
  import Data.Text.Lazy.Builder (singleton, fromText)
  import qualified Data.Map.Strict as Map
  import qualified Data.Text as T
  
  import Hasmin.Config
  import Hasmin.Properties
  import Hasmin.Types.BgSize
  import Hasmin.Class
  import Hasmin.Types.Dimension
  import Hasmin.Types.Numeric
  import Hasmin.Types.PercentageLength
  import Hasmin.Types.Position
  import Hasmin.Types.TransformFunction
  import Hasmin.Types.Value
  import Hasmin.Utils
  
  -- | A CSS <https://www.w3.org/TR/css-syntax-3/#declaration \<declaration\>>.
  data Declaration = Declaration
      { propertyName :: Text   -- ^ Property name of the declaration.
      , valueList    :: Values -- ^ Values used in the declaration.
      , isImportant  :: Bool   -- ^ Whether the declaration is \"!important\" (i.e. ends with it).
      , hasIEhack    :: Bool   -- ^ Whether the declaration ends with the \"\\9\" IE hack.
      } deriving (Eq, Show)
  instance ToText Declaration where
    toBuilder (Declaration p vs i h) = fromText p <> singleton ':'
        <> toBuilder vs <> imp <> iehack
      where imp    = if i then "!important" else mempty
            iehack = if h then "\\9" else mempty
  
  instance Ord Declaration where
    -- Just use a lexicographical ordering, since the instance is required by Set
    d1 <= d2 = toText d1 <= toText d2
  
  instance Minifiable Declaration where
    minify d@(Declaration p vs _ _) = do
        minifiedValues <- minify vs
        conf <- ask
        let name   = case letterCase conf of
                       Lowercase -> T.toLower p
                       Original  -> p
            newDec = d {propertyName = name, valueList = minifiedValues }
        case Map.lookup (T.toCaseFold p) propertyOptimizations of
             Just f  -> propertyTraits newDec >>= f
             Nothing -> propertyTraits newDec
  
  propertyTraits :: Declaration -> Reader Config Declaration
  propertyTraits d@(Declaration p _ _ _) = do
      conf <- ask
      pure $ if shouldUsePropertyTraits conf
                then case Map.lookup (T.toCaseFold p) propertiesTraits of
                       (Just (PropertyInfo vals inhs _ _)) -> minifyDec d vals inhs
                       Nothing           -> d
                else d
  
  -- Map relating properties with specific functions to optimize them
  propertyOptimizations :: Map Text (Declaration -> Reader Config Declaration)
  propertyOptimizations = Map.fromList
    [("transform",                  combineTransformFunctions)
    ,("-webkit-transform",          combineTransformFunctions)
    ,("-moz-transform",             combineTransformFunctions)
    -- ,("font",              optimizeValues optimizeFontFamily)
    ,("font-family",                optimizeValues optimizeFontFamily)
    ,("font-weight",                fontWeightOptimizer)
  
    ,("background-size",            nullPercentageToLength)
    ,("width",                      nullPercentageToLength)
    ,("perspective-origin",         nullPercentageToLength)
    ,("-o-perspective-origin",      nullPercentageToLength)
    ,("-moz-perspective-origin",    nullPercentageToLength)
    ,("-webkit-perspective-origin", nullPercentageToLength)
    ,("background-position",        nullPercentageToLength)
    ,("top",                        nullPercentageToLength)
    ,("right",                      nullPercentageToLength)
    ,("bottom",                     nullPercentageToLength)
    ,("left",                       nullPercentageToLength)
    ,("border-color",               pure . reduceTRBLDec)
    ,("border-width",               pure . reduceTRBLDec)
    ,("border-style",               pure . reduceTRBLDec)
    ,("padding",                    nullPercentageToLength >=> pure . reduceTRBLDec)
    ,("padding-top",                nullPercentageToLength)
    ,("padding-right",              nullPercentageToLength)
    ,("padding-bottom",             nullPercentageToLength)
    ,("padding-left",               nullPercentageToLength)
    ,("margin-top",                 nullPercentageToLength)
    ,("margin-right",               nullPercentageToLength)
    ,("margin-bottom",              nullPercentageToLength)
    ,("margin-left",                nullPercentageToLength)
    ,("margin",                     nullPercentageToLength >=> pure . reduceTRBLDec)
    ,("grid-row-gap",               nullPercentageToLength)
    ,("grid-column-gap",            nullPercentageToLength)
    ,("line-height",                nullPercentageToLength)
    ,("min-height",                 nullPercentageToLength)
    ,("max-width",                  nullPercentageToLength)
    ,("min-width",                  nullPercentageToLength)
    ,("text-indent",                nullPercentageToLength)
    ,("text-transform",             nullPercentageToLength)
    ,("font-size",                  nullPercentageToLength)
    ,("word-spacing",               nullPercentageToLength >=> replaceWithZero "normal")
    ,("vertical-align",             nullPercentageToLength >=> replaceWithZero "baseline")
    -- ,("outline",         replaceWithZero "none")
    -- ,("border",          replaceWithZero "none")
    -- ,("background",      replaceWithZero "0 0")
    ,("transform-origin",         optimizeTransformOrigin >=> nullPercentageToLength)
    ,("-o-transform-origin",      optimizeTransformOrigin >=> nullPercentageToLength)
    ,("-moz-transform-origin",    optimizeTransformOrigin >=> nullPercentageToLength)
    ,("-ms-transform-origin",     optimizeTransformOrigin >=> nullPercentageToLength)
    ,("-webkit-transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
    ]
  
  -- Generic function to map some optimization to a property's values.
  optimizeValues :: (Value -> Reader Config Value)
                 -> Declaration -> Reader Config Declaration
  optimizeValues f d@(Declaration _ vs _ _) = do
      newV <- mapValues f vs
      pure $ d {valueList = newV }
  
  -- converts 0% into 0 (of type <length>)
  -- Do NOT use it with height and max-height, since 0% /= 0
  nullPercentageToLength :: Declaration -> Reader Config Declaration
  nullPercentageToLength d = do
      conf <- ask
      if shouldConvertNullPercentages conf
         then optimizeValues f d
         else pure d
    where f :: Value -> Reader Config Value
          f (PositionV p@(Position _ a _ b)) = pure . PositionV $
              let stripPercentage Nothing  = Nothing
                  stripPercentage (Just x) = if isZero x
                                                then l0
                                                else Just x
              in p { offset1 = stripPercentage a, offset2 = stripPercentage b }
          f (PercentageV p) = pure $ zeroPercentageToLength p
            where zeroPercentageToLength :: Percentage -> Value
                  zeroPercentageToLength 0 = LengthV NullLength
                  zeroPercentageToLength x = PercentageV x
          f (BgSizeV bgsz) = pure . BgSizeV $
              case bgsz of
                BgSize1 x   -> BgSize1 (zeroPerToLength x)
                BgSize2 x y -> BgSize2 (zeroPerToLength x) (zeroPerToLength y)
                x           -> x
            where zeroPerToLength (Left (Left 0)) = Left $ Right NullLength
                  zeroPerToLength z = z
          f x = pure x
  
  -- For word-spacing, normal computes to 0.
  -- For vertical-align, baseline is the same as 0.
  --
  -- Careful: don't apply it for letter-spacing because there is a slight
  -- difference between normal and 0 for that property!
  replaceWithZero :: Text -> Declaration -> Reader Config Declaration
  replaceWithZero s d@(Declaration p (Values v vs) _ _)
      | not (null vs) = pure d -- Some error occured, since there should be only one value
      | otherwise     = pure $
          case Map.lookup (T.toCaseFold p) propertiesTraits of
            Just (PropertyInfo iv inhs _ _) ->
                if f iv inhs == mkOther s
                   then d { valueList = Values (LengthV NullLength) [] }
                   else d
            Nothing -> d
    where f (Just (Values x _)) inh
            | v == Initial || v == Unset && inh == NonInherited = x
            | otherwise                             = v
          f _ _ = v
  
  -- | Converts the keywords "normal" and "bold" to 400 and 700, respectively.
  fontWeightOptimizer :: Declaration -> Reader Config Declaration
  fontWeightOptimizer = optimizeValues f
    where f :: Value -> Reader Config Value
          f x@(Other t) = do
            conf <- ask
            pure $ case fontweightSettings conf of
                     FontWeightMinOn  -> replaceForSynonym t
                     FontWeightMinOff -> x
          f x = pure x
  
          replaceForSynonym :: TextV -> Value
          replaceForSynonym t
            | t == "normal" = NumberV 400
            | t == "bold"   = NumberV 700
            | otherwise     = Other t
  
  optimizeTransformOrigin :: Declaration -> Reader Config Declaration
  optimizeTransformOrigin d@(Declaration _ vals _ _) = do
      conf <- ask
      pure $ if shouldMinifyTransformOrigin conf
                then d { valueList = optimizeTransformOrigin' vals}
                else d
    where
      optimizeTransformOrigin' :: Values -> Values
      optimizeTransformOrigin' v =
          mkValues $ case valuesToList v of
              [x, y, z] -> if isZeroVal z
                              then transformOrigin2 x y
                              else transformOrigin3 x y z
              [x, y]    -> transformOrigin2 x y
              [x]       -> transformOrigin1 x
              x         -> x
  
      transformOrigin1 :: Value -> [Value]
      transformOrigin1 (Other "top")    = [Other "top"]
      transformOrigin1 (Other "bottom") = [Other "bottom"]
      transformOrigin1 (Other "right")  = [PercentageV (Percentage 100)]
      transformOrigin1 (Other "left")   = [LengthV NullLength]
      transformOrigin1 (Other "center") = [PercentageV (Percentage 50)]
      transformOrigin1 (PercentageV 0)  = [LengthV NullLength]
      transformOrigin1 x                = [x]
  
      transformOrigin2 :: Value -> Value -> [Value]
      transformOrigin2 x y
          | equalsCenter x     = firstIsCenter
          | equalsCenter y     = secondIsCenter
          | isYoffsetKeyword x = fmap convertValue [y,x]
          | isXoffsetKeyword y = fmap convertValue [y,x]
          | otherwise          = fmap convertValue [x,y]
        where
          firstIsCenter
              | equalsCenter y           = [per50]
              | isYoffsetKeyword y       = [y]
              | y == per100              = [Other "bottom"]
              | isZeroVal y              = [Other "top"]
              | isPercentageOrLength y = [per50, y]
              | otherwise                = transformOrigin1 y
          secondIsCenter
              | equalsCenter x                                 = [per50]
              | isYoffsetKeyword x || isPercentageOrLength x = [x]
              | otherwise                                      = transformOrigin1 x
          isPercentageOrLength (PercentageV _) = True
          isPercentageOrLength (LengthV _)     = True
          isPercentageOrLength _               = False
          equalsCenter a     = a == Other "center" || a == per50
          isXoffsetKeyword a = a == Other "left" || a == Other "right"
          isYoffsetKeyword a = a == Other "top" || a == Other "bottom"
          per50 = PercentageV $ Percentage 50
          per100 = PercentageV $ Percentage 100
          convertValue (Other t) = fromMaybe (Other t) (Map.lookup (getText t) transformOriginKeywords)
          convertValue n@(PercentageV p)
              | p == 0    = LengthV NullLength
              | otherwise = n
          convertValue i = i
  
      transformOrigin3 :: Value -> Value -> Value -> [Value]
      transformOrigin3 x y z
          | x == Other "top" || x == Other "bottom"
            || y == Other "left" || y == Other "right" = fmap replaceKeywords [y, x, z]
          | otherwise = fmap replaceKeywords [x, y, z]
        where
          replaceKeywords :: Value -> Value
          replaceKeywords (Other t) = fromMaybe x (Map.lookup (getText t) transformOriginKeywords)
          replaceKeywords e         = e
  
  -- isZeroVal is needed because we are using a generic parser for
  -- transform-origin, so 0 parses as a number instead of a distance.
  isZeroVal :: Value -> Bool
  isZeroVal (LengthV (Length 0 _)) = True
  isZeroVal (LengthV NullLength)   = True
  isZeroVal (NumberV (Number 0))   = True
  isZeroVal (PercentageV 0)        = True
  isZeroVal _                      = False
  
  
  -- transform-origin keyword meanings.
  transformOriginKeywords :: Map Text Value
  transformOriginKeywords = Map.fromList
      [("top",    LengthV NullLength)
      ,("right",  PercentageV (Percentage 100))
      ,("bottom", PercentageV (Percentage 100))
      ,("left",   LengthV NullLength)
      ,("center", PercentageV (Percentage 50))]
  
  -- | Minifies a declaration, based on the property's specific traits (i.e. if
  -- it inherits or not, and what its initial value is), and the chosen
  -- configurations.
  minifyDec :: Declaration -> Maybe Values -> Inheritance -> Declaration
  minifyDec d@(Declaration p vs _ _) mv inhs =
      case mv of
        -- Use the found initial values to try to reduce the declaration
        Just initialVals ->
            case Map.lookup (T.toCaseFold p) declarationExceptions of
              -- Use a specific function to reduce the property if
              -- needed, otherwise use the general property reducer
              Just f  -> f d initialVals inhs
              Nothing -> reduceDeclaration d initialVals inhs
        -- Property with no defined initial values. Try to reduce css-wide keywords
        Nothing   ->
            if inhs == NonInherited && vs == initial || inhs == Inherited && vs == inherit
               then d { valueList = unset }
               else d
  
  unset :: Values
  unset = Values Unset mempty
  
  initial :: Values
  initial = Values Initial mempty
  
  inherit :: Values
  inherit = Values Inherit mempty
  
  -- Map of property specific reducers, for properties that don't fit in the
  -- normal declaration minification scheme and need special treatment.
  declarationExceptions :: Map Text (Declaration -> Values -> Inheritance -> Declaration)
  declarationExceptions = Map.fromList $ map (first T.toCaseFold)
      [("background-size",            backgroundSizeReduce)
      ,("-webkit-background-size",    backgroundSizeReduce)
      ,("border-bottom-left-radius",  reduceDefaultingToFirst)
      ,("border-bottom-right-radius", reduceDefaultingToFirst)
      ,("border-top-left-radius",     reduceDefaultingToFirst)
      ,("border-top-right-radius",    reduceDefaultingToFirst)
      ,("font-synthesis",             reduceTwoKeywordInitial)
      ,("overflow",                   reduceDefaultingToFirst)
      ,("overscroll-behavior",        reduceDefaultingToFirst)
      ,("text-emphasis-position",     reduceTwoKeywordInitial)
      -- Needed because otherwise the reducer replaces commas by spaces.
      -- i.e. text-shadow: 1px 1px red,2px 2px blue ==>
      --      text-shadow: 1px 1px red 2px 2px blue.
      ,("text-shadow",             \d _ _ -> d)
      ]
    where
      -- For properties that take two keywords as initial value, such as:
      -- * font-synthesis: weight style
      -- * text-emphasis-position: right over
      reduceTwoKeywordInitial :: Declaration -> Values -> Inheritance -> Declaration
      reduceTwoKeywordInitial d@(Declaration _ vs _ _) initVals inhs =
          case valuesToList initVals \\ valuesToList vs of
            [] -> d {valueList = shortestEquiv initial initVals inhs}
            _  -> d {valueList = shortestEquiv vs initVals inhs}
  
      -- For properties whose second value, if omitted, is copied from the first
      -- (for instance 'overflow').
      reduceDefaultingToFirst :: Declaration -> Values -> Inheritance -> Declaration
      reduceDefaultingToFirst d@(Declaration _ vs _ _) initVals inhs =
          case valuesToList vs of
            [v1,v2] -> if v1 == v2
                          then reduceDeclaration (d { valueList = mkValues [v1] }) initVals inhs
                          else d
            _       -> reduceDeclaration d initVals inhs
  
      backgroundSizeReduce :: Declaration -> Values -> Inheritance -> Declaration
      backgroundSizeReduce d@(Declaration _ vs _ _) initVals inhs =
          case valuesToList vs of
            [v1,v2] -> if v2 == mkOther "auto"
                          then d { valueList = mkValues [v1] }
                          else d
            _       -> d { valueList = shortestEquiv vs initVals inhs }
  
  combineTransformFunctions :: Declaration -> Reader Config Declaration
  combineTransformFunctions d@(Declaration _ vs _ _) = do
      combinedFuncs <- combine (toList tfValues)
      let newVals = fmap TransformV combinedFuncs ++ toList otherValues
      pure $ d { valueList = mkValues newVals}
    where
      decValues               = valuesToList vs
      (tfValues, otherValues) = splitValues decValues
      splitValues = splitValues' (mempty, mempty)
        where
          splitValues' :: (Seq TransformFunction, Seq Value) -> [Value]
                       -> (Seq TransformFunction, Seq Value)
          splitValues' (ts, os) (TransformV x:xs) = splitValues' (ts |> x, os) xs
          splitValues' (ts, os) (x:xs)            = splitValues' (ts, os |> x) xs
          splitValues' (ts, os) []                = (ts, os)
  
  -- Function to reduce the great mayority of properties. Requires that:
  -- 1. The order between values doesn't matter, which is true for most
  --    properties because they take only one value.
  -- 2. Any default value may be removed until a single one remains, which tends
  --    to hold for shorthands because the default value is used when it isn't
  --    present. An example of a property that qualifies is border-bottom, and
  --    one that doesn't is font-synthesis (because it isn't a shorthand).
  reduceDeclaration :: Declaration -> Values -> Inheritance -> Declaration
  reduceDeclaration d@(Declaration _ vs _ _) initVals inhs =
      case analyzeValueDifference vs initVals of
        Just v  -> d {valueList = shortestEquiv v shortestInitialValue inhs}
        Nothing -> d {valueList = minVal inhs shortestInitialValue}
    where
      charLen x y = compare (textualLength x) (textualLength y)
      shortestInitialValue = mkValues [minimumBy charLen (valuesToList initVals)]
  
  -- If the value was a css-wide keyword, return the shortest between css-wide
  -- keywords and the shortest initial value for the property.
  -- Otherwise, no property specific reduction could be done, so just return the
  -- values.
  shortestEquiv :: Values -> Values -> Inheritance -> Values
  shortestEquiv vs siv i
      | i == Inherited && vs == inherit = unset
      | i == NonInherited && vs == unset || vs == initial = minVal i siv
      | otherwise = vs
  
  -- Returns the minimum value (in character length) between the shortest initial
  -- value, and the shortest css-wide keyword (initial or unset)
  minVal :: Inheritance -> Values -> Values
  minVal inhs vs
      | textualLength globalKeyword <= textualLength vs = globalKeyword
      | otherwise                                       = vs
    where globalKeyword = mkValues [if inhs == NonInherited then Unset else Initial]
  
  -- Substract the property's initial value list from the declaration values.
  -- If nothing remains (i.e. every value declared was an initial one, thus all of
  -- them could be left implicit), then just replace it with whatever initial
  -- value is the shortest, otherwise whatever remains is the shortest equivalent
  -- declaration.
  analyzeValueDifference :: Values -> Values -> Maybe Values
  analyzeValueDifference vs initVals =
      case valuesDifference of
        [] -> Nothing -- every value was an initial one
        _  -> Just $ mkValues valuesDifference -- At least a value wasn't an initial one, or it was a css-wide keyword
    where valuesDifference = valuesToList vs \\ valuesToList initVals
  
  -- | Removes longhand rules overwritten by their shorthand further down in the
  -- declaration list, and merges shorthand declarations with longhand properties
  -- later declared.
  clean :: [Declaration] -> [Declaration]
  clean [] = []
  clean (d:ds) =
      let (newD, newDs) = solveClashes ds d pinfo
      in case newD of
           Just x  -> x : clean newDs
           Nothing -> clean newDs -- drop and keep cleaning
    where pinfo = fromMaybe (PropertyInfo Nothing NonInherited mempty mempty) -- No info on the property, use an empty one.
                    (Map.lookup (propertyName d) propertiesTraits)
  
  -- Given a declaration, if it is a shorthand and it has a corresponding
  -- longhand later in the list, merges them. If it is a longhand overwritten by
  -- a shorthand, it deletes it. Otherwise, it keeps the value. In any case, it
  -- returns the new list to analyze and (if any) the value to keep.
  solveClashes :: [Declaration] -> Declaration
               -> PropertyInfo -> (Maybe Declaration, [Declaration])
  solveClashes ds = solveClashes' ds ds
  
  -- Local function, only to be called by solveClashes
  solveClashes' :: [Declaration] -> [Declaration] -> Declaration
                -> PropertyInfo -> (Maybe Declaration, [Declaration])
  solveClashes' newDs []            dec _ = (Just dec, newDs)
  solveClashes' newDs (laterDec:ds) dec pinfo
      -- Do not remove vendor-prefixed values, which are probably fallbacks.
      | hasVendorPrefix dec      = (Just dec, newDs)
      | hasVendorPrefix laterDec || hasIEhack dec /= hasIEhack laterDec =
          solveClashes' newDs ds dec pinfo
      | propertyName laterDec `elem` subproperties pinfo =
          attemptMerge newDs ds dec laterDec pinfo
      | propertyName laterDec `elem` overwrittenBy pinfo =
          if isImportant dec && (not . isImportant) laterDec
             -- important LH with a non-important SH later, or only one of the
             -- two has the \9 ie hack; keep analyzing
             then solveClashes' newDs ds dec pinfo
             -- longhand overwritten by a shorthand; drop it
             else (Nothing, newDs)
      | propertyName dec == propertyName laterDec = {- && dec can be dropped (consider background-image!) -}
          if isImportant dec && (not . isImportant) laterDec
             -- drop non-important one; keep analyzing
             then solveClashes' (delete laterDec newDs) ds dec pinfo
             -- same property twice, drop the overwritten one
             else (Nothing, newDs)
      | otherwise = solveClashes' newDs ds dec pinfo -- keep analyzing the rest
  
  hasVendorPrefix :: Declaration -> Bool
  hasVendorPrefix (Declaration _ vs _ _) = any isVendorPrefixedValue $ valuesToList vs
    where isVendorPrefixedValue :: Value -> Bool
          isVendorPrefixedValue (Other t)         = T.isPrefixOf "-" $ getText t
          isVendorPrefixedValue (GradientV t _)   = T.isPrefixOf "-" t
          isVendorPrefixedValue (GenericFunc t _) = T.isPrefixOf "-" t
          isVendorPrefixedValue _                 = False
  
  attemptMerge :: [Declaration] -> [Declaration] -> Declaration
               -> Declaration -> PropertyInfo
               -> (Maybe Declaration, [Declaration])
  attemptMerge newDs ds dec laterDec pinfo =
      case merge dec laterDec of
        -- Put the merged result back on the list; see if any other
        -- property also conflicts
        Just m -> (Nothing, m : delete dec (delete laterDec newDs))
        -- Couldn't merge, just keep analyzing
        Nothing -> solveClashes' newDs ds dec pinfo
  
  -- TODO: make it such that the order of parameters doesn't matter
  -- First declaration = shorthand
  merge :: Declaration -> Declaration -> Maybe Declaration
  merge d1@(Declaration p1 _ _ _) d2@Declaration{} = do
      mergeFunction <- Map.lookup p1 propertyMergers
      mergeFunction d1 d2
    where propertyMergers :: Map Text (Declaration -> Declaration -> Maybe Declaration)
          propertyMergers = Map.fromList [("margin",       mergeIntoTRBL)
                                         ,("padding",      mergeIntoTRBL)
                                         ,("border-color", mergeIntoTRBL)
                                         ,("border-width", mergeIntoTRBL)
                                         ,("border-style", mergeIntoTRBL)
                                         --,("animation",
                                         --,("background",
                                         --,("background-position",
                                         --,("border",
                                         --,("border-bottom",
                                         --,("border-image",
                                         --,("border-left",
                                         --,("border-radius"
                                         --,("border-right",
                                         --,("border-top"
                                         --,("column-rule",
                                         --,("columns",
                                         --,("flex",
                                         --,("flex-flow",
                                         --,("font",
                                         --,("grid",
                                         --,("grid-area"
                                         --,("grid-column"
                                         --,("grid-gap",
                                         --,("grid-row",
                                         --,("grid-template",
                                         --,("list-style",
                                         --,("mask",
                                         --,("outline",
                                         --,("padding",
                                         --,("text-decoration",
                                         --,("text-emphasis"
                                         --,("transition",
                                         ]
  
  
  -- TODO: consider the check in retDec. We aren't taking the minified length,
  -- only the one as it is. Ideally, it would take the length of the minified
  -- result, according to the program arguments.
  mergeIntoTRBL :: Declaration       -- ^ A margin declaration
                -> Declaration       -- ^ Any of the margin longhands (e.g.: margin-top)
                -> Maybe Declaration -- ^ If successful, the combination of both
  mergeIntoTRBL d1@(Declaration _ (Values v1 vs) i1 h1) d2@(Declaration p2 (Values v2 _) i2 h2)
      | h1 || h2     = Nothing -- TODO handle ie hacks
      | i1 && not i2 = Just $ reduceTRBLDec d1 -- margin:6px !important;margin-top:0; --> margin:6px !important
      | not i1 && i2 = Nothing -- margin:6px;margin-top:0!important; --> stays the same
      | otherwise    = do
            (_,index) <- find (\(x,_) -> T.isInfixOf x (T.toCaseFold p2)) indexTable
            let mkDec ys  = d1 {valueList = mkValues $ replaceAt index v2 ys}
                retDec ys = let mergedDec = reduceTRBLDec (mkDec ys)
                            in if textualLength mergedDec <= originalLength
                                  then Just mergedDec
                                  else Nothing
            case trblValues of
                      [_,_,_,_] -> retDec trblValues
                      [t,r,b]   -> retDec [t,r,b,r]
                      [t,r]     -> retDec [t,r,t,r]
                      [t]       -> retDec [t,t,t,t]
                      _         -> Nothing -- E.g.: an iehack read as a value.
    where originalLength = textualLength d1 + textualLength d2 + 1 -- The (+1) is because of the ;
          trblValues     = v1 : map snd vs
          indexTable     = zip ["top", "right", "bottom", "left"] [0..]
  
  -- E.g.: margin: 6px 6px 6px 6px;  --> margin: 6px;
  --       margin: 1px 0 2px 0;      --> margin: 1px 0 2px;
  -- can be used with "border-image-outset" too.
  reduceTRBLDec :: Declaration -> Declaration
  reduceTRBLDec d@(Declaration _ (Values v1 vs) _ _) =
      d { valueList = mkValues . NE.toList $ reduceTRBL (v1:|map snd vs) }
  
  mapValues :: (Value -> Reader Config Value) -> Values -> Reader Config Values
  mapValues f (Values v1 vs) = do
      x  <- f v1
      xs <- (mapM . mapM) f vs
      pure $ Values x xs
  