<?php

/**
 * @file
 * Install, update and uninstall functions for the group module.
 */

use Drupal\Core\Config\ExtensionInstallStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Entity\EntityTypeListenerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\group\Entity\GroupContent;
use Drupal\group\Entity\Storage\GroupContentStorageSchema;

/**
 * Resave all GroupContent labels and remove orphaned entities.
 */
function group_update_8001(&$sandbox) {
  // Set up the batch by retrieving all of the group content IDs.
  if (!isset($sandbox['progress'])) {
    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
    $entity_type_manager = \Drupal::service('entity_type.manager');
    $storage_handler = $entity_type_manager->getStorage('group_content');

    $sandbox['ids'] = $storage_handler->getQuery()->accessCheck(FALSE)->execute();
    $sandbox['max'] = count($sandbox['ids']);
    $sandbox['progress'] = 0;
  }

  // Try to update 25 GroupContent entities at a time.
  $ids = array_slice($sandbox['ids'], $sandbox['progress'], 25);

  /** @var \Drupal\group\Entity\GroupContentInterface $group_content */
  foreach (GroupContent::loadMultiple($ids) as $group_content) {
    // Re-saving the GroupContent entities will properly set their label.
    if ($group_content->getEntity()) {
      $group_content->save();
    }
    // In early versions we did not delete GroupContent entities along with the
    // entity they represent. Let's clean those up to avoid crashes for people
    // who still have those floating around.
    else {
      $group_content->delete();
    }

    $sandbox['progress']++;
  }

  // Try to update the percentage but avoid division by zero.
  $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);

  // Show a status update for the current progress.
  return t("Updated the label for @progress out of @max group content entities.", ['@progress' => $sandbox['progress'], '@max' => $sandbox['max']]);
}

/**
 * Properly make Group and GroupContent translatable.
 */
function group_update_8002() {
  $container = \Drupal::getContainer();

  /** @var \Drupal\Core\Database\Connection $database */
  $database = $container->get('database');

  /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager */
  $entity_manager = $container->get('entity_type.manager');

  /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
  $field_manager = $container->get('entity_field.manager');

  /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $schema_repository */
  $schema_repository = $container->get('entity.last_installed_schema.repository');

  // Before we start, we need to rebuild the entity type caches so we have the
  // latest definitions in code available to us.
  $entity_manager->clearCachedDefinitions();

  foreach (['group', 'group_content'] as $entity_type_id) {
    // Retrieve the storage handler class name for this entity type.
    $storage_handler = $entity_manager->getHandler($entity_type_id, 'storage');

    // Get the old entity type's field definitions from the key/value storage.
    $old_field_def = $schema_repository->getLastInstalledFieldStorageDefinitions($entity_type_id);

    // Get the old entity type definition from the key/value storage.
    $old_entity_type = $schema_repository->getLastInstalledDefinition($entity_type_id);

    // Get the new entity type definition from code.
    $new_entity_type = $entity_manager->getDefinition($entity_type_id);

    // Instantiate a storage handler for both entity type definitions. Please
    // note we're cloning the old entity type definition because we are altering
    // it further down this update function and don't want those changes to be
    // reflected in the storage handler.
    $old_storage = $entity_manager->createHandlerInstance($storage_handler, clone $old_entity_type);
    $new_storage = $entity_manager->createHandlerInstance($storage_handler, $new_entity_type);

    // We can't update the definition if the storage handler doesn't have the
    // onEntityTypeCreate() method available to it.
    if (!($new_storage instanceof EntityTypeListenerInterface)) {
      // This should probably throw an exception, but we're in alpha.
      return;
    }

    // We check for the SqlContentEntityStorage interface to make sure we do
    // not run queries against a non-SQL backend. Seeing as our content entity
    // types did not specify a storage backend, Drupal defaulted to SQL so all
    // sites should be using that unless they swapped out the storage.
    if (!($new_storage instanceof SqlContentEntityStorage && $old_storage instanceof SqlContentEntityStorage)) {
      // This should probably throw an exception, but we're in alpha.
      return;
    }

    // Get the table names for our data migration.
    $base_table = $new_entity_type->getBaseTable();
    $data_table = $base_table . '_field_data';
    $temp_table = $base_table . '_data_to_migrate';

    // First rename the base table to a temporary table.
    $database->schema()->renameTable($base_table, $temp_table);

    // Then recreate the base table and data table. This will also add the
    // 'default_langcode' base field because we flagged our content entity types
    // as translatable.
    $new_storage->onEntityTypeCreate($new_entity_type);

    // At this point the database structure should match what is defined in
    // code. However, Drupal still thinks we are running the old definitions
    // because it cached them in the key/value storage.
    //
    // We therefore need to adjust the old definition instead of just writing
    // the new one to the key/value storage. By doing so, we ensure that other
    // modules' changes to the definition are kept as well.
    //
    // Inform Drupal of the fact that our content entities are now translatable
    // and have a data table.
    $old_entity_type->set('translatable', TRUE);
    $old_entity_type->set('data_table', $base_table . '_field_data');

    // We had an additional property 'fieldable' which is now gone.
    $additional = $old_entity_type->get('additional');
    unset($additional['fieldable']);
    $old_entity_type->set('additional', $additional);

    // Now that we have added only our changes, we write the adjusted old entity
    // type to the key/value storage as the new entity type.
    $schema_repository->setLastInstalledDefinition($old_entity_type);

    // As mentioned above, Drupal added a new 'default_langcode' field which we
    // didn't have before. It's therefore safe to load the field's definition
    // from code and write it to the key/value storage.
    $field_definitions = $field_manager->getFieldStorageDefinitions($entity_type_id);
    $schema_repository->setLastInstalledFieldStorageDefinition($field_definitions['default_langcode']);

    // Now we just need to migrate the old data into the new table structure. We
    // read the column names from both the old and new tables and select data
    // from the old one into the new ones.
    $temp_cols = $old_storage->getTableMapping($old_field_def)->getAllColumns($base_table);
    $base_cols = $new_storage->getTableMapping()->getAllColumns($base_table);
    $data_cols = $new_storage->getTableMapping()->getAllColumns($data_table);

    // Get the columns the base and data table share with the old base table.
    $base_shared = array_intersect($base_cols, $temp_cols);
    $data_shared = array_intersect($data_cols, $temp_cols);

    // Build subqueries for inserting old data into the new tables.
    $base_query = $database->select($temp_table, 't')->fields('t', $base_shared);
    $data_query = $database->select($temp_table, 't')->fields('t', $data_shared);

    // We add a default value of 1 to the 'default_langcode' field.
    $data_query->addExpression('1', 'default_langcode');

    // Now we select all of the old data into the new tables.
    $database->insert($base_table)->from($base_query)->execute();
    $database->insert($data_table)->from($data_query)->execute();
  }
}

/**
 * Update all group role entities to use the new audience property.
 */
function group_update_8003() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.role.') as $group_role_config_name) {
    $group_role = $config_factory->getEditable($group_role_config_name);

    // Figure out what audience the role is for based on role ID.
    list(, $group_role_id) = explode('-', $group_role->get('id'), 2);
    switch ($group_role_id) {
      case 'anonymous':
        $audience = 'anonymous';
        break;

      case 'outsider':
        $audience = 'outsider';
        break;

      default:
        $audience = 'member';
    }

    $group_role->set('audience', $audience);
    $group_role->save(TRUE);
  }
}

/**
 * Update all group role entities to use the new permissions_ui property.
 */
function group_update_8004() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.role.') as $group_role_config_name) {
    $group_role = $config_factory->getEditable($group_role_config_name);
    $group_role->set('permissions_ui', TRUE);
    $group_role->save(TRUE);
  }
}

/**
 * Update all group types to fix some potentially broken plugin config.
 */
function group_update_8005() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.type.') as $group_type_config_name) {
    $group_type = $config_factory->getEditable($group_type_config_name);
    list(, , $group_type_id) = explode('.', $group_type_config_name);

    // Make sure the group type ID is set in the plugin config.
    $plugins = $group_type->get('content');
    foreach ($plugins as $plugin_id => $config) {
      $group_type->set("content.$plugin_id.group_type", $group_type_id);
    }

    $group_type->save(TRUE);
  }
}

/**
 * Move all plugin config from group types to group content types.
 */
function group_update_8006() {
  $config_factory = \Drupal::configFactory();
  $plugin_config = [];

  // Get the configuration from every group type.
  foreach ($config_factory->listAll('group.type.') as $group_type_config_name) {
    $group_type = $config_factory->getEditable($group_type_config_name);
    list(, , $group_type_id) = explode('.', $group_type_config_name);

    // Store the group type's plugin configuration in an array.
    $plugins = $group_type->get('content');
    foreach ($plugins as $plugin_id => $config) {
      $plugin_config[$group_type_id][$plugin_id] = $config['data'];
    }

    // Remove the plugin config from the group type altogether.
    $group_type->clear('content');
    $group_type->save(TRUE);
  }

  // Store the configuration on the group content types.
  foreach ($config_factory->listAll('group.content_type.') as $group_content_type_config_name) {
    $group_content_type = $config_factory->getEditable($group_content_type_config_name);
    $group_type_id = $group_content_type->get('group_type');
    $plugin_id = $group_content_type->get('content_plugin');
    $group_content_type->set('plugin_config', $plugin_config[$group_type_id][$plugin_id]);
    $group_content_type->save(TRUE);
  }
}

/**
 * Update the members view to use the argument provided title.
 */
function group_update_8007() {
  if (\Drupal::moduleHandler()->moduleExists('views')) {
    $view = \Drupal::configFactory()->getEditable('views.view.group_members');
    if (!$view->isNew()) {
      $view->set('display.default.display_options.arguments.gid.title_enable', TRUE);
      $view->set('display.default.display_options.arguments.gid.title', '{{ arguments.gid|placeholder }} members');
      $view->save(TRUE);
    }
  }
}

/**
 * Make sure the views.view.group_members exists and fix broken copies.
 */
function group_update_8008() {
  $message = NULL;

  $name = 'views.view.group_members';
  $view = \Drupal::configFactory()->getEditable($name);

  // Only update or insert the view if the Views module is enabled.
  if (\Drupal::moduleHandler()->moduleExists('views')) {
    $save_from_yaml = FALSE;

    // If the view does not exist yet, we create it.
    if ($view->isNew()) {
      $save_from_yaml = TRUE;
      $message = 'The view did not exist yet and has therefore been created.';
    }
    // We did not properly add the view in previous update functions, but did
    // add keys to it, assuming the view existed. We should be able to find the
    // broken views by checking for the absence of an ID.
    elseif (!$view->get('id')) {
      $save_from_yaml = TRUE;
      $message = 'The view was broken by previous update hooks and has now been fixed.';
    }

    // If we flagged the view to be saved from the YAML definition, do so.
    if ($save_from_yaml) {
      // Get the storage for optional extension configuration.
      $optional_storage = new ExtensionInstallStorage(
        \Drupal::service('config.storage'),
        InstallStorage::CONFIG_OPTIONAL_DIRECTORY
      );

      // Read the data from the YAML file and save it to the view.
      $view->setData($optional_storage->read($name));
      $view->save(TRUE);
    }
    else {
      $message = 'The view was present and working as intended. Did nothing.';
    }
  }
  // Otherwise delete the view if it exists in the storage.
  elseif (!$view->isNew()) {
    $view->delete();
    $message = 'The view had been added even though the Views module is not installed. Removed the view.';
  }

  return $message;
}

/**
 * Add the 'creator_roles' property to all group types.
 */
function group_update_8009() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.type.') as $group_type_config_name) {
    $group_type = $config_factory->getEditable($group_type_config_name);
    $group_type->set('creator_roles', []);
    $group_type->save(TRUE);
  }
}

/**
 * Change the path field to computed for group entities.
 */
function group_update_8010() {
  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
  if ($entity_definition_update_manager->getEntityType('group')) {
    // Computed field definitions are not tracked by the entity definition
    // update manager, so remove them.
    $storage_definition = $entity_definition_update_manager->getFieldStorageDefinition('path', 'group');
    if ($storage_definition) {
      $entity_definition_update_manager->uninstallFieldStorageDefinition($storage_definition);
    }
  }
}

/**
 * Remove the the 'info_text' config option from all group content types.
 */
function group_update_8011() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.content_type.') as $group_content_type_config_name) {
    $group_content_type = $config_factory->getEditable($group_content_type_config_name);
    $group_content_type->clear('plugin_config.info_text');
    $group_content_type->save(TRUE);
  }
}

/**
 * Change 'edit' into 'update' in permission names.
 */
function group_update_8012() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.role.') as $group_role_config_name) {
    $group_role = $config_factory->getEditable($group_role_config_name);

    // Replace 'edit any/own' with 'update any/own' in permission names.
    $search = '%^edit (any|own) (\S+) (\S+)$%';
    $replace = 'update $1 $2 $3';

    $permissions = $group_role->get('permissions');
    foreach ($permissions as &$permission) {
      $permission = preg_replace($search, $replace, $permission);
    }

    $group_role->set('permissions', $permissions);
    $group_role->save();
  }
}

/**
 * Add the 'creator_membership' property to all group types.
 */
function group_update_8013() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.type.') as $group_type_config_name) {
    $group_type = $config_factory->getEditable($group_type_config_name);
    $group_type->set('creator_membership', TRUE);
    $group_type->save(TRUE);
  }
}

/**
 * Add the 'creator_wizard' property to all group types.
 */
function group_update_8014() {
  $config_factory = \Drupal::configFactory();

  foreach ($config_factory->listAll('group.type.') as $group_type_config_name) {
    $group_type = $config_factory->getEditable($group_type_config_name);
    $group_type->set('creator_wizard', TRUE);
    $group_type->save(TRUE);
  }
}

/**
 * Update the group members view to use the new cache group permissions context.
 */
function group_update_8015() {
  // Empty update. See group_post_update_view_cache_contexts();
}

/**
 * Re-run a fixed version of _8015 and gnode_update_8006 on all views.
 */
function group_update_8016() {
  // Empty update. See group_post_update_view_cache_contexts();
}


/**
 * Add storage schema to group content.
 */
function group_update_8017() {
  $manager = \Drupal::entityDefinitionUpdateManager();

  // Add the new storage schema to the GroupContent entity type, which should
  // automatically reindex the table.
  $entity_type = $manager->getEntityType('group_content');
  $entity_type->setHandlerClass('storage_schema', GroupContentStorageSchema::class);
  $manager->updateEntityType($entity_type);
}

/**
 * Set the 'owner' entity key and update the owner field.
 */
function group_update_8018() {
  $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
  foreach (['group', 'group_content'] as $entity_type_id) {
    $entity_type = $definition_update_manager->getEntityType($entity_type_id);
    $keys = $entity_type->getKeys();
    $keys['owner'] = 'uid';
    $entity_type->set('entity_keys', $keys);
    $definition_update_manager->updateEntityType($entity_type);
    $definition_update_manager->updateFieldStorageDefinition($definition_update_manager->getFieldStorageDefinition('uid', $entity_type_id));
  }
}

/**
 * Make sure Entity API is installed.
 */
function group_update_8019() {
  if (!\Drupal::moduleHandler()->moduleExists('entity')) {
    \Drupal::service('module_installer')->install(['entity']);
    return t('The "Entity API" module has been installed.');
  }
}

/**
 * Make sure VariationCache is installed.
 */
function group_update_8020() {
  if (!\Drupal::moduleHandler()->moduleExists('variationcache')) {
    \Drupal::service('module_installer')->install(['variationcache']);
    return t('The "VariationCache" module has been installed.');
  }
}

/**
 * Updates base field definitions that use the old default value callback.
 */
function group_update_8021() {
  $config_factory = \Drupal::configFactory();
  foreach ($config_factory->listAll('core.base_field_override.') as $config_name) {
    $base_field_override = $config_factory->getEditable($config_name);

    if ($base_field_override->get('field_name') !== 'uid') {
      continue;
    }

    switch ($base_field_override->get('entity_type')) {
      case 'group':
        $class = 'Drupal\group\Entity\Group';
        break;

      case 'group_content':
        $class = 'Drupal\group\Entity\GroupContent';
        break;

      default:
        continue 2;
    }

    if ($base_field_override->get('default_value_callback') !== $class . '::getCurrentUserId') {
      continue;
    }

    $base_field_override->set('default_value_callback', $class . '::getDefaultEntityOwner');
    $base_field_override->save(TRUE);
  }
}
