<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;

use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;

class WorkDay
{
    use ArrayEnabled;

    /**
     * WORKDAY.
     *
     * Returns the date that is the indicated number of working days before or after a date (the
     * starting date). Working days exclude weekends and any dates identified as holidays.
     * Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected
     * delivery times, or the number of days of work performed.
     *
     * Excel Function:
     *        WORKDAY(startDate,endDays[,holidays[,holiday[,...]]])
     *
     * @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer),
     *                                        PHP DateTime object, or a standard date string
     *                         Or can be an array of date values
     * @param array|int $endDays The number of nonweekend and nonholiday days before or after
     *                                        startDate. A positive value for days yields a future date; a
     *                                        negative value yields a past date.
     *                         Or can be an array of int values
     * @param null|mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation
     *
     * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
     *                        depending on the value of the ReturnDateType flag
     *         If an array of values is passed for the $startDate or $endDays,arguments, then the returned result
     *            will also be an array with matching dimensions
     */
    public static function date(mixed $startDate, array|int|string $endDays, mixed ...$dateArgs): array|float|int|DateTime|string
    {
        if (is_array($startDate) || is_array($endDays)) {
            return self::evaluateArrayArgumentsSubset(
                [self::class, __FUNCTION__],
                2,
                $startDate,
                $endDays,
                ...$dateArgs
            );
        }

        //    Retrieve the mandatory start date and days that are referenced in the function definition
        try {
            $startDate = Helpers::getDateValue($startDate);
            $endDays = Helpers::validateNumericNull($endDays);
            $holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs));
        } catch (Exception $e) {
            return $e->getMessage();
        }

        $startDate = (float) floor($startDate);
        $endDays = (int) floor($endDays);
        //    If endDays is 0, we always return startDate
        if ($endDays == 0) {
            return $startDate;
        }
        if ($endDays < 0) {
            return self::decrementing($startDate, $endDays, $holidayArray);
        }

        return self::incrementing($startDate, $endDays, $holidayArray);
    }

    /**
     * Use incrementing logic to determine Workday.
     */
    private static function incrementing(float $startDate, int $endDays, array $holidayArray): float|int|DateTime
    {
        //    Adjust the start date if it falls over a weekend
        $startDoW = self::getWeekDay($startDate, 3);
        if ($startDoW >= 5) {
            $startDate += 7 - $startDoW;
            --$endDays;
        }

        //    Add endDays
        $endDate = (float) $startDate + ((int) ($endDays / 5) * 7);
        $endDays = $endDays % 5;
        while ($endDays > 0) {
            ++$endDate;
            //    Adjust the calculated end date if it falls over a weekend
            $endDow = self::getWeekDay($endDate, 3);
            if ($endDow >= 5) {
                $endDate += 7 - $endDow;
            }
            --$endDays;
        }

        //    Test any extra holiday parameters
        if (!empty($holidayArray)) {
            $endDate = self::incrementingArray($startDate, $endDate, $holidayArray);
        }

        return Helpers::returnIn3FormatsFloat($endDate);
    }

    private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float
    {
        $holidayCountedArray = $holidayDates = [];
        foreach ($holidayArray as $holidayDate) {
            if (self::getWeekDay($holidayDate, 3) < 5) {
                $holidayDates[] = $holidayDate;
            }
        }
        sort($holidayDates, SORT_NUMERIC);
        foreach ($holidayDates as $holidayDate) {
            if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) {
                if (!in_array($holidayDate, $holidayCountedArray)) {
                    ++$endDate;
                    $holidayCountedArray[] = $holidayDate;
                }
            }
            //    Adjust the calculated end date if it falls over a weekend
            $endDoW = self::getWeekDay($endDate, 3);
            if ($endDoW >= 5) {
                $endDate += 7 - $endDoW;
            }
        }

        return $endDate;
    }

    /**
     * Use decrementing logic to determine Workday.
     */
    private static function decrementing(float $startDate, int $endDays, array $holidayArray): float|int|DateTime
    {
        //    Adjust the start date if it falls over a weekend
        $startDoW = self::getWeekDay($startDate, 3);
        if ($startDoW >= 5) {
            $startDate += -$startDoW + 4;
            ++$endDays;
        }

        //    Add endDays
        $endDate = (float) $startDate + ((int) ($endDays / 5) * 7);
        $endDays = $endDays % 5;
        while ($endDays < 0) {
            --$endDate;
            //    Adjust the calculated end date if it falls over a weekend
            $endDow = self::getWeekDay($endDate, 3);
            if ($endDow >= 5) {
                $endDate += 4 - $endDow;
            }
            ++$endDays;
        }

        //    Test any extra holiday parameters
        if (!empty($holidayArray)) {
            $endDate = self::decrementingArray($startDate, $endDate, $holidayArray);
        }

        return Helpers::returnIn3FormatsFloat($endDate);
    }

    private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float
    {
        $holidayCountedArray = $holidayDates = [];
        foreach ($holidayArray as $holidayDate) {
            if (self::getWeekDay($holidayDate, 3) < 5) {
                $holidayDates[] = $holidayDate;
            }
        }
        rsort($holidayDates, SORT_NUMERIC);
        foreach ($holidayDates as $holidayDate) {
            if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) {
                if (!in_array($holidayDate, $holidayCountedArray)) {
                    --$endDate;
                    $holidayCountedArray[] = $holidayDate;
                }
            }
            //    Adjust the calculated end date if it falls over a weekend
            $endDoW = self::getWeekDay($endDate, 3);
            /** int $endDoW */
            if ($endDoW >= 5) {
                $endDate += -$endDoW + 4;
            }
        }

        return $endDate;
    }

    private static function getWeekDay(float $date, int $wd): int
    {
        $result = Functions::scalar(Week::day($date, $wd));

        return is_int($result) ? $result : -1;
    }
}
