<?php
/**
 * @author Jaap Jansma <jaap.jansma@civicoop.org>
 * @license AGPL-3.0
 */

use Civi\DataProcessor\DataFlow\InvalidFlowException;
use Civi\DataProcessor\FilterHandler\AbstractFilterHandler;
use Civi\DataProcessor\Output\UIOutputInterface;
use Civi\DataProcessor\ProcessorType\AbstractProcessorType;

abstract class CRM_Dataprocessor_Form_Output_AbstractUIOutputForm extends CRM_Core_Form_Search {

  /**
   * @var array
   */
  protected $dataProcessor;

  /**
   * @var \Civi\DataProcessor\ProcessorType\AbstractProcessorType;
   */
  protected $dataProcessorClass;

  /**
   * @var int
   */
  protected $dataProcessorId;

  /**
   * @var \CRM_Dataprocessor_BAO_DataProcessorOutput
   */
  protected $dataProcessorOutput;

  /**
   * Return the data processor ID
   *
   * @return String
   */
  abstract protected function getDataProcessorName(): string;

  /**
   * Returns the name of the output for this search
   *
   * @return string
   */
  abstract protected function getOutputName(): string;

  /**
   * Checks whether the output has a valid configuration
   *
   * @return bool
   */
  abstract protected function isConfigurationValid(): bool;

  public function preProcess() {
    parent::preProcess();
    try {
      $this->loadDataProcessor();
    } catch (Exception $e) {
      // Do nothing.
    }
    $this->assign('has_exposed_filters', $this->hasExposedFilters());
  }

  public function buildQuickForm() {
    parent::buildQuickForm();
    $this->add('hidden', 'debug');
    $this->setDefaults(['debug' => $this->isDebug()]);
  }

  /**
   * @return \Civi\DataProcessor\ProcessorType\AbstractProcessorType
   */
  public function getDataProcessorClass(): AbstractProcessorType {
    return $this->dataProcessorClass;
  }

  /**
   * @return array
   */
  public function getDataProcessor(): array {
    return $this->dataProcessor;
  }

  protected function isDebug(): bool {
    try {
      $debug = CRM_Utils_Request::retrieve('debug', 'Boolean');
    } catch (CRM_Core_Exception $e) {
      return false;
    }
    if (!$debug) {
      $debug = $this->_formValues['debug'] ?? FALSE;
    }
    return (bool) $debug;
  }

  /**
   * Retrieve the data processor and the output configuration
   *
   * @throws \Exception
   */
  protected function loadDataProcessor() {
    $factory = dataprocessor_get_factory();
    if (!$this->dataProcessorId) {
      $doNotUseCache = $this->isDebug();

      $dataProcessorName = $this->getDataProcessorName();
      $sql = "
        SELECT civicrm_data_processor.id as data_processor_id,  civicrm_data_processor_output.id AS output_id
        FROM civicrm_data_processor
        INNER JOIN civicrm_data_processor_output ON civicrm_data_processor.id = civicrm_data_processor_output.data_processor_id
        WHERE is_active = 1 AND civicrm_data_processor.name = %1 AND civicrm_data_processor_output.type = %2
      ";
      $params[1] = [$dataProcessorName, 'String'];
      $params[2] = [$this->getOutputName(), 'String'];
      $dao = CRM_Dataprocessor_BAO_DataProcessor::executeQuery($sql, $params, TRUE, 'CRM_Dataprocessor_BAO_DataProcessor');
      if (!$dao->fetch()) {
        throw new Exception('Could not find Data Processor "' . $dataProcessorName.'"');
      }

      $this->dataProcessor = civicrm_api3('DataProcessor', 'getsingle', array('id' => $dao->data_processor_id));
      $this->dataProcessorClass = CRM_Dataprocessor_BAO_DataProcessor::dataProcessorToClass($this->dataProcessor, $doNotUseCache);
      $this->dataProcessorId = $dao->data_processor_id;

      $this->dataProcessorOutput = civicrm_api3('DataProcessorOutput', 'getsingle', array('id' => $dao->output_id));
      $this->dataProcessorOutput = $this->alterDataProcessorOutput($this->dataProcessorOutput);
      $this->assign('output', $this->dataProcessorOutput);

      $outputClass = $factory->getOutputByName($this->dataProcessorOutput['type']);
      if (!$outputClass instanceof UIOutputInterface) {
        throw new Exception('Invalid output');
      }

      if (!$outputClass->checkUIPermission($this->dataProcessorOutput, $this->dataProcessor)) {
        CRM_Utils_System::permissionDenied();
        CRM_Utils_System::civiExit();
      } elseif (!$this->isConfigurationValid()) {
        throw new Exception('Invalid configuration found of the data processor "' . $dataProcessorName . '"');
      }
    }
  }

  /**
   * @return array|\CRM_Dataprocessor_BAO_DataProcessorOutput
   */
  public function getDataProcessorOutput(): array {
    return $this->dataProcessorOutput;
  }

  /**
   * This function could be override in child classes to change default configuration.
   *
   * @param $output
   *
   * @return array
   */
  protected function alterDataProcessorOutput($output): array {
    return $output;
  }

  /**
   * Returns whether the search has required filters.
   *
   * @return bool
   */
  protected function hasRequiredFilters(): bool {
    foreach ($this->dataProcessorClass->getFilterCollections() as $filterCollection) {
      foreach ($filterCollection->getFilterHandlers() as $filter) {
        if ($filter->isRequired() && $filter->isExposed()) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Returns whether the search has required filters.
   *
   * @return bool
   */
  protected function hasExposedFilters(): bool {
    foreach ($this->dataProcessorClass->getFilterCollections() as $filterCollection) {
      foreach ($filterCollection->getFilterHandlers() as $filter) {
        if ($filter->isExposed()) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Validate the filters
   *
   * @return array
   */
  protected function validateFilters(): array {
    $errors = array();
    foreach ($this->dataProcessorClass->getFilterCollections() as $filterCollection) {
      foreach ($filterCollection->getFilterHandlers() as $filter) {
        if ($filter->isExposed()) {
          $errors = array_merge($errors, $filter->validateSubmittedFilterParams($this->getSubmitValues()));
        }
      }
    }
    return $errors;
  }

  /**
   * Returns whether hard limit is enabled.
   *
   * @return bool
   */
  protected function isHardLimitEnabled(): bool {
    if (isset($this->dataProcessorOutput['configuration']) && isset($this->dataProcessorOutput['configuration']['enable_hard_limit']) && $this->dataProcessorOutput['configuration']['enable_hard_limit']) {
      return true;
    }
    return false;
  }

  /**
   * Returns the default hard limit
   *
   * @return string
   */
  protected function getDefaultHardLimit(): string {
    if (isset($this->dataProcessorOutput['configuration']) && isset($this->dataProcessorOutput['configuration']['default_hard_limit'])) {
      return $this->dataProcessorOutput['configuration']['default_hard_limit'];
    }
    return '';
  }

  /**
   * Returns the default row limit.
   *
   * @param array|null $output
   * @return int
   */
  protected static function getDefaultLimit(array $output = null): int {
    return CRM_Utils_Pager::ROWCOUNT;
  }

  /**
   * Returns the count of the current data processor.
   * Taking into account a hard limit (if set).
   *
   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessor
   * @param array|NULL $submittedValues
   *
   * @return int
   */
  public static function getCount(AbstractProcessorType $dataProcessor, array $submittedValues = null): int {
    $count = 0;
    try {
      $count = $dataProcessor->getDataFlow()->recordCount();
      $hardLimit = static::getHardLimit($submittedValues);
      if ($hardLimit && $count > $hardLimit) {
        $count = $hardLimit;
      }
    } catch (InvalidFlowException $e) {
    }
    return $count;
  }

  /**
   * Apply the offset and limit
   *
   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessor
   * @param array|null $submittedValues
   * @param array|null $output
   *
   * @return void
   */
  public static function applyLimit(AbstractProcessorType $dataProcessor, array $submittedValues = null, array $output = null) {
    $hardLimit = static::getHardLimit($submittedValues);
    $limit = static::getDefaultLimit($output);
    if (isset($submittedValues['crmRowCount']) && $submittedValues['crmRowCount']) {
      $limit = $submittedValues['crmRowCount'];
    } elseif (CRM_Utils_Request::retrieve('crmRowCount', 'Integer')) {
      $limit = CRM_Utils_Request::retrieve('crmRowCount', 'Integer');
    }
    $pageId = 1;
    if (isset($submittedValues['crmPID'])) {
      $pageId = $submittedValues['crmPID'];
    } elseif (CRM_Utils_Request::retrieve('crmPID', 'Integer')) {
      $pageId = CRM_Utils_Request::retrieve('crmPID', 'Integer');
    }
    $offset = static::getOffset($pageId, $limit, $hardLimit);
    if ($hardLimit && ($offset + $limit) > $hardLimit) {
      $limit = $hardLimit - $offset;
    }
    try {
      $dataProcessor->getDataFlow()->setOffset($offset);
      $dataProcessor->getDataFlow()->setLimit($limit);
    } catch (InvalidFlowException $e) {
    }
  }

  /**
   * Returns the hard limit values
   *
   * @param array|null $submittedValues
   * @return int|null
   */
  protected static function getHardLimit(array $submittedValues=null):? int {
    if (isset($submittedValues['hard_limit']) && is_numeric($submittedValues['hard_limit'])) {
      return (int) $submittedValues['hard_limit'];
    }
    return null;
  }

  /**
   * Returns the offset for this page
   *
   * @param int $pageId
   * @param int $limit
   * @param int|NULL $hardLimit
   *
   * @return int
   */
  public static function getOffset(int $pageId, int $limit, int $hardLimit = null): int {
    if ($pageId) {
      $offset = ($pageId - 1) * $limit;
    } else {
      $offset = 0;
    }
    if ($hardLimit && $offset > $hardLimit) {
      $pageId = $hardLimit / $limit;
      $offset = ($pageId -1) * $limit;
    }
    return $offset;
  }

  /**
   * Apply the filters to the database processor
   */
  public static function applyFilters(AbstractProcessorType $dataProcessor, $submittedValues) {
    foreach ($dataProcessor->getFilterCollections() as $filterCollection) {
      foreach ($filterCollection->getFilterHandlers() as $filter) {
        if ($filter->isExposed()) {
          $filter->resetFilter();
          $filterValues = $filter->processSubmittedValues($submittedValues);
          if (empty($filterValues)) {
            $filterValues = self::getDefaultFilterValues($filter);
          }
          try {
            $filter->applyFilterFromSubmittedFilterParams($filterValues);
          } catch (Exception $e) {
            // Do nothing.
          }
        }
      }
    }
  }

  /**
   * Get the default filter values for a filter. If there is no default value we allow the value to be set by a URL parameter of the same name as the filter.
   *
   * @param \Civi\DataProcessor\FilterHandler\AbstractFilterHandler $filterHandler
   *
   * @return array
   */
  public static function getDefaultFilterValues(AbstractFilterHandler $filterHandler): array {
    $defaultOp = $filterHandler->getDefaultOperator();
    $type = ($filterHandler->getFieldSpecification()->type === 'Int') ? 'CommaSeparatedIntegers' : $filterHandler->getFieldSpecification()->type;
    $filterValues = [];

    try {
      $valueFromURL = CRM_Utils_Request::retrieveValue($filterHandler->getFieldSpecification()->alias, $type, NULL, FALSE, 'GET');
      if ($valueFromURL) {
        $filterValues = [
          'op' => $defaultOp,
          'value' => $valueFromURL,
        ];
      }
    } catch (CRM_Core_Exception $e) {
      // Do nothing.
    }
    if (empty($filterValues)) {
      $filterValues = $filterHandler->getDefaultFilterValues();
    }
    return $filterValues;
  }

  /**
   * Build the criteria form
   */
  protected function buildCriteriaForm() {
    global $_GET;
    $extraData = [];
    foreach($_GET as $key => $v) {
      try {
        $extraData[$key] = CRM_Utils_Request::retrieveValue($key, 'String');
      } catch (CRM_Core_Exception $e) {
      }
    }
    $extraData = array_merge($extraData, $this->getSubmitValues());
    $filterElements = array();
    foreach($this->dataProcessorClass->getFilterCollections() as $filterCollection) {
      $filterElements[$filterCollection->getFilterCollectionNumber()]['title'] = $filterCollection->getTitle();
      $filterElements[$filterCollection->getFilterCollectionNumber()]['description'] = $filterCollection->getDescription();
      $filterElements[$filterCollection->getFilterCollectionNumber()]['class'] = $filterCollection->isCollapsed() ? 'collapsed' : '';
      $filterElements[$filterCollection->getFilterCollectionNumber()]['filters'] = [];
      foreach ($filterCollection->getFilterHandlers() as $filterHandler) {
        $filterHandler->setExtraData($extraData);
        $fieldSpec = $filterHandler->getFieldSpecification();
        if (!$fieldSpec || !$filterHandler->isExposed()) {
          continue;
        }
        $filterElements[$filterCollection->getFilterCollectionNumber()]['filters'][$fieldSpec->alias]['filter'] = $filterHandler->addToFilterForm($this, self::getDefaultFilterValues($filterHandler), $this->getCriteriaElementSize());
        $filterElements[$filterCollection->getFilterCollectionNumber()]['filters'][$fieldSpec->alias]['template'] = $filterHandler->getTemplateFileName();
      }
      $this->assign('filters', $filterElements);
    }
    $this->assign('additional_criteria_template', $this->getAdditionalCriteriaTemplate());
  }

  /**
   * Returns the size of the criteria form element.
   * There are two sizes full and compact.
   *
   * @return string
   */
  protected function getCriteriaElementSize(): string {
    return 'full';
  }

  /**
   * Returns the name of the additional criteria template.
   *
   * @return false|String|array
   */
  protected function getAdditionalCriteriaTemplate():? string {
    return false;
  }
}
