<?php

/**
 * @file classes/db/DAOResultFactory.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class DAOResultFactory
 *
 * @ingroup db
 *
 * @brief Wrapper around Enumerable providing "factory" features for generating
 * objects from DAOs.
 */

namespace PKP\db;

use APP\submission\DAO;
use Countable;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use PKP\core\ItemIterator;
use ReflectionClass;

/**
 * @template T of \PKP\core\DataObject
 * @extends ItemIterator<mixed,T>
 */
class DAOResultFactory extends ItemIterator
{
    /** @var DAO The DAO used to create objects */
    public $dao;

    /** @var string The name of the DAO's factory function (to be called with an associative array of values) */
    public $functionName;

    /**
     * @var array an array of primary key field names that uniquely
     *   identify a result row in the record set.
     */
    public $idFields;

    /** @var \Generator<int,object>|Enumerable<int,object> The results to be wrapped around */
    public $records;

    /**
     * @var string|Builder|null Fetch SQL
     */
    public $sql;

    /**
     * @var array|null Fetch parameters
     */
    public $params;

    /**
     * @var DBResultRange|null $rangeInfo Range information, if specified.
     */
    public $rangeInfo;

    /**
     * @var bool Does $functionName expect each record to be converted to an array
     */
    public $expectsArray = true;

    /** @var ?int Cached row count */
    private $rowCount = null;

    /**
     * Constructor.
     * Initialize the DAOResultFactory
     *
     * @param object $records ADO record set, Generator, or Enumerable
     * @param object $dao DAO class for factory
     * @param string $functionName The function to call on $dao to create an object
     * @param array $idFields an array of primary key field names that uniquely identify a result row in the record set. Should be data object _data array key, not database column name
     * @param string|Builder|null $sql Optional SQL query used to generate paged result set. Necessary when total row counts will be needed (e.g. when paging). WARNING: New code should not use this.
     * @param array $params Optional parameters for SQL query used to generate paged result set. Necessary when total row counts will be needed (e.g. when paging), only used when the $sql argument is a string. WARNING: New code should not use this.
     * @param ?DBResultRange $rangeInfo Optional pagination information. WARNING: New code should not use this.
     */
    public function __construct($records, $dao, $functionName, $idFields = [], $sql = null, $params = [], $rangeInfo = null)
    {
        parent::__construct();
        $this->functionName = $functionName;
        $this->dao = $dao;
        $this->idFields = $idFields;
        $this->records = $records;
        $this->sql = $sql;
        $this->params = $params;
        $this->rangeInfo = $rangeInfo;

        // Determine if the "fromRow" method expects to receive an array or a stdClass.
        // EntityDAOs expect an object. DAOs that extend PKP\db\DAO expect an array.
        $reflector = new ReflectionClass(get_class($this->dao));
        if ($reflector->hasMethod($this->functionName)) {
            $params = $reflector->getMethod($this->functionName)->getParameters();
            if (!empty($params) && $params[0]->hasType() && $params[0]->getType()->getName() === 'object') {
                $this->expectsArray = false;
            }
        }
    }

    /**
     * Return the object representing the next row.
     *
     * @return ?T
     */
    public function next()
    {
        if ($this->records == null) {
            return $this->records;
        }

        $row = null;
        $functionName = $this->functionName;
        $dao = $this->dao;

        if ($this->records instanceof \Generator) {
            $row = $this->records->current();
            $this->records->next();
        } elseif ($this->records instanceof Collection) {
            $row = $this->records->shift();
        } else {
            throw new \Exception('Unsupported record set type (' . join(', ', class_implements($this->records)) . ')');
        }
        if (!$row) {
            return null;
        }
        if ($this->expectsArray) {
            $row = (array) $row;
        }
        return $dao->$functionName($row);
    }

    /**
     * @copydoc ItemIterator::count()
     */
    public function getCount()
    {
        if ($this->sql === null) {
            throw new \Exception('DAOResultFactory instances cannot be counted unless supplied in constructor (DAO ' . $this->dao::class . ')!');
        }
        // EntityDAOs do not support the countRecords method, but it can
        // be accessed through an instance of PKP\db\DAO attached to them
        $dao = property_exists($this->dao, 'deprecatedDao') ? $this->dao->deprecatedDao : $this->dao;
        return $this->rowCount ??= $dao->countRecords($this->sql, $this->params);
    }

    /**
     * Return the next row, with key.
     *
     * @param null|mixed $idField
     *
     * @return ?array{mixed,T} ($key, $value)
     */
    public function nextWithKey($idField = null)
    {
        $result = $this->next();
        if ($idField) {
            assert($result instanceof \PKP\core\DataObject);
            $key = $result->getData($idField);
        } elseif (empty($this->idFields)) {
            $key = null;
        } else {
            assert($result instanceof \PKP\core\DataObject && is_array($this->idFields));
            $key = '';
            foreach ($this->idFields as $idField) {
                assert(!is_null($result->getData($idField)));
                if (!empty($key)) {
                    $key .= '-';
                }
                $key .= (string)$result->getData($idField);
            }
        }
        return [$key, $result];
    }

    /**
     * Get the page number of a set that this iterator represents.
     *
     * @return int
     */
    public function getPage()
    {
        return $this->rangeInfo->getPage();
    }

    /**
     * Get the total number of pages in this set.
     *
     * @return int
     */
    public function getPageCount()
    {
        return ceil($this->getCount() / $this->rangeInfo->getCount());
    }

    /**
     * Return a boolean indicating whether or not we've reached the end of results
     *
     * @return bool
     */
    public function eof()
    {
        if ($this->records == null) {
            return true;
        }

        return $this->records instanceof Countable ? !count($this->records) : !$this->records->valid();
    }

    /**
     * Return true iff the result list was empty.
     *
     * @return bool
     */
    public function wasEmpty()
    {
        return $this->getCount() === 0;
    }

    /**
     * Convert this iterator to an array.
     *
     * @return T[]
     */
    public function toArray()
    {
        $returner = [];
        while ($row = $this->next()) {
            $returner[] = $row;
        }
        return $returner;
    }

    /**
     * Return an Iterator for this DAOResultFactory.
     *
     * @return DAOResultIterator<T>
     */
    public function toIterator()
    {
        return new DAOResultIterator($this);
    }

    /**
     * Convert this iterator to an associative array by database ID.
     *
     * @return array<array-key,T>
     */
    public function toAssociativeArray($idField = 'id')
    {
        $returner = [];
        while ($row = $this->next()) {
            $returner[$row->getData($idField)] = $row;
        }
        return $returner;
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\db\DAOResultFactory', '\DAOResultFactory');
}
