<?php

namespace Cmixin\BusinessDay;

use Cmixin\BusinessDay\Util\Context;
use Cmixin\BusinessDay\Util\FileConfig;
use InvalidArgumentException;

class HolidaysList extends MixinBase
{
    /**
     * @var array
     */
    public $holidays = [];

    /**
     * @var array
     */
    public $workdays = [];

    /**
     * @var string|null
     */
    public $holidaysRegion = null;

    /**
     * @var array|null
     */
    public $availableRegions = null;

    /**
     * Return a standardized region name.
     *
     * @return \Closure
     */
    public function standardizeHolidaysRegion()
    {
        /**
         * Return a standardized region name.
         *
         * @param string $region
         *
         * @return string
         */
        return static function ($region) {
            $region = preg_replace('/[^a-z0-9_-]/', '', str_replace('_', '-', strtolower($region)));

            return strpos($region, '-') === false ? "$region-national" : $region;
        };
    }

    /**
     * Get an array of available holidays regions.
     *
     * @return \Closure
     */
    public function getHolidaysAvailableRegions()
    {
        $mixin = $this;

        /**
         * Get the current holidays region.
         *
         * @return array
         */
        return static function () use ($mixin) {
            if (is_null($mixin->availableRegions)) {
                $mixin->availableRegions = array_map(
                    static function ($file) {
                        return pathinfo($file, PATHINFO_FILENAME);
                    },
                    glob(__DIR__.'/../Holidays/*.php')
                );
            }

            return $mixin->availableRegions;
        };
    }

    /**
     * Set the holidays region (see src/Cmixin/Holidays for examples).
     *
     * @return \Closure
     */
    public function setHolidaysRegion()
    {
        $mixin = $this;

        /**
         * Set the holidays region (see src/Cmixin/Holidays for examples).
         *
         * @param string $region
         */
        return static function ($region) use ($mixin) {
            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $mixin->holidaysRegion = $region;

            if (!isset($mixin->holidays[$region])) {
                if (!file_exists($file = __DIR__."/../Holidays/$region.php")) {
                    [$country, $subRegion] = array_pad(explode('-', $region, 2), 2, '');
                    $nation = "$country-national";

                    if (!isset($mixin->holidays[$nation])) {
                        if (!file_exists($file = __DIR__."/../Holidays/$nation.php")) {
                            return;
                        }

                        FileConfig::use($file, $mixin, $nation);
                    }

                    if (isset($mixin->holidays[$nation]['regions'], $mixin->holidays[$nation]['regions'][$subRegion])) {
                        $newRegion = $mixin->holidays[$nation]['regions'][$subRegion];
                        $region = "$country-$newRegion";
                        $mixin->holidaysRegion = $region;
                        FileConfig::use(__DIR__."/../Holidays/$region.php", $mixin, $region);

                        return;
                    }

                    $mixin->holidaysRegion = $nation;
                    $file = __DIR__."/../Holidays/$nation.php";
                }

                FileConfig::use($file, $mixin, $region);
            }
        };
    }

    /**
     * Get the current holidays region.
     *
     * @return \Closure
     */
    public function getHolidaysRegion()
    {
        $mixin = $this;

        /**
         * Get the current holidays region.
         *
         * @return null|string
         */
        return static function () use ($mixin) {
            return $mixin->holidaysRegion;
        };
    }

    /**
     * Get the holidays for the current region selected.
     *
     * @return \Closure
     */
    public function getBDDaysList()
    {
        $mixin = $this;

        /**
         * Get the holidays for the current region selected.
         *
         * @param string $region
         *
         * @return array
         */
        return static function (string $list, $region = null) use ($mixin) {
            if (!in_array($list, ['holidays', 'workdays'])) {
                // Thrown exception?
            }

            $region = is_string($region)
                ? call_user_func($mixin->standardizeHolidaysRegion(), $region)
                : $mixin->holidaysRegion;

            if (!$region || !isset($mixin->$list[$region])) {
                return [];
            }

            return $mixin->$list[$region];
        };
    }

    /**
     * Get the holidays for the current region selected.
     *
     * @return \Closure
     */
    public function getHolidays()
    {
        $mixin = $this;

        /**
         * Get the holidays for the current region selected.
         *
         * @param string $region
         *
         * @return array
         */
        return static function ($region = null) use ($mixin) {
            return $mixin->getBDDaysList()('holidays', $region);
        };
    }

    /**
     * Set the holidays list.
     *
     * @return \Closure
     */
    public function setHolidays()
    {
        $mixin = $this;

        /**
         * Set the holidays list.
         *
         * @param string $region
         * @param array  $holidays
         */
        return static function ($region, $holidays) use ($mixin) {
            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $addHolidays = $mixin->addHolidays();
            $mixin->holidays[$region] = [];
            $addHolidays($region, $holidays);
        };
    }

    /**
     * Get the holidays for the current region selected.
     *
     * @return \Closure
     */
    public function getExtraWorkdays()
    {
        $mixin = $this;

        /**
         * Get the holidays for the current region selected.
         *
         * @param string $region
         *
         * @return array
         */
        return static function ($region = null) use ($mixin) {
            return $mixin->getBDDaysList()('workdays', $region);
        };
    }

    /**
     * Set the holidays list.
     *
     * @return \Closure
     */
    public function setExtraWorkdays()
    {
        $mixin = $this;

        /**
         * Set the holidays list.
         *
         * @param string $region
         * @param array  $holidays
         */
        return static function ($region, $holidays) use ($mixin) {
            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $addHolidays = $mixin->addHolidays();
            $mixin->workdays[$region] = [];
            $addHolidays($region, null, $holidays);
        };
    }

    /**
     * Reset the holidays list.
     *
     * @return \Closure
     */
    public function resetHolidays()
    {
        $mixin = $this;

        /**
         * Reset the holidays list.
         */
        return static function () use ($mixin) {
            $mixin->holidaysRegion = null;
            $mixin->holidays = [];
            $mixin->workdays = [];
        };
    }

    /**
     * Initialize holidays region storage.
     *
     * @param string|null $region
     *
     * @return \Closure|$this
     */
    public function initializeHolidaysRegion($region = null)
    {
        if ($region) {
            $region = call_user_func($this->standardizeHolidaysRegion(), $region);

            foreach (['holidays', 'workdays'] as $list) {
                if (!isset($this->$list[$region])) {
                    $this->$list[$region] = [];
                }
            }

            return $this;
        }

        return static function () {
            return true;
        };
    }

    /**
     * Push a day into a given list list of a region.
     *
     * @return \Closure
     */
    public function pushToBDList()
    {
        $mixin = $this;

        /**
         * Push a day into a given list list of a region.
         *
         * @param string          $list   List ID
         * @param string          $region region where the holiday is observed
         * @param string|\Closure $day    date or closure that get the year as parameter and returns the date
         * @param string          $dayId  optional day ID
         *
         * @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|null
         */
        return static function (string $list, $region, $day, $dayId = null) use ($mixin) {
            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $mixin->initializeHolidaysRegion($region);

            is_string($dayId)
                ? ($mixin->$list[$region][$dayId] = $day)
                : ($mixin->$list[$region][] = $day);

            return isset($this) && Context::isNotMixin($this, $mixin) ? $this : null;
        };
    }

    /**
     * Push a holiday to the holidays list of a region.
     *
     * @return \Closure
     */
    public function pushHoliday()
    {
        $mixin = $this;

        /**
         * Push a holiday to the holidays list of a region.
         *
         * @param string          $region    region where the holiday is observed.
         * @param string|\Closure $holiday   date or closure that get the year as parameter and returns the date
         * @param string          $holidayId optional holiday ID
         *
         * @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|null
         */
        return static function ($region, $holiday, $holidayId = null) use ($mixin) {
            $mixin->pushToBDList()('holidays', $region, $holiday, $holidayId);

            return isset($this) && Context::isNotMixin($this, $mixin) ? $this : null;
        };
    }

    /**
     * Push a workday to the workdays list of a region.
     *
     * @return \Closure
     */
    public function pushWorkday()
    {
        $mixin = $this;

        /**
         * Push a workday to the workdays list of a region.
         *
         * @param string          $region    region where the holiday is observed.
         * @param string|\Closure $workday   date or closure that get the year as parameter and returns the date
         * @param string          $workdayId optional workday ID
         *
         * @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|null
         */
        return static function ($region, $workday, $workdayId = null) use ($mixin) {
            $mixin->pushToBDList()('workdays', $region, $workday, $workdayId);

            return isset($this) && Context::isNotMixin($this, $mixin) ? $this : null;
        };
    }

    /**
     * Set the name(s) of a holiday.
     *
     * @return \Closure
     */
    public function setHolidayName()
    {
        $mixin = $this;

        /**
         * Set/change the name of holiday by ID for a given language (or a list of languages).
         *
         * @param string       $holidayKey holiday ID (identifier key)
         * @param string|array $language   language 2-chars code (or an array with languages codes as keys and new names for each language as value).
         * @param string       $name       new name (ignored if $language is an array)
         *
         * @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|null
         */
        return static function ($holidayKey = null, $language = null, $name = null) use ($mixin) {
            static $dictionary;

            if ($mixin instanceof Holiday && ($language = is_string($language) ? [$language => $name] : $language)) {
                if (!isset($dictionary)) {
                    $dictionary = $mixin->getHolidayNamesDictionary();
                }

                foreach ($language as $languageId => $name) {
                    $dictionary($languageId);
                    $mixin->holidayNames[$languageId][$holidayKey] = $name;
                }
            }

            return isset($this) && Context::isNotMixin($this, $mixin) ? $this : null;
        };
    }

    /**
     * Add a holiday to the holidays list of a region and optionally init its ID, name and observed state.
     *
     * @return \Closure
     */
    public function addHoliday()
    {
        $mixin = $this;
        $dictionary = $this->setHolidayName();

        /**
         * Add a holiday to the holidays list of a region and optionally init its ID, name and observed state.
         *
         * @param string          $region    region where the holiday is observed.
         * @param string|\Closure $holiday   date or closure that get the year as parameter and returns the date
         * @param string          $holidayId optional holiday ID
         * @param string          $name      optional name
         * @param bool            $observed  optional observed state
         *
         * @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|null
         */
        return static function ($region, $holiday, $holidayId = null, $name = null, $observed = null) use ($mixin, $dictionary) {
            static $observer;

            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $mixin->initializeHolidaysRegion($region);
            $push = $mixin->pushHoliday();
            $push($region, $holiday, $holidayId);

            $dictionary($holidayId, $name);

            if (isset($observed) && $mixin instanceof HolidayObserver) {
                if (!isset($observer)) {
                    $observer = $mixin->setHolidayObserveStatus();
                }

                $observer($holidayId, $observed);
            }

            return isset($this) && Context::isNotMixin($this, $mixin) ? $this : null;
        };
    }

    /**
     * Add a workday to the workdays list of a region and optionally init its ID and name.
     *
     * @return \Closure
     */
    public function addExtraWorkday()
    {
        $mixin = $this;
        $dictionary = $this->setHolidayName();

        /**
         * Add a workday to the workdays list of a region and optionally init its ID and name.
         *
         * @param string          $region    region where the holiday is observed.
         * @param string|\Closure $workday   date or closure that get the year as parameter and returns the date
         * @param string          $workdayId optional holiday ID
         * @param string          $name      optional name
         *
         * @return \Carbon\Carbon|\Carbon\CarbonImmutable|\Carbon\CarbonInterface|null
         */
        return static function ($region, $workday, $workdayId = null, $name = null) use ($mixin, $dictionary) {
            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $mixin->initializeHolidaysRegion($region);
            $push = $mixin->pushWorkday();
            $push($region, $workday, $workdayId);

            $dictionary($workdayId, $name);

            return isset($this) && Context::isNotMixin($this, $mixin) ? $this : null;
        };
    }

    /**
     * Unpack a holiday array definition.
     *
     * @return \Closure
     */
    public function unpackHoliday()
    {
        /**
         * Unpack a holiday array definition.
         *
         * @param array  &$holiday
         * @param string &$name
         * @param bool   &$observed
         *
         * @return array
         */
        return static function (&$holiday, &$name = null, &$observed = null) {
            if (!isset($holiday['date'])) {
                throw new InvalidArgumentException(
                    'Holiday array definition should at least contains a "date" entry.'
                );
            }

            if (isset($holiday['name'])) {
                $name = $holiday['name'];
            }

            if (isset($holiday['observed'])) {
                $observed = $holiday['observed'];
            }

            $holiday = $holiday['date'];

            return $holiday;
        };
    }

    /**
     * Check a holiday definition and unpack it if it's an array.
     *
     * @return \Closure
     */
    public function checkHoliday()
    {
        $mixin = $this;

        /**
         * Check a holiday definition and unpack it if it's an array.
         *
         * @param array  &$holiday
         * @param string $holidayId
         * @param string &$name
         * @param bool   &$observed
         *
         * @return array
         */
        return static function (&$holiday, $holidayId, &$name = null, &$observed = null) use ($mixin) {
            $unpack = $mixin->unpackHoliday();

            if (is_array($holiday)) {
                if (is_int($holidayId)) {
                    throw new InvalidArgumentException(
                        'Holiday array definition need a string identifier as main array key.'
                    );
                }

                $unpack($holiday, $name, $observed);
            }

            return $holiday;
        };
    }

    /**
     * Add holidays to the holidays list.
     *
     * @return \Closure
     */
    public function addHolidays()
    {
        $mixin = $this;

        /**
         * Add a holiday to the holidays list of a region and optionally init their IDs, names and observed states (if provided as array-definitions).
         *
         * @param string        $region
         * @param iterable|null $holidays
         * @param iterable|null $workingDays
         */
        return static function (string $region, ?iterable $holidays = null, ?iterable $workingDays = null) use ($mixin) {
            $region = call_user_func($mixin->standardizeHolidaysRegion(), $region);
            $mixin->initializeHolidaysRegion($region);
            $addHoliday = $mixin->addHoliday();
            $addWorkday = $mixin->addExtraWorkday();
            $check = $mixin->checkHoliday();

            foreach (($holidays ?: []) as $holidayId => $holiday) {
                $name = null;
                $observed = null;
                $check($holiday, $holidayId, $name, $observed);
                $addHoliday($region, $holiday, $holidayId, $name, $observed);
            }

            foreach (($workingDays ?: []) as $holidayId => $holiday) {
                $name = null;
                $observed = null;
                $check($holiday, $holidayId, $name, $observed);
                $addWorkday($region, $holiday, $holidayId, $name, $observed);
            }
        };
    }
}
