<?php

/**
 * @file
 * Entity Access By Field module file.
 *
 * @todo Prevent creating multiple visibility fields on one node.
 * @todo Add support for multiple entity types.
 */

use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity_access_by_field\EntityAccessHelper;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\NodeInterface;
use Drupal\social_post\Entity\PostInterface;

/**
 * Here we define a constant for our node access grant ID.
 */
const ENTITY_ACCESS_BY_FIELD_ALLOWED_REALM = 1;

/**
 * Implements hook_ENTITY_TYPE_access().
 */
function entity_access_by_field_node_access(
  EntityInterface $entity,
  string $operation,
  AccountInterface $account
): AccessResultInterface {
  return EntityAccessHelper::getEntityAccessResult(
    $entity,
    $operation,
    $account,
    'administer nodes',
  );
}

/**
 * Implements hook_node_grants().
 */
function entity_access_by_field_node_grants(AccountInterface $account, $op): array {
  if ($op === 'view') {
    $grants = &drupal_static(__FUNCTION__);

    // If we haven't stored any grants for this user, let's try and populate it
    // now.
    if (!isset($grants[$account->id()])) {
      $grants[$account->id()] = [];

      $entityAccessPermissions = \Drupal::getContainer()
        ->get('entity_access_by_field.permissions');
      $permissions = $entityAccessPermissions->getRealmWithPermission();

      if (!empty($permissions)) {
        foreach ($permissions as $realm => $permission) {
          if ($account->hasPermission($permission)) {
            $grants[$account->id()][$realm][] = ENTITY_ACCESS_BY_FIELD_ALLOWED_REALM;
          }
          $grants[$account->id()][$realm . '_author'] = [$account->id()];
        }
      }
    }

    return $grants[$account->id()];
  }

  // We don't influence any operation other than `view`.
  return [];
}

/**
 * Implements hook_node_grants_alter().
 */
function entity_access_by_field_node_grants_alter(array &$grants, AccountInterface $account, string $op): void {
  if ($op === 'view') {
    // Statically cache the grant ids per account.
    $gids = &drupal_static(__FUNCTION__);

    // If we haven't stored anything for this user yet, let's try and populate
    // it now.
    if (!isset($gids[$account->id()])) {
      $gids[$account->id()] = [];

      // Get all the memberships for this user.
      if (\Drupal::hasService('social_group.helper_service')) {
        /** @var \Drupal\social_group\SocialGroupHelperService $social_group_helper_service */
        $social_group_helper_service = \Drupal::service('social_group.helper_service');
        $groups = $social_group_helper_service->getAllGroupsForUser($account->id());
        foreach ($groups as $group) {
          $gids[$account->id()][] = $group;
        }
      }
      else {
        // This fallback method exists to ensure there is no direct dependency
        // needed on Social Group. It primarily helps to prevent dependency
        // issues during installation.
        /** @var \Drupal\user\UserStorageInterface $user_storage */
        $user_storage = \Drupal::entityTypeManager()->getStorage('user');
        /** @var \Drupal\group\Entity\Storage\GroupContentStorageInterface $group_content_storage */
        $group_content_storage = \Drupal::entityTypeManager()->getStorage('group_content');

        /** @var \Drupal\user\UserInterface $user */
        $user = $user_storage->load($account->id());
        $memberships = $group_content_storage->loadByEntity($user);
        foreach ($memberships as $membership) {
          $gids[$account->id()][] = $membership->getGroup()->id();
        }
      }
    }

    // Load node types.
    $node_types = \Drupal::entityTypeManager()
      ->getStorage('node_type')
      ->getQuery()
      ->execute();

    // Remove grants of node type if user are not a member in this group.
    foreach ($node_types as $node_type) {
      if (isset($grants["gnode:$node_type"])) {
        $grants["gnode:$node_type"] = array_intersect($grants["gnode:$node_type"], $gids[$account->id()]);
      }
    }
  }
}

/**
 * Implements hook_node_access_records().
 */
function entity_access_by_field_node_access_records(NodeInterface $node): array {

  $grants = [];

  // Get the field definitions of the node.
  $field_definitions = $node->getFieldDefinitions();
  /** @var \Drupal\Core\Field\FieldConfigInterface $field_definition */
  foreach ($field_definitions as $field_name => $field_definition) {
    // Lets add a node access realm if the field is implemented.
    if ($field_definition->getType() === 'entity_access_field') {
      $field_values = $node->get($field_name)->getValue();
      if (!empty($field_values)) {
        foreach ($field_values as $field_value) {
          if (isset($field_value['value'])) {
            $entityAccessPermissions = \Drupal::getContainer()
              ->get('entity_access_by_field.permissions');
            $realm = $entityAccessPermissions->getRealmForFieldValue('view', 'node', $node->getType(), $field_name, $field_value['value']);
            if ($node->isPublished()) {
              $grants[] = [
                'realm' => $realm,
                'gid' => ENTITY_ACCESS_BY_FIELD_ALLOWED_REALM,
                'grant_view' => 1,
                'grant_update' => 0,
                'grant_delete' => 0,
                'priority' => 0,
              ];
            }
            $grants[] = [
              'realm' => $realm . '_author',
              'gid' => $node->getOwnerId(),
              'grant_view' => 1,
              'grant_update' => 1,
              'grant_delete' => 1,
              'priority' => 0,
            ];
          }
        }
      }
    }
  }

  return $grants;
}

/**
 * Implements hook_node_access_explain().
 */
function entity_access_by_field_node_access_explain($row) {

  $entityAccessPermissions = \Drupal::getContainer()
    ->get('entity_access_by_field.permissions');
  $permissions = $entityAccessPermissions->getRealmWithPermission();

  foreach ($permissions as $realm => $permission) {
    if ($row->realm === $realm) {
      return 'Users with permission "' . $permission . '" may view this node.';
    }
  }
}

/**
 * Implements hook_field_widget_info_alter().
 */
function entity_access_by_field_field_widget_info_alter(&$info) {
  if (isset($info['options_buttons'])) {
    $info['options_buttons']['field_types'][] = 'entity_access_field';
  }
}

/**
 * Implements hook_field_formatter_info_alter().
 */
function entity_access_by_field_field_formatter_info_alter(array &$info) {
  if (isset($info['list_default'])) {
    $info['list_default']['field_types'][] = 'entity_access_field';
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function entity_access_by_field_node_presave(NodeInterface $node) {
  $original = $node->original;

  // Get the field definitions of the node.
  $field_definitions = $node->getFieldDefinitions();
  /** @var \Drupal\Core\Field\FieldConfigInterface $field_definition */
  foreach ($field_definitions as $field_name => $field_definition) {
    // Lets add a node access realm if the field is implemented.
    if ($field_definition->getType() === 'entity_access_field') {
      if (isset($node->status) && isset($original->status) && $node->status->value != $original->status->value) {
        // Invalidate cache tags.
        Cache::invalidateTags(['activity_list']);
      }

      if ($node->get($field_name)->isEmpty()) {
        $default_visibility = _entity_access_by_field_get_default_visibility($node);
        $node->get($field_name)->setValue($default_visibility);
      }
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function entity_access_by_field_post_presave(PostInterface $post) {
  $field_definitions = $post->getFieldDefinitions();

  foreach ($field_definitions as $field_name => $field_definition) {
    if ($field_definition->getType() == 'entity_access_field' && $post->get($field_name)->isEmpty()) {
      $default_visibility = _entity_access_by_field_get_default_visibility($post);
      $post->get($field_name)->setValue($default_visibility);
    }
  }
}

/**
 * Implements hook_field_widget_single_element_form_alter().
 */
function entity_access_by_field_field_widget_single_element_form_alter(&$element, FormStateInterface $form_state, $context) {
  $field_definition = $context['items']->getFieldDefinition();

  if ($field_definition->getType() !== 'entity_access_field' && $field_definition->getName() !== 'field_visibility') {
    return;
  }

  if ($field_definition instanceof BaseFieldDefinition && $field_definition->isMultiple()) {
    return;
  }

  $form_object = $form_state->getFormObject();

  if (!$form_object instanceof EntityFormInterface) {
    return;
  }

  $config = \Drupal::config('entity_access_by_field.settings');
  // Load the current user.
  $account = \Drupal::currentUser();

  // Set public visibility by default.
  $entity = $form_object->getEntity();
  $field_name = $field_definition->getName();

  if (($entity instanceof FieldConfig) || !$entity->hasField($field_name)) {
    return;
  }

  // Do not check if visibility already set and user has access to override
  // this option because SM can change the visibility.
  if (!$entity->get($field_name) || (!$entity->get($field_name)->isEmpty() && $account->hasPermission('override disabled public visibility'))) {
    return;
  }

  $element['#default_value'] = _entity_access_by_field_get_default_visibility($entity);

  // Check if the option is enabled and the current user has no permission
  // to override disabled public visibility.
  if ($config->get('disable_public_visibility') === 1 && !$account->hasPermission('override disabled public visibility')) {
    switch ($entity->getEntityTypeId()) {
      case 'node':
        if ($entity->get($field_name)->getString() !== 'public') {
          $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();
        }
        break;

      case 'post':
        // Remove the public option.
        unset($element['#options'][1]);
        break;
    }
  }
}

/**
 * Returns default value for content visibility fields.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The entity object to prepare correct value.
 *
 * @return string
 *   Default value of the field.
 */
function _entity_access_by_field_get_default_visibility(EntityInterface $entity) {
  $default_visibility = &drupal_static(__FUNCTION__ . '_' . $entity->getEntityTypeId());

  if ($default_visibility) {
    return $default_visibility;
  }

  $config = \Drupal::config('entity_access_by_field.settings');

  switch ($entity->getEntityTypeId()) {
    case 'node':
      $account = $entity->get('uid')->entity;
      $default_visibility = $config->get('default_visibility');
      break;

    case 'post':
      $account = $entity->get('user_id')->entity;
      $post_visibility = [
        'public' => '1',
        'community' => '2',
      ];
      $default_visibility = $post_visibility[$config->get('default_visibility')];
      break;

    default:
      return $default_visibility;
  }

  if ($config->get('disable_public_visibility') === 1 && !$account->hasPermission('override disabled public visibility')) {
    switch ($entity->getEntityTypeId()) {
      case 'node':
        if ($default_visibility === 'public') {
          $default_visibility = 'community';
        }
        break;

      case 'post':
        if ($default_visibility === '1') {
          $default_visibility = '2';
        }
        break;
    }
  }

  return $default_visibility;
}
