<?php

/**
 * @file
 * Contains social_tagging.module.
 */

use Drupal\Core\Database\Database;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\select2\Element\Select2;
use Drupal\social_tagging\SocialTaggingServiceInterface;
use Drupal\taxonomy\Entity\Term;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ViewExecutable;

/**
 * Implements hook_help().
 */
function social_tagging_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    // Main module help for the social_tagging module.
    case 'help.page.social_tagging':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Content tagging module') . '</p>';
      return $output;

    case 'entity.taxonomy_vocabulary.overview_form':
      $tag_service = Drupal::getContainer()->get('social_tagging.tag_service');

      if ($tag_service->allowSplit()) {
        /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
        $vocabulary = $route_match->getParameter('taxonomy_vocabulary');
        if ($vocabulary->id() === 'social_tagging') {
          return '<p><strong>' . t('Notice: Drag and drop has intentionally been disabled for this vocabulary.') . '</strong></p>';
        }
      }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_tagging_form_taxonomy_term_social_tagging_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Load all taxonomy terms from the top level.
  $tag_service = Drupal::getContainer()->get('social_tagging.tag_service');

  // Remove these fields.
  $form['relations']['#access'] = FALSE;

  // Move it outside the details.
  $form['parent'] = $form['relations']['parent'];
  unset($form['relations']['parent']);

  // Make some changes.
  $form['weight']['#access'] = FALSE;
  $form['parent']['#title'] = t('Location');
  $form['parent']['#required'] = TRUE;
  $form['parent']['#description'] = t('Integrate this term into an existing category or establish new one');
  $form['parent']['#description_display'] = 'before';

  // Fetch all top level items.
  $options = $tag_service->getCategories();
  // Add the 0 option for a new toplevel item.
  $options[0] = t('New category');
  // Sort the array.
  ksort($options);
  // Add it to the select.
  $form['parent']['#options'] = $options;

  // Add states for tagging usage.
  $form['field_category_usage']['#states'] = [
    'visible' => [
      'select[name="parent[]"]' => ['value' => ['0']],
    ],
  ];
  $form['actions']['submit']['#submit'][] = '_social_tagging_form_taxonomy_term_social_tagging_submit';
}

/**
 * Custom submit for social tagging form.
 */
function _social_tagging_form_taxonomy_term_social_tagging_submit(array $form, FormStateInterface $form_state): void {
  // Clear cache to see updated lists.
  \Drupal::service('cache.render')->invalidateAll();
  \Drupal::service('plugin.cache_clearer')->clearCachedDefinitions();
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_tagging_form_taxonomy_overview_terms_alter(&$form, FormStateInterface $form_state, $form_id) {

  if (Drupal::getContainer()->get('social_tagging.tag_service')->allowSplit()) {
    $storage = $form_state->getStorage();
    /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
    $vocabulary = $storage['taxonomy']['vocabulary'];

    if ($vocabulary->id() === 'social_tagging') {
      // Remove edit/delete links.
      foreach (Element::children($form['terms']) as $name) {
        unset($form['terms'][$name]['weight']);
      }

      // Hide Save button.
      $form['actions']['submit']['#access'] = FALSE;

      // Remove tableDrag.
      unset($form['terms']['#tabledrag']);

      // Remove Weight column.
      unset($form['terms']['#header'][1]);
    }
  }
}

/**
 * Implements hook_inline_entity_form_entity_form_alter().
 *
 * This hook allows to have a compatibility with "Inline Entity Form" module.
 */
function social_tagging_inline_entity_form_entity_form_alter(
  array &$entity_form,
  FormStateInterface &$form_state
): void {
  // "Inline entity form" module has an entity object in the "form" variable.
  social_tagging_social_tagging_field_form_alter(
    $entity_form,
    $form_state,
    $entity_form['#entity'],
  );
}

/**
 * Implements hook_form_alter().
 */
function social_tagging_form_alter(
  array &$form,
  FormStateInterface $form_state,
  string $form_id
): void {
  $form_object = $form_state->getFormObject();

  if ($form_object instanceof EntityFormInterface) {
    social_tagging_social_tagging_field_form_alter(
      $form,
      $form_state,
      $form_object->getEntity(),
    );
  }
}

/**
 * Prepares tags field.
 *
 * @param array $form
 *   The form array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity object.
 */
function social_tagging_social_tagging_field_form_alter(
  array &$form,
  FormStateInterface $form_state,
  EntityInterface $entity
): void {
  /** @var \Drupal\social_tagging\SocialTaggingServiceInterface $tag_service */
  $tag_service = \Drupal::service('social_tagging.tag_service');

  $items = $tag_service->types();

  if (!isset($items[$type = $entity->getEntityTypeId()])) {
    return;
  }

  $items = $items[$type];
  $prefix = ($bundles = !empty($items[0]['bundles'])) ? $type . '_' : '';
  $suffix = $bundles ? $entity->bundle() : $type;

  foreach ($items as &$item) {
    if (!isset($item['field'])) {
      $item['field'] = SocialTaggingServiceInterface::FIELD;
    }
  }

  unset($item);

  // Load tag config.
  $enabled = (bool) \Drupal::configFactory()->getEditable('social_tagging.settings')
    ->get("tag_{$prefix}type_$suffix");

  // Check if tagging is turned on/off.
  if (
    $enabled === FALSE ||
    !$tag_service->active() ||
    !$tag_service->hasContent()
  ) {
    // Remove the field from the form.
    foreach ($items as $item) {
      if (!empty($form[$item['field']])) {
        $form[$item['field']]['#access'] = FALSE;
      }
    }

    return;
  }

  $styled = theme_get_setting('content_entity_form_style') === 'open_social';
  $split = $tag_service->allowSplit();
  $fields = [];

  usort(
    $items,
    fn($a, $b) => !empty($a['bundles']) && empty($b['bundles']) ? -1 : 1,
  );

  foreach ($items as $delta => $item) {
    $field = $item['field'];

    if (empty($form[$field]) || in_array($field, $fields)) {
      continue;
    }

    $fields[] = $field;

    if ($styled) {
      if (
        !empty($item['label']) &&
        (!isset($item['wrapper']) || $item['wrapper'])
      ) {
        $title = $item['label'];
      }
      else {
        $title = NULL;
      }
    }
    else {
      $title = t('Tag content');
    }

    // Get the default value.
    $default_value = $split
      ? _social_tagging_node_form_defaults_values($entity, $field) : NULL;

    $added = $tag_service->field(
      $form,
      $form_state,
      $field,
      $title,
      NULL,
      $wrapper = SocialTaggingServiceInterface::WRAPPER . $delta,
      $default_value,
    );

    if (!$added) {
      continue;
    }

    // We want to move the tagging field in new fieldset "Tags" only when the
    // theme settings are updated.
    if ($styled) {
      $group = 'group_' . ($item['group'] ?? 'social_tags');

      if (isset($form['#fieldgroups'][$group])) {
        $form['#fieldgroups'][$group]->children[] = $wrapper;
      }
      else {
        $group_tags = new stdClass();

        $group_tags->children = [];
        $group_tags->parent_name = '';
        $group_tags->label = $item['label'] ?? t('Tags');
        $group_tags->weight = $item['weight'] ?? 1;
        $group_tags->format_type = 'fieldset';
        $group_tags->format_settings = [
          'required_fields' => TRUE,
          'id' => 'tags',
          'classes' => 'card',
          'label' => $group_tags->label,
        ];
        $group_tags->mode = 'add';
        $group_tags->context = 'form';
        $group_tags->entity_type = $type;
        $group_tags->group_name = $group;

        $form['#fieldgroups'][$group] = $group_tags;
      }

      $form[$wrapper]['#group'] = $group;
      $form['#group_children'][$wrapper] = $group;
    }

    if (
      $type === 'profile' &&
      $field === SocialTaggingServiceInterface::FIELD
    ) {
      if (isset($form['#fieldgroups'][$group = 'group_tags'])) {
        $form[$wrapper]['#type'] = 'container';
        $form[$wrapper]['#group'] = $group;
      }
      else {
        $form[$wrapper]['#title'] = t('Tags');
      }
    }
  }
}

/**
 * Validate function that overrides the tagging field with new values.
 *
 * @param array $form
 *   An associative array containing the structure of the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 */
function _social_tagging_entity_validate(
  array $form,
  FormStateInterface $form_state
): void {
  // Get the tagging service.
  /** @var \Drupal\social_tagging\SocialTaggingServiceInterface $tag_service */
  $tag_service = \Drupal::service('social_tagging.tag_service');

  // Get the main categories.
  $categories = array_values($tag_service->getCategories());

  /** @var \Drupal\social_core\Service\MachineNameInterface $machine_name */
  $machine_name = \Drupal::service('social_core.machine_name');

  foreach ($form_state->get('tags') as $field) {
    // Init categories.
    $tagging_values = [];

    // Loop over the categories.
    foreach ($categories as $counter => $category) {
      $sub_field = $field . '_' . $machine_name->transform($category);

      foreach ((array) $form_state->getValue($sub_field) as $selected) {
        $tagging_values[] = [
          'target_id' => $selected,
          '_weight' => (string) $counter,
        ];
      }
    }

    // Set the values in the social_tagging field.
    $form_state->setValue($field, $tagging_values);
  }
}

/**
 * Helper function to get the current default tagging values of a node.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity object.
 * @param string|null $field
 *   (optional) The field name. Defaults to NULL.
 */
function _social_tagging_node_form_defaults_values(
  EntityInterface $entity,
  string $field = NULL
): array {
  if ($entity->id() === NULL) {
    return [];
  }

  return array_column(
    $entity->get($field ?? SocialTaggingServiceInterface::FIELD)->getValue(),
    'target_id',
  );
}

/**
 * Implements hook_entity_base_field_info().
 */
function social_tagging_entity_base_field_info(EntityTypeInterface $entity_type): array {
  $fields = [];

  /** @var \Drupal\social_tagging\SocialTaggingServiceInterface $helper */
  $helper = \Drupal::service('social_tagging.tag_service');

  $types = $helper->types();

  // Add a Tagging base field.
  if (isset($types[$type = $entity_type->id()])) {
    foreach ($types[$type] as $set) {
      if (!isset($set['field'])) {
        $fields[SocialTaggingServiceInterface::FIELD] = _social_tagging_field();
        break;
      }
    }
  }

  return $fields;
}

/**
 * Implements hook_entity_delete().
 */
function social_tagging_entity_delete(EntityInterface $entity) {
  // When a term from the social_tagging vocbulary is deleted, remove from node.
  /** @var \Drupal\taxonomy\Entity\Term $entity */
  if ($entity instanceof Term &&
    $entity->bundle() === 'social_tagging') {
    /* @see: taxonomy_taxonomy_term_delete(), delete from node field. */
    // We need to use `\Drupal\Core\Database\Database::getConnection()`
    // because we might not yet have access to the container here.
    Database::getConnection()->delete('node__social_tagging')->condition('social_tagging_target_id', $entity->id())->execute();
    Database::getConnection()->delete('node_revision__social_tagging')->condition('social_tagging_target_id', $entity->id())->execute();
  }
}

/**
 * Build output on node view.
 *
 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
 *   A node.
 *
 * @return array
 *   Hierarchic presentation of the terms.
 */
function social_tagging_process_tags(FieldableEntityInterface $entity): array {
  if (!$entity->hasField('social_tagging')) {
    return [];
  }

  $tag_service = \Drupal::service('social_tagging.tag_service');

  if (!$tag_service->active()) {
    return [];
  }

  $terms = $entity->get('social_tagging')->getValue();

  if ($tag_service->allowSplit()) {
    $taghierarchy = $tag_service->buildHierarchy($terms, $entity->getEntityTypeId());
    if (empty($taghierarchy)) {
      return [];
    }
    $renderable = [
      '#theme' => 'social_tagging_split',
      '#taghierarchy' => $taghierarchy,
    ];
  }
  else {
    $tarray = [];

    // Determine the route based on the source of the tags.
    $route = 'view.search_content.page_no_value';
    if ($entity->getEntityTypeId() == 'group') {
      $route = 'view.search_groups.page_no_value';
    }

    // Just a simple way to add all tags to the array.
    foreach ($terms as $term) {
      $url = Url::fromRoute($route, [
        'tag[]' => $term['target_id'],
      ]);

      /** @var \Drupal\taxonomy\TermInterface $taxonomy_term */
      $taxonomy_term = \Drupal::entityTypeManager()
        ->getStorage('taxonomy_term')
        ->load($term['target_id']);
      if ($taxonomy_term->isPublished()) {
        $tarray[$taxonomy_term->getName()] = $url->toString();
      }
    }

    if (empty($tarray)) {
      return [];
    }
    $renderable = [
      '#theme' => 'social_tagging_nosplit',
      '#tagstitle' => t('Tags'),
      '#tags' => $tarray,
    ];
  }

  $renderable['#entity_type'] = $entity->getEntityTypeId();

  return $renderable;
}

/**
 * Implements hook_theme().
 */
function social_tagging_theme($existing, $type, $theme, $path) {
  return [
    'social_tagging_nosplit' => [
      'variables' => [
        'tagstitle' => NULL,
        'tags' => NULL,
        'entity_type' => NULL,
      ],
    ],
    'social_tagging_split' => [
      'variables' => [
        'taghierarchy' => NULL,
        'entity_type' => NULL,
      ],
    ],
  ];
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_tagging_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $keys = [
    'views-exposed-form-search-content-page-no-value' => ['node_page', 'node_event', 'node_topic'],
    'views-exposed-form-search-content-page' => ['node_page', 'node_event', 'node_topic'],
    'views-exposed-form-search-groups-page-no-value' => ['group'],
    'views-exposed-form-search-groups-page' => ['group'],
    'views-exposed-form-search-users-page-no-value' => ['profile'],
    'views-exposed-form-search-users-page' => ['profile'],
    'views-exposed-form-latest-topics-page-latest-topics' => ['node_topic'],
    'views-exposed-form-upcoming-events-page-community-events' => ['node_event'],
    'views-exposed-form-topics-page-profile' => ['node_topic'],
    'views-exposed-form-events-events-overview' => ['node_event'],
    'views-exposed-form-group-topics-page-group-topics' => ['node_topic'],
    'views-exposed-form-group-events-page-group-events' => ['node_event'],
    'views-exposed-form-group-books-page-group-books' => ['node_topic'],
    'views-exposed-form-newest-groups-page-all-groups' => ['group'],
    'views-exposed-form-newest-users-page-newest-users' => ['profile'],
  ];
  $form_ids = array_keys($keys);
  // Must be either one of these form_ids.
  if (!in_array($form['#id'], $form_ids)) {
    return;
  }

  /** @var \Drupal\social_tagging\SocialTaggingService $tag_service */
  $tag_service = \Drupal::service('social_tagging.tag_service');

  if (!($tag_service->active() && $tag_service->hasContent())) {
    return;
  }
  if ($tag_service->allowSplit()) {
    foreach ($tag_service->getCategories() as $tid => $term_name) {
      $label = \Drupal::service('social_core.machine_name')->transform($term_name);

      if (!isset($form[$label])) {
        continue;
      }
      if ($tag_service->termIsVisibleForEntities($tid, $keys[$form['#id']])) {
        $form[$label]['#options'] = [];
        $form[$label]['#options'][''] = t('- Any -');

        // Display parent of tags.
        if ($tag_service->useCategoryParent()) {
          $form[$label]['#options'] += [$tid => $term_name];
        }

        $form[$label]['#options'] += $tag_service->getChildren($tid);
        $form[$label]['#type'] = 'select2';
        $form[$label]['#size'] = NULL;

        if (isset($form[$label]['#context']['#plugin_type']) && $form[$label]['#context']['#plugin_type'] === 'bef') {
          $form[$label]['#select2'] = [];
          $form[$label]['#process'] = [
            [Select2::class, 'processSelect'],
          ];
          $form[$label]['#pre_render'] = [
            [Select2::class, 'preRenderSelect'],
            [Select2::class, 'preRenderAutocomplete'],
            [Select2::class, 'preRenderOverwrites'],
          ];
        }

        $request_params = \Drupal::request()->request->all();
        if (!empty($request_params[$label])) {
          $form[$label]['#value'] = $request_params[$label];
        }
      }
      else {
        // Otherwise, we should not see tag on a form.
        $form[$label]['#access'] = FALSE;
      }
    }
  }
  else {
    // Cleanup and display hierarchical structure of tags.
    $form['tag']['#options'] = [];
    $form['tag']['#options'][''] = t('- Any -');
    $form['tag']['#type'] = 'select2';
    $form['tag']['#size'] = NULL;

    $request_params = \Drupal::request()->request->all();
    if (!empty($request_params['tag'])) {
      $form['tag']['#value'] = $request_params['tag'];
    }

    foreach ($tag_service->getCategories() as $tid => $label) {
      $options = [$tid => $label];
      $options += $tag_service->getChildren($tid);
      $form['tag']['#options'][$label] = $options;
    }
  }
}

/**
 * Main term names to machine name for on node edit / validate.
 *
 * @param string $text
 *   A node.
 *
 * @return mixed|string
 *   A machine name so it can be used programmatically.
 *
 * @deprecated in social:11.3.10 and is removed from social:13.0.0. Use
 *   \Drupal::service('social_core.machine_name')->transform() instead.
 *
 * @see https://www.drupal.org/node/3306214
 */
function social_tagging_to_machine_name($text) {
  @trigger_error('social_tagging_to_machine_name() is deprecated in social:11.3.10 and is removed from social:13.0.0. Use "\Drupal::service(\'social_core.machine_name\')->transform()" instead. See https://www.drupal.org/node/3306214', E_USER_DEPRECATED);
  return preg_replace('@[^a-z0-9-]+@', '_', strtolower($text));
}

/**
 * Implements hook_preprocess_profile().
 */
function social_tagging_preprocess_profile(array &$variables) {
  /** @var \Drupal\social_tagging\SocialTaggingService $tag_service */
  $tag_service = \Drupal::service('social_tagging.tag_service');

  $variables['social_tagging_profile_active'] = FALSE;
  if ($tag_service->active() && $tag_service->profileActive()) {
    $variables['social_tagging_profile_active'] = TRUE;

    /** @var \Drupal\profile\Entity\ProfileInterface $profile */
    $profile = $variables['profile'];

    if (!$profile->get('social_tagging')->isEmpty()) {
      $tags = $profile->get('social_tagging')->getValue();

      $variables['social_tagging_allow_split'] = $tag_service->allowSplit();
      $variables['social_tagging_hierarchy'] = $tag_service->buildHierarchy($tags, 'profile');
    }
  }
}

/**
 * Implements hook_preprocess_views_query_alter().
 */
function social_tagging_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  /** @var \Drupal\social_tagging\SocialTaggingService $tag_service */
  $tag_service = \Drupal::service('social_tagging.tag_service');

  if ($tag_service->queryCondition() === 'OR') {
    return;
  }

  // View id and display id.
  $tagging_views = [
    'latest_topics' => 'page_latest_topics',
    'upcoming_events' => 'page_community_events',
    'events' => 'events_overview',
    'group_topics' => 'default',
    'group_events' => 'default',
    'group_books' => 'default',
    'newest_groups' => 'page_all_groups',
    'newest_users' => 'page',
  ];

  if (array_key_exists((string) $view->id(), $tagging_views) && in_array($view->getDisplay()->getBaseId(), $tagging_views, TRUE)) {
    /** @var \Drupal\views\Plugin\views\query\Sql $query */
    $org_query = $query->where['2'];
    $conditions = $org_query['conditions'] ?? [];
    $count_conditions = count($conditions);

    // This only make sense if there are multiple
    // search terms.
    if ($count_conditions <= 1) {
      return;
    }

    // Extract the term ids.
    $values = [];
    foreach ($conditions as $condition) {
      $values[] = $condition['value'];
    }

    // Update the condition and use IN instead
    // of =.
    $new_condition = end($conditions);
    $new_condition['value'] = $values;
    $new_condition['operator'] = 'IN';
    $field = $new_condition['field'];

    // Make sure we only get results that has all the
    // required terms.
    $query->addHavingExpression(
      0,
      "COUNT(DISTINCT($field)) = $count_conditions",
      [],
    );
  }

  /** @var \Drupal\search_api\Plugin\views\query\SearchApiQuery $query */
  if ($view->id() === 'search_content' && isset($query->getWhere()[2]['type'])) {
    $query->getWhere()[2]['type'] = 'AND';
  }
}

/**
 * Implements hook_modules_installed().
 */
function social_tagging_modules_installed(array $modules): void {
  if (in_array('media', $modules)) {
    $entity_type = \Drupal::entityTypeManager()
      ->getStorage('media')
      ->getEntityType();

    $fields = \Drupal::moduleHandler()
      ->invokeAll('entity_base_field_info', [$entity_type]);

    if (!isset($fields['social_tagging'])) {
      return;
    }

    \Drupal::entityDefinitionUpdateManager()
      ->installFieldStorageDefinition('social_tagging', 'media', 'social_tagging', $fields['social_tagging']);
  }
}

/**
 * Gets base field.
 */
function _social_tagging_field(): FieldStorageDefinitionInterface {
  return BaseFieldDefinition::create('entity_reference')
    ->setLabel(t('Tagging'))
    ->setDescription(t('Tagging field.'))
    ->setSetting('target_type', 'taxonomy_term')
    ->setSetting('handler', 'default:taxonomy_term')
    ->setSetting('handler_settings', [
      'target_bundles' => [
        'social_tagging' => 'social_tagging',
      ],
    ])
    ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED)
    ->setDisplayOptions('view', [
      'region' => 'hidden',
    ])
    ->setDisplayOptions('form', [
      'type' => 'options_select',
      'weight' => 3,
      'settings' => [],
    ])
    ->setDisplayConfigurable('form', TRUE)
    ->setDisplayConfigurable('view', TRUE);
}
