<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;

use PhpOffice\PhpSpreadsheet\Calculation\Exception;

class ArrayArgumentHelper
{
    protected int $indexStart = 0;

    /** @var mixed[] */
    protected array $arguments;

    protected int $argumentCount;

    /** @var int[] */
    protected array $rows;

    /** @var int[] */
    protected array $columns;

    /** @param mixed[] $arguments */
    public function initialise(array $arguments): void
    {
        $keys = array_keys($arguments);
        $this->indexStart = (int) array_shift($keys);
        $this->rows = $this->rows($arguments);
        $this->columns = $this->columns($arguments);

        $this->argumentCount = count($arguments);
        $this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns);

        $this->rows = $this->rows($arguments);
        $this->columns = $this->columns($arguments);

        if ($this->arrayArguments() > 2) {
            throw new Exception('Formulae with more than two array arguments are not supported');
        }
    }

    /** @return mixed[] */
    public function arguments(): array
    {
        return $this->arguments;
    }

    public function hasArrayArgument(): bool
    {
        return $this->arrayArguments() > 0;
    }

    public function getFirstArrayArgumentNumber(): int
    {
        $rowArrays = $this->filterArray($this->rows);
        $columnArrays = $this->filterArray($this->columns);

        for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) {
            if (isset($rowArrays[$index]) || isset($columnArrays[$index])) {
                return ++$index;
            }
        }

        return 0;
    }

    public function getSingleRowVector(): ?int
    {
        $rowVectors = $this->getRowVectors();

        return count($rowVectors) === 1 ? array_pop($rowVectors) : null;
    }

    /** @return int[] */
    private function getRowVectors(): array
    {
        $rowVectors = [];
        for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
            if ($this->rows[$index] === 1 && $this->columns[$index] > 1) {
                $rowVectors[] = $index;
            }
        }

        return $rowVectors;
    }

    public function getSingleColumnVector(): ?int
    {
        $columnVectors = $this->getColumnVectors();

        return count($columnVectors) === 1 ? array_pop($columnVectors) : null;
    }

    /** @return int[] */
    private function getColumnVectors(): array
    {
        $columnVectors = [];
        for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
            if ($this->rows[$index] > 1 && $this->columns[$index] === 1) {
                $columnVectors[] = $index;
            }
        }

        return $columnVectors;
    }

    /** @return int[] */
    public function getMatrixPair(): array
    {
        for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) {
            for ($j = $i + 1; $j < $this->argumentCount; ++$j) {
                if (isset($this->rows[$i], $this->rows[$j])) {
                    return [$i, $j];
                }
            }
        }

        return [];
    }

    public function isVector(int $argument): bool
    {
        return $this->rows[$argument] === 1 || $this->columns[$argument] === 1;
    }

    public function isRowVector(int $argument): bool
    {
        return $this->rows[$argument] === 1;
    }

    public function isColumnVector(int $argument): bool
    {
        return $this->columns[$argument] === 1;
    }

    public function rowCount(int $argument): int
    {
        return $this->rows[$argument];
    }

    public function columnCount(int $argument): int
    {
        return $this->columns[$argument];
    }

    /**
     * @param mixed[] $arguments
     *
     * @return int[]
     */
    private function rows(array $arguments): array
    {
        return array_map(
            fn ($argument): int => is_countable($argument) ? count($argument) : 1,
            $arguments
        );
    }

    /**
     * @param mixed[] $arguments
     *
     * @return int[]
     */
    private function columns(array $arguments): array
    {
        return array_map(
            fn (mixed $argument): int => is_array($argument) && is_array($argument[array_keys($argument)[0]])
                    ? count($argument[array_keys($argument)[0]])
                    : 1,
            $arguments
        );
    }

    public function arrayArguments(): int
    {
        $count = 0;
        foreach (array_keys($this->arguments) as $argument) {
            if ($this->rows[$argument] > 1 || $this->columns[$argument] > 1) {
                ++$count;
            }
        }

        return $count;
    }

    /**
     * @param mixed[] $arguments
     * @param int[] $rows
     * @param int[] $columns
     *
     * @return mixed[]
     */
    private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array
    {
        foreach ($arguments as $index => $argument) {
            if ($rows[$index] === 1 && $columns[$index] === 1) {
                while (is_array($argument)) {
                    $argument = array_pop($argument);
                }
                $arguments[$index] = $argument;
            }
        }

        return $arguments;
    }

    /**
     * @param mixed[] $array
     *
     * @return mixed[]
     */
    private function filterArray(array $array): array
    {
        return array_filter(
            $array,
            fn ($value): bool => $value > 1
        );
    }
}
