<?php

namespace Drupal\advanced_search\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\search_api\Display\DisplayPluginManager;
use Drupal\views\Entity\View;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Provides an Islandora Advanced Search block.
 *
 * @Block(
 *  id = "advanced_search_block",
 *  deriver = "Drupal\advanced_search\Plugin\Block\AdvancedSearchBlockDeriver",
 *  admin_label = @Translation("Islandora Advanced Search"),
 *  category = @Translation("Islandora"),
 * )
 */
class AdvancedSearchBlock extends BlockBase implements ContainerFactoryPluginInterface {
  use ViewAndDisplayIdentifiersTrait;

  // CSS classes used to bind table-drag behavior to.
  const WEIGHT_FIELD_CLASS = 'field-weight';
  const DISPLAY_FIELD_CLASS = 'field-display';

  // Regions in the table which denote if a given field
  // is visible in the Advanced Search Form or not.
  const REGION_VISIBLE = 'visible';
  const REGION_HIDDEN = 'hidden';

  // Keys for settings.
  const SETTING_FIELDS = 'fields';
  const SETTING_CONTEXTUAL_FILTER = 'context_filter';

  /**
   * The display plugin manager.
   *
   * @var \Drupal\search_api\Display\DisplayPluginManager
   */
  protected $displayPluginManager;

  /**
   * The clone of the current request object.
   *
   * @var \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * The view this block affects.
   *
   * @var \Drupal\views\Entity\View
   */
  protected $view;

  /**
   * The view display this block affects.
   *
   * @var array
   */
  protected $display;

  /**
   * Form Builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * Construct a AdvancedSearchBlock instance.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param string $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\search_api\Display\DisplayPluginManager $display_plugin_manager
   *   The display plugin manager.
   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
   *   The form builder service used to build the search form.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   A request object for the current request.
   */
  final public function __construct(array $configuration, $plugin_id, $plugin_definition, DisplayPluginManager $display_plugin_manager, FormBuilderInterface $form_builder, Request $request) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->displayPluginManager = $display_plugin_manager;
    [$view_id, $display_id] = preg_split('/__/', $this->getDerivativeId(), 2);
    $this->view = View::Load($view_id);
    $this->display = $this->view->getDisplay($display_id);
    $this->formBuilder = $form_builder;
    $this->request = clone $request;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('plugin.manager.search_api.display'),
      $container->get('form_builder'),
      $container->get('request_stack')->getMainRequest()
    );
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      self::SETTING_FIELDS => [],
      self::SETTING_CONTEXTUAL_FILTER => NULL,
    ];
  }

  /**
   * Fields which can be enabled / disabled for display in the search form.
   *
   * @return \Drupal\search_api\Item\FieldInterface[]
   *   The $fields sorted by label.
   */
  protected function getFields() {
    $fields = $this->getIndex()->getFields();
    // First pass sort on label, secondary sort will be used
    // when looking at existing configuration for this block.
    uasort($fields, function ($a, $b) {
      return strcmp($a->getLabel(), $b->getLabel());
    });
    return $fields;
  }

  /**
   * Get regions of table to display.
   *
   * @return array
   *   The properties of each region used for building the table of fields.
   */
  protected function getRegions() {
    // Classes for select fields like 'weight' and 'display' are hard-coded
    // and used in js/islandora-advanced-search.admin.js.
    return [
      'visible' => [
        'title' => $this->t('Visible'),
        'invisible' => TRUE,
        'message' => $this->t('No search field is visible.'),
        'weight' => self::WEIGHT_FIELD_CLASS . '-visible',
        'display' => self::DISPLAY_FIELD_CLASS . '-visible',
      ],
      'hidden' => [
        'title' => $this->t('Hidden'),
        'invisible' => FALSE,
        'message' => $this->t('No search field is hidden.'),
        'weight' => self::WEIGHT_FIELD_CLASS . '-hidden',
        'display' => self::DISPLAY_FIELD_CLASS . '-hidden',
      ],
    ];
  }

  /**
   * Options for field display derived from the available regions.
   *
   * @return array
   *   Display select field options.
   */
  protected function getDisplayOptions() {
    $options = [];
    foreach ($this->getRegions() as $region => $settings) {
      $options[$region] = $settings['title'];
    }
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    // At most we will have one row per field.
    $fields = $this->getFields();
    $weight_delta = round(count($fields) / 2);

    // Group each field into a region given our current configuration.
    $visible_fields = $this->configuration[self::SETTING_FIELDS];
    $regions = $this->getRegions();
    $display_options = $this->getDisplayOptions();

    // Field rows are grouped by the region in which they are displayed.
    $field_rows = array_fill_keys(array_keys($regions), []);
    foreach ($fields as $field) {
      // If a field exists in the blocks configuration than it is 'visible' and
      // its weight is equivalent to its order in the configuration,
      // i.e. its index.
      $identifier = $field->getFieldIdentifier();
      $weight = array_search($identifier, $visible_fields);
      $visible = $weight !== FALSE;
      $region = $visible ? self::REGION_VISIBLE : self::REGION_HIDDEN;
      $field_rows[$region][$identifier] = [
        '#attributes' => [
          'class' => ['draggable'],
        ],
        'label' => ['#plain_text' => $field->getLabel()],
        'identifier' => ['#plain_text' => $identifier],
        'weight' => [
          '#type' => 'weight',
          '#title' => $this->t('Weight'),
          '#title_display' => 'invisible',
          '#default_value' => $visible ? $weight : 0,
          '#delta' => $weight_delta,
          '#attributes' => [
            'class' => [self::WEIGHT_FIELD_CLASS, $regions[$region]['weight']],
          ],
        ],
        'display' => [
          '#type' => 'select',
          '#title' => $this->t('Display'),
          '#title_display' => 'invisible',
          '#options' => $display_options,
          '#default_value' => $region,
          '#attributes' => [
            'class' => [self::DISPLAY_FIELD_CLASS, $regions[$region]['display']],
          ],
        ],
      ];
    }
    // Sort the visible rows by their weight.
    uasort($field_rows[self::REGION_VISIBLE], function ($a, $b) {
      $a = $a['weight']['#default_value'];
      $b = $b['weight']['#default_value'];
      if ($a == $b) {
        return 0;
      }
      return ($a < $b) ? -1 : 1;
    });

    // Build Rows.
    $rows = [];
    $table_drag = [];
    foreach ($regions as $region => $properties) {
      $rows += [
        // Conditionally display region title as a row.
        "region-$region" => $properties['invisible'] ? NULL : [
          '#attributes' => [
            'class' => ['region-title', "region-title-$region"],
          ],
          'label' => [
            '#plain_text' => $properties['title'],
            '#wrapper_attributes' => [
              'colspan' => 4,
            ],
          ],
        ],
        // Will dynamically display if the region has fields or not controlled
        // by Drupal behaviors in js/islandora-advanced-search.admin.js.
        "region-$region-message" => [
          '#attributes' => [
            'class' => [
              'region-message',
              "region-$region-message",
              empty($field_rows[$region]) ? 'region-empty' : 'region-populated',
            ],
          ],
          'message' => [
            '#markup' => '<em>' . $properties['message'] . '</em>',
            '#wrapper_attributes' => [
              'colspan' => 4,
            ],
          ],
        ],
      ];

      // Include field rows in this region.
      $rows += $field_rows[$region];

      // Configure order by weight field in region.
      $table_drag[] = [
        'action' => 'order',
        'relationship' => 'sibling',
        'group' => self::WEIGHT_FIELD_CLASS,
        'subgroup' => $properties['weight'],
        'source' => self::WEIGHT_FIELD_CLASS,
      ];

      // Configure drag action for display field in region.
      $table_drag[] = [
        'action' => 'match',
        'relationship' => 'sibling',
        'group' => self::DISPLAY_FIELD_CLASS,
        'subgroup' => $properties['display'],
        'source' => self::DISPLAY_FIELD_CLASS,
      ];
    }

    $form[self::SETTING_FIELDS] = [
      '#type' => 'table',
      '#attributes' => [
        // Identifier is hard-coded and used in
        // js/islandora-advanced-search.admin.js.
        'id' => 'advanced-search-fields',
      ],
      '#header' => [
        $this->t('Label'),
        $this->t('Field'),
        $this->t('Weight'),
        $this->t('Display'),
      ],
      '#empty' => $this->t('No search fields, please check search index configuration.'),
      '#tabledrag' => $table_drag,
    ] + $rows;

    // If there is contextual filters associated with the display that means
    // we can filter on collection / sub-collection. Allow the user to choose
    // which filters collections.
    $id = NULL;
    $field = NULL;
    $options = [];
    if (isset($this->display['display_options']['arguments'])) {
      foreach ($this->display['display_options']['arguments'] as $context_filter) {
        $id = $context_filter['id'];
        $field = $context_filter['field'];
        if (isset($fields[$field])) {
          $options[$id] = $fields[$field]->getLabel() . ':' . $id;
        }
      }
    }
    if (count($options) > 0) {
      $form[self::SETTING_CONTEXTUAL_FILTER] = [
        '#type' => 'select',
        '#title' => $this->t('Context Filter'),
        '#description' => $this->t('If more than one <strong>Context Filter</strong> is defined, specify which is used to <strong>include</strong> only <strong>direct children</strong> of the Collection as it will disabled to allow recursive searching.'),
        '#options' => $options,
        '#default_value' => $this->configuration[self::SETTING_CONTEXTUAL_FILTER],
        '#multiple' => FALSE,
        '#required' => FALSE,
        '#size' => count($options) + 1,
      ];
    }
    $form['#attributes']['class'][] = 'clearfix';
    $form['#attached']['library'][] = 'advanced_search/advanced.search.admin';
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $values = $form_state->getValues();
    $fields = array_filter($values[self::SETTING_FIELDS], function ($field) {
      return $field['display'] == 'visible';
    });
    uasort($fields, '\Drupal\Component\Utility\SortArray::sortByWeightElement');
    $this->configuration[self::SETTING_FIELDS] = array_keys($fields);
    if (isset($values[self::SETTING_CONTEXTUAL_FILTER])) {
      $this->configuration[self::SETTING_CONTEXTUAL_FILTER] = $values[self::SETTING_CONTEXTUAL_FILTER];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $fields = $this->getIndex()->getFields();
    $configured_fields = [];
    foreach ($this->configuration[self::SETTING_FIELDS] as $identifier) {
      $configured_fields[$identifier] = $fields[$identifier];
    }
    return $this->formBuilder->getForm('Drupal\advanced_search\Form\AdvancedSearchForm', $this->view, $this->display, $configured_fields, $this->configuration[self::SETTING_CONTEXTUAL_FILTER]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    // The block cannot be cached, because it must always match the current
    // search results.
    return 0;
  }

  /**
   * Get Search Index.
   */
  protected function getIndex() {
    $id = $this->getDerivativeId();
    return $this->displayPluginManager->createInstance("views_{$this->display['display_plugin']}:{$id}")->getIndex();
  }

}
