{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors -fno-warn-orphans #-}

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedLabels      #-}
{-# LANGUAGE OverloadedLists       #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}

-- | This module implements the patch algorithm for containers.
module GI.Gtk.Declarative.Container.Patch
  ( IsContainer(..)
  , patchInContainer
  )
where

import           Data.Foldable                      (foldMap)
import           Data.Vector                        (Vector, (!?))
import qualified Data.Vector                        as Vector
import qualified GI.Gtk                             as Gtk

import           GI.Gtk.Declarative.Bin
import           GI.Gtk.Declarative.Container.Box
import           GI.Gtk.Declarative.Container.Class
import           GI.Gtk.Declarative.Patch
import           GI.Gtk.Declarative.State
import           GI.Gtk.Declarative.Widget

-- | Patch all children in a container. This does not feature any ID checking,
-- as seen in React, so reordering children in a container can produce many
-- updates.
patchInContainer
  :: ( Gtk.IsWidget container
     , Gtk.IsContainer container
     , Patchable child
     , IsContainer container child
     )
  => StateTree 'ContainerState container child event cs
  -> container
  -> Vector (child e1)
  -> Vector (child e2)
  -> IO (StateTree 'ContainerState container child event cs)
patchInContainer (StateTreeContainer top children) container os' ns' = do
  let maxLength = maximum ([length children, length os', length ns'] :: [Int])
      indices   = Vector.enumFromN 0 (fromIntegral maxLength)
  newChildren <- foldMap
    go
    (Vector.zip4 indices
                 (padMaybes maxLength children)
                 (padMaybes maxLength os')
                 (padMaybes maxLength ns')
    )

  Gtk.widgetQueueResize container
  return (StateTreeContainer top newChildren)
  where
    go = \case
      -- In case we have a corresponding old and new declarative widget, we patch
      -- the GTK widget.
      (i, Just oldChildState, Just old, Just new) ->
        case patch oldChildState old new of
          Modify  modify       -> pure <$> modify
          Replace createWidget -> do
            newChildState  <- createWidget
            oldChildWidget <- someStateWidget oldChildState
            newChildWidget <- someStateWidget newChildState
            replaceChild container new i oldChildWidget newChildWidget
            return (pure newChildState)
          Keep -> return (pure oldChildState)

      -- When there is a new declarative widget, but there already exists a GTK
      -- widget in the corresponding place, we need to replace the GTK widget with
      -- one created from the declarative widget.
      (i, Just oldChildState, Nothing, Just new) -> do
        newChildState  <- create new
        oldChildWidget <- someStateWidget oldChildState
        newChildWidget <- someStateWidget newChildState
        replaceChild container new i oldChildWidget newChildWidget
        return (Vector.singleton newChildState)

      -- When there is a new declarative widget, or one that lacks a corresponding
      -- GTK widget, create and add it.
      (_i, Nothing, _, Just n) -> do
        newChildState <- create n
        w             <- someStateWidget newChildState
        appendChild container n w
        return (Vector.singleton newChildState)

      -- When a declarative widget has been removed, remove the GTK widget from
      -- the container.
      (_i, Just childState, Just _, Nothing) -> do
        Gtk.widgetDestroy =<< someStateWidget childState
        return Vector.empty

      -- When there are more old declarative widgets than GTK widgets, we can
      -- safely ignore the old declarative widgets.
      (_i, Nothing        , Just _ , Nothing) -> return Vector.empty

      -- But, when there are stray GTK widgets without corresponding
      -- declarative widgets, something has gone wrong, and we clean that up by
      -- removing the GTK widgets.
      (_i, Just childState, Nothing, Nothing) -> do
        Gtk.widgetDestroy =<< someStateWidget childState
        return Vector.empty

      -- No more GTK widgets or declarative widgets, we are done.
      (_i, Nothing, Nothing, Nothing) -> return Vector.empty

padMaybes :: Int -> Vector a -> Vector (Maybe a)
padMaybes len xs = Vector.generate len (xs !?)

instance IsContainer Gtk.ListBox (Bin Gtk.ListBoxRow Widget) where
  appendChild box _ widget' =
    Gtk.listBoxInsert box widget' (-1)
  replaceChild box _ i old new = do
    Gtk.widgetDestroy old
    Gtk.listBoxInsert box new i

instance IsContainer Gtk.Box BoxChild where
  appendChild box BoxChild {properties = BoxChildProperties {expand, fill, padding}} widget' =
    Gtk.boxPackStart box widget' expand fill padding
  replaceChild box boxChild' i old new = do
    Gtk.widgetDestroy old
    appendChild box boxChild' new
    Gtk.boxReorderChild box new i