<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;

use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
use Throwable;

class Helpers
{
    /**
     * Identify if a year is a leap year or not.
     *
     * @param int|string $year The year to test
     *
     * @return bool TRUE if the year is a leap year, otherwise FALSE
     */
    public static function isLeapYear(int|string $year): bool
    {
        $year = (int) $year;

        return (($year % 4) === 0) && (($year % 100) !== 0) || (($year % 400) === 0);
    }

    /**
     * getDateValue.
     *
     * @return float Excel date/time serial value
     */
    public static function getDateValue(mixed $dateValue, bool $allowBool = true): float
    {
        if (is_object($dateValue)) {
            $retval = SharedDateHelper::PHPToExcel($dateValue);
            if (is_bool($retval)) {
                throw new Exception(ExcelError::VALUE());
            }

            return $retval;
        }

        self::nullFalseTrueToNumber($dateValue, $allowBool);
        if (!is_numeric($dateValue)) {
            $saveReturnDateType = Functions::getReturnDateType();
            Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
            if (is_string($dateValue)) {
                $dateValue = DateValue::fromString($dateValue);
            }
            Functions::setReturnDateType($saveReturnDateType);
            if (!is_numeric($dateValue)) {
                throw new Exception(ExcelError::VALUE());
            }
        }
        if ($dateValue < 0 && Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) {
            throw new Exception(ExcelError::NAN());
        }

        try {
            SharedDateHelper::excelToDateTimeObject((float) $dateValue);
        } catch (Throwable) {
            throw new Exception(ExcelError::NAN());
        }

        return (float) $dateValue;
    }

    /**
     * getTimeValue.
     *
     * @return float|string Excel date/time serial value, or string if error
     */
    public static function getTimeValue(string $timeValue): string|float
    {
        $saveReturnDateType = Functions::getReturnDateType();
        Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
        /** @var float|string $timeValue */
        $timeValue = TimeValue::fromString($timeValue);
        Functions::setReturnDateType($saveReturnDateType);

        return $timeValue;
    }

    /**
     * Adjust date by given months.
     *
     * @param float|int $dateValue date to be adjusted
     */
    public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime
    {
        // Execute function
        $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
        $oMonth = (int) $PHPDateObject->format('m');
        $oYear = (int) $PHPDateObject->format('Y');

        $adjustmentMonthsString = (string) $adjustmentMonths;
        if ($adjustmentMonths > 0) {
            $adjustmentMonthsString = '+' . $adjustmentMonths;
        }
        if ($adjustmentMonths != 0) {
            $PHPDateObject->modify($adjustmentMonthsString . ' months');
        }
        $nMonth = (int) $PHPDateObject->format('m');
        $nYear = (int) $PHPDateObject->format('Y');

        $monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12);
        if ($monthDiff != $adjustmentMonths) {
            $adjustDays = (int) $PHPDateObject->format('d');
            $adjustDaysString = '-' . $adjustDays . ' days';
            $PHPDateObject->modify($adjustDaysString);
        }

        return $PHPDateObject;
    }

    /**
     * Help reduce perceived complexity of some tests.
     */
    public static function replaceIfEmpty(mixed &$value, mixed $altValue): void
    {
        $value = $value ?: $altValue;
    }

    /**
     * Adjust year in ambiguous situations.
     */
    public static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void
    {
        if (!is_numeric($testVal1) || $testVal1 < 31) {
            if (!is_numeric($testVal2) || $testVal2 < 12) {
                if (is_numeric($testVal3) && $testVal3 < 12) {
                    $testVal3 = (string) ($testVal3 + 2000);
                }
            }
        }
    }

    /**
     * Return result in one of three formats.
     *
     * @param array{year: int, month: int, day: int, hour: int, minute: int, second: int} $dateArray
     */
    public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int
    {
        $retType = Functions::getReturnDateType();
        if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
            return new DateTime(
                $dateArray['year']
                . '-' . $dateArray['month']
                . '-' . $dateArray['day']
                . ' ' . $dateArray['hour']
                . ':' . $dateArray['minute']
                . ':' . $dateArray['second']
            );
        }
        $excelDateValue
            = SharedDateHelper::formattedPHPToExcel(
                $dateArray['year'],
                $dateArray['month'],
                $dateArray['day'],
                $dateArray['hour'],
                $dateArray['minute'],
                $dateArray['second']
            );
        if ($retType === Functions::RETURNDATE_EXCEL) {
            return $noFrac ? floor($excelDateValue) : $excelDateValue;
        }
        // RETURNDATE_UNIX_TIMESTAMP)

        return SharedDateHelper::excelToTimestamp($excelDateValue);
    }

    /**
     * Return result in one of three formats.
     */
    public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime
    {
        $retType = Functions::getReturnDateType();
        if ($retType === Functions::RETURNDATE_EXCEL) {
            return $excelDateValue;
        }
        if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
            return SharedDateHelper::excelToTimestamp($excelDateValue);
        }
        // RETURNDATE_PHP_DATETIME_OBJECT

        return SharedDateHelper::excelToDateTimeObject($excelDateValue);
    }

    /**
     * Return result in one of three formats.
     */
    public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int
    {
        $retType = Functions::getReturnDateType();
        if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
            return $PHPDateObject;
        }
        if ($retType === Functions::RETURNDATE_EXCEL) {
            return (float) SharedDateHelper::PHPToExcel($PHPDateObject);
        }
        // RETURNDATE_UNIX_TIMESTAMP
        $stamp = SharedDateHelper::PHPToExcel($PHPDateObject);
        $stamp = is_bool($stamp) ? ((int) $stamp) : $stamp;

        return SharedDateHelper::excelToTimestamp($stamp);
    }

    private static function baseDate(): int
    {
        if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
            return 0;
        }
        if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) {
            return 0;
        }

        return 1;
    }

    /**
     * Many functions accept null/false/true argument treated as 0/0/1.
     */
    public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true): void
    {
        $number = Functions::flattenSingleValue($number);
        $nullVal = self::baseDate();
        if ($number === null) {
            $number = $nullVal;
        } elseif ($allowBool && is_bool($number)) {
            $number = $nullVal + (int) $number;
        }
    }

    /**
     * Many functions accept null argument treated as 0.
     */
    public static function validateNumericNull(mixed $number): int|float
    {
        $number = Functions::flattenSingleValue($number);
        if ($number === null) {
            return 0;
        }
        if (is_int($number)) {
            return $number;
        }
        if (is_numeric($number)) {
            return (float) $number;
        }

        throw new Exception(ExcelError::VALUE());
    }

    /**
     * Many functions accept null/false/true argument treated as 0/0/1.
     *
     * @phpstan-assert float $number
     */
    public static function validateNotNegative(mixed $number): float
    {
        if (!is_numeric($number)) {
            throw new Exception(ExcelError::VALUE());
        }
        if ($number >= 0) {
            return (float) $number;
        }

        throw new Exception(ExcelError::NAN());
    }

    public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void
    {
        $isoDate = $PHPDateObject->format('c');
        if ($isoDate < '1900-03-01') {
            $PHPDateObject->modify($mod);
        }
    }

    /** @return array{year: int, month: int, day: int, hour: int, minute: int, second: int} */
    public static function dateParse(string $string): array
    {
        /** @var array{year: int, month: int, day: int, hour: int, minute: int, second: int} */
        $temp = self::forceArray(date_parse($string));

        return $temp;
    }

    /** @param mixed[] $dateArray */
    public static function dateParseSucceeded(array $dateArray): bool
    {
        return $dateArray['error_count'] === 0;
    }

    /**
     * Despite documentation, date_parse probably never returns false.
     * Just in case, this routine helps guarantee it.
     *
     * @param array<mixed>|false $dateArray
     *
     * @return mixed[]
     */
    private static function forceArray(array|bool $dateArray): array
    {
        return is_array($dateArray) ? $dateArray : ['error_count' => 1];
    }

    public static function floatOrInt(mixed $value): float|int
    {
        $result = Functions::scalar($value);

        return is_numeric($result) ? ($result + 0) : 0;
    }
}
