<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;

use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;

class Unique
{
    /**
     * UNIQUE
     * The UNIQUE function searches for value either from a one-row or one-column range or from an array.
     *
     * @param mixed $lookupVector The range of cells being searched
     * @param mixed $byColumn Whether the uniqueness should be determined by row (the default) or by column
     * @param mixed $exactlyOnce Whether the function should return only entries that occur just once in the list
     *
     * @return mixed The unique values from the search range
     */
    public static function unique(mixed $lookupVector, mixed $byColumn = false, mixed $exactlyOnce = false): mixed
    {
        if (!is_array($lookupVector)) {
            // Scalars are always returned "as is"
            return $lookupVector;
        }

        $byColumn = (bool) $byColumn;
        $exactlyOnce = (bool) $exactlyOnce;

        return ($byColumn === true)
            ? self::uniqueByColumn($lookupVector, $exactlyOnce)
            : self::uniqueByRow($lookupVector, $exactlyOnce);
    }

    /** @param mixed[] $lookupVector */
    private static function uniqueByRow(array $lookupVector, bool $exactlyOnce): mixed
    {
        // When not $byColumn, we count whole rows or values, not individual values
        //      so implode each row into a single string value
        array_walk(
            $lookupVector,
            //* @phpstan-ignore-next-line
            function (array &$value): void {
                $valuex = '';
                $separator = '';
                $numericIndicator = "\x01";
                foreach ($value as $cellValue) {
                    /** @var scalar $cellValue */
                    $valuex .= $separator . $cellValue;
                    $separator = "\x00";
                    if (is_int($cellValue) || is_float($cellValue)) {
                        $valuex .= $numericIndicator;
                    }
                }
                $value = $valuex;
            }
        );

        /** @var string[] $lookupVector */
        $result = self::countValuesCaseInsensitive($lookupVector);

        if ($exactlyOnce === true) {
            $result = self::exactlyOnceFilter($result);
        }

        if (count($result) === 0) {
            return ExcelError::CALC();
        }

        $result = array_keys($result);

        // restore rows from their strings
        array_walk(
            $result,
            function (string &$value): void {
                $value = explode("\x00", $value);
                foreach ($value as &$stringValue) {
                    if (str_ends_with($stringValue, "\x01")) {
                        // x01 should only end a string which is otherwise a float or int,
                        // so phpstan is technically correct but what it fears should not happen.
                        $stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore-line
                    }
                }
            }
        );

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

    /** @param mixed[] $lookupVector */
    private static function uniqueByColumn(array $lookupVector, bool $exactlyOnce): mixed
    {
        /** @var string[] */
        $flattenedLookupVector = Functions::flattenArray($lookupVector);

        if (count($lookupVector, COUNT_RECURSIVE) > count($flattenedLookupVector, COUNT_RECURSIVE) + 1) {
            // We're looking at a full column check (multiple rows)
            $transpose = Matrix::transpose($lookupVector);
            $result = self::uniqueByRow($transpose, $exactlyOnce);

            return (is_array($result)) ? Matrix::transpose($result) : $result;
        }

        $result = self::countValuesCaseInsensitive($flattenedLookupVector);

        if ($exactlyOnce === true) {
            $result = self::exactlyOnceFilter($result);
        }

        if (count($result) === 0) {
            return ExcelError::CALC();
        }

        $result = array_keys($result);

        return $result;
    }

    /**
     * @param string[] $caseSensitiveLookupValues
     *
     * @return mixed[]
     */
    private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array
    {
        $caseInsensitiveCounts = array_count_values(
            array_map(
                fn (string $value): string => StringHelper::strToUpper($value),
                $caseSensitiveLookupValues
            )
        );

        $caseSensitiveCounts = [];
        foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) {
            if (is_numeric($caseInsensitiveKey)) {
                $caseSensitiveCounts[$caseInsensitiveKey] = $count;
            } else {
                foreach ($caseSensitiveLookupValues as $caseSensitiveValue) {
                    if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) {
                        $caseSensitiveCounts[$caseSensitiveValue] = $count;

                        break;
                    }
                }
            }
        }

        return $caseSensitiveCounts;
    }

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