src/FeedIo/Reader/Result/UpdateStats.php
<?php

declare(strict_types=1);

namespace FeedIo\Reader\Result;

use FeedIo\Feed\ItemInterface;
use FeedIo\FeedInterface;

class UpdateStats
{
    /**
     * default update delay applied when average or median intervals are outdated
     */
    public const DEFAULT_MIN_DELAY = 3600;

    /**
     * default update delay applied when the feed is considered sleepy
     */
    public const DEFAULT_SLEEPY_DELAY = 86400;

    /**
     * default duration after which the feed is considered sleepy
     */
    public const DEFAULT_DURATION_BEFORE_BEING_SLEEPY = 7 * 86400;

    /**
     * default margin ratio applied to update time calculation
     */
    public const DEFAULT_MARGIN_RATIO = 0.1;

    protected array $intervals = [];

    /**
     * UpdateStats constructor.
     * @param FeedInterface $feed
     */
    public function __construct(
        protected FeedInterface $feed
    ) {
        $this->intervals = $this->computeIntervals($this->extractDates($feed));
    }

    /**
     * @param int $minDelay
     * @param int $sleepyDelay
     * @param int $sleepyDuration
     * @param float $marginRatio
     * @return \DateTime
     */
    public function computeNextUpdate(
        int $minDelay = self::DEFAULT_MIN_DELAY,
        int $sleepyDelay = self::DEFAULT_SLEEPY_DELAY,
        int $sleepyDuration = self::DEFAULT_DURATION_BEFORE_BEING_SLEEPY,
        float $marginRatio = self::DEFAULT_MARGIN_RATIO
    ): \DateTime {
        if ($this->isSleepy($sleepyDuration, $marginRatio)) {
            return (new \DateTime())->setTimestamp(time() + $sleepyDelay);
        }
        $feedTimeStamp = $this->getFeedTimestamp();
        $now = time();
        $intervals = [
            $this->getAverageInterval(),
            $this->getMedianInterval(),
        ];
        sort($intervals);
        $newTimestamp = $now + $minDelay;
        foreach ($intervals as $interval) {
            $computedTimestamp = $this->addInterval($feedTimeStamp, $interval, $marginRatio);
            if ($computedTimestamp > $now) {
                $newTimestamp = $computedTimestamp;
                break;
            }
        }
        return (new \DateTime())->setTimestamp($newTimestamp);
    }

    /**
     * @param int $sleepyDuration
     * @param float $marginRatio
     * @return bool
     */
    public function isSleepy(int $sleepyDuration, float $marginRatio): bool
    {
        return time() > $this->addInterval($this->getFeedTimestamp(), $sleepyDuration, $marginRatio);
    }

    /**
     * @param int $ts
     * @param int $interval
     * @param float $marginRatio
     * @return int
     */
    public function addInterval(int $ts, int $interval, float $marginRatio): int
    {
        return $ts + intval($interval + $marginRatio * $interval);
    }

    /**
     * @return array
     */
    public function getIntervals(): array
    {
        return $this->intervals;
    }

    /**
     * @return int
     */
    public function getMinInterval(): int
    {
        return count($this->intervals) ? min($this->intervals) : 0;
    }

    /**
     * @return int
     */
    public function getMaxInterval(): int
    {
        return count($this->intervals) ? max($this->intervals) : 0;
    }

    /**
     * @return int
     */
    public function getAverageInterval(): int
    {
        $total = array_sum($this->intervals);

        return count($this->intervals) ? intval(floor($total / count($this->intervals))) : 0;
    }

    /**
     * @return int
     */
    public function getMedianInterval(): int
    {
        sort($this->intervals);
        $num = floor(count($this->intervals) / 2);

        return isset($this->intervals[$num]) ? $this->intervals[$num] : 0;
    }

    private function computeIntervals(array $dates): array
    {
        rsort($dates);
        $intervals = [];
        $current = 0;
        foreach ($dates as $date) {
            if ($current > 0) {
                $intervals[] = $current - $date;
            }
            $current = $date;
        }
        return $intervals;
    }

    private function extractDates(FeedInterface $feed): array
    {
        $dates = [];
        foreach ($feed as $item) {
            $dates[] = $this->getTimestamp($item) ?? $this->getFeedTimestamp();
        }
        return $dates;
    }

    private function getTimestamp(ItemInterface $item): ?int
    {
        if (! is_null($item->getLastModified())) {
            return $item->getLastModified()->getTimestamp();
        }
        return null;
    }

    private function getFeedTimestamp(): int
    {
        return !! $this->feed->getLastModified() ? $this->feed->getLastModified()->getTimestamp() : time();
    }
}
