<?php

namespace Drupal\eca\Plugin\ECA\Event;

use Drupal\Core\Form\FormStateInterface;
use Drupal\eca\Attribute\Token;
use Drupal\eca\EcaEvents;
use Drupal\eca\Entity\Objects\EcaEvent;
use Drupal\eca\Plugin\DataType\DataTransferObject;
use Drupal\eca\Plugin\ECA\EcaPluginBase;
use Drupal\eca\Plugin\ECA\PluginFormTrait;
use Drupal\eca\Token\TokenInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * Base class for ECA event plugins.
 */
abstract class EventBase extends EcaPluginBase implements EventInterface {

  use PluginFormTrait;

  /**
   * According system event, if available.
   *
   * @var \Symfony\Contracts\EventDispatcher\Event|null
   */
  protected ?Event $event = NULL;

  /**
   * An instance holding event data accessible as Token.
   *
   * @var \Drupal\eca\Plugin\DataType\DataTransferObject|null
   */
  protected ?DataTransferObject $eventData = NULL;

  /**
   * The list of tokens that the event provides when dispatched.
   *
   * @var \Drupal\eca\Attribute\Token[]|null
   */
  protected ?array $tokens = NULL;

  /**
   * The event dispatcher service.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * ECA token service.
   *
   * @var \Drupal\eca\Token\TokenInterface
   */
  protected TokenInterface $token;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->setConfiguration($configuration);
    $instance->eventDispatcher = $container->get('event_dispatcher');
    $instance->token = $container->get('eca.service.token');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  final public function eventClass(): string {
    return $this->pluginDefinition['event_class'];
  }

  /**
   * {@inheritdoc}
   */
  final public function eventName(): string {
    return $this->pluginDefinition['event_name'];
  }

  /**
   * {@inheritdoc}
   */
  final public function subscriberPriority(): int {
    return $this->pluginDefinition['subscriber_priority'];
  }

  /**
   * {@inheritdoc}
   */
  public function generateWildcard(string $eca_config_id, EcaEvent $ecaEvent): string {
    // By default return a small wildcard that should match up for every event
    // that is of the class as returned by ::drupalEventClass.
    return '*';
  }

  /**
   * {@inheritdoc}
   *
   * @throws \InvalidArgumentException
   *   When the given wildcard does not match up with the expected pattern as
   *   generated by ::generateWildcard.
   */
  public static function appliesForWildcard(Event $event, string $event_name, string $wildcard): bool {
    // This default implementation should not be called as a parent method
    // from a child class. When a child class implements this method on its own,
    // it must contain solely the concrete appliance logic of the plugin itself.
    if ($wildcard !== '*') {
      throw new \InvalidArgumentException('The given wildcard is different than expected. If the plugin implements its own appliance logic, the parent method must not be invoked.');
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function setEvent(Event $event): EventInterface {
    $this->event = $event;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getEvent(): ?Event {
    return $this->event;
  }

  /**
   * {@inheritdoc}
   */
  public function getTokens(): array {
    if ($this->tokens === NULL) {
      $this->getSupportedTokens();
    }
    return $this->tokens;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration(): array {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function getConfiguration(): array {
    return $this->configuration;
  }

  /**
   * {@inheritdoc}
   */
  public function setConfiguration(array $configuration): void {
    $this->configuration = $configuration + $this->defaultConfiguration();
  }

  /**
   * Helper function to get token info.
   */
  private function getSupportedTokens(): void {
    $sources = $this->eventDispatcher->getListeners(EcaEvents::BEFORE_INITIAL_EXECUTION);
    array_unshift($sources, [$this::class, 'buildEventData']);
    array_unshift($sources, [$this::class, 'getData']);
    foreach ($this->token->getDataProviders() as $dataProvider) {
      array_unshift($sources, [$dataProvider::class, 'buildEventData']);
      array_unshift($sources, [$dataProvider::class, 'getData']);
    }

    $eventClass = $this->eventClass();
    $tokens = [];
    foreach ($sources as $source) {
      [$class, $methodName] = $source;
      try {
        $reflection = new \ReflectionMethod($class, $methodName);
      }
      catch (\ReflectionException) {
        continue;
      }
      do {
        foreach ($reflection->getAttributes() as $attribute) {
          if ($attribute->getName() === 'Drupal\eca\Attribute\Token') {
            /** @var \Drupal\eca\Attribute\Token $token */
            $token = $attribute->newInstance();
            if ($this->getSupportedProperties($eventClass, $token)) {
              $tokens[] = $token;
            }
          }
        }
        try {
          $reflection = $reflection->getPrototype();
        }
        catch (\ReflectionException) {
          $reflection = NULL;
        }
      } while ($reflection !== NULL);
    }
    $this->tokens = $tokens;
  }

  /**
   * Recursive helper function to get token property info.
   *
   * @param string $eventClass
   *   The event class.
   * @param \Drupal\eca\Attribute\Token $token
   *   The token.
   *
   * @return bool
   *   TRUE, if the token has any attributes, FALSE otherwise.
   */
  private function getSupportedProperties(string $eventClass, Token $token): bool {
    if ($this->isClassSupported($eventClass, $token->classes)) {
      $properties = [];
      foreach ($token->properties as $property) {
        if ($this->getSupportedProperties($eventClass, $property)) {
          $properties[] = $property;
        }
      }
      $token->properties = $properties;
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Determines if the event class is covered by the list of classes.
   *
   * @param string $eventClass
   *   The event class.
   * @param array $classes
   *   The list of classes.
   *
   * @return bool
   *   TRUE, if either the list of classes if empty, or if the event class is
   *   an instance of one of the given classes; FALSE otherwise.
   */
  private function isClassSupported(string $eventClass, array $classes): bool {
    if (empty($classes)) {
      return TRUE;
    }
    foreach ($classes as $class) {
      if (is_a($eventClass, $class, TRUE)) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Helper function to render a supported token and its properties.
   *
   * @param \Drupal\eca\Attribute\Token $token
   *   The token.
   * @param string $prefix
   *   The prefix.
   *
   * @return array
   *   The list of rendered tokens and properties.
   */
  private function renderToken(Token $token, string $prefix = ''): array {
    $tokens = [];
    $name = $prefix . $token->name;
    $tokens[] = '<p><strong>[' . $name . ']:</strong> ' . $token->description . ($token->aliases ? '<br/>(Alias: ' . implode(', ', $token->aliases) . ')' : '') . '</p>';
    foreach ($token->properties as $property) {
      $tokens += $this->renderToken($property, $name);
    }
    return $tokens;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $i = 0;
    $tokens = $this->getTokens();
    if ($tokens) {
      $form['eca_token_header'] = [
        '#markup' => '<h3>' . $this->t('Provided Tokens') . '</h3>',
      ];
      foreach ($tokens as $token) {
        foreach ($this->renderToken($token) as $item) {
          $i++;
          $form['eca_token_' . $i] = [
            '#markup' => $item,
            '#weight' => 900 + $i,
          ];
        }
      }
    }
    return $this->updateConfigurationForm($form);
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {}

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void {}

  /**
   * {@inheritdoc}
   */
  public function getData(string $key): mixed {
    if ($key === 'event') {
      if (!isset($this->eventData)) {
        $this->eventData = DataTransferObject::create($this->buildEventData());
      }

      return $this->eventData;
    }

    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function hasData(string $key): bool {
    return $this->getData($key) !== NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function handleExceptions(): bool {
    return FALSE;
  }

  /**
   * Builds up associative data of the "event" token.
   *
   * At least this should provide a "machine_name" property, so that this may
   * be accessible as "[event:machine_name]" token.
   *
   * @return array
   *   The associative data for the "event" token.
   */
  #[Token(
    name: 'event',
    description: 'The event.',
    properties: [
      new Token(name: 'machine_name', description: 'The machine name of the ECA event.'),
    ],
  )]
  protected function buildEventData(): array {
    return [
      'machine_name' => $this->eventName(),
    ];
  }

}
