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

use Civi\DataProcessor\DataFlow\EndOfFlowException;
use Civi\DataProcessor\DataFlow\InvalidFlowException;
use Civi\DataProcessor\Exception\DataFlowException;
use Civi\DataProcessor\FieldOutputHandler\FieldOutput;
use Civi\DataProcessor\FieldOutputHandler\Markupable;
use Civi\DataProcessor\FieldOutputHandler\OutputHandlerSortable;
use Civi\DataProcessor\Output\ExportOutputInterface;
use Civi\DataProcessor\ProcessorType\AbstractProcessorType;
use CRM_Dataprocessor_ExtensionUtil as E;

abstract class CRM_DataprocessorSearch_Form_AbstractSearch extends CRM_Dataprocessor_Form_Output_AbstractUIOutputForm {

  /**
   * @var String
   */
  protected $title;

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

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

  /**
   * @var bool
   */
  protected $_debug = FALSE;

  /**
   * @var \CRM_Utils_Sort
   */
  protected $sort;

  /**
   * @var \CRM_Utils_Pager
   */
  protected $pager;

  /**
   * @var string
   */
  protected $currentUrl;

  /**
   * The params that are sent to the query.
   *
   * @var array
   */
  protected $_queryParams;

  /**
   * The array of entity IDs from the form
   *
   * @var array
   */
  protected $entityIDs;

  /**
   * Name of action button
   *
   * @var string
   */
  protected $_searchButtonName;

  /**
   * Name of the export button
   *
   * @var string
   */
  protected $_exportButtonNane;

  /**
   * @var array
   */
  protected $defaults = [];

  /**
   * @var bool
   */
  protected $_reset;

  /**
   * Returns the name of the ID field in the dataset.
   *
   * @return string
   */
  abstract public function getIdFieldName(): string;

  /**
   * @return false|string
   */
  abstract protected function getEntityTable();

  /**
   * Builds the list of tasks or actions that a searcher can perform on a result set.
   *
   * @return array
   */
  public function buildTaskList(): array {
    return $this->_taskList;
  }

  /**
   * Returns whether we want to use the prevnext cache.
   * @return bool
   */
  protected function usePrevNextCache(): bool {
    return false;
  }

  /**
   * Returns the key for the (prevnext) cache
   * @return string
   */
  protected function getCacheKey(): string {
    $qfKeyParam = CRM_Utils_Array::value('qfKey', $this->_formValues);
    if (empty($qfKeyParam) && $this->controller->_key) {
      $qfKeyParam = $this->controller->_key;
    }
    return "civicrm search " . $qfKeyParam;
  }

  /**
   * Returns whether the ID field is Visible
   *
   * @return bool
   */
  protected function isIdFieldVisible(): bool {
    if (isset($this->dataProcessorOutput['configuration']['hide_id_field']) && $this->dataProcessorOutput['configuration']['hide_id_field']) {
      return false;
    }
    return true;
  }

  /**
   * @return CRM_Utils_Sort|NULL
   */
  public function getSort(): ?CRM_Utils_Sort {
    return $this->sort;
  }

  /**
   * Returns an array with hidden columns
   *
   * @return array
   */
  protected function getHiddenFields(): array {
    $hiddenFields = array();
    if (!$this->isIdFieldVisible() && !empty($this->getIdFieldName())) {
      $hiddenFields[] = $this->getIdFieldName();
    }
    if (isset($this->dataProcessorOutput['configuration']['hidden_fields']) && is_array($this->dataProcessorOutput['configuration']['hidden_fields'])) {
      $hiddenFields = array_merge($hiddenFields, $this->dataProcessorOutput['configuration']['hidden_fields']);
    }
    return $hiddenFields;
  }

  /**
   * Retrieve the text for no results.
   *
   * @return string
   */
  protected function getNoResultText(): string {
    if (isset($this->dataProcessorOutput['configuration']['no_result_text'])) {
      return $this->dataProcessorOutput['configuration']['no_result_text'];
    }
    return E::ts('No results');
  }

  /**
   * Returns the url for view of the record action
   *
   * @param $row
   *
   * @return false|string
   */
  abstract protected function link($row);

  /**
   * Returns the link text for view of the record action
   *
   * @param $row
   *
   * @return false|string
   */
  abstract protected function linkText($row);

  /**
   * Return altered rows
   *
   * @param array $rows
   * @param array $ids
   *
   */
  protected function alterRows(array &$rows, array $ids) {

  }

  public function preProcess() {
    parent::preProcess();

    $qfKey = CRM_Utils_Request::retrieveValue('qfKey', 'String');
    $urlPath = CRM_Utils_System::currentPath();
    $urlParams = 'force=1';
    if ($qfKey) {
      $urlParams .= "&qfKey=$qfKey";
    }
    $this->currentUrl = CRM_Utils_System::url($urlPath, $urlParams);
    $session = CRM_Core_Session::singleton();
    $session->replaceUserContext($this->currentUrl);

    if (CRM_Utils_Request::retrieve('ssID', 'Integer')) {
      $savedSearchDao = new CRM_Contact_DAO_SavedSearch();
      $savedSearchDao->id = CRM_Utils_Request::retrieve('ssID', 'Integer');
      if ($savedSearchDao->find(TRUE) && !empty($savedSearchDao->form_values)) {
        $this->_formValues = unserialize($savedSearchDao->form_values);
        $this->_submitValues = $this->_formValues;
        $this->controller->set('formValue', $this->_formValues);
      }
    } else {
      $this->_formValues = $this->controller->exportValues($this->_name);
    }

    $this->_searchButtonName = $this->getButtonName('refresh');
    $this->_actionButtonName = $this->getButtonName('next', 'action');
    $this->_done = FALSE;
    // we allow the controller to set force/reset externally, useful when we are being
    // driven by the wizard framework
    $this->_debug = $this->isDebug();
    $this->_reset = CRM_Utils_Request::retrieveValue('reset', 'Boolean');
    $this->_force = CRM_Utils_Request::retrieveValue('force', 'Boolean');
    $this->_context = CRM_Utils_Request::retrieveValue('context', 'String', 'search');
    $this->set('context', $this->_context);
    $this->assign("context", $this->_context);
    $this->assign('debug', $this->_debug);
    $this->assign('sticky_header', TRUE);

    if ($this->isCriteriaFormCollapsed()) {
      if ($this->isHardLimitEnabled() && !isset($this->_formValues['hard_limit'])) {
        $this->_formValues['hard_limit'] = $this->getDefaultHardLimit();
      }
      $sortFields = $this->addColumnHeaders();
      $this->sort = new CRM_Utils_Sort($sortFields);
      if (isset($this->_formValues[CRM_Utils_Sort::SORT_ID])) {
        $this->sort->initSortID($this->_formValues[CRM_Utils_Sort::SORT_ID]);
      }
      $this->assign_by_ref('sort', $this->sort);
      $pageId = CRM_Utils_Request::retrieveValue('crmPID', 'Positive', 1);
      $this->buildRows($pageId);
      $this->addExportOutputs();
    }

  }

  /**
   * @return bool
   */
  protected function isCriteriaFormCollapsed(): bool {
    $initialExpanded = false;
    if (isset($this->dataProcessorOutput['configuration']['expanded_search'])) {
      $initialExpanded = $this->dataProcessorOutput['configuration']['expanded_search'];
    }
    if(!$this->hasRequiredFilters() && !$initialExpanded) {
      return  true;
    }
    if ((!empty($this->_formValues) && count($this->validateFilters()) == 0)) {
      return true;
    }
    return false;
  }

  /**
   * Retrieve the records from the data processor
   *
   * @param $pageId
   */
  protected function buildRows($pageId) {
    $rows = [];
    $ids = array();
    $prevnextData = array();
    $showLink = false;

    $id_field = $this->getIdFieldName();
    $this->assign('id_field', $id_field);
    try {
      static::applyFilters($this->dataProcessorClass, $this->_formValues);

      // Set the sort
      if ($this->sort->getCurrentSortID() > 1) {
        $sortDirection = 'ASC';
        $sortField = $this->sort->_vars[$this->sort->getCurrentSortID()];
        if ($this->sort->getCurrentSortDirection() == CRM_Utils_Sort::DESCENDING) {
          $sortDirection = 'DESC';
        }
        $this->dataProcessorClass->getDataFlow()->resetSort();
        $this->dataProcessorClass->getDataFlow()->addSort($sortField['name'], $sortDirection);
        if ($id_field && $id_field != $sortField['name']) {
          $this->dataProcessorClass->getDataFlow()->addSort($id_field, 'ASC');
        }
      } elseif ($id_field) {
        $this->dataProcessorClass->getDataFlow()->addSort($id_field, 'ASC');
      }
      static::applyLimit($this->dataProcessorClass, $this->_formValues, $this->dataProcessorOutput);

      $this->alterDataProcessor($this->dataProcessorClass);
      $this->dataProcessorClass->postInitialize();
      $pagerParams = $this->getPagerParams();
      $pagerParams['total'] = static::getCount($this->dataProcessorClass, $this->_formValues);
      $pagerParams['pageID'] = $pageId;
      $this->pager = new CRM_Utils_Pager($pagerParams);
      $this->assign('pager', $this->pager);
      $this->controller->set('rowCount', $this->dataProcessorClass->getDataFlow()->recordCount());

      while($record = $this->dataProcessorClass->getDataFlow()->nextRecord()) {
        $row = array();

        $row['id'] = null;
        if ($id_field && isset($record[$id_field])) {
          $row['id'] = $record[$id_field]->rawValue;
        }
        if ($id_field) {
          $row['checkbox'] = CRM_Core_Form::CB_PREFIX . $row['id'];
        }
        $row['record'] = array();
        foreach($record as $column => $value) {
          if ($value instanceof Markupable) {
            $row['record'][$column] = $value->getMarkupOut();
          } elseif ($value instanceof FieldOutput) {
            if (isset($value->formattedValue)) {
              $row['record'][$column] = htmlspecialchars($value->formattedValue);
            } else {
              $row['record'][$column] = '';
            }
          }
        }

        $link = $this->link($row);
        if ($link) {
          $row['url'] = $link;
          $row['link_text'] = $this->linkText($row);
          $showLink = true;
        }

        if (isset($row['checkbox'])) {
          $this->addElement('checkbox', $row['checkbox'], NULL, NULL, ['class' => 'select-row']);
        }

        if ($row['id'] && $this->usePrevNextCache()) {
          $prevnextData[] = array(
            'entity_id1' => $row['id'],
            'entity_table' => $this->getEntityTable(),
            'data' => '', // json_encode($record),
          );
        }
        $ids[] = $row['id'];

        $rows[] = $row;
      }
    } catch (EndOfFlowException $e) {
      // Do nothing
    } catch (DataFlowException|InvalidFlowException $e) {
      $this->assign('debug_info', $e->getMessage());
      CRM_Core_Session::setStatus(E::ts('Error in data processor'), E::ts('Error'), 'error');
    }

    $this->alterRows($rows, $ids);

    $this->addElement('checkbox', 'toggleSelect', NULL, NULL, ['class' => 'select-rows']);
    $this->assign('dataprocessor_rows', $rows);
    $this->assign('no_result_text', $this->getNoResultText());
    $this->assign('showLink', $showLink);
    try {
      $this->assign('debug_info', $this->dataProcessorClass->getDataFlow()->getDebugInformation());
    } catch (InvalidFlowException $e) {
      $this->assign('debug_info', $e->getMessage());
    }

    if ($this->usePrevNextCache()) {
      CRM_DataprocessorSearch_Utils_PrevNextCache::fillWithArray($this->getCacheKey(), $prevnextData);
    } else {
      $this->retrieveEntityIds();
    }
  }

  /**
   * Function to retrieve the entity ids
   */
  protected function retrieveEntityIds() {
    // Could be overriden in child classes.
  }

  /**
   * Add the headers for the columns
   *
   * @return array
   *   Array with all possible sort fields.
   */
  protected function addColumnHeaders(): array {
    $sortFields = array();
    $hiddenFields = $this->getHiddenFields();
    $columnHeaders = array();
    $sortColumnNr = 2; // Start at two as 1 is the default sort.
    try {
      $outputFieldHandlers =$this->dataProcessorClass->getDataFlow()->getOutputFieldHandlers();
      foreach ($outputFieldHandlers as $outputFieldHandler) {
        $field = $outputFieldHandler->getOutputFieldSpecification();
        if (!in_array($field->alias, $hiddenFields)) {
          $columnHeaders[$field->alias] = $field->title;
          if ($outputFieldHandler instanceof OutputHandlerSortable) {
            $sortFields[$sortColumnNr] = [
              'name' => $field->title,
              'sort' => $field->alias,
              'direction' => CRM_Utils_Sort::DONTCARE,
            ];
            $sortColumnNr++;
          }
        }
      }
    } catch (InvalidFlowException $e) {
      // Do nothing
    }
    $this->assign('columnHeaders', $columnHeaders);
    return $sortFields;
  }

  /**
   * @return array
   */
  protected function getPagerParams(): array {
    $params = [];
    $params['total'] = 0;
    $params['status'] =E::ts('%%StatusMessage%%');
    $params['csvString'] = NULL;
    $params['rowCount'] =  static::getDefaultLimit($this->dataProcessorOutput);
    $params['buttonTop'] = 'PagerTopButton';
    $params['buttonBottom'] = 'PagerBottomButton';
    return $params;
  }

  /**
   * Add buttons for other outputs of this data processor
   */
  protected function addExportOutputs() {
    $factory = dataprocessor_get_factory();
    $otherOutputs = array();
    try {
      $outputs = civicrm_api3('DataProcessorOutput', 'get', [
        'data_processor_id' => $this->dataProcessorId,
        'options' => ['limit' => 0],
      ]);
      foreach($outputs['values'] as $output) {
        if ($output['id'] == $this->dataProcessorOutput['id']) {
          continue;
        }
        $outputClass = $factory->getOutputByName($output['type']);
        if ($outputClass instanceof ExportOutputInterface) {
          $otherOutput = array();
          $otherOutput['title'] = $outputClass->getTitleForExport($output, $this->dataProcessor);
          $otherOutput['id'] = $output['id'];
          $otherOutput['icon'] = $outputClass->getExportFileIcon($output, $this->dataProcessor);
          $otherOutput['class'] = "";
          $otherOutputs[] = $otherOutput;
        }
      }
    } catch (CiviCRM_API3_Exception $e) {
    }
    try {
      $this->add('hidden', 'export_id');
      $this->assign('other_outputs', $otherOutputs);
      if (count($otherOutputs)) {
        $this->_exportButtonNane = $this->getButtonName('next', 'export');
        $this->assign('exportButtonName', $this->_exportButtonNane);
        $this->add('xbutton', $this->_exportButtonNane, ts('Export'), [
          'type' => 'submit',
          'class' => 'hiddenElement crm-search-export-button',
        ]);
      }
    } catch (CRM_Core_Exception $e) {
    }
  }

  public function buildQuickForm() {
    parent::buildQuickForm();

    $this->buildCriteriaForm();
    $selectedIds = $this->getSelectedIds(true);
    $this->assign_by_ref('selectedIds', $selectedIds);
    $this->add('hidden', 'context');
    $this->add('hidden', CRM_Utils_Sort::SORT_ID);

    $this->assign('hard_limit_enabled', $this->isHardLimitEnabled());
    if ($this->isHardLimitEnabled()) {
      $this->add('text', 'hard_limit', E::ts('Hard Limit'));
      $this->setDefaults(['hard_limit' => $this->getDefaultHardLimit()]);
    }
  }

  public function setDefaultValues(): array {
    $defaults = parent::setDefaultValues();
    $defaults['context'] = 'search';
    if ($this->sort && $this->sort->getCurrentSortID()) {
      $defaults[CRM_Utils_Sort::SORT_ID] = CRM_Utils_Sort::sortIDValue($this->sort->getCurrentSortID(), $this->sort->getCurrentSortDirection());
    }
    return $defaults;
  }

  public function validate(): bool {
    $this->_errors = $this->validateFilters();
    return parent::validate();
  }

  public function postProcess() {
    if ($this->_done) {
      return;
    }
    $this->_done = TRUE;

    $crmPID = 0;
    //for prev/next pagination
    try {
      $crmPID = CRM_Utils_Request::retrieve('crmPID', 'Integer');
    } catch (CRM_Core_Exception $e) {
    }

    if (($this->_searchButtonName && array_key_exists($this->_searchButtonName, $_POST)) ||
      ($this->_force && !$crmPID)
    ) {
      //reset the cache table for new search
      CRM_DataprocessorSearch_Utils_PrevNextCache::deleteItem(NULL, $this->getCacheKey());
    }

    if (!empty($_POST)) {
      $this->_formValues = $this->controller->exportValues($this->_name);
    }
    $this->set('formValues', $this->_formValues);
    $buttonName = $this->controller->getButtonName();
    if ($buttonName && $buttonName == $this->_actionButtonName) {
      // check actionName and if next, then do not repeat a search, since we are going to the next page
      // hack, make sure we reset the task values
      $formName = $this->controller->getStateMachine()->getTaskFormName();
      $this->controller->resetPage($formName);
    }
  }

  /**
   * Return a descriptive name for the page, used in wizard header
   *
   * @return string
   */
  public function getTitle(): string {
    try {
      $this->loadDataProcessor();
      return $this->dataProcessor['title'];
    } catch (Exception $e) {
    }
    return '';
  }

  /**
   * Alter the data processor.
   *
   * Use this function in child classes to add for example additional filters.
   *
   * E.g. The contact summary tab uses this to add additional filtering on the contact id of
   * the displayed contact.
   *
   * @param \Civi\DataProcessor\ProcessorType\AbstractProcessorType $dataProcessorClass
   */
  protected function alterDataProcessor(AbstractProcessorType $dataProcessorClass) {

  }

  /**
   * Returns the selected IDs.
   *
   * @return array
   */
  public function getSelectedIds(bool $skipSelectAll=false): array {
    if (!empty($this->entityIDs) && is_array($this->entityIDs)) {
      return $this->entityIDs;
    }
    $this->entityIDs = [];
    // We use ajax to handle selections only if the search results component_mode is set to "contacts"
    if ($this->usePrevNextCache()) {
      $this->addClass('crm-ajax-selection-form');
      $selectedIdsArr = CRM_DataprocessorSearch_Utils_PrevNextCache::getSelection($this->getCacheKey());
      if (isset($selectedIdsArr[$this->getCacheKey()]) && is_array($selectedIdsArr[$this->getCacheKey()])) {
        $this->entityIDs = array_keys($selectedIdsArr[$this->getCacheKey()]);
      }
    } else {
      if (isset($this->_formValues['radio_ts']) && $this->_formValues['radio_ts'] == 'ts_sel') {
        foreach ($this->_formValues as $name => $value) {
          if (substr($name, 0, CRM_Core_Form::CB_PREFIX_LEN) == CRM_Core_Form::CB_PREFIX) {
            $this->entityIDs[] = substr($name, CRM_Core_Form::CB_PREFIX_LEN);
          }
        }
      } elseif (!$skipSelectAll && (!isset($this->_formValues['radio_ts']) || $this->_formValues['radio_ts'] == 'ts_all')) {
        try {
          $limit = FALSE;
          if ($this->getHardLimit()) {
            $limit = $this->getHardLimit();
          }
          $this->dataProcessorClass->getDataFlow()->setLimit($limit);
          $this->dataProcessorClass->getDataFlow()->setOffset(0);
          $id_field = $this->getIdFieldName();
          while ($record = $this->dataProcessorClass->getDataFlow()
            ->nextRecord()) {
            if (!empty($id_field) && isset($record[$id_field])) {
              $this->entityIDs[] = $record[$id_field]->rawValue;
            }
          }
        } catch (EndOfFlowException|DataFlowException|InvalidFlowException $e) {
          // Do nothing
        }
      }
    }
    return $this->entityIDs;
  }

  /**
   * Returns the number of selected records.
   *
   * @return int
   */
  public function getSelectionCount(): int {
    // We use ajax to handle selections only if the search results component_mode is set to "contacts"
    if ($this->allRecordsSelected()) {
      try {
        return static::getCount($this->dataProcessorClass, $this->_formValues);
      } catch (InvalidFlowException $e) {
        // Do nothing
      }
    } else {
      return count($this->getSelectedIds());
    }
    return 0;
  }

  public function allRecordsSelected(): bool {
    if (isset($this->_formValues['radio_ts']) && $this->_formValues['radio_ts'] == 'ts_all') {
      return true;
    } elseif (!isset($this->_formValues['radio_ts']) || $this->_formValues['radio_ts'] === "") {
      return true;
    }
    return false;
  }

}
