<?php

namespace Drupal\media_library;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
 * Service which builds the media library.
 *
 * @internal
 *   This service is an internal part of the modal media library dialog and
 *   does not provide any extension points.
 */
class MediaLibraryUiBuilder {

  use StringTranslationTrait;

  /**
   * The form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The currently active request object.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * The views executable factory.
   *
   * @var \Drupal\views\ViewExecutableFactory
   */
  protected $viewsExecutableFactory;

  /**
   * The media library opener resolver.
   *
   * @var \Drupal\media_library\OpenerResolverInterface
   */
  protected $openerResolver;

  /**
   * Constructs a MediaLibraryUiBuilder instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\views\ViewExecutableFactory $views_executable_factory
   *   The views executable factory.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The currently active request object.
   * @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
   *   The opener resolver.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory, FormBuilderInterface $form_builder, OpenerResolverInterface $opener_resolver) {
    $this->entityTypeManager = $entity_type_manager;
    $this->request = $request_stack->getCurrentRequest();
    $this->viewsExecutableFactory = $views_executable_factory;
    $this->formBuilder = $form_builder;
    $this->openerResolver = $opener_resolver;
  }

  /**
   * Get media library dialog options.
   *
   * @return array
   *   The media library dialog options.
   */
  public static function dialogOptions() {
    return [
      'classes' => [
        'ui-dialog' => 'media-library-widget-modal',
      ],
      'title' => t('Add or select media'),
      'height' => '75%',
      'width' => '75%',
    ];
  }

  /**
   * Build the media library UI.
   *
   * @param \Drupal\media_library\MediaLibraryState $state
   *   (optional) The current state of the media library, derived from the
   *   current request.
   *
   * @return array
   *   The render array for the media library.
   */
  public function buildUi(?MediaLibraryState $state = NULL) {
    if (!$state) {
      $state = MediaLibraryState::fromRequest($this->request);
    }
    // When navigating to a media type through the vertical tabs, we only want
    // to load the changed library content. This is not only more efficient, but
    // also provides a more accessible user experience for screen readers.
    if ($state->get('media_library_content') === '1') {
      return $this->buildLibraryContent($state);
    }
    else {
      return [
        '#theme' => 'media_library_wrapper',
        '#attributes' => [
          'id' => 'media-library-wrapper',
        ],
        'menu' => $this->buildMediaTypeMenu($state),
        'content' => $this->buildLibraryContent($state),
        // Attach the JavaScript for the media library UI. The number of
        // available slots needs to be added to make sure users can't select
        // more items than allowed.
        '#attached' => [
          'library' => ['media_library/ui'],
          'drupalSettings' => [
            'media_library' => [
              'selection_remaining' => $state->getAvailableSlots(),
            ],
          ],
        ],
      ];
    }
  }

  /**
   * Build the media library content area.
   *
   * @param \Drupal\media_library\MediaLibraryState $state
   *   The current state of the media library, derived from the current request.
   *
   * @return array
   *   The render array for the media library.
   */
  protected function buildLibraryContent(MediaLibraryState $state) {
    return [
      '#type' => 'container',
      '#theme_wrappers' => [
        'container__media_library_content',
      ],
      '#attributes' => [
        'id' => 'media-library-content',
      ],
      'form' => $this->buildMediaTypeAddForm($state),
      'view' => $this->buildMediaLibraryView($state),
    ];
  }

  /**
   * Check access to the media library.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   Run access checks for this account.
   * @param \Drupal\media_library\MediaLibraryState $state
   *   (optional) The current state of the media library, derived from the
   *   current request.
   *
   * @return \Drupal\Core\Access\AccessResult
   *   The access result.
   */
  public function checkAccess(AccountInterface $account, ?MediaLibraryState $state = NULL) {
    if (!$state) {
      try {
        $state = MediaLibraryState::fromRequest($this->request);
      }
      catch (BadRequestHttpException $e) {
        return AccessResult::forbidden($e->getMessage());
      }
      catch (\InvalidArgumentException $e) {
        return AccessResult::forbidden($e->getMessage());
      }
    }
    // Deny access if the view or display are removed.
    $view = $this->entityTypeManager->getStorage('view')->load('media_library');
    if (!$view) {
      return AccessResult::forbidden('The media library view does not exist.')
        ->setCacheMaxAge(0);
    }
    if (!$view->getDisplay('widget')) {
      return AccessResult::forbidden('The media library widget display does not exist.')
        ->addCacheableDependency($view);
    }

    // The user must at least be able to view media in order to access the media
    // library.
    $can_view_media = AccessResult::allowedIfHasPermission($account, 'view media')
      ->addCacheableDependency($view);

    // Delegate any further access checking to the opener service nominated by
    // the media library state.
    return $this->openerResolver->get($state)->checkAccess($state, $account)
      ->andIf($can_view_media);
  }

  /**
   * Get the media type menu for the media library.
   *
   * @param \Drupal\media_library\MediaLibraryState $state
   *   The current state of the media library, derived from the current request.
   *
   * @return array
   *   The render array for the media type menu.
   */
  protected function buildMediaTypeMenu(MediaLibraryState $state) {
    // Add the menu for each type if we have more than 1 media type enabled for
    // the field.
    $allowed_type_ids = $state->getAllowedTypeIds();
    if (count($allowed_type_ids) <= 1) {
      return [];
    }

    // @todo Add a class to the li element.
    //   https://www.drupal.org/project/drupal/issues/3029227
    $menu = [
      '#theme' => 'links__media_library_menu',
      '#links' => [],
      '#attributes' => [
        'class' => ['js-media-library-menu'],
      ],
    ];

    $allowed_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($allowed_type_ids);

    $selected_type_id = $state->getSelectedTypeId();
    foreach ($allowed_types as $allowed_type_id => $allowed_type) {
      $link_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $allowed_type_id, $state->getAvailableSlots(), $state->getOpenerParameters());
      // Add the 'media_library_content' parameter so the response will contain
      // only the updated content for the tab.
      // @see self::buildUi()
      $link_state->set('media_library_content', 1);

      $title = $allowed_type->label();
      $display_title = [
        '#markup' => $this->t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span>', ['@title' => $title]),
      ];
      if ($allowed_type_id === $selected_type_id) {
        $display_title = [
          '#markup' => $this->t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>', ['@title' => $title]),
        ];
      }

      $menu['#links']['media-library-menu-' . $allowed_type_id] = [
        'title' => $display_title,
        'url' => Url::fromRoute('media_library.ui', [], [
          'query' => $link_state->all(),
        ]),
        'attributes' => [
          'role' => 'button',
          'data-title' => $title,
        ],
      ];
    }

    // Set the active menu item.
    $menu['#links']['media-library-menu-' . $selected_type_id]['attributes']['class'][] = 'active';

    return $menu;
  }

  /**
   * Get the add form for the selected media type.
   *
   * @param \Drupal\media_library\MediaLibraryState $state
   *   The current state of the media library, derived from the current request.
   *
   * @return array
   *   The render array for the media type add form.
   */
  protected function buildMediaTypeAddForm(MediaLibraryState $state) {
    $selected_type_id = $state->getSelectedTypeId();

    $access_handler = $this->entityTypeManager->getAccessControlHandler('media');
    $context = [
      'media_library_state' => $state,
    ];
    if (!$access_handler->createAccess($selected_type_id, NULL, $context)) {
      return [];
    }

    $selected_type = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
    $plugin_definition = $selected_type->getSource()->getPluginDefinition();

    if (empty($plugin_definition['forms']['media_library_add'])) {
      return [];
    }

    // After the form to add new media is submitted, we need to rebuild the
    // media library with a new instance of the media add form. The form API
    // allows us to do that by forcing empty user input.
    // @see \Drupal\Core\Form\FormBuilder::doBuildForm()
    $form_state = new FormState();
    if ($state->get('_media_library_form_rebuild')) {
      $form_state->setUserInput([]);
      $state->remove('_media_library_form_rebuild');
    }
    $form_state->set('media_library_state', $state);
    return $this->formBuilder->buildForm($plugin_definition['forms']['media_library_add'], $form_state);
  }

  /**
   * Get the media library view.
   *
   * @param \Drupal\media_library\MediaLibraryState $state
   *   The current state of the media library, derived from the current request.
   *
   * @return array
   *   The render array for the media library view.
   */
  protected function buildMediaLibraryView(MediaLibraryState $state) {
    // @todo Make the view configurable in
    //   https://www.drupal.org/project/drupal/issues/2971209
    $view = $this->entityTypeManager->getStorage('view')->load('media_library');
    $view_executable = $this->viewsExecutableFactory->get($view);
    $display_id = $state->get('views_display_id', 'widget');

    // Make sure the state parameters are set in the request so the view can
    // pass the parameters along in the pager, filters etc.
    $view_request = $view_executable->getRequest();
    $view_request->query->add($state->all());
    $view_executable->setRequest($view_request);

    $args = [$state->getSelectedTypeId()];

    $view_executable->setDisplay($display_id);
    $view_executable->preExecute($args);
    $view_executable->execute($display_id);

    return $view_executable->buildRenderable($display_id, $args, FALSE);
  }

}
