<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Util;

/**
 * Utilities to compare date periods.
 *
 * This class allow to compare periods when there are period covering. The
 * argument `minCovers` allow to find also when there are more than 2 period
 * which intersects.
 *
 * Example: a team may have maximum 2 leaders on a same period: you will
 * find here all periods where there are more than 2 leaders.
 *
 * Usage:
 *
 * ```php
 * $cover = new DateRangeCovering(2); // 2 means we will have periods
 *    // when there are 2+ periods intersecting
 * $cover
 *     ->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), null)
 *     ->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), null)
 *     ->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), null)
 *     ->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), null)
 *     ->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), null)
 *     ->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), null)
 *     ->compute()
 *     ;
 * $cover->getIntersections();
 * ```
 */
class DateRangeCovering
{
    private bool $computed = false;

    private array $intersections = [];

    private array $intervals = [];

    private array $metadatas = [];

    private readonly int $minCover;

    private array $sequence = [];

    private int $uniqueKeyCounter = 0;

    /**
     * @param int $minCover the minimum of covering required
     */
    public function __construct(int $minCover, private readonly \DateTimeZone $tz)
    {
        if (0 > $minCover) {
            throw new \LogicException('argument minCover cannot be lower than 0');
        }

        $this->minCover = $minCover;
    }

    public function add(\DateTimeInterface $start, ?\DateTimeInterface $end = null, $metadata = null): self
    {
        if ($this->computed) {
            throw new \LogicException('You cannot add intervals to a computed instance');
        }

        $k = $this->uniqueKeyCounter++;
        $this->intervals[$k] = [$start, $end];
        $this->metadatas[$k] = $metadata;

        $this->addToSequence($start->getTimestamp(), $k, null);
        $this->addToSequence(
            null === $end ? \PHP_INT_MAX : $end->getTimestamp(),
            null,
            $k
        );

        return $this;
    }

    public function compute(): self
    {
        \ksort($this->sequence);

        $currentPeriod = [];
        $currents = [];
        $isOpen = false;
        $overs = [];

        foreach ($this->sequence as $ts => $moves) {
            $currents = \array_merge($currents, $moves['s']);
            $currents = \array_diff($currents, $moves['e']);

            if (\count($currents) > $this->minCover && !$isOpen) {
                $currentPeriod[0] = $ts;
                $currentPeriod[2] = $currents;
                $isOpen = true;
            } elseif ($isOpen && \count($currents) <= $this->minCover) {
                $currentPeriod[1] = $ts;
                $overs[] = $currentPeriod;
                $currentPeriod = [];
                $isOpen = false;
            } elseif ($isOpen) {
                $currentPeriod[2] = \array_merge($currentPeriod[2], $currents);
            }
        }

        // process metadata
        foreach ($overs as [$start, $end, $metadata]) {
            $this->intersections[] = [
                (new \DateTimeImmutable('@'.$start))
                    ->setTimezone($this->tz),
                \PHP_INT_MAX === $end ? null : (new \DateTimeImmutable('@'.$end))
                    ->setTimezone($this->tz),
                \array_values(
                    \array_intersect_key(
                        $this->metadatas,
                        \array_flip(\array_unique($metadata))
                    )
                ),
            ];
        }

        $this->computed = true;

        return $this;
    }

    public function getIntersections(): array
    {
        if (!$this->computed) {
            throw new \LogicException(sprintf('You cannot call the method %s before '."'process'", __METHOD__));
        }

        return $this->intersections;
    }

    public function hasIntersections(): bool
    {
        if (!$this->computed) {
            throw new \LogicException(sprintf('You cannot call the method %s before '."'process'", __METHOD__));
        }

        return \count($this->intersections) > 0;
    }

    private function addToSequence($timestamp, ?int $start = null, ?int $end = null)
    {
        if (!\array_key_exists($timestamp, $this->sequence)) {
            $this->sequence[$timestamp] = ['s' => [], 'e' => []];
        }

        if (null !== $start) {
            $this->sequence[$timestamp]['s'][] = $start;
        }

        if (null !== $end) {
            $this->sequence[$timestamp]['e'][] = $end;
        }
    }
}
