<?php

namespace Drupal\islandora\Form;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Stomp\Client;
use Stomp\Exception\StompException;
use Stomp\StatefulStomp;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Config form for Islandora settings.
 */
class IslandoraSettingsForm extends ConfigFormBase {

  const CONFIG_NAME = 'islandora.settings';
  const BROKER_URL = 'broker_url';
  const BROKER_USER = 'broker_user';
  const BROKER_PASSWORD = 'broker_password';
  const JWT_EXPIRY = 'jwt_expiry';
  const UPLOAD_FORM = 'upload_form';
  const UPLOAD_FORM_LOCATION = 'upload_form_location';
  const UPLOAD_FORM_ALLOWED_MIMETYPES = 'upload_form_allowed_mimetypes';
  const GEMINI_PSEUDO = 'gemini_pseudo_bundles';
  const FEDORA_URL = 'fedora_url';
  const TIME_INTERVALS = [
    'sec',
    'second',
    'min',
    'minute',
    'hour',
    'day',
    'week',
    'month',
    'year',
  ];
  const GEMINI_PSEUDO_FIELD = 'field_gemini_uri';
  const NODE_DELETE_MEDIA_AND_FILES = 'delete_media_and_files';
  const REDIRECT_AFTER_MEDIA_SAVE = 'redirect_after_media_save';
  const ALLOW_HEADER_LINKS = 'allow_header_links';
  const FAST_TERM_QUERIES = 'fast_term_queries';

  /**
   * To list the available bundle types.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  private $entityTypeBundleInfo;

  /**
   * The saved password (if set).
   *
   * @var string
   */
  private $brokerPassword;

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

  /**
   * Constructs a \Drupal\system\ConfigFormBase object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The factory for configuration objects.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The EntityTypeBundleInfo service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The EntityTypeManager service.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory,
    EntityTypeBundleInfoInterface $entity_type_bundle_info,
    EntityTypeManagerInterface $entity_type_manager,
  ) {
    $this->setConfigFactory($config_factory);
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
    $this->brokerPassword = $this->config(self::CONFIG_NAME)->get(self::BROKER_PASSWORD);
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
          $container->get('config.factory'),
          $container->get('entity_type.bundle.info'),
          $container->get('entity_type.manager')
      );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'islandora_admin_settings';
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return [
      self::CONFIG_NAME,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config(self::CONFIG_NAME);

    $form['broker_info'] = [
      '#type' => 'details',
      '#title' => $this->t('Broker'),
      '#open' => TRUE,
    ];
    $form['broker_info'][self::BROKER_URL] = [
      '#type' => 'textfield',
      '#title' => $this->t('URL'),
      '#default_value' => $config->get(self::BROKER_URL),
      '#config' => [
        'key' => 'islandora.settings:' . self::BROKER_URL,
      ],
    ];
    $broker_user = $config->get(self::BROKER_USER);
    $form['broker_info']['provide_user_creds'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Provide user identification'),
      '#default_value' => $broker_user ? TRUE : FALSE,
    ];
    $state_selector = 'input[name="provide_user_creds"]';
    $form['broker_info'][self::BROKER_USER] = [
      '#type' => 'textfield',
      '#title' => $this->t('User'),
      '#default_value' => $broker_user,
      '#states' => [
        'visible' => [
          $state_selector => ['checked' => TRUE],
        ],
        'required' => [
          $state_selector => ['checked' => TRUE],
        ],
      ],
      '#config' => [
        'key' => 'islandora.settings:' . self::BROKER_USER,
      ],
    ];
    $form['broker_info'][self::BROKER_PASSWORD] = [
      '#type' => 'password',
      '#title' => $this->t('Password'),
      '#description' => $this->t('If this field is left blank and the user is filled out, the current password will not be changed.'),
      '#states' => [
        'visible' => [
          $state_selector => ['checked' => TRUE],
        ],
      ],
      '#config' => [
        'key' => 'islandora.settings:' . self::BROKER_PASSWORD,
        'secret' => TRUE,
      ],
    ];
    $form[self::JWT_EXPIRY] = [
      '#type' => 'textfield',
      '#title' => $this->t('JWT Expiry'),
      '#default_value' => $config->get(self::JWT_EXPIRY),
      '#description' => $this->t('A positive time interval expression. Eg: "60 secs", "2 days", "10 hours", "7 weeks". Be sure you provide the time units (@unit), plurals are accepted.',
        ['@unit' => implode(", ", self::TIME_INTERVALS)]
      ),
    ];

    $form[self::UPLOAD_FORM] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Add Children / Media Form'),
    ];

    $form[self::UPLOAD_FORM][self::UPLOAD_FORM_LOCATION] = [
      '#type' => 'textfield',
      '#title' => $this->t('Upload location'),
      '#description' => $this->t('Tokenized URI pattern where the uploaded file should go.  You may use tokens to provide a pattern (e.g. "fedora://[current-date:custom:Y]-[current-date:custom:m]")'),
      '#default_value' => $config->get(self::UPLOAD_FORM_LOCATION),
      '#element_validate' => ['token_element_validate'],
      '#token_types' => ['system'],
    ];

    $form[self::UPLOAD_FORM]['TOKEN_HELP'] = [
      '#theme' => 'token_tree_link',
      '#token_type' => ['system'],
    ];

    $form[self::UPLOAD_FORM][self::UPLOAD_FORM_ALLOWED_MIMETYPES] = [
      '#type' => 'textarea',
      '#title' => $this->t('Allowed Mimetypes'),
      '#description' => $this->t('Add mimetypes as a space delimited list with no periods before the extension.'),
      '#default_value' => $config->get(self::UPLOAD_FORM_ALLOWED_MIMETYPES),
    ];

    $flysystem_config = Settings::get('flysystem');
    if ($flysystem_config != NULL) {
      $fedora_url = $flysystem_config['fedora']['config']['root'];
    }
    else {
      $fedora_url = NULL;
    }

    $form[self::NODE_DELETE_MEDIA_AND_FILES] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Node Delete with Media and Files'),
      '#description' => $this->t('Adds a checkbox in the "Delete" tab of islandora objects to delete media and files associated with the object.'
      ),
      '#default_value' => (bool) $config->get(self::NODE_DELETE_MEDIA_AND_FILES),
    ];

    $form[self::REDIRECT_AFTER_MEDIA_SAVE] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Redirect after media save.'),
      '#description' => $this->t('Redirect to node-specific media list after creation of media.'),
      '#default_value' => (bool) $config->get(self::REDIRECT_AFTER_MEDIA_SAVE),
    ];

    $form[self::FAST_TERM_QUERIES] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Use multiple queries for term URI lookups.'),
      '#description' => $this->t('Using multiple queries to look up taxonomy terms by URI is faster in most setups. You may want to disable this if you have a term access protection module enabled.'),
      '#default_value' => (bool) $config->get(self::FAST_TERM_QUERIES),
    ];

    $form[self::FEDORA_URL] = [
      '#type' => 'textfield',
      '#title' => $this->t('Fedora URL'),
      '#description' => $this->t('Read-only. This value is set in settings.php as the URL for the Fedora flysystem.'),
      '#attributes' => [
        'readonly' => 'readonly',
        'disabled' => 'disabled',
      ],
      '#default_value' => $fedora_url,
    ];

    $selected_bundles = $config->get(self::GEMINI_PSEUDO);

    $options = [];
    foreach (['node', 'media', 'taxonomy_term'] as $content_entity) {
      $bundles = $this->entityTypeBundleInfo->getBundleInfo($content_entity);
      foreach ($bundles as $bundle => $bundle_properties) {
        $options["{$bundle}:{$content_entity}"] =
                $this->t('@label (@type)', [
                  '@label' => $bundle_properties['label'],
                  '@type' => $content_entity,
                ]);
      }
    }

    $form['bundle_container'] = [
      '#type' => 'details',
      '#title' => $this->t('Fedora URL Display'),
      '#description' => $this->t('Selected bundles can display the Fedora URL of repository content.'),
      '#open' => TRUE,
      self::GEMINI_PSEUDO => [
        '#type' => 'checkboxes',
        '#options' => $options,
        '#default_value' => $selected_bundles,
      ],
    ];

    $form['rdf_namespaces'] = [
      '#type' => 'link',
      '#title' => $this->t('Update RDF namespace configurations in the JSON-LD module settings.'),
      '#url' => Url::fromRoute('system.jsonld_settings'),
    ];

    $form[self::ALLOW_HEADER_LINKS] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Allow header links'),
      '#description' => $this->t('If checked, links in the HTTP header will be added'),
      '#default_value' => (bool) $config->get(self::ALLOW_HEADER_LINKS),
    ];

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Validate broker url by actually connecting with a stomp client.
    $brokerUrl = $form_state->getValue(self::BROKER_URL);
    // Attempt to subscribe to a dummy queue.
    try {
      $client = new Client($brokerUrl);
      if ($form_state->getValue('provide_user_creds')) {
        $broker_password = $form_state->getValue(self::BROKER_PASSWORD);
        // When stored password type fields aren't rendered again.
        if (!$broker_password) {
          // Use the stored password if it exists.
          if (!$this->brokerPassword) {
            $form_state->setErrorByName(self::BROKER_PASSWORD, $this->t('A password must be supplied'));
          }
          else {
            $broker_password = $this->brokerPassword;
          }
        }
        $client->setLogin($form_state->getValue(self::BROKER_USER), $broker_password);
      }
      $stomp = new StatefulStomp($client);
      $stomp->subscribe('dummy-queue-for-validation');
      $stomp->unsubscribe();
    }
    // Invalidate the form if there's an issue.
    catch (StompException $e) {
      $form_state->setErrorByName(
        self::BROKER_URL,
        $this->t(
          'Cannot connect to message broker at @broker_url',
          ['@broker_url' => $brokerUrl]
        )
      );
    }

    // Validate jwt expiry as a valid time string.
    $expiry = trim($form_state->getValue(self::JWT_EXPIRY));
    $expiry = strtolower($expiry);
    if (strtotime($expiry) === FALSE) {
      $form_state->setErrorByName(
        self::JWT_EXPIRY,
        $this->t(
          '"@expiry" is not a valid time or interval expression.',
          ['@expiry' => $expiry]
        )
      );
    }
    elseif (substr($expiry, 0, 1) == "-") {
      $form_state->setErrorByName(
        self::JWT_EXPIRY,
        $this->t('Time or interval expression cannot be negative')
          );
    }
    elseif (intval($expiry) === 0) {
      $form_state->setErrorByName(
        self::JWT_EXPIRY,
        $this->t('No numeric interval specified, for example "1 day"')
          );
    }
    else {
      if (!preg_match("/\b(" . implode("|", self::TIME_INTERVALS) . ")s?\b/", $expiry)) {
        $form_state->setErrorByName(
          self::JWT_EXPIRY,
          $this->t("No time interval found, please include one of (@int). Plurals are also accepted.",
            ['@int' => implode(", ", self::TIME_INTERVALS)]
          )
        );
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $config = $this->configFactory->getEditable(self::CONFIG_NAME);

    $new_pseudo_types = array_filter($form_state->getValue(self::GEMINI_PSEUDO));

    $broker_password = $form_state->getValue(self::BROKER_PASSWORD);

    // If there's no user set delete what may have been here before as password
    // fields will also be blank.
    if (!$form_state->getValue('provide_user_creds')) {
      $config->clear(self::BROKER_USER);
      $config->clear(self::BROKER_PASSWORD);
    }
    else {
      $config->set(self::BROKER_USER, $form_state->getValue(self::BROKER_USER));
      // If the password has changed update it as well.
      if ($broker_password && $broker_password != $this->brokerPassword) {
        $config->set(self::BROKER_PASSWORD, $broker_password);
      }
    }

    // Check for types being unset and remove the field from them first.
    $current_pseudo_types = $config->get(self::GEMINI_PSEUDO);
    $this->updateEntityViewConfiguration($current_pseudo_types, $new_pseudo_types);

    $config
      ->set(self::BROKER_URL, $form_state->getValue(self::BROKER_URL))
      ->set(self::JWT_EXPIRY, $form_state->getValue(self::JWT_EXPIRY))
      ->set(self::UPLOAD_FORM_LOCATION, $form_state->getValue(self::UPLOAD_FORM_LOCATION))
      ->set(self::UPLOAD_FORM_ALLOWED_MIMETYPES, $form_state->getValue(self::UPLOAD_FORM_ALLOWED_MIMETYPES))
      ->set(self::GEMINI_PSEUDO, $new_pseudo_types)
      ->set(self::NODE_DELETE_MEDIA_AND_FILES, $form_state->getValue(self::NODE_DELETE_MEDIA_AND_FILES))
      ->set(self::FAST_TERM_QUERIES, $form_state->getValue(self::FAST_TERM_QUERIES))
      ->set(self::REDIRECT_AFTER_MEDIA_SAVE, $form_state->getValue(self::REDIRECT_AFTER_MEDIA_SAVE))
      ->set(self::ALLOW_HEADER_LINKS, $form_state->getValue(self::ALLOW_HEADER_LINKS))
      ->save();

    parent::submitForm($form, $form_state);
  }

  /**
   * Removes the Fedora URI field from entity bundles that have be unselected.
   *
   * @param array $current_config
   *   The current set of entity types & bundles to have the pseudo field,
   *   format {bundle}:{entity_type}.
   * @param array $new_config
   *   The new set of entity types & bundles to have the pseudo field, format
   *   {bundle}:{entity_type}.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  private function updateEntityViewConfiguration(array $current_config, array $new_config) {
    $removed = array_diff($current_config, $new_config);
    $added = array_diff($new_config, $current_config);
    $entity_view_display = $this->entityTypeManager->getStorage('entity_view_display');
    foreach ($removed as $bundle_type) {
      [$bundle, $type_id] = explode(":", $bundle_type);
      $results = $entity_view_display->getQuery()
        ->condition('bundle', $bundle)
        ->condition('targetEntityType', $type_id)
        ->exists('content.' . self::GEMINI_PSEUDO_FIELD . '.region')
        ->execute();
      $entities = $entity_view_display->loadMultiple($results);
      foreach ($entities as $entity) {
        $entity->removeComponent(self::GEMINI_PSEUDO_FIELD);
        $entity->save();
      }
    }
    if (count($removed) > 0 || count($added) > 0) {
      // If we added or cleared a type then clear the extra_fields cache.
      // @see Drupal/Core/Entity/EntityFieldManager::getExtraFields
      Cache::invalidateTags(["entity_field_info"]);
    }
  }

}
