<?php

namespace Laminas\Db\Sql;

use Countable;
use Iterator;
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
use ReturnTypeWillChange;

use function array_shift;
use function count;
use function is_array;
use function is_string;
use function key;
use function sprintf;

/**
 * Aggregate JOIN specifications.
 *
 * Each specification is an array with the following keys:
 *
 * - name: the JOIN name
 * - on: the table on which the JOIN occurs
 * - columns: the columns to include with the JOIN operation; defaults to
 *   `Select::SQL_STAR`.
 * - type: the type of JOIN being performed; see the `JOIN_*` constants;
 *   defaults to `JOIN_INNER`
 */
class Join implements Iterator, Countable
{
    public const JOIN_INNER       = 'inner';
    public const JOIN_OUTER       = 'outer';
    public const JOIN_FULL_OUTER  = 'full outer';
    public const JOIN_LEFT        = 'left';
    public const JOIN_RIGHT       = 'right';
    public const JOIN_RIGHT_OUTER = 'right outer';
    public const JOIN_LEFT_OUTER  = 'left outer';

    /**
     * Current iterator position.
     *
     * @var int
     */
    private $position = 0;

    /**
     * JOIN specifications
     *
     * @var array
     */
    protected $joins = [];

    /**
     * Initialize iterator position.
     */
    public function __construct()
    {
        $this->position = 0;
    }

    /**
     * Rewind iterator.
     */
    #[ReturnTypeWillChange]
    public function rewind()
    {
        $this->position = 0;
    }

    /**
     * Return current join specification.
     *
     * @return array
     */
    #[ReturnTypeWillChange]
    public function current()
    {
        return $this->joins[$this->position];
    }

    /**
     * Return the current iterator index.
     *
     * @return int
     */
    #[ReturnTypeWillChange]
    public function key()
    {
        return $this->position;
    }

    /**
     * Advance to the next JOIN specification.
     */
    #[ReturnTypeWillChange]
    public function next()
    {
        ++$this->position;
    }

    /**
     * Is the iterator at a valid position?
     *
     * @return bool
     */
    #[ReturnTypeWillChange]
    public function valid()
    {
        return isset($this->joins[$this->position]);
    }

    /**
     * @return array
     */
    public function getJoins()
    {
        return $this->joins;
    }

    /**
     * @param string|array|TableIdentifier $name A table name on which to join, or a single
     *     element associative array, of the form alias => table, or TableIdentifier instance
     * @param string|Predicate\Expression $on A specification describing the fields to join on.
     * @param string|string[]|int|int[] $columns A single column name, an array
     *     of column names, or (a) specification(s) such as SQL_STAR representing
     *     the columns to join.
     * @param string $type The JOIN type to use; see the JOIN_* constants.
     * @return $this Provides a fluent interface
     * @throws Exception\InvalidArgumentException For invalid $name values.
     */
    public function join($name, $on, $columns = [Select::SQL_STAR], $type = self::JOIN_INNER)
    {
        if (is_array($name) && (! is_string(key($name)) || count($name) !== 1)) {
            throw new Exception\InvalidArgumentException(
                sprintf("join() expects '%s' as a single element associative array", array_shift($name))
            );
        }

        if (! is_array($columns)) {
            $columns = [$columns];
        }

        $this->joins[] = [
            'name'    => $name,
            'on'      => $on,
            'columns' => $columns,
            'type'    => $type ? $type : self::JOIN_INNER,
        ];

        return $this;
    }

    /**
     * Reset to an empty list of JOIN specifications.
     *
     * @return $this Provides a fluent interface
     */
    public function reset()
    {
        $this->joins = [];
        return $this;
    }

    /**
     * Get count of attached predicates
     *
     * @return int
     */
    #[ReturnTypeWillChange]
    public function count()
    {
        return count($this->joins);
    }
}
