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

namespace Civi\DataProcessor\ProcessorType;

use Civi\DataProcessor\DataFlow\CombinedDataFlow\CombinedDataFlow;
use Civi\DataProcessor\DataFlow\CombinedDataFlow\CombinedSqlDataFlow;
use Civi\DataProcessor\DataFlow\MultipleDataFlows\DataFlowDescription;
use Civi\DataProcessor\DataFlow\MultipleDataFlows\JoinInterface;
use Civi\DataProcessor\DataFlow\SqlDataFlow;
use Civi\DataProcessor\DataSpecification\FieldSpecification;
use Civi\DataProcessor\FieldOutputHandler\AbstractFieldOutputHandler;
use Civi\DataProcessor\FilterCollection\FilterCollection;
use Civi\DataProcessor\FilterHandler\AbstractFilterHandler;
use Civi\DataProcessor\Storage\StorageInterface;

abstract class AbstractProcessorType {

  /**
   * @var array
   */
  protected $dataSources = array();

  /**
   * @var bool
   */
  protected $allSqlDataFlows = true;

  /**
   * @var \Civi\DataProcessor\Storage\StorageInterface|null
   */
  protected $storage = null;

  /**
   * @var \Civi\DataProcessor\DataFlow\AbstractDataFlow|null
   */
  protected $dataflow = null;

  /**
   * @var \Civi\DataProcessor\FieldOutputHandler\AbstractFieldOutputHandler[]
   */
  protected $outputFieldHandlers;

  /**
   * @var \Civi\DataProcessor\FilterCollection\FilterCollection[]
   */
  protected $filterCollections;

  public function __construct() {
    $this->addFilterCollection(new FilterCollection(0, 'AND', 'AND'));
  }

  /**
   * Add a data source to the processor
   * @param \Civi\DataProcessor\Source\SourceInterface $datasource
   * @param \Civi\DataProcessor\DataFlow\MultipleDataFlows\JoinInterface|NULL $combineSpecification
   */
  public function addDataSource(\Civi\DataProcessor\Source\SourceInterface $datasource, JoinInterface $combineSpecification=null) {
    $d['datasource'] = $datasource;
    $d['combine_specification'] = $combineSpecification;
    $this->dataSources[] = $d;
    if (!$datasource->getDataFlow() instanceof SqlDataFlow) {
      $this->allSqlDataFlows = false;
    }
  }

  public function isAllSql(): bool {
    return $this->allSqlDataFlows;
  }

  /**
   * @return \Civi\DataProcessor\Source\SourceInterface[]
   */
  public function getDataSources() {
    $return = array();
    foreach($this->dataSources as $d) {
      $return[] = $d['datasource'];
    }
    return $return;
  }

  /**
   * @param $alias
   *
   * @return null|\Civi\DataProcessor\Source\SourceInterface
   */
  public function getDataSourceByFieldAlias($alias) {
    foreach($this->dataSources as $dataSource) {
      foreach($dataSource['datasource']->getAvailableFields()->getFields() as $field) {
        if ($field->alias == $alias) {
          return $dataSource['datasource'];
        }
      }
    }
    return null;
  }

  /**
   * Returns the data source
   *
   * @param $source_name
   * @return null|\Civi\DataProcessor\Source\SourceInterface
   */
  public function getDataSourceByName($source_name) {
    foreach($this->dataSources as $dataSource) {
      if ($dataSource['datasource']->getSourceName() == $source_name) {
        return $dataSource['datasource'];
      }
    }
    return null;
  }

  /**
   * @param \Civi\DataProcessor\FieldOutputHandler\AbstractFieldOutputHandler $outputFieldHandler
   */
  public function addOutputFieldHandlers(AbstractFieldOutputHandler $outputFieldHandler) {
    $this->outputFieldHandlers[] = $outputFieldHandler;
    if ($this->dataflow) {
      $this->dataflow->setOutputFieldHandlers($this->outputFieldHandlers);
    }
  }

  /**
   * @param \Civi\DataProcessor\FilterCollection\FilterCollection $filterCollection
   *
   * @return void
   */
  public function addFilterCollection(FilterCollection $filterCollection) {
    if (!isset($this->filterCollections[$filterCollection->getFilterCollectionNumber()])) {
      $filterCollection->setDataProcessor($this);
      $this->filterCollections[$filterCollection->getFilterCollectionNumber()] = $filterCollection;
      ksort($this->filterCollections);
    }
  }

  /**
   * @param int $collectionNumber
   *
   * @return \Civi\DataProcessor\FilterCollection\FilterCollection
   */
  public function getFilterCollectionByNumber(int $collectionNumber): FilterCollection {
    if (!isset($this->filterCollections[$collectionNumber])) {
      $this->addFilterCollection(new FilterCollection($collectionNumber, 'AND', 'AND'));
    }
    return $this->filterCollections[$collectionNumber];
  }

  public function getFilterCollections(): array {
    if (!isset($this->filterCollections) || !is_array($this->filterCollections)) {
      $this->filterCollections = [];
    }
    return $this->filterCollections;
  }

  public function ensureFieldInDataSource(FieldSpecification $fieldSpecification) {
    foreach($this->dataSources as $dataSource) {
      $dataSource['datasource']->ensureFieldInSource($fieldSpecification);
    }
  }

  /**
   * Sets the storage of the data processor.
   *
   * @param \Civi\DataProcessor\Storage\StorageInterface|null $storage
   */
  public function setStorage(StorageInterface $storage = null) {
    $this->storage = $storage;
    if ($this->storage && $this->dataflow) {
      $this->storage->setSourceDataFlow($this->dataflow);
    }
  }

  /**
   * @return \Civi\DataProcessor\DataFlow\AbstractDataFlow
   * @throws \Civi\DataProcessor\DataFlow\InvalidFlowException
   */
  public function getDataFlow() {
    if (!$this->dataflow) {
      if ($this->allSqlDataFlows) {
        $dataflow = new CombinedSqlDataFlow();
      }
      else {
        $dataflow = new CombinedDataFlow();
      }
      foreach ($this->dataSources as $datasource) {
        $dataFlowDescription = new DataFlowDescription($datasource['datasource']->getDataFlow(), $datasource['combine_specification']);
        $dataflow->addSourceDataFlow($dataFlowDescription);
      }
      $this->dataflow = $dataflow;
      $this->dataflow->setOutputFieldHandlers($this->outputFieldHandlers);
      if ($this->filterCollections && is_array($this->filterCollections)) {
        foreach ($this->filterCollections as $filterCollection) {
          $filterCollection->initializeDataFlow($this->dataflow);
        }
      }

      if ($this->storage) {
        $this->storage->setSourceDataFlow($this->dataflow);
      }
    }
    if ($this->storage) {
      return $this->storage->getDataFlow();
    }
    return $this->dataflow;
  }

  public function postInitialize() {
    foreach($this->getDataSources() as $dataSource) {
      $dataSource->postInitialize();
    }
  }

  /**
   * Sets the default filter values for all filters and calls loadFromCache on a source.
   *
   * @throws \Exception
   */
  public function loadedFromCache() {
    if ($this->dataSources && is_array($this->dataSources)) {
      foreach ($this->dataSources as $dataSource) {
        $dataSource['datasource']->sourceLoadedFromCache();
      }
    }

    if ($this->filterCollections && is_array($this->filterCollections)) {
      foreach ($this->filterCollections as $filterCollection) {
        foreach($filterCollection->getFilterHandlers() as $filterHandler) {
          $filterHandler->setDefaultFilterValues();
        }
      }
    }
  }

  public function resetDataFlow() {
    $this->dataflow = null;
  }

}
