{-# LANGUAGE CPP                   #-}
{-# LANGUAGE JavaScriptFFI         #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeApplications      #-}
{-# OPTIONS_GHC -ddump-splices #-}


module Shpadoinkle.Html.TH.CSS
  ( extractNamespace
  , textProperty'
  ) where


import           Control.Compactable
import           Data.ByteString.Lazy        as BS (ByteString)
import qualified Data.ByteString.Lazy.Char8  as BS
import           Data.Char                   (toLower)
import           Data.Containers.ListUtils   (nubOrd)
import qualified Data.Set                    as Set
import           Data.String                 (IsString)
import           Data.Text                   (Text)
import           Language.Haskell.TH.Syntax

#ifdef ghcjs_HOST_OS
import           Data.Text.Encoding
import           GHCJS.Marshal.Pure
import           GHCJS.Types                 as T
import           Language.Javascript.JSaddle
import           System.IO.Unsafe            (unsafePerformIO)
#else
import           Text.Regex.PCRE
#endif

import           Shpadoinkle                 (Prop)
import           Shpadoinkle.Html            (ClassList (..))
import           Shpadoinkle.Html.Property   (textProperty')


extractNamespace :: FilePath -> Q [Dec]
extractNamespace fp = do
  css <- runIO $ BS.readFile fp
  return . split . nubOrd $ getAll css


#ifdef ghcjs_HOST_OS

foreign import javascript unsafe "Array.from($1.match(new RegExp($2, 'g')))"
  js_match :: T.JSString -> T.JSString -> IO JSVal


notMempty :: (Eq m, Monoid m) => m -> Maybe m
notMempty x | x == mempty = Nothing
            | otherwise   = Just x


getAll :: ByteString -> [ByteString]
getAll css = unsafePerformIO $ do
  matches <- js_match (pFromJSVal . pToJSVal $ BS.unpack css) selectors
  maybe [] (fmapMaybe $ notMempty . BS.fromStrict . encodeUtf8) <$> fromJSVal matches

#else

getAll :: ByteString -> [ByteString]
getAll css = getAllTextMatches $ css =~ selectors @ByteString

#endif


split :: [ByteString] -> [Dec]
split ss = (toClassDec =<< classes) <> (toIdDec =<< ids) where
  (classes, ids) = fforEither ss $ \selector -> case BS.uncons selector of
    Just ('.', class') -> Left class'
    Just ('#', id')    -> Right id'
    _                  -> error "Selector found that is not and id or class"


toIdDec :: ByteString -> [Dec]
toIdDec ""   = []
toIdDec name = let
    a = VarT $ mkName "a"
    m = VarT $ mkName "m"
    l = mkName "textProperty'"
    name' = case BS.unpack name of
              '#':rs -> rs
              rs     -> rs
    n = mkName $ "id'" <> sanitize name'
  in
    [ SigD n
      (AppT (AppT (TupleT 2) (ConT ''Data.Text.Text)) (AppT (AppT (ConT ''Shpadoinkle.Prop) m) a))
    , ValD (VarP n) (NormalB (AppE (AppE (VarE l) (LitE (StringL "id"))) (LitE (StringL name')))) []
    ]


toClassDec :: ByteString -> [Dec]
toClassDec "" = []
toClassDec n' = let
  n = mkName . sanitize $ case BS.unpack n' of
    '.':rs -> rs
    rs     -> rs
  in
  [ SigD n (ConT ''ClassList)
  , ValD (VarP n) (NormalB (AppE (ConE 'ClassList)
      (AppE (VarE 'Set.singleton) (LitE (StringL $ BS.unpack n'))))) []
  ]


sanitize :: String -> String
sanitize = lowerFirst . (=<<) (\case
  '/' -> "''"
  '-' -> "_"
  ':' -> "'"
  '>' -> "GT"
  x   -> pure x)


lowerFirst :: String -> String
lowerFirst (x:xs) = toLower x : xs
lowerFirst x      = x


selectors :: IsString s => s
selectors = "(#|\\.)-?[_a-zA-Z]+[_a-zA-Z0-9-]*(?=[^}]*\\{)"