<?php

namespace Drupal\media_bulk_upload\Form;

use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Environment;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\file\FileInterface;
use Drupal\file\FileRepositoryInterface;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\media_bulk_upload\Entity\MediaBulkConfigInterface;
use Drupal\media_bulk_upload\MediaSubFormManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class BulkMediaUploadForm.
 *
 * @package Drupal\media_upload\Form
 */
class MediaBulkUploadForm extends FormBase {

  /**
   * Media Type storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $mediaTypeStorage;

  /**
   * Media Bulk Config storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $mediaBulkConfigStorage;

  /**
   * Media entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $mediaStorage;

  /**
   * File entity storage.
   *
   * @var \Drupal\file\FileStorageInterface
   */
  protected $fileStorage;

  /**
   * Media SubForm Manager.
   *
   * @var \Drupal\media_bulk_upload\MediaSubFormManager
   */
  protected $mediaSubFormManager;

  /**
   * The max file size for the media bulk form.
   *
   * @var string
   */
  protected $maxFileSizeForm;

  /**
   * The allowed extensions for the media bulk form.
   *
   * @var array
   */
  protected $allowed_extensions = [];

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The file repository.
   *
   * @var \Drupal\file\FileRepositoryInterface
   */
  protected $fileRepository;

  /**
   * BulkMediaUploadForm constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\media_bulk_upload\MediaSubFormManager $mediaSubFormManager
   *   Media Sub Form Manager.
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   Current User.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, MediaSubFormManager $mediaSubFormManager, AccountProxyInterface $currentUser, MessengerInterface $messenger, FileRepositoryInterface $fileRepository) {
    $this->mediaTypeStorage = $entityTypeManager->getStorage('media_type');
    $this->mediaBulkConfigStorage = $entityTypeManager->getStorage('media_bulk_config');
    $this->mediaStorage = $entityTypeManager->getStorage('media');
    $this->fileStorage = $entityTypeManager->getStorage('file');
    $this->mediaSubFormManager = $mediaSubFormManager;
    $this->currentUser = $currentUser;
    $this->messenger = $messenger;
    $this->fileRepository = $fileRepository;
  }

  /**
   * {@inheritdoc}
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('media_bulk_upload.subform_manager'),
      $container->get('current_user'),
      $container->get('messenger'),
      $container->get('file.repository')
    );
  }

  /**
   * Returns a unique string identifying the form.
   *
   * @return string
   *   The unique string identifying the form.
   */
  public function getFormId() {
    return 'media_bulk_upload_form';
  }

  /**
   * Form constructor.
   *
   * @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.
   * @param \Drupal\media_bulk_upload\Entity\MediaBulkConfigInterface|null $media_bulk_config
   *   The media bulk configuration entity.
   *
   * @return array
   *   The form structure.
   *
   * @throws \Exception
   */
  public function buildForm(array $form, FormStateInterface $form_state, MediaBulkConfigInterface $media_bulk_config = NULL) {
    $mediaBulkConfig = $media_bulk_config;

    if ($mediaBulkConfig === NULL) {
      return $form;
    }

    $mediaTypeManager = $this->mediaSubFormManager->getMediaTypeManager();
    $mediaTypes = $this->mediaSubFormManager->getMediaTypeManager()->getBulkMediaTypes($mediaBulkConfig);
    $mediaTypeLabels = [];

    foreach ($mediaTypes as $mediaType) {
      $extensions = $mediaTypeManager->getMediaTypeExtensions($mediaType);
      natsort($extensions);
      $this->addAllowedExtensions($extensions);

      $maxFileSize = $mediaTypeManager->getTargetFieldMaxSize($mediaType);
      $mediaTypeLabels[] = $mediaType->label() . ' (max ' . $maxFileSize . '): ' . implode(', ', $extensions);
      if (!empty($maxFileSize) && $this->isMaxFileSizeLarger($maxFileSize)) {
        $this->setMaxFileSizeForm($maxFileSize);
      }
    }

    if (empty($this->maxFileSizeForm)) {
      $this->maxFileSizeForm = $this->mediaSubFormManager->getDefaultMaxFileSize();
    }

    $form['#tree'] = TRUE;
    $form['information_wrapper'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'media-bulk-upload-information-wrapper',
        ],
      ],
    ];
    $form['information_wrapper']['information_label'] = [
      '#type' => 'html_tag',
      '#tag' => 'label',
      '#value' => $this->t('Information'),
      '#attributes' => [
        'class' => [
          'form-control-label',
        ],
        'for' => 'media_bulk_upload_information',
      ],
    ];

    $form['information_wrapper']['information'] = [
      '#theme' => 'item_list',
      '#title' => $this->t('Media Types:'),
      '#items' => $mediaTypeLabels,
    ];

    if (count($mediaTypes) > 1) {
      $form['information_wrapper']['warning'] = [
        '#type' => 'html_tag',
        '#tag' => 'p',
        '#id' => 'media_bulk_upload_information',
        '#name' => 'media_bulk_upload_information',
        '#value' => $this->t('Please be aware that if file extensions overlap between the media types that are available in this upload form, that the media entity will be assigned automatically to one of these types.'),
      ];
    }

    $form['dropzonejs'] = [
      '#type' => 'dropzonejs',
      '#title' => $this->t('Dropzone'),
      '#required' => TRUE,
      '#dropzone_description' => $this->t('Click or drop your files here'),
      '#max_filesize' => $this->maxFileSizeForm,
      '#extensions' => implode(' ', $this->allowed_extensions),
    ];


    if ($this->mediaSubFormManager->validateMediaFormDisplayUse($mediaBulkConfig)) {
      $form['fields'] = [
        '#type' => 'fieldset',
        '#title' => $this->t('Fields'),
        'shared' => [
          '#field_parents' => ['fields', 'shared'],
          '#parents' => ['fields', 'shared'],
        ],
      ];
      $this->mediaSubFormManager->buildMediaSubForm($form['fields']['shared'], $form_state, $mediaBulkConfig);
    }

    $form['media_bundle_config'] = [
      '#type' => 'value',
      '#value' => $mediaBulkConfig->id(),
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * Add allowed extensions.
   *
   * @param array $extensions
   *   Allowed Extensions.
   *
   * @return $this
   *   MediaBulkUploadForm.
   */
  protected function addAllowedExtensions(array $extensions) {
    $this->allowed_extensions = array_unique(array_merge($this->allowed_extensions, $extensions));
    return $this;
  }

  /**
   * Validate if a max file size is bigger then the current max file size.
   *
   * @param string $MaxFileSize
   *   File Size.
   *
   * @return bool
   *  TRUE if the given size is larger than the one that is set.
   */
  protected function isMaxFileSizeLarger($MaxFileSize) {
    $size = Bytes::toNumber($MaxFileSize);
    $currentSize = Bytes::toNumber($this->maxFileSizeForm);

    return ($size > $currentSize);
  }

  /**
   * Set the max File size for the form.
   *
   * @param string $newMaxFileSize
   *   File Size.
   *
   * @return $this
   *   MediaBulkUploadForm.
   */
  protected function setMaxFileSizeForm($newMaxFileSize) {
    $this->maxFileSizeForm = $newMaxFileSize;
    return $this;
  }

  /**
   * Submit handler to create the file entities and media entities.
   *
   * @param array $form
   *   The form render array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();
    $mediaBundleConfigId = $values['media_bundle_config'];

    /** @var MediaBulkConfigInterface $mediaBulkConfig */
    $mediaBulkConfig = $this->mediaBulkConfigStorage->load($mediaBundleConfigId);
    $files = $values['dropzonejs']['uploaded_files'];

    $mediaTypes = $this->mediaSubFormManager->getMediaTypeManager()->getBulkMediaTypes($mediaBulkConfig);
    $mediaType = reset($mediaTypes);
    $mediaFormDisplay = $this->mediaSubFormManager->getMediaFormDisplay($mediaBulkConfig, $mediaType);

    $this->prepareFormValues($form_state);

    $batchOperations = [];
    $operationId = 1;
    foreach ($files as $file) {
      $batchOperations[] = [
        [$this, 'batchOperation'],
        [
          $operationId,
          [
            'media_bulk_config' => $mediaBulkConfig,
            'media_form_display' => $mediaFormDisplay,
            'file' => $file,
            'form' => $form,
            'form_state' => $form_state,
          ],
        ],
      ];
      $operationId++;
    }
    $operationsCount = count($batchOperations);
    $batch = [
      'title' => $this->formatPlural(
        $operationsCount,
        'Preparing 1 media item',
        'Preparing @count media items', ['@count' => $operationsCount]
      ),
      'operations' => $batchOperations,
      'finished' => [$this, 'batchFinished'],
    ];
    batch_set($batch);
  }

  /**
   * Batch operation callback.
   *
   * @param string $id
   *   Batch operation id.
   * @param array $operation_details
   *   Batch operation details.
   * @param array $context
   *   Batch context.
   */
  public function batchOperation($id, array $operation_details, array &$context) {
    $mediaBulkConfig = $operation_details['media_bulk_config'];
    $mediaFormDisplay = $operation_details['media_form_display'];
    $file = $operation_details['file'];
    $form = $operation_details['form'];
    $form_state = $operation_details['form_state'];
    try {
      $media = $this->processFile($mediaBulkConfig, $file);
      if (!$media) {
        // @todo be more verbose.
        throw new \Exception('Media not created.');
      }
      if ($this->mediaSubFormManager->validateMediaFormDisplayUse($operation_details['media_bulk_config'])) {
        $extracted = $mediaFormDisplay->extractFormValues($media, $form['fields']['shared'], $form_state);
        $this->copyFormValuesToEntity($media, $extracted, $form_state);
      }
      $media->save();
      $context['results'][] = $id;
      // @todo be more verbose.
      $context['message'] = $this->t('Running batch operation @id.',
        [
          '@id' => $id,
        ]
      );
    } catch (\Exception $e) {
      watchdog_exception('media_bulk_upload', $e);
    }
  }

  /**
   * Batch finished callback.
   *
   * @param boolean $success
   *   Batch success.
   * @param array $results
   *   Batch results.
   * @param array $operations
   *   Batch operations.
   */
  public function batchFinished($success, array $results, array $operations) {
    if ($success) {
      $this->messenger()->addMessage($this->t('@count media have been created.', ['@count' => count($results)]));
    }
    else {
      $errorOperation = reset($operations);
      $this->messenger()->addError(
        $this->t('An error occurred while processing @operation with arguments : @args',
          [
            '@operation' => $errorOperation[0],
            '@args' => print_r($errorOperation[0], TRUE),
          ]
        )
      );
    }
  }

  /**
   * Process a file upload.
   *
   * Will create a file entity and prepare a media entity with data.
   *
   * @param \Drupal\media_bulk_upload\Entity\MediaBulkConfigInterface $mediaBulkConfig
   *   Media Bulk Config.
   * @param array $file
   *   File upload data.
   *
   * @return \Drupal\media\MediaInterface
   *   The unsaved media entity that is created.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Exception
   */
  protected function processFile(MediaBulkConfigInterface $mediaBulkConfig, array $file) {
    $fileInfo = pathinfo($file['filename']);
    $filename = $fileInfo['basename'];

    if (!$this->validateFilename($fileInfo)) {
      $this->messenger()->addError($this->t('File :filename does not have a valid extension or filename.', [':filename' => $filename]));
      throw new \Exception("File $filename does not have a valid extension or filename.");
    }

    $allowedMediaTypes = $this->mediaSubFormManager->getMediaTypeManager()
      ->getBulkMediaTypes($mediaBulkConfig);
    $matchingMediaTypes = $this->mediaSubFormManager->getMediaTypeManager()
      ->getMediaTypeIdsByFileExtension($fileInfo['extension']);

    $mediaTypes = array_intersect_key($matchingMediaTypes, $allowedMediaTypes);
    $mediaType = reset($mediaTypes);

    if (!$this->validateFileSize($mediaType, $file['path'])) {
      $fileSizeSetting = $this->mediaSubFormManager->getMediaTypeManager()
        ->getTargetFieldMaxSize($mediaType);
      $mediaTypeLabel = $mediaType->label();
      $this->messenger()
        ->addError($this->t('File :filename exceeds the maximum file size of :file_size for media type :media_type exceeded.', [
          ':filename' => $filename,
          ':file_size' => $fileSizeSetting,
          ':media_type' => $mediaTypeLabel,
        ]));
      throw new \Exception("File $filename exceeds the maximum file size of $fileSizeSetting for media type $mediaTypeLabel exceeded.");
    }

    $uri_scheme = $this->mediaSubFormManager->getTargetFieldDirectory($mediaType);
    $destination = $uri_scheme . '/' . $file['filename'];
    $file_default_scheme = \Drupal::config('system.file')->get('default_scheme') . '://';
    if ($uri_scheme === $file_default_scheme) {
      $destination = $uri_scheme . $file['filename'];
    }

    /** @var \Drupal\file\FileInterface $fileEntity */
    $fileEntity = $this->fileStorage->create([
      'uri' => $file['path'],
      'uid' => $this->currentUser->id(),
      'status' => FILE_STATUS_PERMANENT,
    ]);

    try {
      $fileEntity->save();
      $this->fileRepository->move($fileEntity, $destination, FileSystemInterface::EXISTS_RENAME);
    }
    catch (EntityStorageException | FileException $e) {
      $this->messenger()->addError($this->t('File :filename could not be created.', [':filename' => $filename]), 'error');
    }

    $values = $this->getNewMediaValues($mediaType, $fileInfo, $fileEntity);
    /** @var \Drupal\media\MediaInterface $media */

    return $this->mediaStorage->create($values);
  }

  /**
   * Validate if the filename and extension are valid in the provided file info.
   *
   * @param array $fileInfo
   *   File info.
   *
   * @return bool
   *   If the file info validates, returns true.
   */
  protected function validateFilename(array $fileInfo) {
    return !(empty($fileInfo['filename']) || empty($fileInfo['extension']));
  }

  /**
   * Check the size of a file.
   *
   * @param \Drupal\media\MediaTypeInterface $mediaType
   *   Media Type.
   * @param string $filePath
   *   File path.
   *
   * @return bool
   *   True if max size for a given file do not exceeds max size for its type.
   */
  protected function validateFileSize(MediaTypeInterface $mediaType, $filePath) {
    $fileSizeSetting = $this->mediaSubFormManager->getMediaTypeManager()->getTargetFieldMaxSize($mediaType);
    $fileSize = filesize($filePath);
    $maxFileSize = !empty($fileSizeSetting)
      ? Bytes::toInt($fileSizeSetting)
      : Environment::getUploadMaxSize();

    if ($maxFileSize == 0) {
      return true;
    }

    return $fileSize <= $maxFileSize;
  }

  /**
   * Builds the array of all necessary info for the new media entity.
   *
   * @param \Drupal\media\MediaTypeInterface $mediaType
   *   Media Type ID.
   * @param array $fileInfo
   *   File info.
   * @param \Drupal\file\FileInterface $file
   *   File entity.
   *
   * @return array
   *   Return an array describing the new media entity.
   */
  protected function getNewMediaValues(MediaTypeInterface $mediaType, array $fileInfo, FileInterface $file) {
    $targetFieldName = $this->mediaSubFormManager->getMediaTypeManager()
      ->getTargetFieldName($mediaType);
    return [
      'bundle' => $mediaType->id(),
      'name' => $fileInfo['filename'],
      $targetFieldName => [
        'target_id' => $file->id(),
        'title' => $fileInfo['filename'],
      ],
    ];
  }

  /**
   * Copy the submitted values for the media subform to the media entity.
   *
   * @param \Drupal\media\MediaInterface $media
   *   Media Entity.
   * @param array $extracted
   *   Extracted entity values.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form State.
   */
  protected function copyFormValuesToEntity(MediaInterface $media, array $extracted, FormStateInterface $form_state) {
    foreach ($form_state->getValues() as $name => $values) {
      if (isset($extracted[$name]) || !$media->hasField($name)) {
        continue;
      }
      $media->set($name, $values);
    }
  }

  /**
   * Prepare form submitted values.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form State.
   *
   * @return $this
   *   Media Bulk Upload Form.
   */
  protected function prepareFormValues(FormStateInterface $form_state) {
    // If the shared name is empty, remove it from the form state.
    // Otherwise the extractFormValues function will override with an empty value.
    $shared = $form_state->getValue(['fields', 'shared']);
    if (empty($shared['name'][0]['value'])) {
      unset($shared['name']);
      $form_state->setValue(['fields', 'shared'], $shared);
    }
    return $this;
  }
}
