<?php

/**
 * @file
 * The Social group module.
 */

use Drupal\block\Entity\Block;
use Drupal\bootstrap\Bootstrap;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\group\Entity\GroupContent;
use Drupal\group\Entity\GroupContentInterface;
use Drupal\group\Entity\GroupContentType;
use Drupal\group\Entity\GroupInterface;
use Drupal\group\Entity\GroupType;
use Drupal\group\GroupMembership;
use Drupal\image\Entity\ImageStyle;
use Drupal\menu_link_content\MenuLinkContentInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drupal\social_core\Service\MembersPageHelperService;
use Drupal\social_group\Controller\SocialGroupController;
use Drupal\social_group\Controller\SocialGroupListBuilder;
use Drupal\social_group\Element\SocialGroupEntityAutocomplete;
use Drupal\social_group\Entity\Access\SocialGroupAccessControlHandler;
use Drupal\social_group\Entity\Group;
use Drupal\social_group\Form\SocialGroupAddForm;
use Drupal\social_group\GroupContentVisibilityUpdate;
use Drupal\social_group\SocialGroupInterface;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\Plugin\views\row\EntityRow;
use Drupal\views\ViewExecutable;
use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;

/**
 * Implements hook_theme().
 */
function social_group_theme() {
  return [
    'group_settings_help' => [
      'variables' => [
        'group_type' => NULL,
        'join_method' => NULL,
        'allowed_visibility' => NULL,
      ],
    ],
    'flag__mute_group_notifications' => [
      'base hook' => 'flag',
    ],
    'join' => [
      'variables' => [
        'primary' => NULL,
        'secondaries' => NULL,
        'attributes' => [],
        'entity' => NULL,
      ],
      'file' => 'social_group.theme.inc',
    ],
    'visibility' => [
      'variables' => [
        'name' => NULL,
        'icon' => NULL,
        'title' => [],
        'description' => NULL,
      ],
    ],
  ];
}

/**
 * Implements hook_entity_type_alter().
 */
function social_group_entity_type_alter(array &$entity_types) {
  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
  if (isset($entity_types['group'])) {
    $entity_types['group']
      ->setClass(Group::class)
      ->setListBuilderClass(SocialGroupListBuilder::class)
      ->setAccessClass(SocialGroupAccessControlHandler::class);
  }
}

/**
 * Prepares variables for group settings help text templates.
 *
 * Default template: group-settings-help.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - group_type: The group type.
 *   - join_method: The join methods.
 *   - allowed_visibility: The allowed visibilities.
 */
function template_preprocess_group_settings_help(array &$variables) {
}

/**
 * Check if a User is able to edit a Group's GroupType.
 *
 * @return bool
 *   TRUE if a user may edit a existings groups group type.
 */
function social_group_group_type_permission_check() {
  // Get the Group object from the route.
  if (($group = _social_group_get_current_group()) === NULL) {
    return FALSE;
  }

  // Check if we have a default visibility, if we don't it must be a custom
  // group type, we need to have a visibility in order to update group content.
  // please see hook_social_group_default_visibility_alter.
  if (\Drupal::service('social_group.helper_service')
    ->getDefaultGroupVisibility($group->getGroupType()->id()) === NULL) {
    return FALSE;
  }

  // Otherwise, return true when we are able to edit the current group type.
  return \Drupal::currentUser()->hasPermission('edit group types');
}

/**
 * Get group overview route.
 *
 * @return array
 *   An array with route name and parameters and group.
 */
function _social_group_get_overview_route(GroupInterface $group) {
  $route = [
    'name' => 'view.groups.page_user_groups',
    'parameters' => ['user' => \Drupal::currentUser()->id()],
  ];
  \Drupal::moduleHandler()
    ->alter('social_group_overview_route', $route, $group);
  return $route;
}

/**
 * Form submit redirect for social groups.
 *
 * @param array $form
 *   Group add or group edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_edit_submit_redirect(array $form, FormStateInterface $form_state) {
  // Set redirect to the group about page.
  $route_parameters = $form_state->getRedirect()->getRouteParameters();
  if (!empty($route_parameters['group'])) {
    $form_state->setRedirect(
      'view.group_information.page_group_about',
      $route_parameters
    );
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_group_form_entity_access_by_field_visibility_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Add option to disable public group creation.
  $form['#submit'][] = '_social_group_visibility_settings_submit';
}

/**
 * Form submit for visibility settings.
 *
 * @param array $form
 *   Group add or group edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_visibility_settings_submit(array $form, FormStateInterface $form_state) {
  $roles = ['authenticated', 'verified'];
  $disable_public_visibility = $form_state->getValue('disable_public_visibility');

  foreach ($roles as $role) {
    /** @var \Drupal\user\Entity\Role $role */
    $role = Role::load($role);
    if ($role instanceof RoleInterface) {
      $current_permission = $role->hasPermission('create public_group group');

      if ($disable_public_visibility === 1 && $current_permission === TRUE) {
        user_role_revoke_permissions($role->id(), [
          'create public_group group',
        ]);
      }
      if ($disable_public_visibility === 0 && $current_permission === FALSE) {
        user_role_grant_permissions($role->id(), [
          'create public_group group',
        ]);
      }
    }
  }

}

/**
 * Load group content label and group label.
 *
 * @return array
 *   array of group labels.
 */
function _social_group_get_group_labels() {

  // Load the entity label from the group content being handled.
  $group_content = \Drupal::routeMatch()
    ->getParameter('group_content')
    ->label();

  // Load group name.
  $group = \Drupal::routeMatch()->getParameter('group')->label();

  return [
    $group_content,
    $group,
  ];

}

/**
 * Implements hook_preprocess().
 */
function social_group_preprocess(array &$variables, string $hook): void {
  if (
    isset(
      $variables['elements']['#view_mode'],
      $variables['elements']['#' . $hook],
    ) &&
    in_array($variables['elements']['#view_mode'], ['hero', 'statistic'])
  ) {
    /** @var \Drupal\social_group\JoinManagerInterface $manager */
    $manager = \Drupal::service('plugin.manager.social_group.join');

    $manager->preprocess($variables, $hook);
  }
}

/**
 * Prepares variables for profile templates.
 *
 * Default template: profile.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An array of elements to display in view mode.
 *   - profile: The profile object.
 *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
 */
function social_group_preprocess_group(array &$variables) {
  /** @var \Drupal\social_group\GroupStatistics $group_statistics */
  $group_statistics = \Drupal::service('social_group.group_statistics');

  /** @var \Drupal\social_group\SocialGroupInterface $group */
  $group = $variables['group'];

  $variables['title'] = $group->label();
  $variables['joined'] = FALSE;
  $variables['closed_group'] = FALSE;
  $variables['cta'] = '';
  $variables['closed_group_lock'] = $variables['secret_group_shield'] = FALSE;

  $group_type_id = $group->getGroupType()->id();
  if (!array_key_exists('direct', _social_group_get_join_methods($group))) {
    $variables['closed_group_lock'] = TRUE;
  }
  $variables['group_type_id'] = $group_type_id;
  $variables['group_type'] = $group->getGroupType()->label();

  /** @var \Drupal\Core\Render\RendererInterface $renderer */
  $renderer = \Drupal::service('renderer');

  // Render the group settings help, gear icon with popover.
  $group_settings_help = _social_group_render_group_settings_hero($group);
  $variables['group_settings_help'] = $renderer->renderPlain($group_settings_help);

  $account = \Drupal::currentUser();

  if (
    in_array($variables['view_mode'], ['statistic', 'hero']) &&
    $group->hasMember($account)
  ) {
    if ($account->isAuthenticated()) {
      $content = $renderer->render($variables['content']['flag_mute_group_notifications']);

      if ((string) $content) {
        $variables['join']['#secondaries'][] = $content;
      }
    }
  }
  // Prevent access to mute group notifications if a user is not a group member.
  else {
    unset($variables['content']['flag_mute_group_notifications']);
  }

  // Add the hero styled image.
  if ($group->hasField('field_group_image') && !empty($group->get('field_group_image')->entity)) {
    // Fetch image style from field info.
    $original_image_style = $variables['content']['field_group_image'][0]['#image_style'] ?? '';
    $image_style = ImageStyle::load($original_image_style);

    if ($image_style instanceof ImageStyle) {
      $variables['group_hero_styled_image_url'] = $image_style->buildUrl($group->get('field_group_image')->entity->getFileUri());
    }
  }

  // Add group edit url for management.
  if ($group instanceof Group) {
    // Get the current route name to check if
    // the user is on the edit or delete page.
    $route = \Drupal::routeMatch()->getRouteName();
    if (!in_array($route, [
      'entity.group.edit_form',
      'entity.group.delete_form',
    ])) {
      if ($group->access('update', $account)) {
        $variables['group_edit_url'] = $group->toUrl('edit-form')->toString();
        $variables['#cache']['contexts'][] = 'route.name';
      }
    }

    // Ensure all groups get the group.type differentiating labels in teasers
    // hero's and full nodes.
    // But also if the allowed join method is there, this should be added.
    $variables['#cache']['contexts'][] = 'route.group';
    if ($group->hasField('field_group_allowed_join_method') &&
      !empty($group->getFieldValue('field_group_allowed_join_method', 'value'))) {
      $variables['#cache']['contexts'][] = 'social_group_join_method';
    }
  }
  // Count number of group members.
  $variables['group_members'] = $group_statistics->getGroupMemberCount($group);

  // Prepare variables for statistic block.
  if ($variables['view_mode'] === 'statistic') {
    // Add context, since we render join / invite only etc links in the block.
    $variables['#cache']['contexts'][] = 'route.group';
    $variables['#cache']['contexts'][] = 'user';
    $about_url = Url::fromRoute('view.group_information.page_group_about', [
      'group' => $group->id(),
    ]);
    $variables['about_url'] = $about_url;
    if ($group->getGroupType()->hasContentPlugin('group_node:event')) {
      $variables['group_events'] = $group_statistics->getGroupNodeCount($group, 'event');
      $variables['group_events_label'] = \Drupal::translation()
        ->formatPlural($variables['group_events'],
          'event',
          'events'
        );
    }
    if ($group->getGroupType()->hasContentPlugin('group_node:topic')) {
      $variables['group_topics'] = $group_statistics->getGroupNodeCount($group, 'topic');
      $variables['group_topics_label'] = \Drupal::translation()
        ->formatPlural($variables['group_topics'],
          'topic',
          'topics'
        );
    }
  }

  // Update if user is member of a group.
  $variables['joined'] = $group->hasMember($account);
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_group__hero(array &$variables) {
  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $variables['group'];
  $account = \Drupal::currentUser();

  if ($group->bundle() == 'public_group' && $account->isAnonymous()) {
    $variables['group_operations_url'] = Url::fromRoute('user.register', [], [
      'query' => [
        'destination' => Url::fromRoute('entity.group.join', [
          'group' => $group->id(),
        ])->toString(),
      ],
    ])->toString();
  }

  // Render the group settings help, gear icon with popover.
  $group_settings_help = _social_group_render_group_settings_hero($group);
  $variables['group_settings_help'] = \Drupal::service('renderer')
    ->renderPlain($group_settings_help);
}

/**
 * Renders the group settings based on available fields for the hero.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 *
 * @return array
 *   the actual tooltip render array.
 */
function _social_group_render_group_settings_hero(GroupInterface $group) {
  $description = '';
  // Optional after 10.0.x we can render the group visibility.
  if ($group_visibility_option = _social_group_get_group_visibility($group)) {
    // Wrap our chosen description in a container.
    $description .= '<span class="title">' . t('Group visibility') . '</span>';
    $description .= '<div class="group-visibility-details container-background">';
    foreach ($group_visibility_option as $key => $group_visibility_string) {
      $description .= $group_visibility_string;
    }
    $description .= '</div>';
  }
  // Optional after 9.x we can render the allowed content visibility.
  if ($allowed_visibility_option = _social_group_get_allowed_visibility($group)) {
    // Wrap our chosen description in a container.
    $description .= '<span class="title">' . t('Group content visibility') . '</span>';
    $description .= '<div class="group-visibility-details container-background">';
    foreach ($allowed_visibility_option as $key => $allowed_visibility_string) {
      $description .= $allowed_visibility_string;
    }
    $description .= '</div>';
  }
  // Optional after 9.x we can render the join methods.
  if ($join_methods_option = _social_group_get_join_methods($group)) {
    // Wrap our chosen description in a container.
    $description .= '<span class="title">' . t('Join method') . '</span>';
    $description .= '<div class="group-visibility-details container-background">';
    foreach ($join_methods_option as $key => $join_methods_string) {
      $description .= $join_methods_string;
    }
    $description .= '</div>';
  }

  return social_group_render_tooltip('group_hero', t('Access permissions'), $description);
}

/**
 * Get the join methods of a group.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 *
 * @return array
 *   Returns join methods of a group.
 */
function _social_group_get_join_methods(GroupInterface $group) {
  $group_type = $group->getGroupType();
  $group_type_id = $group_type->id();
  $join_methods = [];

  $group_types = ['flexible_group'];
  \Drupal::moduleHandler()->alter('social_group_settings', $group_types);

  if (in_array($group_type_id, $group_types)) {
    // Try to retrieve join methods from Group directly.
    if ($group->hasField('field_group_allowed_join_method')) {
      $allowed_options = $group->get('field_group_allowed_join_method')
        ->getValue();
      foreach ($allowed_options as $option) {
        // Lets grab the value from the selected radio item.
        if (!empty($option['value']) && is_string($option['value'])) {
          $join_methods[$option['value']] = social_group_allowed_join_method_description($option['value']);
        }
      }
    }
  }

  // Get join method based on group type. TODO get it programmatically.
  switch ($group_type_id) {
    case 'secret_group':
    case 'closed_group':
      $join_methods['added'] = social_group_allowed_join_method_description('added');
      break;

    case 'public_group':
    case 'open_group':
      $join_methods['direct'] = social_group_allowed_join_method_description('direct');
      $join_methods['added'] = social_group_allowed_join_method_description('added');
      break;
  }

  return $join_methods;
}

/**
 * Get the group visibility label of a group.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 * @param string $field_name
 *   The field name of the visibility field for a group type.
 *
 * @return array
 *   Returns the visibility options of a group.
 */
function _social_group_get_group_visibility(GroupInterface $group, $field_name = NULL) {
  $group_type = $group->getGroupType();
  $group_type_id = $group_type->id();
  $group_visibility = [];

  $group_types = ['flexible_group'];
  \Drupal::moduleHandler()->alter('social_group_settings', $group_types);

  if (in_array($group_type_id, $group_types)) {
    // By default we ship with the below field, lets grab its value(s).
    if ($group->hasField('field_flexible_group_visibility')) {
      $visibility_values = $group->get('field_flexible_group_visibility')
        ->getValue();
      // Lets grab the rendered description for the group visibility.
      if (!empty($visibility_values)) {
        foreach ($visibility_values as $visibility_value) {
          if (!empty($visibility_value['value']) && is_string($visibility_value['value'])) {
            $group_visibility[$visibility_value['value']] = social_group_group_visibility_description($visibility_value['value']);
          }
        }
      }
      return $group_visibility;
    }
    // If the above field doesn't exist the user might have a different one
    // so lets see if it's given as argument and get it's description.
    if ($field_name !== NULL && $group->hasField($field_name)) {
      $visibility_values = $group->get($field_name)->getValue();
      // Lets grab the rendered description for the group visibility.
      if (!empty($visibility_values)) {
        foreach ($visibility_values as $visibility_value) {
          if (!empty($visibility_value['value']) && is_string($visibility_value['value'])) {
            $group_visibility[$visibility_value['value']] = social_group_group_visibility_description($visibility_value['value']);
          }
        }
      }
      return $group_visibility;
    }
  }

  // Get join method based on group type.
  switch ($group_type_id) {
    case 'secret_group':
    case 'closed_group':
    case 'public_group':
    case 'open_group':
      return $group_visibility;

    default:
      // For non Open Social group types, we can allow developers to pass
      // a field name to return a label instead.
      if ($field_name !== NULL && $group->hasField($field_name)) {
        $group_visibility = $group->get($field_name)->getValue();
      }
      return $group_visibility;
  }
}

/**
 * Get the allowed visibility of a group.
 *
 * @param Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 *
 * @return array
 *   Returns allowed visibility of a group.
 */
function _social_group_get_allowed_visibility(GroupInterface $group) {
  $group_type = $group->getGroupType();
  $group_type_id = $group_type->id();
  $allowed_visibility = [];

  $group_types = ['flexible_group'];
  \Drupal::moduleHandler()->alter('social_group_settings', $group_types);

  if (in_array($group_type_id, $group_types)) {
    // Try to retrieve allowed visibility from Group directly.
    $allowed_options = $group->get('field_group_allowed_visibility')
      ->getValue();
    foreach ($allowed_options as $option) {
      if (!empty($option['value']) && is_string($option['value'])) {
        $allowed_visibility[$option['value']] = social_group_allowed_visibility_description($option['value']);
      }
    }
  }

  // Get allowed visibility based on group type. TODO get it programmatically.
  switch ($group_type_id) {
    case 'secret_group':
    case 'closed_group':
      $allowed_visibility['group'] = social_group_allowed_visibility_description('group');
      break;

    case 'open_group':
      $allowed_visibility['community'] = social_group_allowed_visibility_description('community');
      break;

    case 'public_group':
      $allowed_visibility['public'] = social_group_allowed_visibility_description('public');
      break;
  }

  return $allowed_visibility;
}

/**
 * Gets a description as HTML markup for the selected group visibility option.
 *
 * @param string $key
 *   The visibility option name.
 */
function social_group_group_visibility_description(string $key): string {
  /** @var \Drupal\social_group\SocialGroupHelperServiceInterface $helper */
  $helper = \Drupal::service('social_group.helper_service');

  return $helper->description(
    $key,
    'social_group_group_visibility_description',
  );
}

/**
 * Sets dynamic allowed values for the join method field.
 *
 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
 *   The field storage definition.
 * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity
 *   (optional) The entity object.
 */
function _social_group_allowed_values_callback(
  FieldStorageDefinitionInterface $definition,
  FieldableEntityInterface $entity = NULL
): array {
  $hook = 'social_group_join_method_info';

  $values = \Drupal::moduleHandler()->invokeAll($hook);

  \Drupal::moduleHandler()->alter($hook, $values, $entity);

  $values = array_map(
    fn ($id, $item) => ['id' => $id] + $item,
    array_keys($values),
    array_values($values),
  );

  usort($values, [SortArray::class, 'sortByWeightElement']);

  return array_column($values, 'title', 'id');
}

/**
 * Implements hook_social_group_content_visibility_field().
 */
function social_group_social_group_content_visibility_field(): string {
  return 'field_content_visibility';
}

/**
 * Implements hook_social_group_content_visibility_description_alter().
 */
function social_group_social_group_content_visibility_description_alter(string $key, &$description): void {
  $items = [
    'public' => [
      'title' => t('Public'),
      'description' => t('visible to all visitors to the platform.'),
    ],
    'community' => [
      'title' => t('Community'),
      'description' => t('only visible to all logged-in users.'),
    ],
    'group' => [
      'title' => t('Group members only'),
      'description' => t('only visible to logged-in group members.'),
      'icon' => 'lock',
    ],
  ];

  if (isset($items[$key])) {
    $description = $items[$key];
  }
}

/**
 * Implements hook_social_group_group_visibility_description_alter().
 */
function social_group_social_group_group_visibility_description_alter(string $key, &$description): void {
  $items = [
    'public' => [
      'title' => t('Public'),
      'description' => t('All visitors of the platform can see this group'),
    ],
    'community' => [
      'title' => t('Community'),
      'description' => t('Only members who are logged in can see this group'),
    ],
    'members' => [
      'title' => t('Group members only (Secret)'),
      'description' => t('Only group members can see this group'),
      'icon' => 'lock',
    ],
  ];

  if (isset($items[$key])) {
    $description = $items[$key];
  }
}

/**
 * Implements hook_social_group_join_method_usage().
 */
function social_group_social_group_join_method_usage(): array {
  return [
    [
      'entity_type' => 'group',
      'bundle' => 'closed_group',
      'method' => 'added',
    ],
    [
      'entity_type' => 'group',
      'bundle' => ['open_group', 'public_group'],
      'method' => ['direct', 'added'],
    ],
  ];
}

/**
 * Implements hook_social_group_join_method_info().
 */
function social_group_social_group_join_method_info(): array {
  return [
    'direct' => [
      'title' => t('Open to join'),
      'description' => t('users can join this @entity_type_id without approval.'),
      'icon' => 'join_open',
      'weight' => 10,
    ],
    'added' => [
      'title' => t('Invite only'),
      'description' => t('users can only join this @entity_type_id if they are
added/invited by @entity_type_id managers.'),
      'icon' => 'invite',
      'weight' => 20,
    ],
  ];
}

/**
 * Returns a description for the field_group_allowed_join_method options.
 *
 * @param string $key
 *   The join method key.
 * @param string $entity_type_id
 *   (optional) The entity type ID. Defaults to 'group'.
 */
function social_group_allowed_join_method_description(
  string $key,
  string $entity_type_id = 'group'
): string {
  $description = '';

  // We need it to be specified otherwise we can't build the markup.
  if (empty($key)) {
    return $description;
  }

  $hook = 'social_group_join_method_info';

  \Drupal::moduleHandler()->invokeAllWith($hook, function (callable $hook, string $module) use (&$implementors) {
    // There is minimal overhead since the hook is not invoked.
    $implementors[] = $module;
  });

  $found = FALSE;

  foreach ($implementors as $module) {
    /** @var callable $function */
    $function = $module . '_' . $hook;

    foreach ($function() as $id => $item) {
      if ($id === $key) {
        $found = TRUE;
        break 2;
      }
    }
  }

  if (!$found || !isset($item)) {
    return $description;
  }

  /** @var \Drupal\Core\StringTranslation\TranslatableMarkup $text */
  $text = $item['description'];

  $definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id);

  if ($definition === NULL) {
    return $description;
  }

  // @codingStandardsIgnoreStart
  $text = t(
    $text->getUntranslatedString(),
    $text->getArguments() + [
      '@entity_type_id' => $definition->getSingularLabel(),
    ],
  );
  // @codingStandardsIgnoreEnd

  // Add explanatory descriptive text after the icon.
  $description = '<p>';
  $description .= '<p><strong><svg class="icon-small"><use xlink:href="#icon-' . $item['icon'] . '"></use></svg></strong>';
  $description .= '<strong>' . $item['title']->render() . '</strong>';
  $description .= ' - ' . $text->render();
  $description .= '</p>';

  // Allow modules to provide their own markup for a given key in the
  // join method #options array.
  \Drupal::moduleHandler()->alter(
    'social_group_allowed_join_method_description',
    $key,
    $description,
  );

  return $description;
}

/**
 * Implements hook_entity_view_alter().
 */
function social_group_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
  // For group entities.
  if ($entity instanceof Group) {
    // Add cache contexts for some view modes.
    switch ($build['#view_mode']) {
      case 'hero':
        // Add cache contexts for group type & permissions.
        // We need join / leave for the CTA and also update permissions
        // for the button to edit a group.
        $build['#cache']['contexts'][] = 'route.group';
        $build['#cache']['contexts'][] = 'user.group_permissions';
        $build['#cache']['tags'][] = 'group_block:' . $entity->id();
        break;

      case 'teaser':
        $build['#cache']['contexts'][] = 'user';
        break;
    }
  }
}

/**
 * Implements hook_form_alter().
 */
function social_group_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $social_group_types = [
    'open_group',
    'closed_group',
    'public_group',
  ];

  \Drupal::moduleHandler()->alter('social_group_types', $social_group_types);

  $join_forms = [];
  $leave_forms = [];
  $membership_add_forms = [];
  $membership_edit_forms = [];
  $membership_delete_forms = [];
  $group_forms = [];

  foreach ($social_group_types as $social_group_type) {
    $join_forms[] = "group_content_{$social_group_type}-group_membership_group-join_form";
    $leave_forms[] = "group_content_{$social_group_type}-group_membership_group-leave_form";
    $membership_add_forms[] = "group_content_{$social_group_type}-group_membership_add_form";
    $membership_edit_forms[] = "group_content_{$social_group_type}-group_membership_edit_form";
    $membership_delete_forms[] = "group_content_{$social_group_type}-group_membership_delete_form";
    $group_forms['edit'][] = "group_{$social_group_type}_edit_form";
    $group_forms['add'][] = "group_{$social_group_type}_add_form";
    $group_forms['delete'][] = "group_{$social_group_type}_delete_form";
  }

  $action_forms = array_merge($join_forms, $leave_forms, $membership_delete_forms, $membership_add_forms);
  $membership_forms = array_merge($membership_add_forms, $membership_edit_forms);

  // Perform alterations on joining / leaving groups.
  if (in_array($form_id, $membership_delete_forms)) {
    $form['actions']['submit']['#submit'][] = '_social_membership_delete_form_submit';

    [$name, $group] = _social_group_get_group_labels();

    // Give a better title, description and submit button value.
    $form['#title'] = t('Remove %name from %group', [
      '%name' => $name,
      '%group' => $group,
    ]);
    $form['actions']['submit']['#value'] = t('Remove');
    $form['description']['#markup'] = t('Are you sure you want to remove %name from %group?', [
      '%name' => $name,
      '%group' => $group,
    ]);
  }

  // Set some helpful text on the group join form now it's there.
  if (in_array($form_id, $join_forms)) {
    $markup = t('By submitting this form you will become a member of the group. As a member of the group you will begin to receive notifications about changes and interactions within the group. Your profile will also be included in group membership.');

    // When the user is accepting an invite, change the text to something more
    // meaningful.
    if (\Drupal::routeMatch()->getRouteName() === 'ginvite.invitation.accept') {
      $markup = t('Are you sure to accept the invitation and join this group? You can leave this group at any time.');
    }

    $form['help'] = [
      '#type' => 'item',
      '#markup' => $markup,
    ];
    $form['path']['#type'] = 'hidden';
  }

  // Perform alterations on joining / leaving groups.
  if (in_array($form_id, $action_forms)) {
    if (isset($form['path'])) {
      $form['path']['#access'] = FALSE;
    }

    // Add cancel option on join and leave form.
    $form['actions']['cancel'] = [
      '#type' => 'submit',
      '#value' => t('Cancel'),
      '#submit' => ['_social_group_cancel_join_leave_form'],
      '#limit_validation_errors' => [],
    ];

    $form['actions']['submit']['#submit'][] = '_social_group_action_form_submit';
  }

  if (in_array($form_id, $membership_forms) && Drupal::currentUser()
    ->id() != 1) {
    // Change titles on membership forms.
    $form['entity_id']['widget'][0]['target_id']['#title'] = t('Find people by name');
    $form['group_roles']['widget']['#title'] = t('Group roles');
    $form['path']['#type'] = 'hidden';
  }

  /** @var \Drupal\social_group\SocialGroupHelperServiceInterface $helper */
  $helper = \Drupal::service('social_group.helper_service');

  // Change the form when adding members directly in groups.
  if (
    in_array($form_id, $membership_add_forms, TRUE) &&
    \Drupal::routeMatch()->getRouteName() !== 'grequest.group_request_membership_approve'
  ) {
    // Lets add the new select 2 widget to add members to a group.
    $form['entity_id']['widget'][0]['target_id'] = $helper->addMemberFormField();

    // Group roles and the automated URL alias don't make sense here.
    foreach (['group_roles', 'path'] as $field) {
      if (isset($form[$field])) {
        unset($form[$field]);
      }
    }
  }

  $form_object = $form_state->getFormObject();

  if ($form_object instanceof EntityFormInterface) {
    $entity = $form_object->getEntity();
  }

  // Check if form is group content create form.
  if (isset($form['#entity_type'])) {
    if ($form['#entity_type'] === 'node') {
      // Add custom validate handler to check if groups field values in node.
      $form['#validate'][] = '_social_group_node_form_cross_posting_validate';

      $group = _social_group_get_current_group();

      if (!empty($group)) {
        // Add custom submit handler just for redirect purposes.
        // We don't want to override the form::save in group.
        $form['actions']['submit']['#submit'][] = '_social_group_node_form_submit';
      }
      elseif (isset($entity)) {
        // If node don't belong to any group as a group content plugin
        // we don't need group visibility field.
        /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $gc_manager */
        $gc_manager = \Drupal::service('plugin.manager.group_content_enabler');

        if (!$gc_manager->getGroupContentTypeIds('group_node:' . $entity->bundle())) {
          unset($form['groups']);
        }
      }
    }

    // Let's remove the original submit function in favor of this submit.
    if (isset($form['groups'])) {
      $entity_types = \Drupal::moduleHandler()->invokeAll('social_group_linked_type');

      if (in_array($form['#entity_type'], $entity_types)) {
        $form['actions']['submit']['#submit'] = array_filter(
          $form['actions']['submit']['#submit'],
          fn ($callback): bool => $callback !== 'group_content_entity_submit',
        );
      }
    }
  }

  if (in_array($form_id, $group_forms['add']) || in_array($form_id, $group_forms['edit']) || in_array($form_id, $group_forms['delete'])) {
    // Add custom submit handler just for redirect purposes.
    $form['actions']['submit']['#submit'][] = '_social_group_edit_submit_redirect';

    if (in_array($form_id, $group_forms['add']) || in_array($form_id, $group_forms['edit'])) {
      $form['path']['#type'] = 'hidden';
      $form['actions']['submit']['#value'] = t('Save');
      // Hide default title from Address field.
      if (isset($form['field_group_address'])) {
        $form['field_group_address']['widget'][0]['#title'] = '';
      }
    }

    if (in_array($form_id, $group_forms['edit'])) {
      $social_group_form = SocialGroupAddForm::create(\Drupal::getContainer());
      $group_type_element = $social_group_form->getGroupTypeElement(TRUE);

      // Get the current group.
      if (($group = _social_group_get_current_group()) !== NULL) {
        // Set the default value in the form.
        $group_type_element['widget']['#default_value'] = $group->getGroupType()
          ->id();
      }

      // If user doesn't have permission to change group types disable it.
      // Or if group types can't be edited due to visibility issues.
      if (!social_group_group_type_permission_check()) {
        $group_type_element['#disabled'] = TRUE;
      }
      else {
        $group_type_element['#prefix'] = '<div id="group-type-result"></div>';
        $group_type_element['widget']['#ajax'] = [
          'callback' => '_social_group_inform_group_type_selection',
          'effect' => 'fade',
          'event' => 'change',
        ];
      }

      $form['group_type'] = $group_type_element;

      // Disable all group types that can't be edited. Because they don't have
      // a visibility.
      /** @var string $type */
      foreach (array_keys($form['group_type']['widget']['#options']) as $type) {
        if ($helper->getDefaultGroupVisibility($type) === NULL) {
          $form['group_type']['widget'][$type] = [
            '#disabled' => TRUE,
          ];
        }
      }

      $form['#fieldgroups']['group_settings']->children[] = 'group_type';
      $form['#group_children']['group_type'] = 'group_settings';

      $form['actions']['submit']['#submit'][] = '_social_group_type_edit_submit';
    }

    if (
      in_array($form_id, $group_forms['delete']) &&
      ($group = _social_group_get_current_group()) !== NULL
    ) {
      // Add custom submit handler to delete all content of the group.
      $form['description']['#markup'] = t('Are you sure you want to delete your group "@group" along with all of the posts, events and topics inside this group? This action cannot be undone.', ['@group' => $group->label()]);
      $form['actions']['cancel'] = [
        '#type' => 'submit',
        '#value' => t('Cancel'),
        '#submit' => ['_social_group_cancel_join_leave_form'],
        '#limit_validation_errors' => [],
      ];
      array_unshift($form['actions']['submit']['#submit'], '_social_group_delete_group');
    }

    // Move "path" field to "Settings" section if the "Social Path Manager"
    // module is disabled.
    if (!\Drupal::moduleHandler()->moduleExists('social_path_manager')) {
      if (!empty($form['path'])) {
        $form['path']['#type'] = 'fieldset';
        if (Element::isVisibleElement($form['path'])) {
          if (!empty($form['#fieldgroups']['group_settings'])) {
            $form['path']['#group'] = 'group_settings';
            $form['#fieldgroups']['group_settings']->children[] = 'path';
          }
        }
      }
    }

    // Hide default field group "advanced" for group entities.
    if (!empty($form['advanced']) && Element::isVisibleElement($form['advanced'])) {
      // Hide revision fields.
      $form['advanced']['#access'] =
      $form['revision']['#access'] =
      $form['revision_log']['#access'] =
      $form['revision_information']['#access'] = FALSE;
    }
  }

  if (
    isset($entity) &&
    $form_object instanceof EntityFormInterface &&
    in_array($form_object->getOperation(), ['default', 'add', 'edit'])
  ) {
    /** @var \Drupal\social_group\JoinManagerInterface $manager */
    $manager = \Drupal::service('plugin.manager.social_group.join');

    $found = FALSE;
    $entity_type_id = $entity->getEntityTypeId();

    $bundle = $entity->getEntityType()->getBundleEntityType() === NULL
      ? NULL : $entity->bundle();

    foreach ($manager->relations() as $data) {
      if (
        isset($data['field'], $form[$data['field']]) &&
        !isset($data['method']) &&
        $data['entity_type'] === $entity_type_id &&
        (
          !isset($data['bundle']) &&
          $bundle === NULL ||
          in_array($bundle, (array) $data['bundle'])
        )
      ) {
        $found = TRUE;
        break;
      }
    }

    if ($found && isset($data)) {
      $field = &$form[$data['field']]['widget'];

      if ($field['#type'] === 'checkboxes') {
        $field['#type'] = 'radios';
      }

      if ($entity->isNew()) {
        $hook = 'social_group_join_method_info';
        $join_methods = [];

        // We could've used `invokeAll` here but decided to make our array
        // associative and later use `id` so we need to do some stuff manually.
        \Drupal::moduleHandler()->invokeAllWith($hook, function (callable $hook, string $module) use (&$join_methods) {
          foreach ($hook() as $id => $join_method) {
            $join_methods[] = [
              'id' => $id,
              'weight' => $join_method['weight'],
            ];
          }
        });

        usort($join_methods, [SortArray::class, 'sortByWeightElement']);

        $join_methods = array_column($join_methods, 'id');
        $join_method = array_pop($join_methods);
        $join_methods = array_reverse($join_methods);

        foreach ($join_methods as $current_join_method) {
          if (in_array($current_join_method, $field['#default_value'])) {
            $default_join_method = $current_join_method;
            break;
          }
        }

        $field['#default_value'] = $default_join_method ?? $join_method;
      }
      elseif (isset($field['#default_value']) && is_array($field['#default_value'])) {
        $field['#default_value'] = reset($field['#default_value']);
      }
    }
  }

  // Exposed Filter block on the all-groups overview.
  if ($form['#id'] === 'views-exposed-form-newest-groups-page-all-groups' ||
    $form['#id'] === 'views-exposed-form-search-groups-page-no-value' ||
    $form['#id'] === 'views-exposed-form-search-groups-page') {
    $account = \Drupal::currentUser();
    if (!empty($form['type']['#options'])) {
      // Make sure we add cache tags so whenever a type is added/removed in the
      // vocabulary this gets cleared.
      $form['#cache']['tags'][] = 'taxonomy_term_list:group_type';

      foreach ($form['type']['#options'] as $type => $label) {
        // All / Any we can skip they are optional translatable options
        // and not group types.
        if ($label instanceof TranslatableMarkup) {
          continue;
        }
        if (!social_group_can_view_groups_of_type($type, $account)) {
          unset($form['type']['#options'][$type]);
        }
      }
      // If we determined the user can only see the All / Any option
      // And perhaps one more group type, like flexible groups.
      // this filter doesn't make sense, and we hide it.
      // This also allows other filters, depending on the options to show up.
      if (count($form['type']['#options']) === 2 &&
        array_key_exists('All', $form['type']['#options'])) {
        $form['type']['#type'] = 'hidden';
      }
    }
  }

  // Add custom validation for "group_type" group field.
  if (!empty($form['#entity_type']) && $form['#entity_type'] === 'group') {
    if (isset($form['group_type']) && Element::isVisibleElement($form['group_type'])) {
      $form['group_type']['#element_validate'][] = '_social_group_cross_posting_group_type_validate';
    }
  }

  if (in_array($form_id, $group_forms['edit']) || in_array($form_id, $group_forms['add'])) {
    // Move the Published status to group settings with a new wrapper.
    if (!empty($form['status']) && Element::isVisibleElement($form['status'])) {
      // Add an details element to status.
      $form['status_label'] = [
        '#type' => 'details',
        '#title' => t('Publish status'),
        '#description' => '',
        '#open' => TRUE,
        '#weight' => 110,
        '#group' => 'group_settings',
      ];
      $form['status']['#group'] = 'status_label';
      $form['status_label']['status'] = $form['status'];
      unset($form['status']);
    }
  }
}

/**
 * Validation callback checking if group can be converted to another group type.
 *
 * @param array $element
 *   Form element array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state object.
 */
function _social_group_cross_posting_group_type_validate(array $element, FormStateInterface $form_state) {
  // Cross posting is disabled globally.
  $group_settings = \Drupal::config('social_group.settings');
  if (!$group_settings->get('cross_posting.status')) {
    return;
  }

  $allowed_group_types = $group_settings->get('cross_posting.group_types');

  $value = $element['widget']['#value'];
  $previous_value = $element['widget'][$value]['#default_value'];
  // If the value changed nothing to validate.
  if ($value === $previous_value) {
    return;
  }

  // Allow change "group_type" only for group if the previous and current
  // group type are using cross-group posting feature.
  if (in_array($previous_value, $allowed_group_types) && !in_array($value, $allowed_group_types)) {
    $form_state->setError($element['widget'], t('Cross-group posting for @group_type is disabled', ['@group_type' => $element['widget'][$value]['#title']]));
  }
}

/**
 * Function to validate entries against group members.
 *
 * @param array $element
 *   The element being processed.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 * @param array $complete_form
 *   The complete form structure.
 *
 * @deprecated in social:11.1.0 and is removed from social:13.0.0. Use
 *   \Drupal\social_group\Element\SocialGroupEntityAutocomplete::validateEntityAutocompleteSelect2()
 *   instead.
 *
 * @see https://www.drupal.org/node/3254715
 */
function _social_group_unique_members(
  array &$element,
  FormStateInterface $form_state,
  array $complete_form
): void {
  // Call the autocomplete function to make sure enrollees are unique.
  SocialGroupEntityAutocomplete::validateEntityAutocompleteSelect2(
    $element,
    $form_state,
    $complete_form,
  );
}

/**
 * Function that gives information to a CM+ when they edit a Group Type.
 *
 * @return \Drupal\Core\Ajax\AjaxResponse
 *   An ajax command containing the informative text.
 */
function _social_group_inform_group_type_selection() {
  $text = t('Please note that changing the group type will also change the
  visibility of the group content and the way users can join the group.');
  $ajax_response = new AjaxResponse();
  $ajax_response->addCommand(new HtmlCommand('#group-type-result', $text));

  return $ajax_response;
}

/**
 * Form submit for removing members from a group so we can clear caches.
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 */
function _social_group_type_edit_submit($form, FormStateInterface $form_state) {
  // Check if the group_type changed.
  // Visibility be empty for flexible groups.
  $group = _social_group_get_current_group();
  if (
    $group instanceof GroupInterface &&
    !empty($form['group_type']) &&
    (\Drupal::service('social_group.helper_service')
      ->getDefaultGroupVisibility($group->getGroupType()->id()) != NULL)
  ) {
    $default_type = $form['group_type']['widget']['#default_value'];
    $new_type = $form_state->getValue('group_type')[0]['value'];

    // Update the Group entity and all it's content when the type changed.
    if ($new_type !== $default_type) {
      // Update the default visibility of all the content.
      GroupContentVisibilityUpdate::batchUpdateGroupContentVisibility($group, $new_type);
    }
  }

  if ($group instanceof GroupInterface) {
    // Make sure we clear cache tags accordingly.
    $cache_tags = _social_group_cache_tags($group);
    foreach ($cache_tags as $cache_tag) {
      Cache::invalidateTags([$cache_tag]);
    }
  }
}

/**
 * Form submit for removing members from a group so we can clear caches.
 */
function _social_membership_delete_form_submit($form, FormStateInterface $form_state) {
  $group = _social_group_get_current_group();

  if (is_object($group)) {
    // Invalidate cache tags.
    $cache_tags = _social_group_cache_tags($group);
    foreach ($cache_tags as $cache_tag) {
      Cache::invalidateTags([$cache_tag]);
    }
  }
}

/**
 * Form submit for group join / leave form.
 */
function _social_group_action_form_submit($form, FormStateInterface $form_state) {
  $group = _social_group_get_current_group();

  if (is_object($group)) {
    // Invalidate cache tags.
    $cache_tags = _social_group_cache_tags($group);
    foreach ($cache_tags as $cache_tag) {
      Cache::invalidateTags([$cache_tag]);
    }

    // Get form that was submitted.
    $complete_form = $form_state->getCompleteForm();
    if (in_array($complete_form['#form_id'], [
      'group_content_' . $group->bundle() . '-group_membership_group-join_form',
      'group_content_' . $group->bundle() . '-group_membership_add_form',
    ])) {
      // Set redirect to group home page for people joining the group.
      $form_state->setRedirect(
        'entity.group.canonical',
        ['group' => $group->id(), []]
      );

      // For adding people we redirect to the manage members page.
      if ($complete_form['#form_id'] == 'group_content_' . $group->bundle() . '-group_membership_add_form') {
        // We passed the validation.
        // Let's create the Group Content for all our members just like
        // Group does it for creating a Group and adding its creator as member.
        $values = ['group_roles' => $form_state->getValue('group_roles')];
        $count = 0;
        if (!empty($form_state->getValue('entity_id_new'))) {
          // For multiple Group Members that are valid we add multiple entities.
          foreach ($form_state->getValue('entity_id_new') as $key => $uid) {
            $group->addMember(User::load($uid['target_id']), $values);
            $count++;
          }
        }

        // Add nice messages.
        if (!empty($count)) {
          $message = \Drupal::translation()
            ->formatPlural($count, '@count new member joined the group.', '@count new members joined the group.');
          \Drupal::messenger()->addMessage($message, 'status');
        }

        $form_state->setRedirect(
          'view.group_manage_members.page_group_manage_members',
          ['group' => $group->id(), []]
        );
      }
    }
    else {
      // Set redirect to the Group overview page
      // when a user saves their profile.
      $route = _social_group_get_overview_route($group);
      $form_state->setRedirect(
        $route['name'],
        $route['parameters']
      );
    }
  }
}

/**
 * This function checks if the user should get the admin role within a group.
 *
 * Also check if the user has already a membership.
 * If so, update it with the new role.
 */
function _social_group_grant_admin_role($uid, $gid) {
  $account = User::load($uid);
  $group = Group::load($gid);

  // Must be a valid account AND a valid group.
  if ($account instanceof User && $group instanceof Group) {
    // Must have manage all groups permission.
    // Otherwise normal flow will be fine.
    if (!$account->hasPermission('manage all groups')) {
      return;
    }
    // Check if the user is already a member in the group
    // (could be in update mode here).
    /** @var \Drupal\group\GroupMembership $membership */
    $admin_role = $group->bundle() . '-group_admin';
    $membership = $group->getMember($account);

    // Check what roles are there.
    $roles = [];
    if ($membership instanceof GroupMembership) {
      foreach ($membership->getGroupContent()->get('group_roles') as $group_role_ref) {
        /** @var \Drupal\Core\Field\EntityReferenceFieldItemList $group_role_ref */
        $roles[] = $group_role_ref->getValue();
      }

      // No admin? Add it.
      if (!in_array($admin_role, $roles)) {
        $membership->getGroupContent()->set('group_roles', ['target_id' => $admin_role]);
        $membership->getGroupContent()->save();
      }
    }
  }
}

/**
 * When creating a new group membership or a node with group reference.
 *
 * @param \Drupal\group\Entity\GroupContentInterface $group_content
 *   The group content.
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 */
function social_group_group_content_insert(GroupContentInterface $group_content) {
  if ($group_content->getEntity()->getEntityTypeId() === 'user') {
    if ($group_content->bundle() === $group_content->getGroup()
      ->bundle() . '-group_membership') {
      _social_group_grant_admin_role($group_content->getEntity()
        ->id(), $group_content->getGroup()->id());
    }
  }
  // When moving a node from 1 group to the other as SM
  // it means you as SM are the new owner, instead of the original author.
  // Which results in wrong activity authors being used.
  $referenced_entity = $group_content->getEntity();
  if ($referenced_entity instanceof NodeInterface) {
    $author = $referenced_entity->getRevisionUser();
    if (!is_null($author)) {
      $group_content->setOwnerId((int) $author->id());
      $group_content->save();
    }
  }
}

/**
 * Form submit for membership edit form.
 *
 * @param array $form
 *   Membership edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_membership_edit_form_submit(array $form, FormStateInterface $form_state) {
  $group = _social_group_get_current_group();

  if (is_object($group)) {
    // Set redirect to the Group overview page when a user saves their profile.
    $form_state->setRedirect(
      'entity.group_content.collection',
      ['group' => $group->id(), []]
    );
  }
}

/**
 * Form cancel for join and leave form.
 *
 * @param array $form
 *   Join and leave form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_cancel_join_leave_form(array $form, FormStateInterface $form_state) {
  // Get form that was submitted.
  $group = _social_group_get_current_group();

  if (is_object($group)) {
    // Set redirect only for cancel buttons. Just go back to the group page.
    $form_state->setRedirect(
      'entity.group.canonical',
      ['group' => $group->id(), []]
    );
  }
}

/**
 * Validation callback for group content create form.
 *
 * @param array $form
 *   Group node form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_node_form_cross_posting_validate(array $form, FormStateInterface $form_state) {
  if (empty($form_state->getValue('groups'))) {
    return;
  }
  // Cross posting is disabled globally.
  $group_settings = \Drupal::config('social_group.settings');
  if (!$group_settings->get('cross_posting.status')) {
    return;
  }
  // Check if group type enabled for cross-group posting.
  $allowed_group_types = $group_settings->get('cross_posting.group_types');

  $values = $form_state->getValue('groups');
  // We need to check only if user try to add a node to multiple groups.
  if (is_array($values) && count($values) > 1) {
    $gids = array_column($values, 'target_id');
    foreach (Group::loadMultiple($gids) as $group) {
      // If the group type for the group is not allowed - raise the error.
      if (!in_array($group->getGroupType()->id(), $allowed_group_types)) {
        $form_state->setErrorByName('groups', t('Cross posting is not allowed for @type group type', [
          '@type' => $group->getGroupType()->label(),
        ]));
      }
    }
  }
}

/**
 * Form submit for group content create form.
 *
 * @param array $form
 *   Group node form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_node_form_submit(array $form, FormStateInterface $form_state) {
  $nid = $form_state->getValue('nid');
  // Set redirect.
  $form_state->setRedirect(
    'entity.node.canonical',
    ['node' => $nid]
  );
}

/**
 * Alter the visibility field within groups.
 *
 * Implements hook_field_widget_single_element_form_alter().
 */
function social_group_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) {
  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  $field_definition = $context['items']->getFieldDefinition();

  // Unset the visibility field options which are not enabled.
  if ($field_definition->getType() === 'entity_access_field') {
    $default_value = $context['items']->getValue();
    $field_cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
    if ($field_cardinality === 1) {
      if (isset($default_value[0]['value'])) {
        $element['#default_value'] = $default_value[0]['value'];
      }
      else {
        $default_visibility = \Drupal::configFactory()
          ->get('entity_access_by_field.settings')
          ->get('default_visibility');
        $element['#default_value'] = $default_visibility;
      }
    }
    $entity = $form_state->getFormObject()->getEntity();

    $current_group = _social_group_get_current_group($entity);
    if (!empty($current_group)) {
      $group_type = $current_group->getGroupType();
      $group_type_id = $group_type->id();
      // If it's an existing entity we don't need to set the default
      // rather follow the saved one.
      if (!$entity->id() && $field_cardinality === 1) {
        $element['#default_value'] = \Drupal::service('social_group.helper_service')
          ->getDefaultGroupVisibility($group_type_id);
      }
    }
    else {
      $group_type_id = NULL;
      // This is not a group so lets disable this visibility option.
      $element['group']['#disabled'] = TRUE;
    }

    $visibility_options = social_group_get_allowed_visibility_options_per_group_type($group_type_id, NULL, $entity, $current_group);
    // Based on the allowed visibility options for a group type,
    // we disable the options which are not valid for the group.
    // Remember for flexible groups we can have multiple options.
    foreach ($visibility_options as $visibility => $allowed) {
      $element[$visibility]['#disabled'] = !$allowed;
      // If the element is disabled, we also need to move it from
      // the default value.
      if (!$allowed && $element['#default_value'] === $visibility) {
        $element['#default_value'] = NULL;
      }
    }

    // This contains the group that is passed back from the form_state as
    // chosen by selecting a group in the $form['groups'] select widget.
    // With this we can ensure that on validation the content visibility
    // field is disabling the correct values based on the selected group.
    if (!empty($form_state->getValue('groups'))) {
      $group_id = NULL;
      $group_value = $form_state->getValue('groups');
      if (is_array($group_value)) {
        $group_id = $group_value[0]['target_id'];
      }
      if (is_string($group_value)) {
        $group_id = (int) $group_value;
      }
      // Load the correct group.
      $selected_groups = Group::loadMultiple([$group_id]);

      // Ensure we disable the correct visibility options.
      foreach ($selected_groups as $current_group) {
        if ($current_group !== NULL) {
          $group_type = $current_group->getGroupType();
          $group_type_id = $group_type->id();

          $visibility_options = social_group_get_allowed_visibility_options_per_group_type($group_type_id, NULL, NULL, $current_group);

          foreach ($visibility_options as $visibility => $allowed) {
            $element[$visibility]['#disabled'] = !$allowed;
            // If the element is disabled, we also need to move it from
            // the default value.
            if (!$allowed && $element['#default_value'] === $visibility) {
              $element['#default_value'] = NULL;
            }
          }
        }
      }
    }

    // For flexible groups it could potentially still be empty.
    // But in any way, from the available options, if its empty
    // lets choose the most safe one.
    if ($element['#default_value'] === NULL) {
      // So if public isn't disabled, it will become public.
      if (!$element['public']['#disabled']) {
        $element['#default_value'] = 'public';
      }
      // So if community isn't disabled, it will become community.
      if (!$element['community']['#disabled']) {
        $element['#default_value'] = 'community';
      }
      // And if members isn't disabled, it will become members.
      if (!$element['group']['#disabled']) {
        $element['#default_value'] = 'group';
      }
    }
  }

  // Disable the public visibility option on group create/edit.
  if ($field_definition->getType() === 'list_string' && $field_definition->getName() === 'field_flexible_group_visibility') {
    $config = \Drupal::config('entity_access_by_field.settings');
    $account = \Drupal::currentUser();
    if ($config->get('disable_public_visibility') === 1 && !$account->hasPermission('override disabled public visibility')) {
      $element['public']['#disabled'] = TRUE;
      $element['#description'] = t('The public visibility setting has been disabled. In order to change the visibility to public, please contact a site manager.')->render();
    }
  }
}

/**
 * Implements hook_views_post_render().
 *
 * Alter "Group Members" views. Replace user IDs with profile teasers.
 */
function social_group_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
  if ($view->id() == 'group_members') {
    if (!empty($output['#rows'][0]['#rows'])) {
      foreach ($output['#rows'][0]['#rows'] as $key => $row) {
        // Get Group membership content entity.
        $group_content = $row['#group_content'];
        // Get Profile.
        $user_profile = _social_group_get_member_profile($group_content);
        if ($user_profile !== NULL) {
          // Use teaser for page and small_teaser for block.
          $view_mode = ($view->current_display === 'block_newest_members') ? 'small_teaser' : 'teaser';
          // Replace output with profile teaser.
          $output['#rows'][0]['#rows'][$key] = \Drupal::entityTypeManager()
            ->getViewBuilder('profile')
            ->view($user_profile, $view_mode);
        }
        else {
          // Remove output if user don't have profile (admin).
          unset($output['#rows'][0]['#rows'][$key]);
        }
      }
    }
  }
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function social_group_menu_local_tasks_alter(&$data, $route_name) {
  // Rename Group "Related Entities" tab.
  if (isset($data['tabs'][0]['group.content']['#link'])) {
    $data['tabs'][0]['group.content']['#link']['title'] = t('Manage members');
    $data['tabs'][0]['group.content']['#weight'] = 70;
  }

  // Remove the default View tab.
  if (isset($data['tabs'][0]['group.view'])) {
    unset($data['tabs'][0]['group.view']);
  }

  // Remove Edit tab. Edit will always go through Floating Edit Button.
  if (isset($data['tabs'][0]['group.edit_form'])) {
    unset($data['tabs'][0]['group.edit_form']);
  }

  $account = \Drupal::currentUser();

  // Get the Group object from the route.
  $group = \Drupal::routeMatch()->getParameter('group');

  if (
    $group instanceof SocialGroupInterface &&
    // Check if it's a closed group.
    $group->getGroupType()->id() === 'closed_group' &&
    // And if the user is not user 1.
    $account->id() !== 1
  ) {
    if ($account->hasPermission('manage all groups')) {
      return;
    }

    // If the user is not an member of this group.
    if (!$group->hasMember($account)) {
      // Disable these local tasks.
      $data['tabs'][0]['group.view'] = [];

      if (!$group->hasPermission('administer members', $account)) {
        $data['tabs'][0]['group.content'] = [];
      }

      $data['tabs'][0]['social_group.events'] = [];
      $data['tabs'][0]['social_group.topics'] = [];
    }
  }

  // We don't want to display the "Nodes" tab on groups anymore. Before, we
  // just disabled the view on hook update, but sometimes it is back to us,
  // this is the reason why we want to hide the tab. Also, we don't want to
  // add custom access to that route since some other developers of Open Social
  // might use it.
  if (isset($data['tabs'][0]['views_view:view.group_nodes.page_1'])) {
    unset($data['tabs'][0]['views_view:view.group_nodes.page_1']);
  }
}

/**
 * Implements hook_local_tasks_alter().
 */
function social_group_local_tasks_alter(&$local_tasks) {
  // Remove local delete task from group page.
  unset($local_tasks['group.delete_form']);

  $local_tasks['group.content']['route_name'] = 'view.group_manage_members.page_group_manage_members';
  $local_tasks['group.content']['weight'] = 70;
}

/**
 * Implements hook_menu_local_actions_alter().
 */
function social_group_menu_local_actions_alter(&$local_actions) {
  // Remove "Add Relationship" button.
  if (isset($local_actions['group_content.add_page'])) {
    unset($local_actions['group_content.add_page']);
  }
  // Remove Create new entity in group.
  if (isset($local_actions['group_content.create_page'])) {
    unset($local_actions['group_content.create_page']);
  }
}

/**
 * Return user profile by given group membership content.
 *
 * @param \Drupal\group\Entity\GroupContent $group_content
 *   Group content entity.
 *
 * @return \Drupal\profile\Entity\Profile|null
 *   Returns the Profile entity for the member.
 */
function _social_group_get_member_profile(GroupContent $group_content) {
  $user_profile = NULL;
  // Get User entity.
  $user_entity = $group_content->getEntity();
  if ($user_entity instanceof UserInterface) {
    // Get Profile storage.
    $storage = \Drupal::entityTypeManager()->getStorage('profile');
    // Get Profile entity.
    $user_profile = $storage->loadByUser($user_entity, 'profile');
  }
  return $user_profile;
}

/**
 * Gets current Group entity from the route.
 *
 * @param mixed $entity
 *   (optional) The entity object. Defaults to NULL.
 */
function _social_group_get_current_group($entity = NULL): ?SocialGroupInterface {
  $cache = &drupal_static(__FUNCTION__, []);

  // For the same $node input, within the same request the return is always
  // the same.
  $entity_id = NULL;
  if (is_null($entity)) {
    $entity_id = -1;
  }
  elseif ($entity instanceof EntityInterface) {
    $entity_id = $entity->id();
  }

  // If we have a cache key and it has a value, we're done early.
  if (!is_null($entity_id) && isset($cache[$entity_id])) {
    // Translate FALSE (so isset works) back to NULL.
    return $cache[$entity_id] ?: NULL;
  }

  $group = \Drupal::routeMatch()->getParameter('group');

  if (is_numeric($group)) {
    $group = \Drupal::entityTypeManager()->getStorage('group')->load($group);
  }

  if ($group === NULL) {
    if (
      !is_object($entity) &&
      ($entity = \Drupal::routeMatch()->getParameter('node')) === NULL &&
      ($route_name = \Drupal::routeMatch()->getRouteName()) !== NULL &&
      preg_match('/^entity\.([^.]+)/', $route_name, $matches)
    ) {
      $entity = \Drupal::routeMatch()->getParameter($matches[1]);
    }

    if ($entity instanceof EntityInterface) {
      $entity = [
        'target_type' => $entity->getEntityTypeId(),
        'target_id' => $entity->id(),
      ];
      $gid_from_entity = \Drupal::service('social_group.helper_service')
        ->getGroupFromEntity($entity);
      if ($gid_from_entity !== NULL) {
        $group = \Drupal::entityTypeManager()
          ->getStorage('group')
          ->load($gid_from_entity);
      }
    }
  }

  // If we have a cache key we store the value.
  if (!is_null($entity_id)) {
    // Translate NULL to FALSE so that isset works.
    $cache[$entity_id] = $group ?? FALSE;
  }

  /** @var \Drupal\social_group\SocialGroupInterface|null $group */
  return $group;
}

/**
 * Get group cache tags.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   The GroupInterface.
 *
 * @return array
 *   An array of cache tags related to groups.
 */
function _social_group_cache_tags(GroupInterface $group) {
  // Group views.
  $tags = [
    'group_list',
    'group_content_list',
    'group_view',
    'group_content_view',
  ];

  // Add cache tags that are based on id.
  $tags[] = 'group_hero:' . $group->id();
  // Add cache tags for the blocks.
  $tags[] = 'group_block:' . $group->id();

  $current_user = \Drupal::currentUser();
  if ($group_membership = $group->getMember($current_user)) {
    $group_content = $group_membership->getGroupContent();
    $tags[] = 'group_content:' . $group_content->id();
  }

  return $tags;
}

/**
 * Implements hook_block_view_BASE_BLOCK_ID_alter().
 *
 * Add Group cache context for system "Tabs" block.
 */
function social_group_block_view_local_tasks_block_alter(array &$build, BlockPluginInterface $block) {
  $build['#cache']['contexts'][] = 'user.group_permissions';
}

/**
 * Implements hook_block_build_alter().
 */
function social_group_block_build_alter(array &$build, BlockPluginInterface $block) {
  if (!empty($block->getPluginId()) &&
    ($block->getPluginId() === 'views_exposed_filter_block:newest_groups-page_all_groups' ||
      $block->getPluginId() === 'views_exposed_filter_block:search_groups-page')) {
    // The Group Type filter has to listen to changes in
    // group type role permissions. Using the role list ensures
    // it's cleared correctly.
    $build['#cache']['tags'][] = 'config:group_role_list';
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_profile(&$variables) {
  $group = _social_group_get_current_group();

  if ($group instanceof Group && $variables['elements']['#view_mode'] == 'teaser') {
    $account = $variables['elements']['#profile']->get('uid')->entity;

    if (!$member = $group->getMember($account)) {
      return;
    }

    $roles = $member->getRoles();

    if (isset($roles[$group->bundle() . '-group_manager'])) {
      $variables['badges'] = [
        [
          'label' => t('Group manager'),
          'classes' => ['badge-secondary teaser__badge'],
        ],
      ];
    }
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_views_view(&$variables) {
  /** @var \Drupal\views\ViewExecutable $view */
  $view = &$variables['view'];

  if ($view->id() === 'group_manage_members') {
    $entity = \Drupal::entityTypeManager()->getStorage('block')
      ->load('socialblue_local_actions');

    $variables['header']['actions'] = \Drupal::entityTypeManager()
      ->getViewBuilder('block')
      ->view($entity);
  }

  if ($view->id() === 'activity_stream_group') {
    $group = _social_group_get_current_group();
    if ($group instanceof GroupInterface) {
      $variables['#cache']['tags'][] = 'group:' . $group->id();
      $variables['#cache']['contexts'][] = 'route.group';
      $variables['#cache']['contexts'][] = 'user.group_permissions';
    }
  }
}

/**
 * Implements theme_preprocess_menu_local_task().
 *
 * Sets an active class on stream tab when viewing group canonical page.
 */
function social_group_preprocess_menu_local_task(array &$variables): void {
  $route_name = \Drupal::routeMatch()->getRouteName();
  if ($route_name != 'entity.group.canonical') {
    return;
  }

  if (isset($variables['link']['#url'])
    && $variables['link']['#url'] instanceof Url
    && $variables['link']['#url']->getRouteName() === 'social_group.stream') {
    $variables['element']['#active'] = TRUE;
    $variables['attributes']['class'][] = 'active';
  }

}

/**
 * Implements hook_block_access().
 *
 * Check if the user is viewing a closed_group, check if the user is a member.
 * If the user is not a member, the user has no access to view this block.
 */
function social_group_block_access(Block $block, $operation, AccountInterface $account) {
  $block_id = $block->getPluginId();

  // Lets hide the hero block when adding members directly, it doesnt
  // add anything there.
  if ($operation === 'view' && $block_id === 'group_hero_block') {
    $route_name = \Drupal::routeMatch()->getRouteName();
    $excluded_routes = [
      'entity.group_content.add_form',
    ];
    if (in_array($route_name, $excluded_routes, TRUE)) {
      return AccessResult::forbidden();
    }
  }

  // This is a list of the blocks that this function cares about, if we're being
  // called for a different block we exit early.
  $managed_blocks = [
    'views_exposed_filter_block:newest_groups-page_all_groups',
    'views_block:groups-block_user_groups',
    'views_block:upcoming_events-upcoming_events_group',
    'views_block:latest_topics-group_topics_block',
  ];
  if (!in_array($block_id, $managed_blocks)) {
    return AccessResult::neutral();
  }

  $group = _social_group_get_current_group();
  $user = Drupal::currentUser();
  // Check if there is a group set and if its an closed group.
  if ($group && $group->getGroupType()
    ->id() == 'closed_group' && $account->id() != 1) {
    if ($account->hasPermission('manage all groups')) {
      return AccessResult::neutral();
    }
    elseif (!$group->hasMember($user)) {
      // If it is closed and the current user is not an member of this group,
      // then it is not allowed to see these blocks.
      $forbidden_blocks = [
        'views_block:upcoming_events-upcoming_events_group',
        'views_block:latest_topics-group_topics_block',
      ];
      foreach ($forbidden_blocks as $forbidden_block) {
        if ($operation == 'view' && $block->getPluginId() == $forbidden_block) {
          return AccessResult::forbidden();
        }
      }
    }
  }
  // Check if we're alloweed to view the joined groups block.
  if ($operation === 'view' && $block->getPluginId() === 'views_block:groups-block_user_groups') {
    // Here we're going to assume by default access is not granted.
    $groupController = SocialGroupController::create(\Drupal::getContainer());
    $access = $groupController->myGroupAccess($account);
    // If the 'myGroupAccess' returns 'AccessResultNeutral', we have to assume
    // that access must be denied.
    if ($access instanceof AccessResultNeutral) {
      // Return forbidden, since access was not explicitly granted.
      return AccessResult::forbidden();
    }
    return $access;
  }

  return AccessResult::neutral();
}

/**
 * Determine the amount of group_types a user can see.
 *
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The user to check for.
 *
 * @return int
 *   The amount of group_types
 */
function _social_group_get_current_group_types(AccountInterface $account) {
  $group_types = 0;

  /** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
  foreach (GroupType::loadMultiple() as $group_type) {
    $group_types += (int) social_group_can_view_groups_of_type($group_type->id(), $account);
  }

  return $group_types;
}

/**
 * Delete the group and all of its content.
 */
function _social_group_delete_group() {
  // Get the group.
  if ($group = _social_group_get_current_group()) {
    $group_content_types = GroupContentType::loadByEntityTypeId('node');
    $group_content_types = array_keys($group_content_types);

    // Get all the node's related to the current group.
    $query = \Drupal::database()->select('group_content_field_data', 'gcfd');
    $query->addField('gcfd', 'entity_id');
    $query->condition('gcfd.gid', $group->id());
    $query->condition('gcfd.type', $group_content_types, 'IN');
    $query->execute()->fetchAll();

    $entity_ids = $query->execute()->fetchAllAssoc('entity_id');
    // Store all the node ids.
    $nids = array_keys($entity_ids);

    // Get all the posts from this group.
    $query = \Drupal::database()->select('post__field_recipient_group', 'pfrg');
    $query->addField('pfrg', 'entity_id');
    $query->condition('pfrg.field_recipient_group_target_id', $group->id());
    $query->execute()->fetchAll();

    $post_ids = $query->execute()->fetchAllAssoc('entity_id');
    // Store all the post entity ids.
    $posts = array_keys($post_ids);

    // Pass the $nids and $posts as 2 parameters in the operations.
    // See /social_group/src/Controller/DeleteGroup.php for further process.
    $batch = [
      'title' => t('Deleting the group and all the content within the group...'),
      'init_message' => t("Preparing to delete the group and all it's topic's, event's and post's..."),
      'operations' => [
        [
          '\Drupal\social_group\Controller\DeleteGroup::deleteGroupAndContent',
          [$nids, $posts],
        ],
      ],
      'finished' => '\Drupal\social_group\Controller\DeleteGroup::deleteGroupAndContentFinishedCallback',
    ];
    batch_set($batch);
  }
}

/**
 * Get all public groups.
 */
function social_group_get_all_public_groups(): array {
  $query = \Drupal::service('entity_type.manager')
    ->getStorage('group')
    ->getQuery()
    ->accessCheck()
    ->condition('type', 'public_group');
  return $query->execute();
}

/**
 * Get all open groups.
 */
function social_group_get_all_open_groups() {
  $query = \Drupal::service('entity_type.manager')
    ->getStorage('group')
    ->getQuery()
    ->accessCheck()
    ->condition('type', 'open_group');
  return $query->execute();
}

/**
 * Get all groups.
 */
function social_group_get_all_groups() {
  $query = \Drupal::service('entity_type.manager')
    ->getStorage('group')
    ->getQuery()
    ->accessCheck()
    ->sort('type');
  return $query->execute();
}

/**
 * Implements hook_group_types_alter().
 *
 * Remove 'group_type_<group_type>' fields from the node and user entities as
 * these fields providing issues with cross posting and translations. Also, we
 * do not use these fields in Open Social, so we can disable them.
 */
function social_group_group_types_alter(array &$types): void {
  $types = array_fill_keys(array_keys($types), FALSE);
}

/**
 * Implements hook_social_group_linked_type().
 */
function social_group_social_group_linked_type(): string {
  return 'node';
}

/**
 * Implements hook_entity_base_field_info_alter().
 */
function social_group_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
  if (!isset($fields['groups'])) {
    return;
  }

  $entity_types = \Drupal::moduleHandler()->invokeAll('group_types');
  \Drupal::moduleHandler()->alter('group_types', $entity_types);

  if (isset($entity_types[$entity_type->id()])) {
    $entity_types = \Drupal::moduleHandler()->invokeAll('social_group_linked_type');

    if (in_array($entity_type->id(), $entity_types)) {
      /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
      $fields['groups']
        ->setLabel(t('Group'))
        ->setDescription('')
        ->setSetting('handler', 'social')
        ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
        ->setDisplayOptions('form', [
          'type' => 'social_group_selector_widget',
          'settings' => [],
          'weight' => 16,
        ])
        ->setDisplayConfigurable('form', TRUE);
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Make sure the correct group is saved when group is selected on node form.
 */
function social_group_form_node_form_alter(&$form, FormStateInterface $form_state) {
  if (isset($form['#entity_type']) && $form['#entity_type'] === 'node') {
    if (isset($form['groups'])) {
      // Check if we are adding a translation or it's the default being editted.
      /** @var \Drupal\Core\Entity\EntityForm $form_object */
      $form_object = $form_state->getFormObject();
      /** @var \Drupal\social_node\Entity\Node $node */
      $node = $form_object->getEntity();
      $is_original_language = (bool) $node->getFieldValue('default_langcode', 'value');
      // If it's not the original, we can safely say its a translation being
      // added or editted. In that case we don't want users to override the
      // group or visibility. We only allow those changes on the default as
      // these can not change per language for now.
      if (!$is_original_language) {
        $form['field_content_visibility']['widget']['#disabled'] = TRUE;
        $form['field_content_visibility']['widget']['#description'] = t('Changing visibility is disabled when translating.');
        $form['groups']['widget']['#disabled'] = TRUE;
        $form['groups']['widget']['#description'] = t('Changing groups is disabled when translating.');
      }

      $change_fieldgroup_titles = [
        'group_topic_visibility',
        'group_event_visibility',
      ];
      foreach ($change_fieldgroup_titles as $fieldgroup_title) {
        if (isset($form['#fieldgroups'][$fieldgroup_title])) {
          $form['#fieldgroups'][$fieldgroup_title]->label = t('Access permissions')->render();
        }
      }

      /** @var \Drupal\node\NodeInterface $node */
      $node = $form_state->getFormObject()->getEntity();
      // Store if the node is new or not.
      $form['is_new'] = [
        '#type' => 'value',
        '#value' => $node->isNew(),
      ];

      $form['actions']['submit']['#submit'][] = 'social_group_save_group_from_node';
    }
  }
}

/**
 * Form submit to save the group from a node form.
 *
 * @param array $form
 *   Node add or node edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function social_group_save_group_from_node(array $form, FormStateInterface $form_state) {
  // Check if we are adding a translation or it's the default being editted.
  /** @var \Drupal\Core\Entity\EntityForm $form_object */
  $form_object = $form_state->getFormObject();
  /** @var \Drupal\social_node\Entity\Node $node */
  $node = $form_object->getEntity();
  $is_original_language = (bool) $node->getFieldValue('default_langcode', 'value');

  // Check if the created node is new or updated.
  $is_new = NULL !== $form_state->getValue('is_new') ? $form_state->getValue('is_new') : FALSE;

  $original_groups = [];
  $groups_to_add = [];
  $groups_to_remove = [];
  foreach ($form_state->getValue('groups') as $new_group_key => $new_group) {
    $groups_to_add[$new_group['target_id']] = $new_group['target_id'];
  }
  // The node already exist so lets also change the logic accordingly,
  // only if there is already a group that needs to be removed.
  if (!empty($form['groups']['widget']['#default_value']) &&
    $form['#form_id'] === 'node_' . $node->bundle() . '_edit_form') {
    $original_groups = $form['groups']['widget']['#default_value'];
    foreach ($original_groups as $original_group_key => $original_group) {
      if (!in_array($original_group, $groups_to_add)) {
        $groups_to_remove[$original_group] = $original_group;
      }
      else {
        unset($groups_to_add[$original_group]);
      }
    }
  }

  // Now make sure the relevant GroupContent is removed or added.
  // But only when we are on the original language, we don't want to update
  // this when translating. The field is disabled for that scenario so no
  // need to run this.
  if ($is_original_language) {
    $setGroupsForNodeService = \Drupal::service('social_group.set_groups_for_node_service');
    $setGroupsForNodeService->setGroupsForNode($node, $groups_to_remove, $groups_to_add, $original_groups, $is_new);
  }
}

/**
 * Get the allowed visibility options for a given group type.
 *
 * @param string|null $group_type_id
 *   The group type. Can be NULL to get visibility when it is not in a group.
 * @param \Drupal\Core\Session\AccountInterface|null $account
 *   The account object that may have impact on the visibility options.
 *
 * @return array
 *   An array of visibility options for the given group type.
 */
function social_group_get_allowed_visibility_options_per_group_type($group_type_id, AccountInterface $account = NULL, $entity = NULL, $group = NULL) {

  // Get the logged in user.
  if ($account === NULL) {
    $account = \Drupal::currentUser();
  }

  $visibility_options = [];
  $visibility_options['public'] = FALSE;
  $visibility_options['community'] = TRUE;
  $visibility_options['group'] = FALSE;

  switch ($group_type_id) {
    case 'closed_group':
      $visibility_options['community'] = FALSE;
      $visibility_options['group'] = TRUE;
      break;

    case 'public_group':
      $visibility_options['public'] = TRUE;
      $visibility_options['community'] = FALSE;
      break;

    case 'open_group':
      $visibility_options['public'] = FALSE;
      $visibility_options['community'] = TRUE;
      $visibility_options['group'] = FALSE;
      break;

    case 'secret_group':
      $visibility_options['public'] = FALSE;
      $visibility_options['community'] = FALSE;
      $visibility_options['group'] = TRUE;
      break;

    default:
      if (empty($group)) {
        $group = _social_group_get_current_group($entity);
      }
      if ($group !== NULL && $group->hasField('field_group_allowed_visibility')) {
        $visibility_options['public'] = FALSE;
        $visibility_options['community'] = FALSE;
        $visibility_options['group'] = FALSE;
        // Try to retrieve allowed options from Group directly.
        $allowed_options = $group->get('field_group_allowed_visibility')->getValue();
        foreach ($allowed_options as $option) {
          $value = $option['value'];
          $visibility_options[$value] = TRUE;
        }

        // Disabling public visibility trumps group settings.
        $config = \Drupal::config('entity_access_by_field.settings');
        if ($config->get('disable_public_visibility') === 1 && !$account->hasPermission('override disabled public visibility')) {
          $visibility_options['public'] = FALSE;
        }
      }
      else {
        $config = \Drupal::config('entity_access_by_field.settings');
        $visibility_options['public'] = TRUE;
        if ($config->get('disable_public_visibility') === 1 && !$account->hasPermission('override disabled public visibility')) {
          // This is a new entity.
          if (!$entity) {
            $visibility_options['public'] = FALSE;
          }
          else {
            /** @var \Drupal\node\Entity\Node $entity */
            if ($entity->hasField('field_content_visibility')) {
              $current_visibility = $entity->get('field_content_visibility')
                ->getString();
              if ($current_visibility !== 'public') {
                $visibility_options['public'] = FALSE;
              }
            }
          }
        }
        $visibility_options['community'] = TRUE;
        $visibility_options['group'] = FALSE;
      }
      break;
  }

  \Drupal::moduleHandler()
    ->alter('social_group_allowed_visibilities', $visibility_options, $group_type_id);

  return $visibility_options;
}

/**
 * Implements hook_views_query_alter().
 */
function social_group_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  if (\Drupal::currentUser()->isAuthenticated()) {
    return;
  }

  if (
    empty($view->rowPlugin) ||
    !($view->rowPlugin instanceof EntityRow) ||
    $view->rowPlugin->getEntityTypeId() !== 'group'
  ) {
    return;
  }

  $found = FALSE;

  /** @var \Drupal\views\Plugin\views\query\Sql $query */
  $where = $query->where;
  foreach ($where as &$conditions) {
    foreach ($conditions['conditions'] as &$condition) {
      if ($condition['field'] === 'groups_field_data.type') {
        $found = TRUE;
        break 2;
      }
    }
  }

  if (!$found) {
    unset($condition);

    $condition = [
      'field' => 'groups_field_data.type',
      'operator' => 'not in',
    ];
  }

  $hiddens = isset($condition) && $condition['operator'] === 'not in';

  // Define the variable as array otherwise it can be interpreted as string
  // And breaking the code below.
  $condition['value'] = [];

  /** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
  foreach (GroupType::loadMultiple() as $group_type) {
    $new = !in_array($group_type->id(), $condition['value']);
    $permissions = $group_type->getAnonymousRole()->getPermissions();
    $hidden = !in_array('view group', $permissions);

    if ($new && $hiddens === $hidden) {
      $condition['value'][] = $group_type->id();
    }
  }

  if (!$found) {
    if (isset($conditions)) {
      $conditions['conditions'][] = $condition;
    }
    else {
      $query->where[] = [
        'conditions' => [$condition],
        'type' => 'AND',
      ];
    }
  }
}

/**
 * Implements hook_user_cancel_methods_alter().
 */
function social_group_user_cancel_methods_alter(&$methods) {
  $methods['user_cancel_reassign']['title'] .= ' ' . t('Reassign your groups to the super administrator.');
}

/**
 * Implements hook_social_user_account_header_create_links().
 *
 * Adds the "Create Group" link to the content creation menu.
 * When the user can only create groups of one type, the user skips the
 * group type selection page.
 */
function social_group_social_user_account_header_create_links($context) {

  // If we have a user to work with, we can check if we should redirect it to
  // the create group overview or a single group.
  // If we do not have a user we'll default to the overview.
  $route_add_group = NULL;
  if (!empty($context['user'])) {
    /** @var \Drupal\Core\Session\AccountInterface $user */
    $account = $context['user'];
    /** @var \Drupal\Core\Url|NULL $route_add_group */
    $route_add_group = \Drupal::service('social_group.helper_service')
      ->getGroupsToAddUrl($account);

    // Get groups to add url can return Url service
    // It will change it when necessary.
    if ($route_add_group instanceof Url) {
      $route_add_group = Link::fromTextAndUrl((new TranslatableMarkup('New Group')), $route_add_group);
    }
  }

  if (empty($route_add_group)) {
    $route_add_group = Link::createFromRoute((new TranslatableMarkup('New Group')), 'entity.group.add_page');
  }

  return [
    'add_group' => [
      '#type' => 'link',
      '#attributes' => [
        'title' => new TranslatableMarkup('Create New Group'),
      ],
      '#weight' => 500,
      '#access' => $route_add_group->getUrl()->access(),
    ] + $route_add_group->toRenderable(),
  ];
}

/**
 * Implements hook_social_user_account_header_items().
 *
 * Adds the My Groups button to the account header block if it's enabled by a
 * site manager.
 */
function social_group_social_user_account_header_items(array $context) {
  if (\Drupal::config('social_user.navigation.settings')
    ->get('display_my_groups_icon') !== TRUE) {
    return [];
  }

  // We can't create a "My Groups" link without user.
  if (empty($context['user'])) {
    return [];
  }

  return [
    'groups' => [
      '#type' => 'account_header_element',
      '#wrapper_attributes' => [
        'class' => ['desktop'],
      ],
      '#title' => new TranslatableMarkup('My Groups'),
      '#url' => Url::fromRoute('view.groups.page_user_groups', [
        'user' => $context['user']->id(),
      ]),
      '#icon' => 'group',
      '#label' => new TranslatableMarkup('My Groups'),
    ],
  ];
}

/**
 * Implements hook_social_user_account_header_account_links().
 *
 * Adds the "View my groups" link to the user menu.
 */
function social_group_social_user_account_header_account_links(array $context) {
  // We require a user for this link.
  if (empty($context['user']) || !($context['user'] instanceof AccountInterface)) {
    return [];
  }

  $link = Link::createFromRoute((new TranslatableMarkup('My groups')), 'view.groups.page_user_groups', [
    'user' => $context['user']->id(),
  ]);

  return [
    'my_groups' => [
      '#type' => 'link',
      '#attributes' => [
        'title' => new TranslatableMarkup('View my groups'),
      ],
      '#weight' => 800,
      '#access' => $link->getUrl()->access(),
    ] + $link->toRenderable(),
  ];
}

/**
 * Determine whether a user can see groups of a given type as an outsider.
 *
 * @param string $type
 *   The group type to check.
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The user to check for.
 *
 * @return bool
 *   Whether the user is allowed to view groups of the given type.
 */
function social_group_can_view_groups_of_type(string $type, AccountInterface $account): bool {
  $group_type = GroupType::load($type);

  // If the group type doesn't exist then the user can't see the group type.
  if ($group_type === NULL) {
    return FALSE;
  }

  // Anonymous users have a special role.
  if ($account->isAnonymous()) {
    /** @var \Drupal\group\Entity\GroupRoleInterface $role */
    return $group_type->getAnonymousRole()->hasPermission('view group');
  }

  // The service that keeps platform roles in sync with group roles for
  // outsiders.
  /** @var \Drupal\group\GroupRoleSynchronizerInterface $group_role_synchroniser */
  $group_role_synchroniser = \Drupal::service('group_role.synchronizer');
  /** @var \Drupal\group\Entity\GroupRoleInterface[] $group_roles */
  $group_roles = $group_type->getRoles();

  $user_roles = $account->getRoles(TRUE);

  // Authenticated is a locked Role,
  // So first we check outsiders also easier on the performance :)
  $outsider = $group_type->getOutsiderRole();
  if (NULL !== $outsider && $outsider->hasPermission('view group')) {
    return TRUE;
  }

  // If members can see it, we also need to show group types because
  // we are not calculation memberships.
  $member = $group_type->getMemberRole();
  if (NULL !== $member && $member->hasPermission('view group')) {
    return TRUE;
  }

  // Check each role this user has whether its group counterpart has the correct
  // permission. This counts for the advanced group permissions.
  // Admin, SM, CM.
  foreach ($user_roles as $role) {
    $group_role = $group_role_synchroniser->getGroupRoleId($type, $role);
    if ($group_roles[$group_role]->hasPermission('view group')) {
      return TRUE;
    }
  }

  // The user has no role that can view the group type.
  return FALSE;
}

/**
 * Implements hook_entity_operation_alter().
 */
function social_group_entity_operation_alter(array &$operations, EntityInterface $entity) {
  if ($entity->getEntityTypeId() === 'group_content') {
    $titles = [
      'edit' => t('Edit'),
      'delete' => t('Remove'),
      'view' => t('View'),
    ];

    foreach ($operations as $key => &$operation) {
      // Remove users' view page from actions.
      if ($operation['url']->getRouteName() === 'entity.user.canonical') {
        unset($operations[$key]);
        continue;
      }
      if (isset($titles[$key])) {
        $operation['title'] = $titles[$key];
      }
    }
  }
}

/**
 * Implements hook_default_route_group_tabs_alter().
 */
function social_group_social_group_default_route_tabs_alter(&$tabs) {
  // Unset some tabs created by group.
  unset($tabs['group.view'], $tabs['group.edit_form'], $tabs['group.content']);
}

/**
 * Implements hook_social_path_manager_group_tabs_alter().
 */
function social_group_social_path_manager_group_tabs_alter(array &$tabs) {
  // Unset some tabs created by group.
  unset($tabs['group.view'], $tabs['group.edit_form'], $tabs['group.content']);

  // Add manage members tab.
  $tabs['social_group.membership'] = 'view.group_manage_members.page_group_manage_members';
}

/**
 * Implements hook_batch_alter().
 */
function social_group_batch_alter(&$batch) {
  if (!isset($batch['source_url'])) {
    return;
  }

  $actions = [
    'social_group_members_export_member_action',
    'social_group_change_member_role_action',
    'social_group_delete_group_content_action',
    'social_group_send_email_action',
  ];

  /** @var \Drupal\Core\Url $url */
  $url = &$batch['source_url'];

  if ($url->getRouteName() === 'social_group_gvbo.views_bulk_operations.confirm' ||
    $url->getRouteName() === 'views_bulk_operations.confirm' ||
    $url->getRouteName() === 'views_bulk_operations.execute_batch' ||
    $url->getRouteName() === 'social_group_gvbo.views_bulk_operations.execute_configurable') {

    // Get the action ID.
    $action_id = _social_group_get_action_id($batch);

    $batch['sets'][0]['results']['action'] = $action_id;
    if (in_array($action_id, $actions, TRUE)) {
      $batch['sets'][0]['finished'] = '_social_group_action_batch_finish';
    }
  }
}

/**
 * Function to get the action id of a batch.
 *
 * @param array $batch
 *   The batch array.
 *
 * @return string
 *   Returns the batch action id.
 */
function _social_group_get_action_id(array &$batch) {
  $form_state = &$batch['form_state'];

  $action_id = '';
  if ($form_state instanceof FormStateInterface) {
    $data = $form_state->get('views_bulk_operations');
    $action_id = $data['action_id'];
  }
  else {
    foreach ($batch['sets'][0]['operations'] as $operations) {
      if (empty($operations) || !is_array($operations)) {
        break;
      }
      foreach ($operations as $operation) {
        if (empty($operation) || !is_array($operation)) {
          break;
        }
        foreach ($operation as $items) {
          if (empty($items) || !is_array($items)) {
            break;
          }

          if (!empty($items['action_id'])) {
            $action_id = $items['action_id'];
            break;
          }
        }
      }
    }
  }
  return $action_id;
}

/**
 * Action batch finished callback.
 *
 * @param bool $success
 *   Was the process successfull?
 * @param array $results
 *   Batch process results array.
 * @param array $operations
 *   Performed operations array.
 */
function _social_group_action_batch_finish($success, array $results, array $operations) {
  // When we do a bulk action on all the items in a view, across multiple pages,
  // the saveList function needs to be called. So after pre-populating the list
  // the actual action is performed on the entities.
  if (!empty($results['view_id']) && !empty($results['display_id'])) {
    ViewsBulkOperationsBatch::saveList(TRUE, $results, $operations);
    return;
  }

  // Count changed entities.
  if (is_array($results['operations'][0]) && $results['operations'][0]['count']) {
    $results_count = $results['operations'][0]['count'];
  }
  else {
    $results_count = count($results['operations']);
  }

  $hook = 'social_group_action_' . $results['action'] . '_finish';

  // Get a list of hook implementors.
  \Drupal::moduleHandler()->invokeAllWith($hook, function (callable $hook, string $module) use ($success, $results_count) {
    $messages = $hook($success);

    if (is_array($messages)) {
      $fields = 0;

      foreach ($messages as $type => $message) {
        if (($type === 'singular' || $type === 'plural') && !empty($message) && is_string($message)) {
          $fields++;
        }
      }

      if ($fields === 2) {
        $message = \Drupal::translation()
          ->formatPlural($results_count, $messages['singular'], $messages['plural']);
        $type = $success ? MessengerInterface::TYPE_STATUS : MessengerInterface::TYPE_WARNING;
        \Drupal::messenger()->addMessage($message, $type);
      }
    }
  });
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_members_export_member_action_finish($success) {
  if ($success) {
    return [
      'singular' => '1 selected member has been exported successfully',
      'plural' => '@count selected members have been exported successfully',
    ];
  }

  return [
    'singular' => '1 selected member has not been exported',
    'plural' => '@count selected members have not been exported',
  ];
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_delete_group_content_action_finish($success) {
  if ($success) {
    return [
      'singular' => '1 selected member has been removed successfully',
      'plural' => '@count selected members have been removed successfully',
    ];
  }

  return [
    'singular' => '1 selected member has not been removed successfully',
    'plural' => '@count selected members have been removed successfully',
  ];
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_change_member_role_action_finish($success) {
  if ($success) {
    return [
      'singular' => 'The role of 1 selected member has been changed successfully',
      'plural' => 'The role of @count selected members have been changed successfully',
    ];
  }

  return [
    'singular' => 'The role of 1 selected member has not been changed successfully',
    'plural' => 'The role of @count selected members have not been changed successfully',
  ];
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_send_email_action_finish($success) {
  if ($success) {
    // When the queue storage module is enabled the email is send in the
    // background.
    if (\Drupal::moduleHandler()->moduleExists('social_queue_storage')) {
      return [
        'singular' => 'Your email will be send to 1 selected member',
        'plural' => 'Your email will be send to @count selected members',
      ];
    }

    return [
      'singular' => 'Your email has been sent to 1 selected member successfully',
      'plural' => 'Your email has been sent to @count selected members successfully',
    ];
  }

  return [
    'singular' => 'Your email has not been sent to 1 selected member successfully',
    'plural' => 'Your email has not been sent to @count selected members successfully',
  ];
}

/**
 * Implements hook_preprocess_field().
 */
function social_group_preprocess_field(&$variables) {
  $formatter = $variables['element']['#formatter'];
  if (in_array($formatter, ['address_plain', 'address_default'])) {
    $entity = $variables['element']['#object'];
    if ($entity instanceof SocialGroupInterface) {
      $social_group_settings = \Drupal::config('social_group.settings');
      $address_visibility_settings = $social_group_settings->get('address_visibility_settings');
      if (isset($address_visibility_settings['street_code_private']) && !empty($address_visibility_settings['street_code_private'])) {
        if (!$entity->hasMember(\Drupal::currentUser())) {
          switch ($formatter) {
            case 'address_plain':
              if (isset($variables['items'][0]['content']['#address_line1'])) {
                $variables['items'][0]['content']['#address_line1'] = NULL;
              }
              if (isset($variables['items'][0]['content']['#postal_code'])) {
                $variables['items'][0]['content']['#postal_code'] = NULL;
              }
              break;

            case 'address_default':
              if (isset($variables['items'][0]['content']['address_line1']['#value'])) {
                $variables['items'][0]['content']['address_line1']['#value'] = '';
              }
              if (isset($variables['items'][0]['content']['postal_code']['#value'])) {
                $variables['items'][0]['content']['postal_code']['#value'] = '';
              }
              break;
          }
        }
      }
    }
  }
}

/**
 * Returns a new popover tooltip based on a descriptive text and field name.
 *
 * @param string $field_name
 *   The field name to create a unique id for.
 * @param \Drupal\Core\StringTranslation\TranslatableMarkup $data_title
 *   The title of the pop-up.
 * @param string $description
 *   A string containing the description, this needs to be rendered markup.
 *
 * @return array
 *   The render array containing the tooltip.
 */
function social_group_render_tooltip($field_name, TranslatableMarkup $data_title, $description) {
  $build = [];
  $id = Html::getUniqueId('tooltip-' . $field_name);
  $icon = Bootstrap::glyphicon('question-sign');
  $title = '';

  // We special case this for the hero.
  if ($field_name === 'group_hero') {
    $data_title = t('Access permissions');

    // For SKY we can render a bigger icon.
    $icon_size = 'icon-small';
    if (theme_get_setting('style') === 'sky') {
      $icon_size = 'icon-medium';
      $title = t('Access permissions');
    }

    $icon = [
      '#type' => "inline_template",
      '#template' => '<svg class="icon-white ' . $icon_size . '"><use xlink:href="#icon-cog"></use></svg>',
    ];
  }

  $build['toggle'] = [
    '#type' => 'link',
    '#url' => Url::fromUserInput("#$id"),
    '#icon' => $icon,
    '#title' => $title,
    '#attributes' => [
      'class' => ['icon-before'],
      'data-toggle' => 'popover',
      'data-container' => 'body',
      'data-html' => 'true',
      'data-placement' => 'bottom',
      'data-title' => $data_title,
    ],
  ];
  $build['requirements'] = [
    '#type' => 'container',
    '#theme_wrappers' => ['container__file_upload_help'],
    '#attributes' => [
      'id' => $id,
      'class' => ['hidden', 'help-block'],
      'aria-hidden' => 'true',
    ],
  ];
  // As documented in Render API, Note that the value is passed through
  // \Drupal\Component\Utility\Xss::filterAdmin(), which strips known XSS
  // vectors while allowing a permissive list of HTML tags.
  $build['requirements']['descriptions'] = [
    '#markup' => $description,
    '#allowed_tags' => [
      'strong',
      'span',
      'svg',
      'p',
      'div',
      'em',
      'img',
      'a',
      'span',
      'use',
    ],
  ];

  return $build;
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_fieldset(array &$variables): void {
  // Make sure our flexible group visibility field renders a tooltip, since
  // this field is rendered as fieldset with legend and radios as children
  // we need to do it in this preprocess.
  $element = $variables['element'];

  if (empty($element['#field_name'])) {
    return;
  }

  $fields = \Drupal::moduleHandler()->invokeAll('social_group_content_visibility_field');

  if (in_array($element['#field_name'], $fields)) {
    $description = '';

    /** @var string $key */
    foreach (array_keys($element['#options']) as $key) {
      $description .= social_group_allowed_visibility_description($key);
    }

    // Render a specific tooltip based on a field name and description.
    // This is done in the fieldset, next to the <legend>.
    $variables['popover'] = social_group_render_tooltip(
      $element['#field_name'],
      t('Visibility'),
      $description,
    );

    return;
  }

  $parameters = \Drupal::routeMatch()->getParameters();

  if ($parameters->has('entity_type_id')) {
    $entity_type_id = $parameters->get('entity_type_id');

    if ($parameters->has('bundle_parameter')) {
      /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $type */
      $type = $parameters->get($parameters->get('bundle_parameter'));

      $bundle = $type->id();
    }
  }
  else {
    $found = FALSE;

    foreach ($parameters->all() as $parameter) {
      if ($parameter instanceof EntityInterface) {
        $entity_type_id = $parameter->getEntityTypeId();

        if ($parameter->getEntityType()->getBundleEntityType() !== NULL) {
          $bundle = $parameter->bundle();
        }

        $found = TRUE;
        break;
      }
    }

    if (!$found || !isset($entity_type_id)) {
      return;
    }
  }

  /** @var \Drupal\social_group\JoinManagerInterface $manager */
  $manager = \Drupal::service('plugin.manager.social_group.join');

  $found = FALSE;

  foreach ($manager->relations() as $data) {
    if (
      !isset($data['method']) &&
      $data['entity_type'] === $entity_type_id &&
      isset($bundle) === isset($data['bundle']) &&
      (!isset($bundle) || $data['bundle'] === $bundle) &&
      $data['field'] === $element['#field_name']
    ) {
      $found = TRUE;
      break;
    }
  }

  if (!$found || !isset($data)) {
    return;
  }

  $description = '';

  /** @var string $key */
  foreach (array_keys($element['#options']) as $key) {
    $description .= social_group_allowed_join_method_description(
      $key,
      $entity_type_id,
    );
  }

  // Render a specific tooltip based on a field name and description.
  // This is done in the fieldset, next to the <legend>.
  $variables['popover'] = social_group_render_tooltip(
    $data['field'],
    t('Join methods'),
    $description,
  );
}

/**
 * Gets a description as HTML markup for the selected content visibility option.
 *
 * @param string $key
 *   The visibility option name.
 */
function social_group_allowed_visibility_description(string $key): string {
  /** @var \Drupal\social_group\SocialGroupHelperServiceInterface $helper */
  $helper = \Drupal::service('social_group.helper_service');

  return $helper->description(
    $key,
    'social_group_content_visibility_description',
  );
}

/**
 * Implements hook_entity_display_build_alter().
 */
function social_group_entity_display_build_alter(array &$build, array $context) {
  // For our nodes, renderd as small_teaser
  // since thee Group 1.0 update it renders all group details.
  // As our group types are optional and not a dependency
  // we cant update our yml files but have to do it like this.
  if (!$context['entity'] instanceof Node && $context['view_mode'] !== 'small_teaser') {
    return;
  }
  $os_groups = [
    'groups_type_flexible_group',
    'groups_type_public_group',
    'groups_type_open_group',
    'groups_type_secret_group',
    'groups_type_closed_group',
    'groups',
  ];
  // For each Open Social group type we check if there are any
  // group details in the build array, if so unset it.
  // If it's not an OS group, people likely wanted to add it.
  foreach ($os_groups as $key) {
    if (array_key_exists($key, $build)) {
      unset($build[$key]);
    }
  }
}

/**
 * Implements hook_preprocess_page().
 */
function social_group_preprocess_page(array &$variables) {
  /** @var \Drupal\Core\Routing\AdminContext $admin_context */
  $admin_context = \Drupal::service('router.admin_context');
  // Don't add the modal for admin pages.
  // Only for sky theme.
  if ($admin_context->isAdminRoute() || theme_get_setting('style') !== 'sky') {
    return;
  }

  $route_matches = [
    'social_group_gvbo.views_bulk_operations.confirm',
    'views_bulk_operations.confirm',
    'views_bulk_operations.execute_batch',
    'social_group_gvbo.views_bulk_operations.execute_configurable',
  ];

  // Make sure we do the expensive checks only after we see that there's
  // something in the URL for nodes.
  foreach ($route_matches as $route_name) {
    if (\Drupal::routeMatch()->getRouteName() === $route_name) {
      $variables['page']['complementary_top'] = [];
    }
  }
}

/**
 * Implements hook_social_core_default_main_menu_links_alter().
 */
function social_group_social_core_default_main_menu_links_alter(array &$links) {
  $link = \Drupal::entityTypeManager()->getStorage('menu_link_content')
    ->loadByProperties([
      'title' => 'All groups',
      'menu_name' => 'main',
      'link' => 'internal:/all-groups',
    ]);
  $link = end($link);

  // Ensure that the end() doesn't return FALSE, and we have link instance.
  if ($link instanceof MenuLinkContentInterface) {
    $links[] = $link;
  }
}

/**
 * Implements hook_social_core_title().
 */
function social_group_social_core_title(): array {
  return [
    'group_content' => [
      'route_name' => 'entity.group_content.create_form',
      'callback' => function () {
        /** @var \Drupal\social_group\SocialGroupInterface $group */
        $group = \Drupal::routeMatch()->getParameter('group');

        $plugin_id = \Drupal::routeMatch()->getParameter('plugin_id');

        $plugin = $group->getGroupType()->getContentPlugin($plugin_id);

        $definition = \Drupal::entityTypeManager()
          ->getDefinition($plugin->getEntityTypeId());

        if (
          $definition !== NULL &&
          ($entity_type_id = $definition->getBundleEntityType()) !== NULL
        ) {
          return \Drupal::entityTypeManager()->getStorage($entity_type_id)
            ->load($plugin->getEntityBundle());
        }

        return NULL;
      },
    ],
  ];
}

/**
 * Implements hook_social_tagging_type().
 */
function social_group_social_tagging_type(): string {
  return 'group';
}

/**
 * Implements hook_form_form_ID_alter().
 */
function social_group_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
  if ($form['#id'] === 'views-exposed-form-group-manage-members-page-group-manage-members') {
    /** @var \Drupal\views\ViewExecutable $view */
    $view = $form_state->get('view');

    // Declare value 'module_name' since we send it to callback.
    $form['module_name'] = [
      '#type' => 'value',
      '#value' => 'social_group',
    ];
    // Declare 'items_per_value_key' that will be sent in users_data table.
    $form['items_per_value_key'] = [
      '#type' => 'value',
      '#value' => $view->id() . '__items_per_page',
    ];
    $form['actions']['submit']['#submit'][] = [MembersPageHelperService::class, 'setItemsPerPage'];
  }
}

/**
 * Implements hook_views_pre_view().
 */
function social_group_views_pre_view(ViewExecutable $view, string $display_id, array &$args): void {
  if ($view->id() === 'group_manage_members' && $display_id === 'page_group_manage_members') {
    // Get related '__items_per_value' value from users_data table.
    \Drupal::service('social_core.members_page_helper')
      ->getItemsPerPage(
        $view,
        'social_group',
        $view->id() . '__items_per_page'
      );
  }
}
