<?php

namespace Drupal\pagedesigner;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Render\Markup;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\pagedesigner\Entity\Element;
use Drupal\pagedesigner\Entity\ElementInterface;
use Drupal\pagedesigner\Plugin\pagedesigner\HandlerPluginInterface;
use Drupal\pagedesigner\Service\Render\Styles;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * View builder handler for pagedesigner elements.
 */
class ElementViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface {

  /**
   * The public render constant.
   */
  public const RENDER_MODE_PUBLIC = 'public';

  /**
   * The internal render constant.
   */
  public const RENDER_MODE_INTERNAL = 'internal';

  /**
   * The edit render constant.
   */
  public const RENDER_MODE_EDIT = 'edit';

  /**
   * The element handler.
   *
   * @var \Drupal\pagedesigner\Entity\ElementHandler
   */
  protected $elementHandler = NULL;

  /**
   * Undocumented variable.
   *
   * @var [type]
   */
  protected static $styles = NULL;

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    $instance = parent::createInstance($container, $entity_type);
    $instance->setElementHandler($container->get('pagedesigner.service.element_handler'));
    return $instance;
  }

  /**
   * Set the pagedesigner element handler.
   *
   * @param \Drupal\pagedesigner\Plugin\pagedesigner\HandlerPluginInterface $element_handler
   *   The pagedesigner element handler.
   */
  public function setElementHandler(HandlerPluginInterface $element_handler) {
    $this->elementHandler = $element_handler;
  }

  /**
   * {@inheritdoc}
   */
  public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
    $build_list = parent::viewMultiple($entities, $view_mode, $langcode);
    if (in_array($view_mode, $this->getSpecialViewModes())) {
      foreach ($entities as $key => $entity) {
        $build_list[$key] = $this->elementHandler->view($entity, $view_mode, $build_list[$key]);
      }
    }
    return $build_list;
  }

  /**
   * {@inheritdoc}
   */
  public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
    $build = [];
    if ($entity instanceof ElementInterface) {
      if ($view_mode == self::RENDER_MODE_PUBLIC) {
        $entity = $entity->loadNewestPublished();
      }
      if (!empty($entity)) {
        $build_list = $this->viewMultiple([$entity], $view_mode, $langcode);
        $build = $build_list[0];
        if (!in_array($view_mode, $this->getSpecialViewModes())) {
          $build['#pre_render'][] = [$this, 'build'];
        }
      }
    }
    return $build;
  }

  /**
   * Return pagedesigner specific view modes.
   *
   * @return string[]
   *   The specific view modes.
   */
  public function getSpecialViewModes() {
    return [self::RENDER_MODE_PUBLIC, self::RENDER_MODE_INTERNAL, self::RENDER_MODE_EDIT];
  }

  /**
   * Create a delayed build for rendering, e.g prerender or lazy_builder.
   *
   * @param array $build_info
   *   The build info.
   *
   * @return array
   *   The renderable build.
   */
  public function render(array $build_info) {
    $entity_type_key = "#{$this->entityTypeId}";
    $entity = $build_info[$entity_type_key];
    $view_mode = $build_info['#view_mode'];
    return $this->elementHandler->build($entity, $view_mode, $build_info);
  }

  /**
   * {@inheritdoc}
   */
  protected function isViewModeCacheable($view_mode) {
    if ($view_mode == self::RENDER_MODE_PUBLIC || $view_mode == self::RENDER_MODE_INTERNAL) {
      // Non-editing view modes should always be cacheable.
      return TRUE;
    }
    if ($view_mode == self::RENDER_MODE_EDIT) {
      // Editing view modes should never be cacheable.
      return FALSE;
    }

    return parent::isViewModeCacheable($view_mode);
  }

  /**
   * {@inheritdoc}
   */
  protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
    $build = parent::getBuildDefaults($entity, $view_mode);
    $this->adjustCacheTags($build, $view_mode);
    return $build;
  }

  /**
   * Adjust cache tags within #cache in a build array depending on view mode.
   *
   * As pagedesigner elements are rendered both in an internal or external mode,
   * we add the view mode to the cache tags.
   * This prevents unnessecary cache invalidations during editing.
   *
   * @param array $build
   *   A build array.
   * @param string $view_mode
   *   The current view mode.
   */
  public function adjustCacheTags(array &$build, string $view_mode) {
    if (in_array($view_mode, $this->getSpecialViewModes())) {
      if (!empty($build['#cache']) && !empty($build['#cache']['tags'])) {
        $tag_suffix = ($view_mode == self::RENDER_MODE_EDIT) ? self::RENDER_MODE_INTERNAL : $view_mode;
        foreach ($build['#cache']['tags'] as $key => $tag) {
          if (str_starts_with($tag, 'pagedesigner_element:') && !str_ends_with($tag, $tag_suffix)) {
            $build['#cache']['tags'][$key] = $tag . ':' . $tag_suffix;
          }
        }
      }
    }
  }

  /**
   * Adds the default lazy builder for an element.
   *
   * @param \Drupal\pagedesigner\Entity\Element $entity
   *   The element.
   * @param string $view_mode
   *   The view mode.
   * @param array $build
   *   The build arry.
   */
  public function addLazyBuilder(Element $entity, string $view_mode, array &$build = []) {
    $arguments = ['#entity_id' => $entity->id(), '#view_mode' => $view_mode];
    if (isset($build['#cache'])) {
      $arguments['#cache'] = $build['#cache'];
    }
    // The render array can only have some properties.
    // Keeping the weight to retain ordering.
    $build = [
      '#weight' => $build['#weight'] ?? 0,
    ];
    // Add the lazy builder to the render array.
    $build['#lazy_builder'] = ['\Drupal\pagedesigner\ElementViewBuilder::lazyBuilder', [Json::encode($arguments)]];
  }

  /**
   * Lazy builder callback to execute build method on handlers.
   *
   * @param string $args
   *   The serialized arguments
   *   containing #entity_id, #view_mode, #cache (optional).
   *
   * @return array
   *   The render array.
   */
  public static function lazyBuilder(string $args) {
    $arguments = Json::decode($args);
    $entity = Element::load($arguments['#entity_id']);
    $view_mode = $arguments['#view_mode'];
    $build = [];
    if (isset($arguments['#cache'])) {
      $build['#cache'] = $arguments['#cache'];
    }
    return \Drupal::service('pagedesigner.service.element_handler')->build($entity, $view_mode, $build);
  }

  /**
   * Add the styles of the given entity to the renderer.
   *
   * @param \Drupal\pagedesigner\Entity\Element $entity
   *   The entity to add the styles of.
   * @param string $view_mode
   *   The view mode.
   */
  public function addStyles(Element $entity, string $view_mode) {
    $sizes = ['large' => FALSE, 'medium' => FALSE, 'small' => FALSE];
    $styles = $entity->field_styles;
    if (empty($styles)) {
      return;
    }
    foreach ($styles as $item) {
      $style = $item->entity;
      if ($view_mode == self::RENDER_MODE_PUBLIC && $style != NULL) {
        $style = $style->loadNewestPublished();
      }
      if (!empty($style)) {
        unset($sizes[$style->name->value]);
        self::addStyle(
          $style->name->value,
          $style->field_css->value,
          $entity->id()
        );
      }
    }
    foreach ($sizes as $size => $used) {
      self::addStyle(
        $size,
        '',
        $entity->id()
      );
    }

  }

  /**
   * Add a style definition.
   *
   * @param string $key
   *   The size for which to add the style definition (mn, sm, xs).
   * @param string $style
   *   The style definition to add.
   * @param int $id
   *   The id of the element.
   */
  public static function addStyle($key, $style, $id) {
    if (self::$styles == NULL) {
      self::$styles = new Styles();
    }
    self::$styles->addStyle($key, $style, $id);
  }

  /**
   * Add entity and type information to the markup for editing in PD.
   *
   * @param \Drupal\Component\Render\MarkupInterface|string $markupObject
   *   The markup.
   * @param array $element
   *   The render element.
   *
   * @return \Drupal\Component\Render\MarkupInterface|string
   *   The resulting markup.
   */
  public static function postRender(MarkupInterface|string $markupObject, array $element) {
    if (!empty($element['#pagedesigner_element']) && $markupObject) {
      $entity = $element['#pagedesigner_element'];
      if ($entity instanceof Element) {
        $markup = $markupObject->__toString();
        return Markup::create(self::augmentEditPattern(
          $markup,
          $entity,
          $element['#view_mode'] == self::RENDER_MODE_EDIT)
        );
      }
    }
    return $markupObject;
  }

  /**
   * Alter the markup to add attributes to rows and components.
   *
   * @param string $markup
   *   The markup to alter.
   * @param \Drupal\pagedesigner\Entity\Element $entity
   *   The entity to use.
   * @param bool $edit
   *   Whether to add the edit attributes.
   *
   * @return string
   *   The altered markup
   */
  public static function augmentEditPattern($markup, Element $entity, $edit = FALSE) {
    $dom = new \DOMDocument();
    $dom->encoding = 'UTF-8';
    libxml_use_internal_errors(TRUE);
    // Proper UTF-8 handling for special characters.
    // Wrap in complete HTML document structure with UTF-8 meta tag.
    $wrappedMarkup = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>' . $markup . '</body></html>';
    $dom->loadHTML($wrappedMarkup, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOWARNING);
    libxml_clear_errors();
    $body = $dom->getElementsByTagName('body')->item(0);
    // Find the first element node (skip text nodes).
    $root = NULL;
    foreach ($body->childNodes as $child) {
      if ($child->nodeType === XML_ELEMENT_NODE) {
        $root = $child;
        break;
      }
    }

    // If no root element found, return the original markup.
    if (!$root instanceof \DOMElement) {
      return $markup;
    }

    $root->normalize();
    $root->setAttribute('id', 'pd-cp-' . $entity->id());
    $patternClasses = explode(' ', (string) $root->getAttribute('class'));
    $styleClasses = explode(' ', (string) $entity->field_classes->value);
    $classes = implode(' ', array_filter(array_unique([
      ...$patternClasses,
      ...$styleClasses,
    ])));
    $root->setAttribute('class', $classes);
    if ($edit) {
      $root->setAttribute('data-entity-id', $entity->id());
      $root->setAttribute('data-gjs-type', $entity->field_pattern->value);
    }
    $columns = [];
    $children = $root->childNodes;
    foreach ($children as $item) {
      if ($item instanceof \DOMElement && !str_starts_with($item->nodeName, '#') && str_contains((string) $item->getAttribute('class'), 'iq-column')) {
        $columns[] = $item;
      }
    }
    $i = 0;
    foreach ($entity->children as $item) {
      if ($item->entity != NULL && $item->entity->bundle() == 'cell') {
        if ($item->entity == NULL || empty($columns[$i])) {
          continue;
        }
        $column = $columns[$i];
        if ($column instanceof \DOMElement) {
          if (empty($column->getAttribute('id'))) {
            $column->setAttribute('id', 'pd-cp-' . $item->entity->id());
          }
          if ($edit) {
            if (empty($column->getAttribute('data-entity-id'))) {
              $column->setAttribute('data-entity-id', $item->entity->id());
            }
            if (empty($column->getAttribute('data-gjs-type'))) {
              $column->setAttribute('data-gjs-type', 'cell');
            }
          }
        }
        $i++;
      }
    }

    // Extract body content, preserving UTF-8.
    $result = '';
    foreach ($body->childNodes as $child) {
      $result .= $dom->saveHTML($child);
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    $callbacks = parent::trustedCallbacks();
    $callbacks[] = 'postRender';
    $callbacks[] = 'render';
    $callbacks[] = 'lazyBuilder';
    return $callbacks;
  }

}
