<?php

/**
 * @file
 * Contains installation and update routines for Lightning Layout.
 */

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\node\Entity\NodeType;

/**
 * Implements hook_install().
 */
function lightning_layout_install() {
  // Set up layout_manager permissions.
  lightning_layout_update_8003();

  // Add layout permissions to Lightning Core's content role configuration.
  lightning_layout_update_8005();
}

/**
 * Installs Panelizer and default configuration for landing page content type.
 */
function lightning_layout_update_8001() {
  \Drupal::service('module_installer')->install(['panelizer']);
  \Drupal::service('config.installer')->installDefaultConfig('module', 'lightning_layout');
}

/**
 * Creates the layout_manager role.
 */
function lightning_layout_update_8002() {
  lightning_core_create_config('user_role', 'layout_manager', 'lightning_layout');
}

/**
 * Adds Panelizer permissions to layout_manager role.
 */
function lightning_layout_update_8003() {
  /** @var \Drupal\node\NodeTypeInterface $node_type */
  foreach (NodeType::loadMultiple() as $node_type) {
    \Drupal::moduleHandler()
      ->invoke('lightning_layout', 'node_type_insert', [$node_type]);
  }
}

/**
 * Installs Panelizer defaults for the landing_page content type.
 */
function lightning_layout_update_8004() {
  // Because this update interacts with Panelizer, rebuild the container to
  // pick up any changes to Panelizer's service definitions.
  lightning_core_rebuild_container();

  // Sanity check! We can only proceed if the landing_page node type exists.
  if (NodeType::load('landing_page') == NULL) {
    return;
  }

  // There is always a default view display, so unconditionally update it.
  _lightning_layout_update_panelizer_default_displays('node.landing_page.default');

  try {
    _lightning_layout_update_panelizer_default_displays('node.landing_page.full');
  }
  catch (EntityStorageException $e) {
    $values = _lightning_layout_get_view_display('node.landing_page.full');
    EntityViewDisplay::create($values)->save();
  }

  EntityFormDisplay::load('node.landing_page.default')
    ->setComponent('panelizer', [
      'type' => 'panelizer',
    ])
    ->save();
}

/**
 * Adds Panels and Panelizer permissions to content roles.
 */
function lightning_layout_update_8005() {
  \Drupal::service('lightning.content_roles')
    ->grantPermissions('creator', [
      'access panels in-place editing',
      'change layouts in place editing',
      'administer panelizer node ? content',
      'administer panelizer node ? layout',
    ]);
}

/**
 * Removes administrative privileges from layout_manager role.
 */
function lightning_layout_update_8006() {
  $role_storage = \Drupal::entityTypeManager()->getStorage('user_role');

  /** @var \Drupal\user\RoleInterface[] $roles */
  $roles = $role_storage->loadByProperties([
    'is_admin' => TRUE,
  ]);
  $keys = array_keys($roles);
  sort($keys);
  if ($keys == ['administrator', 'layout_manager']) {
    $roles['layout_manager']
      ->setIsAdmin(FALSE)
      // grantPermission() has no effect on administrative roles -- the grant
      // is literally tossed into the big storage backend in the sky. This, in
      // my opinion, is the absolute stupidest thing in core -- it's deliberate
      // data loss!! Sigh...but anyway...having stripped layout_manager of its
      // administrative status, we now need to ensure it has all the permissions
      // that it would have out of the box.
      ->grantPermission('administer node display')
      ->grantPermission('administer panelizer')
      ->save();

    foreach (NodeType::loadMultiple() as $node_type) {
      \Drupal::moduleHandler()
        ->invoke('lightning_layout', 'node_type_insert', [$node_type]);
    }
  }
}

/**
 * Returns the raw values for a bundled view display.
 *
 * @param string $display_id
 *   The bundled view display ID (e.g., node.page.full)
 *
 * @return array
 *   The view display's raw values, suitable for Entity::create().
 *
 * @throws \Exception
 *   If the view display is not bundled with Lightning Layout or otherwise
 *   cannot be read.
 */
function _lightning_layout_get_view_display($display_id) {
  // Read in our bundled version of the view display.
  $values = lightning_core_read_config('core.entity_view_display.' . $display_id, 'lightning_layout');
  if (empty($values)) {
    throw new \Exception('View display does not exist: ' . $display_id);
  }

  // Reference the bundled Panelizer displays, just for readability later on.
  $displays = &$values['third_party_settings']['panelizer']['displays'];

  // Get the ID of the default view mode by replacing the final ID component
  // with 'default'.
  $default_id = preg_replace('/\.[a-z0-9_]+$/', '.default', $display_id);
  $defaults = EntityViewDisplay::load($default_id)
    ->getThirdPartySetting('panelizer', 'displays', []);

  // Every bundled display that does not define any blocks should inherit the
  // ones from the corresponding display of the default view mode.
  foreach ($defaults as $id => $default) {
    if (isset($displays[$id]) && empty($displays[$id]['blocks'])) {
      $displays[$id]['blocks'] = $default['blocks'];
    }
  }

  return $values;
}

/**
 * Updates an entity view display with Lightning-provided Panelizer defaults.
 *
 * @param string $display_id
 *   The existing view display entity ID (e.g. node.page.default)
 *
 * @throws EntityStorageException
 *   If the view display cannot be loaded.
 */
function _lightning_layout_update_panelizer_default_displays($display_id) {
  /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
  $display = EntityViewDisplay::load($display_id);
  // Let us sanity check!
  if (empty($display)) {
    throw new EntityStorageException('View display does not exist: ' . $display_id);
  }

  $values = lightning_core_read_config($display->getConfigDependencyName(), 'lightning_layout');

  // Meekly merge in new values from our bundled configuration, preserving
  // anything that already exists...
  $panelizer_displays = array_merge_canadian(
    $display->getThirdPartySetting('panelizer', 'displays', []),
    $values['third_party_settings']['panelizer']['displays']
  );
  // ...except for certain important defaults which may not already exist, due
  // to changes in the Panels and Panelizer APIs.
  foreach ($panelizer_displays as $id => &$panelizer_display) {
    // Previously, there was no UI to edit Panelizer displays, which means they
    // might not have had any label set (this is true for the default display,
    // at least). If there is no label on the display, use the layout plugin's
    // label.
    if (empty($panelizer_display['label'])) {
      try {
        $layout = \Drupal::service('plugin.manager.layout_plugin')->getDefinition($panelizer_display['layout']);
        $panelizer_display['label'] = $layout['label'];
      }
      catch (PluginNotFoundException $e) {
      }
    }

    // All displays must specify a storage type.
    if (empty($panelizer_display['storage_type'])) {
      $panelizer_display['storage_type'] = 'panelizer_default';
    }

    // All displays must specify a storage ID. By default, this is
    // ENTITY_TYPE:BUNDLE:VIEW_MODE:DISPLAY_ID.
    if (empty($panelizer_display['storage_id'])) {
      $panelizer_display['storage_id'] = implode(':', [
        $display->getTargetEntityTypeId(),
        $display->getTargetBundle(),
        $display->getMode(),
        $id,
      ]);
    }

    // Panels now implements a 'pattern' plugin type. I'm not sure this is
    // absolutely necessary, but I'm erring on the side of caution here.
    if (empty($panelizer_display['pattern'])) {
      $panelizer_display['pattern'] = 'panelizer';
    }
  }

  $display
    ->setThirdPartySetting('panelizer', 'displays', $panelizer_displays)
    ->save();
}

/**
 * Recursively merges arrays using the + method.
 *
 * Existing keys at all levels of $a, both numeric and associative, will always
 * be preserved. That's why I'm calling this a "Canadian" merge -- it does not
 * want to step on any toes.
 *
 * @param array $a
 *   The input array.
 * @param array $b
 *   The array to merge into $a.
 *
 * @return array
 *   The merged arrays.
 */
function array_merge_canadian(array $a, array $b) {
  $a += $b;
  foreach ($a as $k => $v) {
    if (is_array($v) && isset($b[$k]) && is_array($b[$k])) {
      $a[$k] = call_user_func(__FUNCTION__, $a[$k], $b[$k]);
    }
  }
  return $a;
}

/**
 * Implements hook_update_dependencies().
 */
function lightning_layout_update_dependencies() {
  return [
    'lightning_layout' => [
      8003 => [
        'lightning' => 8002,
      ],
      // 8005 requires the lightning_core.settings config object, which is
      // created by lightning_core 8001.
      8005 => [
        'lightning_core' => 8001,
      ],
    ],
  ];
}
