{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | While you can instantiate 'Patchable' and 'EventSource' for your
-- own data types, it's a bit complicated. The 'CustomWidget' data
-- type takes care of some lower-level detail, so that you can focus
-- on the custom behavior of your widget. You still need to think
-- about and implement a patching function, but in an easier way.
module GI.Gtk.Declarative.CustomWidget
  ( CustomPatch(..)
  , CustomWidget(..)
  )
where

import           Data.Typeable
import           Data.Vector                    ( Vector )
import qualified GI.Gtk                        as Gtk
import           GI.Gtk.Declarative.Attributes
import           GI.Gtk.Declarative.Attributes.Collected
import           GI.Gtk.Declarative.EventSource
import           GI.Gtk.Declarative.Patch
import           GI.Gtk.Declarative.State

-- | Similar to 'Patch', describing a possible action to perform on a
-- 'Gtk.Widget', decided by 'customPatch'.
data CustomPatch widget internalState
  = CustomReplace
  | CustomModify (widget -> IO internalState)
  | CustomKeep

-- | A custom widget specification, with all functions needed to
-- instantiate 'Patchable' and 'EventSource'. A custom widget:
--
-- * is based on a top 'widget'
-- * can use 'internalState' as a way of keeping an internal state
--   value threaded through updates, which is often useful for passing
--   references to child widgets used in a custom widget
-- * emits events of type 'event'
data CustomWidget widget params internalState event
  = CustomWidget
      { -- | The widget constructor
        customWidget :: Gtk.ManagedPtr widget -> widget,
        -- | Action that creates the initial widget
        customCreate :: params -> IO (widget, internalState),
        -- | Patch function, calculating a 'CustomPatch' based on the state,
        -- old custom data, and new custom data
        customPatch :: params -> params -> internalState -> CustomPatch widget internalState,
        -- | Action that creates an event subscription for the custom widget
        customSubscribe :: params -> internalState -> widget -> (event -> IO ()) -> IO Subscription,
        -- | Declarative 'Attribute's for the custom widget (properties and
        -- classes are handled automatically in patching)
        customAttributes :: Vector (Attribute widget event),
        -- | Parameters passed when constructing the declarative custom widget
        customParams :: params
      }
  deriving (Functor)

instance
  ( Typeable widget,
    Typeable internalState,
    Gtk.IsWidget widget
  ) =>
  Patchable (CustomWidget widget params internalState)
  where

  create custom = do
    (widget, internalState) <- customCreate custom (customParams custom)
    Gtk.widgetShow widget
    let collected = collectAttributes (customAttributes custom)
    updateProperties widget mempty (collectedProperties collected)
    sc <- Gtk.widgetGetStyleContext widget
    updateClasses sc mempty (collectedClasses collected)
    pure
      (SomeState
        (StateTreeWidget (StateTreeNode widget sc collected internalState))
      )

  patch (SomeState (stateTree :: StateTree st w e c cs)) old new =
    case (eqT @cs @internalState, eqT @widget @w) of
      (Just Refl, Just Refl) ->
        let
          oldCollected = stateTreeCollectedAttributes (stateTreeNode stateTree)
          newCollected = collectAttributes (customAttributes new)
          oldCollectedProps = collectedProperties oldCollected
          newCollectedProps = collectedProperties newCollected
          canBeModified = oldCollectedProps `canBeModifiedTo` newCollectedProps
        in
          case
            customPatch new
                        (customParams old)
                        (customParams new)
                        (stateTreeCustomState (stateTreeNode stateTree))
          of
            CustomReplace -> Replace (create new)
            p
              | canBeModified -> Modify $ do
                let widget' = stateTreeNodeWidget stateTree
                updateProperties widget' oldCollectedProps newCollectedProps
                updateClasses
                  (stateTreeStyleContext (stateTreeNode stateTree))
                  (collectedClasses oldCollected)
                  (collectedClasses newCollected)
                let node = stateTreeNode stateTree
                internalState' <- case p of
                  CustomModify f ->
                    f =<< Gtk.unsafeCastTo (customWidget new) widget'
                  CustomKeep    -> pure (stateTreeCustomState node)
                  CustomReplace -> pure (stateTreeCustomState node) -- already handled above
                return
                  (SomeState
                    (StateTreeWidget node
                      { stateTreeCustomState         = internalState'
                      , stateTreeCollectedAttributes = newCollected
                      }
                    )
                  )
              | otherwise -> Replace (create new)
      _ -> Replace (create new)

instance
  (Typeable internalState, Gtk.GObject widget) =>
  EventSource (CustomWidget widget params internalState)
  where
  subscribe custom (SomeState (stateTree :: StateTree st w e c cs)) cb =
    case eqT @cs @internalState of
      Just Refl -> do
        w' <- Gtk.unsafeCastTo (customWidget custom)
                               (stateTreeNodeWidget stateTree)
        customSubscribe custom
                        (customParams custom)
                        (stateTreeCustomState (stateTreeNode stateTree))
                        w'
                        cb
      Nothing -> pure (fromCancellation (pure ()))