<?php

declare(strict_types=1);

namespace Drupal\display_builder\Event;

use Drupal\display_builder\Island\IslandPluginManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * The event subscriber for Display Builder islands.
 */
class DisplayBuilderEventsSubscriber implements EventSubscriberInterface {

  /**
   * Constructs a new DisplayBuilderEventsSubscriber object.
   */
  public function __construct(
    protected IslandPluginManagerInterface $islandManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      DisplayBuilderEvents::ON_ACTIVE => 'onActive',
      DisplayBuilderEvents::ON_ATTACH_TO_ROOT => 'onAttachToRoot',
      DisplayBuilderEvents::ON_ATTACH_TO_SLOT => 'onAttachToSlot',
      DisplayBuilderEvents::ON_DELETE => 'onDelete',
      DisplayBuilderEvents::ON_HISTORY_CHANGE => 'onHistoryChange',
      DisplayBuilderEvents::ON_RESTORE => 'onRestore',
      DisplayBuilderEvents::ON_REVERT => 'onRevert',
      DisplayBuilderEvents::ON_MOVE => 'onMove',
      DisplayBuilderEvents::ON_UPDATE => 'onUpdate',
      DisplayBuilderEvents::ON_PUBLISH => 'onPublish',
      DisplayBuilderEvents::ON_PRESET_SAVE => 'onPresetSave',
    ];
  }

  /**
   * Event handler for when a block becomes active.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onActive(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getData()]);
  }

  /**
   * Event handler for when a block is attached to the root.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onAttachToRoot(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId()]);
  }

  /**
   * Event handler for when a block is attached to a slot.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onAttachToSlot(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId(), $event->getParentId()]);
  }

  /**
   * Event handler for when a block is deleted.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onDelete(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getParentId()]);
  }

  /**
   * Event handler for when the history changes.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onHistoryChange(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__);
  }

  /**
   * Event handler for when the builder is restored to its last saved state.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onRestore(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__);
  }

  /**
   * Event handler for when an override is reverted to its default state.
   *
   * Reuses the history-change island callbacks since the UI refresh is
   * identical: all islands must re-render the updated state and history.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onRevert(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__);
  }

  /**
   * Event handler for when a block is moved.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onMove(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId()]);
  }

  /**
   * Event handler for when a block is updated.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onUpdate(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId()]);
  }

  /**
   * Event handler for when a display is saved.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onPublish(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__, [$event->getData()]);
  }

  /**
   * Event handler for when a preset is saved.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   */
  public function onPresetSave(DisplayBuilderEvent $event): void {
    $this->dispatchToIslands($event, __FUNCTION__);
  }

  /**
   * Dispatch the event with a generic code.
   *
   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
   *   The event object.
   * @param string $method
   *   The method to dispatch.
   * @param array $parameters
   *   (Optional) The parameters to the method.
   */
  private function dispatchToIslands(DisplayBuilderEvent $event, string $method, array $parameters = []): void {
    \array_unshift($parameters, $event->getInstance());

    $configuration = $event->getIslandConfiguration();
    $contexts = $event->getInstance()->getAvailableContexts();
    $islands = $this->islandManager->createInstances($this->islandManager->getDefinitions(), $contexts, $configuration);

    $island_enabled = $event->getEnabledIslands();

    foreach ($islands as $island_id => $island) {
      if (!isset($island_enabled[$island_id])) {
        continue;
      }

      // Skip the island triggering the HTMX event. Useful to avoid swapping
      // the content of an island which is already in the expected state.
      // For examples, if we move an instance in Builder, Layers or Tree
      // panels, if we change the settings in InstanceForm.
      // @see Drupal\display_builder\Controller\ApiControllerBase::islandId
      if ($island_id === $event->getCurrentIslandId()) {
        continue;
      }

      $result = $island->{$method}(...$parameters);

      if ($result !== NULL) {
        $event->appendResult($island_id, $result);
      }
    }
  }

}
