<?php

namespace Plainwork\DateTime;

class TimeFactory
{
    private int $weekStartsOn = 0;
    private \DateTimeZone $timezone;

    public static function utcTimezone(): \DateTimeZone
    {
        static $ret;
        if (!isset($ret)) {
            $ret = new \DateTimeZone('UTC');
        }

        return $ret;
    }

    public function setWeekStartsOn(int $v): self
    {
        $this->weekStartsOn = $v;

        return $this;
    }

    public function fromDateTime(int $date, int $timeInSeconds): \DateTimeImmutable
    {
        $ret = new \DateTimeImmutable($date);
        $ret = $this->addSeconds($ret, $timeInSeconds);

        return $ret;
    }

    public function fromString(string $v): \DateTimeImmutable
    {
        $ret = new \DateTimeImmutable($v, $this->getTimezone());

        return $ret;
    }

    public function fromUtcString(string $v): \DateTimeImmutable
    {
        $ret = (new \DateTimeImmutable($v, static::utcTimezone()))->setTimezone($this->getTimezone());

        return $ret;
    }

    public function fromTimestamp(int $timestamp): \DateTimeImmutable
    {
        return $this->fromString('@' . $timestamp);
    }

    public function toUtc(\DateTimeInterface $v): \DateTimeImmutable
    {
        return new \DateTimeImmutable($this->toString($v), static::utcTimezone());
    }

    public function toString(\DateTimeInterface $v): string
    {
        return $v->format('YmdHi');
    }

    public function now(): \DateTimeImmutable
    {
        return $this->fromString('now');
    }

    public function addSeconds(\DateTimeImmutable $dt, int $seconds)
    {
        return $dt->setTimestamp($dt->getTimestamp() + $seconds);
    }

    public function startDay(\DateTimeImmutable $dt): \DateTimeImmutable
    {
        return $dt->setTime(0, 0);
    }

    public function endDay(\DateTimeImmutable $dt): \DateTimeImmutable
    {
        return $dt->setTime(23, 59, 59);
    }

    public function getDayOfWeek(\DateTimeInterface $dt): int
    {
        return (int) $dt->format('w');
    }

    public function getDayOfMonth(\DateTimeInterface $dt): int
    {
        return (int) $dt->format('j');
    }

    public function startWeek(\DateTimeInterface $dt): \DateTimeInterface
    {
        $weekStartsOn = $this->weekStartsOn;
        $dayOfWeek = $this->getDayOfWeek($dt);
        $diff = $dayOfWeek - $this->weekStartsOn;

        $ret = $dt;
        if ($diff > 0) {
            $ret = $ret->modify('-' . $diff  . ' day');
        } elseif ($diff < 0) {
            $ret = $ret->modify('-' . (7 - (-$diff))  . ' day');
        }
        $ret = $this->startDay($ret);

        return $ret;
    }

    public function endWeek(\DateTimeInterface $dt): \DateTimeInterface
    {
        $ret = $this->startWeek($dt);
        $ret = $ret->modify('+1 week')->modify('-1 day');
        $ret = $this->endDay($ret);

        return $ret;
    }

    public function isEndWeek(\DateTimeInterface $dt): bool
    {
        $weekStartsOn = $this->weekStartsOn;
        $wkd = $this->getDayOfWeek($dt);

        return ($weekStartsOn == (($wkd+1) % 7));
    }

    public function startMonth(\DateTimeInterface $dt): \DateTimeInterface
    {
        $ret = $dt->setDate($dt->format('Y'), $dt->format('m'), 1);
        $ret = $this->startDay($ret);

        return $ret;
    }

    public function endMonth(\DateTimeInterface $dt): \DateTimeInterface
    {
        $ret = $this->startMonth($dt);
        $ret = $ret->modify('+1 month')->modify('-1 day');
        $ret = $this->endDay($ret);

        return $ret;
    }

    public function startYear(\DateTimeInterface $dt): \DateTimeInterface
    {
        $ret = $dt->setDate($dt->format('Y'), 1, 1);
        $ret = $this->startDay($ret);

        return $ret;
    }

    public function endYear(\DateTimeInterface $dt): \DateTimeInterface
    {
        $ret = $dt->setDate($dt->format('Y'), 12, 31);
        $ret = $this->endDay($ret);

        return $ret;
    }

    public function getTimeInDay(\DateTimeInterface $dt): int
    {
        $h = (int) $dt->format('G');
        $m = (int) $dt->format('i');
        $ret = 60*60*$h + 60*$m;

        return $ret;
    }

    public function getDaysOfWeek(): array
    {
        $ret = [];

        $ret = [0, 1, 2, 3, 4, 5, 6];
        $startIndex = array_search($this->weekStartsOn, $ret);
        if (0 !== $startIndex) {
            $ret = array_merge(array_slice($ret, $startIndex), array_slice($ret, 0, $startIndex));
        }

        return $ret;
    }

    public function getWeeksMatrix(\DateTimeInterface $dt1, \DateTimeInterface $dt2): array
    {
        $ret = [];

        $daysOfWeek = $this->getDaysOfWeek();

        $start = $this->startWeek($dt1);
        $end = $this->endWeek($dt2);

        $rex = $start;
        while ($rex < $end) {
            $week = [];
            foreach ($daysOfWeek as $wkd) {
                $week[$wkd] = (($rex < $start) or ($rex > $end)) ? null : $rex;
                $rex = $rex->modify('+1 day');
            }
            $ret[] = $week;
        }

        return $ret;
    }

    public function getMonthMatrix(?\DateTimeInterface $d = null): array
    {
        $ret = [];

        if (null === $d) {
            $d = $this->now();
        }

        $daysOfWeek = $this->getDaysOfWeek();

        $start = $this->startMonth($d);
        $end = $this->endMonth($d);

        $rex = $this->startWeek($start);
        while ($rex < $end) {
            $week = [];
            foreach ($daysOfWeek as $wkd) {
                $week[$wkd] = (($rex < $start) or ($rex > $end)) ? null : $rex;
                $rex = $rex->modify('+1 day');
            }
            $ret[] = $week;
        }

        return $ret;
    }

    public function getTimezone(): \DateTimeZone
    {
        if (!isset($this->timezone)) {
            $this->timezone = new \DateTimeZone($this->getDefaultTimezoneString());
        }

        return $this->timezone;
    }

    private function getDefaultTimezoneString(): string
    {
        $ret = '';

    // wordpress?
        if (defined('WPINC')) {
            $ret = get_option('timezone_string');
            if (!strlen($ret)) {
                $offset = get_option('gmt_offset');
                if ($offset) {
                    $ret = 'Etc/GMT';
                    if ($offset > 0) {
                        $ret .= '+' . $offset;
                    } else {
                        $ret .= '-' . -$offset;
                    }
                }
            }
        } else {
            $ret = date_default_timezone_get();
        }

        if (!strlen($ret)) {
            $ret = 'UTC';
        }

    // weird stuff: Etc/GMT timezones are reversed so Etc/GMT+1 is actually Etc/GMT-1
        $prfx = 'Etc/GMT';
        if ($prfx === substr($ret, 0, strlen($prfx))) {
            $sign = substr($ret, strlen($prfx), 1);
            $ret = $prfx . (('-' === $sign) ? '+' : '-') . substr($ret, strlen($prfx) + 1);
        }

        return $ret;
    }

    public function diffDays(\DateTimeInterface $dt1, \DateTimeInterface $dt2): int
    {
        if ($dt1 == $dt2) {
            return 0;
        }

        $y1 = $dt1->format('Y');
        $m1 = $dt1->format('n');
        $d1 = $dt1->format('j');

        $y2 = $dt2->format('Y');
        $m2 = $dt2->format('n');
        $d2 = $dt2->format('j');

        if (($y1 === $y2) && ($m1 === $m2) && ($d1 === $d2)) {
            $ret = 0;
        } elseif (($y1 === $y2) && ($m1 === $m2)) {
            $ret = $d2 - $d1;
        } else {
            if (function_exists('gregoriantojd')) {
                $ret = gregoriantojd($m1, $d1, $y1) - gregoriantojd($m2, $d2, $y2);
            } else {
                $diffSec = $dt1->getTimestamp() - $dt2->getTimestamp();
                $ret = floor($diffSec / (24*60*60));
                $ret = (int) $ret;
            }
        }
        $ret = abs($ret);

        return $ret;
    }

    public function getWeekNo(\DateTimeInterface $dt): int
    {
        $ret = $dt->format('W'); // but it works out of the box when the week starts on monday

        if (0 === $this->weekStartsOn) {
            $wkd = $this->getDayOfWeek($dt);
        // sunday?
            if (0 === $wkd) {
                $ret = $ret + 1;
            }
        }

        return $ret;
    }

    public function getDuration(\DateTimeInterface $dt1, \DateTimeInterface $dt2): int
    {
        $ret = abs($dt1->getTimestamp() - $dt2->getTimestamp());

        return $ret;
    }

// like 2w
    public function unpackRange(string $in): array
    {
        $qty = substr($in, 0, -1);
        $qty = (int) $qty;
        $unit = substr($in, -1);

        if ('m' === $unit) {
            $unit = 'month';
        } elseif ('w' === $unit) {
            $unit = 'week';
        } elseif ('d' === $unit) {
            $unit = 'day';
        }

        return [$qty, $unit];
    }

    public function buildRange(\DateTimeInterface $dt, int $qty, string $range, array $skipDaysOfWeek = []): array
    {
        $ret1 = clone $dt;

        if ('year' === $range) {
            $ret1 = $this->startYear($ret1);
        } elseif ('month' === $range) {
            $ret1 = $this->startMonth($ret1);
        } elseif ('week' === $range) {
            $ret1 = $this->startWeek($ret1);
        } else {
            $ret1 = $this->startDay($ret1);
        }

        if ('day' === $range && $skipDaysOfWeek) {
        // resolve start
            $rex = $this->startDay($ret1);
            while (in_array($this->getDayOfWeek($rex), $skipDaysOfWeek)) {
                $rex = $rex->modify('+1 day');
            }
            $ret1 = $rex;

            if ($qty > 1) {
                $rex = clone $ret1;
                $count = 1;
                while ($count < $qty) {
                    $rex = $rex->modify('+1 day');
                    while (in_array($this->getDayOfWeek($rex), $skipDaysOfWeek)) {
                        $rex = $rex->modify('+1 day');
                    }
                    $count++;
                }
                $ret2 = $rex;
            } else {
                $ret2 = clone $ret1;
            }
        } else {
            $ret2 = clone $ret1;
            if ($qty > 1) {
                $ret2 = $ret2->modify('+' . ($qty - 1) . ' ' . $range);
            }
        }

        if ('year' === $range) {
            $ret2 = $this->endYear($ret2);
        } elseif ('month' === $range) {
            $ret2 = $this->endMonth($ret2);
        } elseif ('week' === $range) {
            $ret2 = $this->endWeek($ret2);
        } else {
            $ret2 = $this->endDay($ret2);
        }

        return [$ret1, $ret2];
    }

    public function buildNextRange(\DateTimeInterface $dt, int $qty, string $range, array $skipDaysOfWeek = []): array
    {
        list($dt1, $dt2) = $this->buildRange($dt, $qty, $range, $skipDaysOfWeek);
        $ret1 = $dt2->modify('+1 day');

        return $this->buildRange($ret1, $qty, $range, $skipDaysOfWeek);
    }

    public function buildPreviousRange(\DateTimeInterface $dt, int $qty, string $range, $skipDaysOfWeek): array
    {
        list($dt1, $dt2) = $this->buildRange($dt, $qty, $range, $skipDaysOfWeek);
        $ret2 = $dt1->modify('-1 day');

        $ret1 = clone $ret2;
        if ($qty > 1) {
            $ret1 = $ret1->modify('-' . ($qty - 1) . ' ' . $range);
        }

        return $this->buildRange($ret1, $qty, $range, $skipDaysOfWeek);
    }
}
