{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Reflex.Dom.Builder.Static where

import Blaze.ByteString.Builder.Html.Utf8
import Control.Lens hiding (element)
import Control.Monad.Exception
import Control.Monad.Identity
import Control.Monad.Primitive
import Control.Monad.Ref
import Control.Monad.State.Strict
import Control.Monad.Trans.Reader
import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder, byteString, toLazyByteString)
import qualified Data.ByteString.Lazy as LBS
import Data.Default
import Data.Dependent.Map (DMap)
import qualified Data.Dependent.Map as DMap
import Data.Dependent.Sum (DSum (..))
import Data.Functor.Compose
import Data.Functor.Constant
import qualified Data.Map as Map
import Data.Map.Misc (applyMap)
import Data.Monoid
import qualified Data.Set as Set
import Data.Text (Text)
import Data.Text.Encoding
import Data.Tuple
import GHC.Generics
import Reflex.Adjustable.Class
import Reflex.Class
import Reflex.Dom.Main (DomHost, DomTimeline, runDomHost)
import Reflex.Dom.Builder.Class
import Reflex.Dynamic
import Reflex.Host.Class
import Reflex.PerformEvent.Base
import Reflex.PerformEvent.Class
import Reflex.PostBuild.Base
import Reflex.TriggerEvent.Class

data StaticDomBuilderEnv t = StaticDomBuilderEnv
  { _staticDomBuilderEnv_shouldEscape :: Bool
  , _staticDomBuilderEnv_selectValue :: Maybe (Behavior t Text)
    -- ^ When the parent element is a "select" whose value has been set, this value tells us the current value.
    -- We use this to add a "selected" attribute to the appropriate "option" child element.
    -- This is not yet a perfect simulation of what the browser does, but it is much closer than doing nothing.
    -- TODO: Handle edge cases, e.g. setting to a value for which there is no option, then adding that option dynamically afterwards.
  }

newtype StaticDomBuilderT t m a = StaticDomBuilderT
    { unStaticDomBuilderT :: ReaderT (StaticDomBuilderEnv t) (StateT [Behavior t Builder] m) a -- Accumulated Html will be in reversed order
    }
  deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException, MonadAsyncException)

instance PrimMonad m => PrimMonad (StaticDomBuilderT x m) where
  type PrimState (StaticDomBuilderT x m) = PrimState m
  primitive = lift . primitive

instance MonadTrans (StaticDomBuilderT t) where
  lift = StaticDomBuilderT . lift . lift

runStaticDomBuilderT :: (Monad m, Reflex t) => StaticDomBuilderT t m a -> StaticDomBuilderEnv t -> m (a, Behavior t Builder)
runStaticDomBuilderT (StaticDomBuilderT a) e = do
  (result, a') <- runStateT (runReaderT a e) []
  return (result, mconcat $ reverse a')

instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (StaticDomBuilderT t m) where
  {-# INLINABLE newEventWithTrigger #-}
  newEventWithTrigger = lift . newEventWithTrigger
  {-# INLINABLE newFanEventWithTrigger #-}
  newFanEventWithTrigger f = lift $ newFanEventWithTrigger f

instance PerformEvent t m => PerformEvent t (StaticDomBuilderT t m) where
  type Performable (StaticDomBuilderT t m) = Performable m
  {-# INLINABLE performEvent_ #-}
  performEvent_ e = lift $ performEvent_ e
  {-# INLINABLE performEvent #-}
  performEvent e = lift $ performEvent e

instance MonadSample t m => MonadSample t (StaticDomBuilderT t m) where
  {-# INLINABLE sample #-}
  sample = lift . sample

instance MonadHold t m => MonadHold t (StaticDomBuilderT t m) where
  {-# INLINABLE hold #-}
  hold v0 v' = lift $ hold v0 v'
  {-# INLINABLE holdDyn #-}
  holdDyn v0 v' = lift $ holdDyn v0 v'
  {-# INLINABLE holdIncremental #-}
  holdIncremental v0 v' = lift $ holdIncremental v0 v'
  {-# INLINABLE buildDynamic #-}
  buildDynamic a0 = lift . buildDynamic a0
  {-# INLINABLE headE #-}
  headE = lift . headE

instance (Monad m, Ref m ~ Ref IO, Reflex t) => TriggerEvent t (StaticDomBuilderT t m) where
  {-# INLINABLE newTriggerEvent #-}
  newTriggerEvent = return (never, const $ return ())
  {-# INLINABLE newTriggerEventWithOnComplete #-}
  newTriggerEventWithOnComplete = return (never, \_ _ -> return ())
  {-# INLINABLE newEventWithLazyTriggerWithOnComplete #-}
  newEventWithLazyTriggerWithOnComplete _ = return never

instance MonadRef m => MonadRef (StaticDomBuilderT t m) where
  type Ref (StaticDomBuilderT t m) = Ref m
  newRef = lift . newRef
  readRef = lift . readRef
  writeRef r = lift . writeRef r

instance MonadAtomicRef m => MonadAtomicRef (StaticDomBuilderT t m) where
  atomicModifyRef r = lift . atomicModifyRef r

type SupportsStaticDomBuilder t m = (Reflex t, MonadIO m, MonadHold t m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m, Ref m ~ Ref IO, Adjustable t m)

data StaticDomSpace

-- | Static documents never produce any events, so this type has no inhabitants
data StaticDomEvent (a :: k)

-- | Static documents don't process events, so all handlers are equivalent
data StaticDomHandler (a :: k) (b :: k) = StaticDomHandler

data StaticEventSpec (er :: EventTag -> *) = StaticEventSpec deriving (Generic)

instance Default (StaticEventSpec er)

instance DomSpace StaticDomSpace where
  type EventSpec StaticDomSpace = StaticEventSpec
  type RawDocument StaticDomSpace = ()
  type RawTextNode StaticDomSpace = ()
  type RawElement StaticDomSpace = ()
  type RawFile StaticDomSpace = ()
  type RawInputElement StaticDomSpace = ()
  type RawTextAreaElement StaticDomSpace = ()
  type RawSelectElement StaticDomSpace = ()
  addEventSpecFlags _ _ _ _ = StaticEventSpec

instance (SupportsStaticDomBuilder t m, Monad m) => HasDocument (StaticDomBuilderT t m) where
  askDocument = pure ()

instance (Reflex t, Adjustable t m, MonadHold t m) => Adjustable t (StaticDomBuilderT t m) where
  runWithReplace a0 a' = do
    e <- StaticDomBuilderT ask
    (result0, result') <- lift $ runWithReplace (runStaticDomBuilderT a0 e) (flip runStaticDomBuilderT e <$> a')
    o <- hold (snd result0) $ fmapCheap snd result'
    StaticDomBuilderT $ modify $ (:) $ join o
    return (fst result0, fmapCheap fst result')
  traverseDMapWithKeyWithAdjust = hoistDMapWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap
  traverseDMapWithKeyWithAdjustWithMove = hoistDMapWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove

hoistDMapWithKeyWithAdjust :: forall (k :: * -> *) v v' t m p.
  ( Adjustable t m
  , MonadHold t m
  , PatchTarget (p k (Constant (Behavior t Builder))) ~ DMap k (Constant (Behavior t Builder))
  , Patch (p k (Constant (Behavior t Builder)))
  )
  => (forall vv vv'.
         (forall a. k a -> vv a -> m (vv' a))
      -> DMap k vv
      -> Event t (p k vv)
      -> m (DMap k vv', Event t (p k vv'))
     ) -- ^ The base monad's traversal
  -> (forall vv vv'. (forall a. vv a -> vv' a) -> p k vv -> p k vv') -- ^ A way of mapping over the patch type
  -> (forall a. k a -> v a -> StaticDomBuilderT t m (v' a))
  -> DMap k v
  -> Event t (p k v)
  -> StaticDomBuilderT t m (DMap k v', Event t (p k v'))
hoistDMapWithKeyWithAdjust base mapPatch f dm0 dm' = do
  e <- StaticDomBuilderT ask
  (children0, children') <- lift $ base (\k v -> fmap (Compose . swap) (runStaticDomBuilderT (f k v) e)) dm0 dm'
  let result0 = DMap.map (snd . getCompose) children0
      result' = ffor children' $ mapPatch $ snd . getCompose
      outputs0 :: DMap k (Constant (Behavior t Builder))
      outputs0 = DMap.map (Constant . fst . getCompose) children0
      outputs' :: Event t (p k (Constant (Behavior t Builder)))
      outputs' = ffor children' $ mapPatch $ Constant . fst . getCompose
  outputs <- holdIncremental outputs0 outputs'
  StaticDomBuilderT $ modify $ (:) $ pull $ do
    os <- sample $ currentIncremental outputs
    fmap mconcat $ forM (DMap.toList os) $ \(_ :=> Constant o) -> do
      sample o
  return (result0, result')

instance SupportsStaticDomBuilder t m => NotReady t (StaticDomBuilderT t m) where
  notReadyUntil _ = pure ()
  notReady = pure ()

-- TODO: the uses of illegal lenses in this instance causes it to be somewhat less efficient than it can be. replacing them with explicit cases to get the underlying Maybe Event and working with those is ideal.
instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) where
  type DomBuilderSpace (StaticDomBuilderT t m) = StaticDomSpace
  {-# INLINABLE textNode #-}
  textNode (TextNodeConfig initialContents mSetContents) = StaticDomBuilderT $ do
    --TODO: Do not escape quotation marks; see https://stackoverflow.com/questions/25612166/what-characters-must-be-escaped-in-html-5
    shouldEscape <- asks _staticDomBuilderEnv_shouldEscape
    let escape = if shouldEscape then fromHtmlEscapedText else byteString . encodeUtf8
    modify . (:) =<< case mSetContents of
      Nothing -> return (pure (escape initialContents))
      Just setContents -> hold (escape initialContents) $ fmapCheap escape setContents --Only because it doesn't get optimized when profiling is on
    return $ TextNode ()
  {-# INLINABLE element #-}
  element elementTag cfg child = do
    -- https://www.w3.org/TR/html-markup/syntax.html#syntax-elements
    let voidElements = Set.fromList ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
    let noEscapeElements = Set.fromList ["style", "script"]
    let toAttr (AttributeName _mns k) v = byteString (encodeUtf8 k) <> byteString "=\"" <> fromHtmlEscapedText v <> byteString "\""
    es <- newFanEventWithTrigger $ \_ _ -> return (return ())
    StaticDomBuilderT $ do
      let shouldEscape = elementTag `Set.notMember` noEscapeElements
      (result, innerHtml) <- lift $ lift $ runStaticDomBuilderT child $ StaticDomBuilderEnv shouldEscape Nothing
      attrs0 <- foldDyn applyMap (cfg ^. initialAttributes) (cfg ^. modifyAttributes)
      selectValue <- asks _staticDomBuilderEnv_selectValue
      let addSelectedAttr attrs sel = case Map.lookup "value" attrs of
            Just v | v == sel -> attrs <> Map.singleton "selected" ""
            _ -> Map.delete "selected" attrs
      let attrs1 = case (elementTag, selectValue) of
            ("option", Just sv) -> pull $ addSelectedAttr <$> sample (current attrs0) <*> sample sv
            _ -> current attrs0
      let attrs2 = ffor attrs1 $ mconcat . fmap (\(k, v) -> " " <> toAttr k v) . Map.toList
      let tagBS = encodeUtf8 elementTag
      if Set.member elementTag voidElements
        then modify $ (:) $ mconcat [constant ("<" <> byteString tagBS), attrs2, constant (byteString " />")]
        else do
          let open = mconcat [constant ("<" <> byteString tagBS), attrs2, constant (byteString ">")]
          let close = constant $ byteString $ "</" <> tagBS <> ">"
          modify $ (:) $ mconcat [open, innerHtml, close]
      let e = Element
            { _element_events = es
            , _element_raw = ()
            }
      return (e, result)
  {-# INLINABLE inputElement #-}
  inputElement cfg = do
    (e, _result) <- element "input" (cfg ^. inputElementConfig_elementConfig) $ return ()
    let v0 = constDyn $ cfg ^. inputElementConfig_initialValue
    let c0 = constDyn $ cfg ^. inputElementConfig_initialChecked
    let hasFocus = constDyn False -- TODO should this be coming from initialAtttributes
    return $ InputElement
      { _inputElement_value = v0
      , _inputElement_checked = c0
      , _inputElement_checkedChange = never
      , _inputElement_input = never
      , _inputElement_hasFocus = hasFocus
      , _inputElement_element = e
      , _inputElement_raw = ()
      , _inputElement_files = constDyn mempty
      }
  {-# INLINABLE textAreaElement #-}
  textAreaElement cfg = do
    --TODO: Support setValue event
    (e, _domElement) <- element "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
    let v0 = constDyn $ cfg ^. textAreaElementConfig_initialValue
    let hasFocus = constDyn False -- TODO should this be coming from initialAtttributes
    return $ TextAreaElement
      { _textAreaElement_value = v0
      , _textAreaElement_input = never
      , _textAreaElement_hasFocus = hasFocus
      , _textAreaElement_element = e
      , _textAreaElement_raw = ()
      }
  selectElement cfg child = do
    v <- holdDyn (cfg ^. selectElementConfig_initialValue) (cfg ^. selectElementConfig_setValue)
    (e, result) <- element "select" (_selectElementConfig_elementConfig cfg) $ do
      (a, innerHtml) <- StaticDomBuilderT $ lift $ lift $ runStaticDomBuilderT child $ StaticDomBuilderEnv False $ Just (current v)
      StaticDomBuilderT $ lift $ modify $ (:) innerHtml
      return a
    let wrapped = SelectElement
          { _selectElement_value = v
          , _selectElement_change = never
          , _selectElement_hasFocus = constDyn False --TODO: How do we make sure this is correct?
          , _selectElement_element = e
          , _selectElement_raw = ()
          }
    return (wrapped, result)
  placeRawElement () = return ()
  wrapRawElement () _ = return $ Element (EventSelector $ const never) ()

--TODO: Make this more abstract --TODO: Put the WithWebView underneath PerformEventT - I think this would perform better
type StaticWidget x = PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))

{-# INLINE renderStatic #-}
renderStatic :: StaticWidget x a -> IO (a, ByteString)
renderStatic w = do
  runDomHost $ do
    (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
    let env0 = StaticDomBuilderEnv True Nothing
    ((res, bs), FireCommand fire) <- hostPerformEventT $ runStaticDomBuilderT (runPostBuildT w postBuild) env0
    mPostBuildTrigger <- readRef postBuildTriggerRef
    forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
    bs' <- sample bs
    return (res, LBS.toStrict $ toLazyByteString bs')