<?php

namespace Drupal\pagedesigner_debug\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\node\Entity\Node;
use Drupal\pagedesigner\Entity\Element;
use Drupal\ui_patterns\UiPatternsManager;
use Drupal\user\Entity\User;

/**
 * Class for structure debugging.
 */
class StructureDebugger {

  /**
   * The default checks to perform.
   */
  public const ALL_CHECKS = [
    'entity', 'parent', 'container',
    'undelete', 'delete',
    'unpublish', 'publish',
    'langcode', 'child', 'copy', 'overflow_child',
    'container_reference',
    'create_container', 'create_container_translation',
    'container_entity',
  ];

  /**
   * The checks and corrections to be performed.
   *
   * @var string[]
   */
  protected $checks = [];

  /**
   * The strategy to use.
   *
   * @var string
   */
  protected $strategy = 'preserve';

  /**
   * The database object.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection = NULL;

  /**
   * The list of corrections.
   *
   * @var array
   */
  protected $corrections = [];

  /**
   * The list of corrections.
   *
   * @var array
   */
  protected $encountered = [];

  /**
   * The list of corrections.
   *
   * @var array
   */
  protected $errors = [];

  /**
   * The currently processed node.
   *
   * @var \Drupal\node\Entity\Node
   */
  protected $node = NULL;

  /**
   * The currently processed container.
   *
   * @var \Drupal\pagedesigner\Entity\Element
   */
  protected $container = NULL;

  /**
   * The node storage manager.
   *
   * @var \Drupal\node\Entity\NodeStorage
   */
  protected $nodeStorageManager = NULL;

  /**
   * The pattern manager.
   *
   * @var \Drupal\ui_patterns\UiPatternsManager
   */
  protected $patternManager = NULL;

  /**
   * The pattern definitions.
   *
   * @var \Drupal\ui_patterns\Definition\PatternDefinition[]
   */
  protected $patternDefinitions = [];

  /**
   * The container data.
   *
   * @var array
   */
  protected $containerIds = [];

  /**
   * Contains the element data keyed by element id.
   *
   * @var array
   */
  protected $elementData = [];

  /**
   * Contains the element children keyed by element id.
   *
   * @var array
   */
  protected $elementChildren = [];

  /**
   * Contains the element parents keyed by element id.
   *
   * @var array
   */
  protected $elementChildrenParents = [];

  /**
   * Contains the element styles keyed by element id.
   *
   * @var array
   */
  protected $elementStyles = [];

  /**
   * Contains the element style parents keyed by element id.
   *
   * @var array
   */
  protected $elementStylesParent = [];

  /**
   * The field content elements.
   *
   * @var array
   */
  protected $fieldcontent = [];

  /**
   * The patterns for each element.
   *
   * @var array
   */
  protected $fieldpattern = [];

  /**
   * The element ids to be copied.
   *
   * @var array
   */
  protected $copyElements = [];

  /**
   * Create a new structure debugger.
   *
   *   The database connection.
   *   The entity type manager.
   *   The pattern manager.
   */
  public function __construct(
    Connection $connection,
    EntityTypeManager $entity_type_manager,
    UiPatternsManager $pattern_manager,
  ) {
    $this->connection = $connection;
    $this->nodeStorageManager = $entity_type_manager->getStorage('node');
    $this->patternManager = $pattern_manager;
    $this->patternDefinitions = $this->patternManager->getDefinitions();
    $this->setChecks();
    $this->setStrategy();
  }

  /**
   * Overwrite the default check selection.
   *
   * @param string[] $checks
   *   The checks to perform.
   */
  public function setChecks(array $checks = self::ALL_CHECKS) {
    $this->checks = $checks;
  }

  /**
   * Set the strategy to use.
   *
   * @param string $strategy
   *   The strategy to employ.
   */
  public function setStrategy(string $strategy = 'preserve') {
    $this->strategy = $strategy;
  }

  /**
   * Check the pagedesigner structure of the given nodes and collect errors.
   *
   * @param array $nids
   *   The nodes to check.
   */
  public function checkStructure(array $nids) {
    if (empty($nids)) {
      return;
    }

    // Reset data.
    $this->containerIds = [];
    $this->elementData = [];
    $this->elementChildren = [];
    $this->elementChildrenParents = [];
    $this->elementStyles = [];
    $this->elementStylesParent = [];
    $this->copyElements = [];
    $this->fieldcontent = [];
    $this->corrections = [];
    $this->errors = [];

    $defs = \Drupal::service('entity_field.manager')
      ->getFieldStorageDefinitions('node');
    $mappings = $this->nodeStorageManager->getTableMapping();
    /** @var \Drupal\field\Entity\FieldConfigStorage $field  */
    foreach ($defs as $field) {
      if ($field->getType() == 'pagedesigner_item') {

        // Collect all container relationships on this field.
        $table = $mappings->getFieldTableName($field->getName());
        $columns = $mappings->getColumnNames($field->getName());
        $select = $this->connection->select('node_field_data', 'nfd');
        $select->addField('nfd', 'default_langcode');
        $select->addJoin('INNER', $table, 'nfpc', 'nfd.nid = nfpc.entity_id AND nfd.langcode = nfpc.langcode');
        $select->addField('nfpc', 'entity_id');
        $select->addField('nfpc', $columns['target_id']);
        $select->addField('nfpc', 'langcode');
        $select->condition('nfd.nid', $nids, 'IN');
        $select->orderBy('nfpc.entity_id');
        $select->orderBy('nfd.default_langcode', 'DESC');
        $select->orderBy($columns['target_id'], 'DESC');
        $data = $select->execute();
        $defaultLangcode = "";
        $skipNodeTranslations = 0;
        foreach ($data as $row) {
          if ($row->entity_id == $skipNodeTranslations) {
            continue;
          }
          $skipNodeTranslations = 0;
          if ($row->default_langcode == 0) {
            if ($row->{$columns['target_id']}) {
              $container = Element::load($row->{$columns['target_id']});
            }
            if (!empty($container) && $container->langcode->value != $defaultLangcode) {
              $this->addCorrection(
                $row->{$columns['target_id']},
                'change_default_language',
                [$row->entity_id, $defaultLangcode]
              );
              $this->containerIds[$row->entity_id] = $row->{$columns['target_id']};
            }
          }
          else {
            $defaultLangcode = $row->langcode;
            if (in_array($row->{$columns['target_id']}, $this->containerIds)) {
              if (!empty($this->corrections[$row->{$columns['target_id']}]) && !empty($this->corrections[$row->{$columns['target_id']}]['container_reference'])) {
                $this->corrections[$row->{$columns['target_id']}]['container_reference'][] = $row->entity_id;
              }
              else {
                $nodes = array_keys($this->containerIds, $row->{$columns['target_id']});
                $nodes = array_unique($nodes);
                $nodes[] = $row->entity_id;
                $this->addCorrection($row->{$columns['target_id']}, 'container_reference', $nodes);
              }
              $skipNodeTranslations = $row->entity_id;
            }
            else {
              $this->containerIds[$row->entity_id] = $row->{$columns['target_id']};
            }
          }
        }
      }
    }

    // Collect all element data.
    $data = $this->connection->query('SELECT * FROM pagedesigner_element_field_data')->fetchAll();
    foreach ($data as $row) {
      $this->elementData[$this->makeKey($row->langcode, $row->id)] = $row;
    }

    // Collect children relationship.
    $data = $this->connection->query('SELECT * FROM pagedesigner_element__children')->fetchAll();
    foreach ($data as $row) {
      // Collect all children keyed by parent.
      if (empty($this->elementChildren[$this->makeKey($row->langcode, $row->entity_id)])) {
        $this->elementChildren[$this->makeKey($row->langcode, $row->entity_id)] = [];
      }
      $this->elementChildren[$this->makeKey($row->langcode, $row->entity_id)][] = $row;

      // Collect all parents keyed by child.
      if (empty($this->elementChildrenParents[$this->makeKey($row->langcode, $row->children_target_id)])) {
        $this->elementChildrenParents[$this->makeKey($row->langcode, $row->children_target_id)] = [];
      }
      $this->elementChildrenParents[$this->makeKey($row->langcode, $row->children_target_id)][] = $row;
    }

    // Collect style relationships.
    $data = $this->connection->query('SELECT * FROM pagedesigner_element__field_styles')->fetchAll();
    foreach ($data as $row) {

      // Collect all children keyed by parent.
      if (empty($this->elementStyles[$this->makeKey($row->langcode, $row->entity_id)])) {
        $this->elementStyles[$this->makeKey($row->langcode, $row->entity_id)] = [];
      }
      $this->elementStyles[$this->makeKey($row->langcode, $row->entity_id)][] = $row;

      // Collect all parents keyed by child.
      if (empty($this->elementStylesParent[$this->makeKey($row->langcode, $row->field_styles_target_id)])) {
        $this->elementStylesParent[$this->makeKey($row->langcode, $row->field_styles_target_id)] = [];
      }
      $this->elementStylesParent[$this->makeKey($row->langcode, $row->field_styles_target_id)][] = $row;
    }

    // Collect all content elements.
    $data = $this->connection->query('SELECT * FROM pagedesigner_element__field_content')->fetchAll();
    foreach ($data as $row) {
      $this->fieldcontent[$this->makeKey($row->langcode, $row->entity_id)][] = $row;
    }

    // Collect all patterns.
    $data = $this->connection->query('SELECT * FROM pagedesigner_element__field_pattern')->fetchAll();
    foreach ($data as $row) {
      $this->fieldpattern[$this->makeKey($row->langcode, $row->entity_id)] = $row;
    }

    // Loop through the given nodes.
    foreach ($nids as $nid) {
      $this->node = $this->nodeStorageManager->load($nid);
      if (empty($this->containerIds[$this->node->id()])) {
        $this->addCorrection($this->node->id() * -1, 'create_container', $this->node->id());
        continue;
      }
      $containerId = $this->containerIds[$this->node->id()];
      $containerKey = $this->makeKey($this->node->langcode->value, $containerId);
      if (empty($this->elementData[$containerKey])) {
        $this->addCorrection($this->makeKey($containerId, $this->node->langcode->value), 'create_container_translation', $this->node->langcode->value);
        continue;
      }
      $this->container = $this->elementData[$containerKey];
      if (empty($this->container)) {
        $this->addCorrection($this->node->id() * -1, 'create_container', $this->node->id());
        continue;
      }

      $this->encountered[] = $containerId;
      // Loop through all node languages.
      foreach ($this->node->getTranslationLanguages() as $language) {
        $this->node = $this->node->getTranslation($language->getId());
        $this->container = $this->elementData[$this->makeKey($this->node->langcode->value, $containerId)];
        if (empty($this->container)) {
          $this->addCorrection($this->makeKey($containerId, $this->node->langcode->value), 'create_container_translation', $this->node->langcode->value);
          continue;
        }
        if ($this->container->deleted != 0) {
          // Containers always need to be published and not marked as deleted.
          $this->addCorrection($this->makeKey($containerId, $this->node->langcode->value), 'undelete', $this->node->langcode->value);
        }
        if (empty($this->container->entity)) {
          // If the entity field is empty.
          $this->addCorrection(
            $this->makeKey($containerId, $this->node->langcode->value),
            'container_entity',
            [$this->node->id(), $this->node->langcode->value]
          );
        }

        // Recursively check tree.
        if (!empty($this->elementChildren[$this->makeKey($language->getId(), $this->container->id)])) {
          $children = $this->elementChildren[$this->makeKey($language->getId(), $this->container->id)];
          foreach ($children as $row) {
            if (!empty($row->children_target_id)) {
              $this->doCheckStructure($row->children_target_id, $this->container);
            }
          }
        }
      }
    }

    // Clear up errors.
    $errors = $this->errors;
    foreach ($errors as $element => $data) {
      foreach ($data as $type => $value) {
        if ($type == 'multiple parents') {
          if (count(array_intersect($value, $this->encountered)) <= 1) {
            unset($this->errors[$element]['multiple parents']);
            if (empty($this->errors[$element])) {
              unset($this->errors[$element]);
            }
          }
        }
      }
    }
  }

  /**
   * Recursively run through a pagedesigner tree and collect corrections.
   *
   * @param int $element
   *   The id of the element to check.
   * @param object $parentInfo
   *   The info object of the parent.
   */
  protected function doCheckStructure(int $element, object $parentInfo) {
    $this->encountered[] = $element;
    $elementKey = $this->makeKey($parentInfo->langcode, $element);
    $elementData = $this->elementData[$elementKey];
    $numFields = 9999999;

    if (in_array($element, $this->copyElements)) {
      return;
    }

    if (!empty($this->fieldpattern[$elementKey])) {
      $pattern = $this->fieldpattern[$elementKey]->field_pattern_value;
      if (!empty($this->patternDefinitions[$pattern])) {
        $numFields = count($this->patternDefinitions[$pattern]->getFields());
      }
    }

    if ($elementData->type == 'style') {
      $parentRows = $this->elementStylesParent[$elementKey] ?: [];
    }
    else {
      $parentRows = ($this->elementChildrenParents[$elementKey]) ?: [];
    }

    // If there are more than two parents.
    if (count($parentRows) > 1) {
      $parents = [];
      foreach ($parentRows as $row) {
        if ($this->isOrphan($this->makeKey($row->langcode, $row->entity_id))) {
          continue;
        }
        if ($row->entity_id != $elementData->parent) {
          // Copy element if referenced the wrong parent.
          $this->addCorrection(
            $this->makeKey($element, $this->makeKey($row->delta, $row->entity_id)),
            'copy',
            [$row->entity_id, $row->delta, $row->langcode]
          );
          $this->copyElements[] = $element;
        }
        else {
          // Copy element if referenced multiple times on the correct parent.
          if (in_array($row->entity_id, $parents)) {
            $this->addCorrection(
              $this->makeKey($element, $this->makeKey($row->delta, $row->entity_id)),
              'copy',
              [$row->entity_id, $row->delta, $row->langcode]
            );
            $this->copyElements[] = $element;
          }
        }
        $parents[] = $row->entity_id;
      }
    }
    if (empty($elementData->entity)) {
      // If the entity field is empty.
      $this->addCorrection($element, 'entity', $this->node->id());
    }
    elseif ($elementData->entity > $this->node->id() && (is_countable($parentRows) ? count($parentRows) : 0) == 1) {
      // If we have a mismatch on node and element entity field.
      // We assume content is always copied from older nodes to newer nodes.
      $this->addCorrection($element, 'entity', $this->node->id());
    }
    if ($elementData->langcode != $this->node->langcode->value) {
      // If the language of the element is not matching node language.
      $this->addCorrection($element, 'langcode', $this->node->langcode->value);
    }
    if ($elementData->container != $this->container->id) {
      // If the language of the element is not matching node language.
      $this->addCorrection($element, 'container', $this->container->id);
    }
    if ($elementData->status && $this->isOrphan($this->makeKey($elementData->langcode, $element))) {
      // Element marked as published but not in tree, so unpublish and delete.
      $this->addCorrection($element, 'unpublish', $elementData->langcode);
      $this->addCorrection($element, 'delete', $elementData->langcode);
    }
    if ($elementData->deleted == 1) {
      if ($elementData->status && $this->isOrphan($this->makeKey($elementData->langcode, $element))) {
        // Element marked as deleted and not in tree, so unpublish.
        $this->addCorrection($element, 'unpublish', $elementData->langcode);
      }
      else {
        // Undelete element marked as deleted, but in tree.
        // Will only appear in edit mode, if it's not marked as published.
        $this->addCorrection($element, 'undelete', $elementData->langcode);
      }

    }
    if ((is_countable($parentRows) ? count($parentRows) : 0) == 1) {
      if ($elementData->parent != $parentInfo->id) {
        // Only one parent in database and parent field does not match.
        $this->addCorrection($element, 'parent', $parentInfo->id);
      }
      else {
        $fieldContent = $this->fieldcontent[$elementKey] ?? [];
        if (
          (is_countable($fieldContent) ? count($fieldContent) : 0) > 0 &&
          !$elementData->status &&
          $parentInfo->status
        ) {
          // Element is content and not published, but parent is.
          $this->addCorrection($element, 'publish', $elementData->langcode);
        }
      }
    }

    $i = 0;
    if (!empty($this->elementChildren[$elementKey])) {
      foreach ($this->elementChildren[$elementKey] as $row) {
        if (!empty($row->children_target_id)) {
          if ($i >= $numFields) {
            $this->addCorrection($row->children_target_id, 'overflow_child', $element);
          }
          else {
            $this->doCheckStructure($row->children_target_id, $elementData);
            $i++;
          }
        }
      }
    }
    if (!empty($this->elementStyles[$elementKey])) {
      foreach ($this->elementStyles[$elementKey] as $row) {
        if (!empty($row->field_styles_target_id)) {
          $this->doCheckStructure($row->field_styles_target_id, $elementData);
        }
      }
    }
  }

  /**
   * Add a correction.
   *
   * @param string $element
   *   The element id.
   * @param string $type
   *   The type of correction.
   * @param mixed $value
   *   The value for the correction.
   */
  protected function addCorrection(string $element, string $type, mixed $value) {
    if (in_array($type, $this->checks)) {
      if (empty($this->corrections[$element])) {
        $this->corrections[$element] = [];
      }
      $this->corrections[$element][$type] = $value;
    }
  }

  /**
   * Returns the corrections found in the last run.
   *
   * @return array
   *   The corrections of the last run.
   */
  public function getCorrections() {
    return $this->corrections;
  }

  /**
   * Returns the errors found in the last run.
   *
   * @return array
   *   The errors of the last run.
   */
  public function getErrors() {
    return $this->errors;
  }

  /**
   * Set the corrections to apply.
   *
   * @param array $corrections
   *   The corrections to apply.
   */
  public function setCorrections(array $corrections) {
    $this->corrections = $corrections;
  }

  /**
   * Copy an element and assign it to the given parent.
   *
   * @param \Drupal\pagedesigner\Entity\Element $element
   *   The source element.
   *
   * @return \Drupal\pagedesigner\Entity\Element
   *   The clone.
   */
  protected function copy(Element $element) {
    /** @var \Drupal\pagedesigner\Entity\ElementHandler $element_handler */
    $element_handler = \Drupal::service('pagedesigner.service.element_handler');
    /** @var \Drupal\pagedesigner\Entity\Element $clone */
    $clone = $element_handler->copy($element);
    $this->recursiveCopyCleanup($element, $clone);
    return $clone;
  }

  /**
   * Adjust deletion and publication state.
   *
   * @param \Drupal\pagedesigner\Entity\Element $element
   *   The source element.
   * @param \Drupal\pagedesigner\Entity\Element $clone
   *   The clone.
   */
  protected function recursiveCopyCleanup(Element $element, Element $clone) {
    foreach ($element->children as $delta => $item) {
      if ($item->entity) {
        $this->recursiveCopyCleanup($item->entity, $clone->children->get($delta)->entity);
      }
    }
    if ($element->hasField('field_styles')) {
      foreach ($element->field_styles as $delta => $item) {
        if ($item->entity) {
          $this->recursiveCopyCleanup($item->entity, $clone->field_styles->get($delta)->entity);
        }
      }
    }
    $this->alterField($clone, 'deleted', $element->deleted->value);
    $this->alterField($clone, 'status', $element->isPublished());
  }

  /**
   * Applies the found corrections.
   */
  public function applyCorrections() {
    foreach ($this->corrections as $elementId => $corrections) {
      if (str_contains($elementId, ':')) {
        $elementId = explode(':', $elementId)[0];
      }
      $element = Element::load($elementId);
      $saveElement = FALSE;
      foreach ($corrections as $type => $value) {
        switch ($type) {
          case 'entity':
          case 'container':
          case 'langcode':
          case 'parent':
            if (in_array($type, $this->checks)) {
              $this->alterField($element, $type, $value);
            }
            break;

          case 'publish':
            if (in_array($type, $this->checks)) {
              if ($element->hasTranslation($value)) {
                $element = $element->getTranslation($value);
                $this->alterField($element, 'status', 1);
              }
            }
            break;

          case 'unpublish':
            if (in_array($type, $this->checks)) {
              if ($element->hasTranslation($value)) {
                $element = $element->getTranslation($value);
                $this->alterField($element, 'status', 0);
              }
            }
            break;

          case 'undelete':
            if (in_array($type, $this->checks)) {
              if ($element->hasTranslation($value)) {
                $element = $element->getTranslation($value);
                $this->alterField($element, 'deleted', 0);
              }
            }
            break;

          case 'delete':
            if (in_array($type, $this->checks)) {
              if ($element->hasTranslation($value)) {
                $element = $element->getTranslation($value);
                $this->alterField($element, 'deleted', 1);
              }
            }
            break;

          case 'child':
            if (in_array($type, $this->checks)) {
              foreach ($value as $parentId) {
                $parent = Element::load($parentId);
                $this->removeFromParent($element, $parent);
              }
              $element->deleted->value = 0;
              $saveElement = TRUE;
            }
            break;

          case 'copy':
            if (in_array($type, $this->checks)) {
              $parent = Element::load($value[0]);
              if (!empty($parent) && $parent->hasTranslation($value[2])) {
                // Get parent in correct language.
                $parent = $parent->getTranslation($value[2]);

                // Clone element in undeleted state.
                $element->deleted->value = 0;
                $clone = $this->copy($element, $value, $parent);
                // Replace existing refernce with clone.
                $delta = $value[1];
                $parent->children->set($delta, $clone);
                $parent->save();
              }
            }
            break;

          case 'overflow_child':
            if (in_array($type, $this->checks)) {
              $parent = Element::load($value);
              if (!empty($parent)) {
                $this->removeFromParent($element, $parent);
                if (!empty($element->parent->entity)) {
                  $this->removeFromParent($element, $element->parent->entity);
                }
              }
              $element->delete();
            }
            break;

          case 'create_container':
            if (in_array($type, $this->checks)) {
              Node::load($value)->save();
            }
            break;

          case 'create_container_translation':
            if (in_array($type, $this->checks)) {
              if (!$element->hasTranslation($value)) {
                $element->addTranslation($value);
                $saveElement = TRUE;
              }
            }
            break;

          // Add missing entity to containers.
          case 'container_entity':
            if (in_array($type, $this->checks)) {
              if ($element->hasTranslation($value[1])) {
                $element = $element->getTranslation($value[1]);
                $this->alterField($element, 'entity', $value[0]);
              }
            }
            break;

          // Make containers unique per node.
          case 'container_reference':
            if (in_array($type, $this->checks)) {
              $source = NULL;
              $nids = $value;
              if ((is_countable($nids) ? count($nids) : 0) > 1) {
                $source = Node::load($nids[0]);
                array_shift($nids);
                foreach ($nids as $nid) {
                  $target = Node::load($nid);
                  \Drupal::service('pagedesigner.service')->overrideContainers($target);
                  foreach ($source->getTranslationLanguages() as $language) {
                    if ($target->hasTranslation($language->getId())) {
                      $source = $source->getTranslation($language->getId());
                      $target = $target->getTranslation($language->getId());
                      \Drupal::service('pagedesigner_duplication.service.duplicator')->duplicate($source, $target);
                      if ($element->isPublished()) {
                        \Drupal::service('pagedesigner.service.statechanger')->publish($target);
                      }
                    }
                  }
                  $target->save();
                }
              }
            }
            break;
        }
      }
      if ($saveElement && !empty($element)) {
        $element->save();
      }
    }
  }

  /**
   * Removes the element from a given parent element.
   *
   *   The element to remove.
   *   The parent to remove the element from.
   */
  protected function removeFromParent(Element $element, Element $parent) {
    if (empty($parent)) {
      return;
    }
    $parent = $parent->getTranslation($element->langcode->value);
    if ($element->bundle() == 'style') {
      foreach ($parent->field_styles as $delta => $item) {
        if ($item->target_id == $element->id()) {
          $parent->field_styles->removeItem($delta);
        }
      }
    }
    else {
      foreach ($parent->children as $delta => $item) {
        if ($item->target_id == $element->id()) {
          $parent->children->removeItem($delta);
        }
      }
    }
    $parent->save();
  }

  /**
   * Alter a base field on the element.
   *
   * @param \Drupal\pagedesigner\Entity\Element $element
   *   The element to change.
   * @param string $field
   *   The field to edit.
   * @param mixed $value
   *   The value to set.
   */
  public function alterField(Element $element, $field, $value) {
    $transaction = $this->connection->startTransaction();
    $this->connection->update('pagedesigner_element_field_data')
      ->fields([$field => $value])
      ->condition('id', $element->id())
      ->condition('langcode', $element->langcode->value)
      ->execute();
    $this->connection->update('pagedesigner_element_field_revision')
      ->fields([$field => $value])
      ->condition('id', $element->id())
      ->condition('vid', $element->getRevisionId())
      ->condition('langcode', $element->langcode->value)
      ->execute();
    unset($transaction);
  }

  /**
   * Adjust non-entity elements.
   */
  public function clearSpecialElements() {
    $eids = \Drupal::entityQuery('pagedesigner_element')
      ->condition('type', ['layout', 'gallery_item', 'gallery_gallery'], 'IN')
      ->addMetaData('account', User::load(1))
      ->accessCheck(FALSE)
      ->execute();
    foreach ($eids as $eid) {
      $element = Element::load($eid);
      $this->setEntityNull($element);
      if ($element->type->target_id == 'layout') {
        $this->mark($element, $element->deleted->value, FALSE);
      }
      if ($element->type->target_id == 'gallery_gallery') {
        foreach ($element->children as $item) {
          if (!empty($item->entity)) {
            $this->mark($item->entity, FALSE, FALSE);
          }
        }
      }
    }
  }

  /**
   * Set the entity to null for non-entity elements and their children.
   *
   *   The element to correct.
   */
  protected function setEntityNull(Element $element) {
    if (!$element->entity->isEmpty()) {
      $this->alterField($element, 'entity', NULL);
    }
    foreach ($element->children as $item) {
      if (!empty($item->entity)) {
        $this->setEntityNull($item->entity);
      }
    }
    if ($element->hasField('field_styles')) {
      foreach ($element->field_styles as $item) {
        if (!empty($item->entity)) {
          $this->setEntityNull($item->entity);
        }
      }
    }
  }

  /**
   * Set deleted and status flag for all children.
   *
   *   The element to correct.
   *
   * @param \Drupal\pagedesigner\Entity\Element $element
   *   The element to mark.
   * @param bool $deleted
   *   The deleted flag to set.
   * @param bool $published
   *   The published flag to set.
   */
  protected function mark(Element $element, $deleted = FALSE, $published = FALSE) {
    if ($element->deleted->value != $deleted || $element->status->value != $published) {
      $this->alterField($element, 'deleted', ($deleted) ? 1 : 0);
      $this->alterField($element, 'status', ($published) ? 1 : 0);
    }
    if ($element->type->target_id == 'layout') {
      $deleted = FALSE;
    }
    foreach ($element->children as $item) {
      if (!empty($item->entity)) {
        $this->mark($item->entity, $deleted, $published);
      }
    }
    if ($element->hasField('field_styles')) {
      foreach ($element->field_styles as $item) {
        if (!empty($item->entity)) {
          $this->mark($item->entity, $deleted, $published);
        }
      }
    }
  }

  /**
   * Find orphaned elements.
   *
   * Assumes all checks on all nodes to ensure no false deletions.
   *
   * @return int[]
   *   The ids of orphaned elements.
   */
  public function findOrphans() {
    $candidates = [];
    // Check for orphaned elements.
    $result = $this->connection->query(
      "SELECT id FROM pagedesigner_element_field_data WHERE `entity` IS NOT NULL AND COALESCE(deleted, 0) = 0");
    $rows = $result->fetchAll();
    foreach ($rows as $row) {
      $candidates[] = $row->id;
    }
    $orphans = array_diff($candidates, $this->encountered);
    return $orphans;
  }

  /**
   * Remove orphaned elements.
   *
   * Assumes all checks on all nodes to ensure no false deletions.
   *
   * @param bool $delete
   *   Whether to mark the orphans as deleted.
   *
   * @return int
   *   The number of corrected orphans.
   */
  public function removeOrphans(bool $delete = FALSE) {
    if ((empty($this->corrections) && !empty($this->encountered))) {
      $orphaned = $this->findOrphans();
      if ($delete) {
        foreach ($orphaned as $element => $eid) {
          $element = Element::load($eid);
          if (!empty($element)) {
            if ($element->bundle() == 'container') {
              $element->delete();
            }
            else {
              foreach ($element->getTranslationLanguages() as $language) {
                $element = $element->getTranslation($language->getId());
                $this->alterField($element, 'deleted', 1);
                $this->alterField($element, 'status', 0);
              }
            }
          }
        }
      }
      return count($orphaned);
    }
    return 0;
  }

  /**
   * Determine if the key belongs to an orphaned element.
   *
   * @param string $element_key
   *   The element key.
   *
   * @return bool
   *   True if element is an orphan.
   */
  public function isOrphan(string $element_key) {
    $elementData = $this->elementData[$element_key];
    if ($elementData->type == 'container') {
      return FALSE;
    }
    else {
      $key = $this->makeKey($elementData->langcode, $elementData->id);
      if (!empty($this->elementChildrenParents[$key])) {
        $result = TRUE;
        foreach ($this->elementChildrenParents[$key] as $parent) {
          $result = $result && $this->isOrphan($this->makeKey($parent->langcode, $parent->entity_id));
        }
        return $result;
      }
      elseif (!empty($this->elementStylesParent[$key])) {
        $result = TRUE;
        foreach ($this->elementStylesParent[$key] as $parent) {
          $result = $result && $this->isOrphan($this->makeKey($parent->langcode, $parent->entity_id));
        }
        return $result;
      }
      else {
        return TRUE;
      }
    }
  }

  /**
   * Create a key to store and read element data.
   *
   * @param string $first
   *   First part of the key.
   * @param string $second
   *   Second part of the key.
   *
   * @return string
   *   The key.
   */
  protected function makeKey($first, $second) {
    return $first . ':' . $second;
  }

}
