<?php declare(strict_types=1);
namespace Newland\Toubiz\Sync\Neos\Domain\Model;

/*
 * This file is part of the "toubiz-sync-neos" package.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 */

use DateTime;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Utility\Now;
use Newland\Toubiz\Api\Constants\EventScope;
use Newland\Toubiz\Api\ObjectAdapter\Concern\EventConstants;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasClient;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\SavesRawJson;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\UrlIdentifierTrait;
use Newland\Toubiz\Sync\Neos\Orm\Uuid\CustomUuidGeneration;
use Newland\Toubiz\Sync\Neos\Orm\Uuid\UuidGenerator;
use Newland\Toubiz\Sync\Neos\Translation\TranslatableEntity;
use Ramsey\Uuid\UuidInterface;

/**
 * @Flow\Entity
 * @ORM\Table(indexes={
 *      @ORM\Index(name="newland_toubiz_sync_neos_event_import_ident", columns={"language", "originalid"})
 * })
 */
class Event extends AbstractEntity implements CustomUuidGeneration, RecordConfigurationSubject
{
    use TranslatableEntity,
        UrlIdentifierTrait,
        HasClient,
        SavesRawJson;

    public function generateUuid(): UuidInterface
    {
        return UuidGenerator::uuidFromProperties([ $this->originalId, $this->language, $this->client ]);
    }

    /** @var string */
    protected $dataSource = '';

    /** @var string */
    protected $eventType;

    /** @var string */
    protected $title;

    /**
     * @var string|null
     * @ORM\Column(type="text", nullable=true)
     */
    protected $description;

    /**
     * Additional information about the location.
     *
     * @var string|null
     * @ORM\Column(type="text", nullable=true)
     */
    protected $additionalInformation;

    /**
     * Information about admission.
     *
     * @var string|null
     * @ORM\Column(type="text", nullable=true)
     */
    protected $admission;

    /** @var bool */
    protected $isHighlight = false;

    /** @var bool */
    protected $isTipp = false;

    /** @var bool */
    protected $hidden = false;

    /**
     * Date and time of first day's beginning.
     *
     * @var DateTimeInterface|null
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $beginsAt;

    /**
     * Date and time of last day's ending.
     *
     * @var DateTimeInterface|null
     * @ORM\Column(type="datetime", nullable=true)
     */
    protected $endsAt;

    /**
     * URI to buy a ticket.
     *
     * @var string|null
     * @ORM\Column(type="text", nullable=true)
     */
    protected $ticketUri;

    /**
     * Contact options for buying tickets.
     *
     * @var string|null
     * @ORM\Column(type="text", nullable=true)
     */
    protected $ticketContact;

    /**
     * The link for additional information.
     *
     * @var string|null
     * @ORM\Column(type="text", nullable=true)
     */
    protected $link;

    /**
     * @ORM\ManyToMany(
     *     targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Category",
     *     inversedBy="events",
     *     fetch="EAGER"
     * )
     * @var Collection<int, Category>
     */
    protected $categories;

    /**
     * @ORM\OneToMany(
     *     targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\EventDate",
     *     mappedBy="event",
     *     cascade={"remove", "persist"},
     *     orphanRemoval=true
     * )
     * @ORM\OrderBy({"beginsAt"="ASC"})
     * @var Collection<int, EventDate>
     */
    protected $eventDates;

    /**
     * @ORM\ManyToMany(
     *     targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\EventTag",
     *     inversedBy="events",
     *     fetch="EAGER",
     *     orphanRemoval=true,
     *     cascade={"persist"}
     * )
     * @var Collection<int, EventTag>
     */
    protected $eventTags;

    /**
     * @ORM\ManyToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Medium", inversedBy="events", fetch="LAZY")
     * @var Collection<int, Medium>
     */
    protected $media;

    /** @var DateTimeInterface */
    protected $updatedAt;

    /**
     * @var Address|null
     * @ORM\ManyToOne(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Address")
     */
    protected $location;

    /**
     * @var Address|null
     * @ORM\ManyToOne(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Address")
     */
    protected $organizer;

    /**
     * @var string|null
     * @ORM\Column(nullable=true)
     */
    protected $sourceName;

    /**
     * @ORM\Column(type="array", nullable=true)
     * @var array|null
     */
    protected $attributes;

    /**
     * @ORM\Column(type="smallint", nullable=true)
     * @var int|null
     */
    protected $scope;

    /**
     * @var Collection<int, Article>
     * @ORM\ManyToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Article", inversedBy="eventsBelongingToCity")
     * @ORM\JoinTable(name="newland_toubiz_sync_neos_domain_model_event_locations_join")
     */
    protected $cities;

    /**
     * @var Article
     * @Flow\Transient()
     * @deprecated Events can now be connected to many cities. This simply returns the first one.
     */
    protected $city;

    /**
     * @var string
     * @ORM\Column(type="text")
     */
    protected $additionalSearchString = '';

    /**
     * @var Now
     * @Flow\Inject()
     */
    protected $now;

    public function __construct()
    {
        $this->categories = new ArrayCollection();
        $this->eventDates = new ArrayCollection();
        $this->media = new ArrayCollection();
        $this->cities = new ArrayCollection();
        $this->eventTags = new ArrayCollection();
        $this->updatedAt = new DateTime;
    }

    public function getDataSource(): string
    {
        return $this->dataSource;
    }

    public function setDataSource(string $dataSource): void
    {
        $this->dataSource = $dataSource;
    }

    public function getEventType(): string
    {
        return $this->eventType;
    }

    public function setEventType(string $eventType): void
    {
        $this->eventType = $eventType;
    }

    public function setTitle(string $title): void
    {
        $this->title = $title;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function setDescription(?string $description): void
    {
        $this->description = $description;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setAdditionalInformation(?string $additionalInformation): void
    {
        $this->additionalInformation = $additionalInformation;
    }

    public function getAdditionalInformation(): ?string
    {
        return $this->additionalInformation;
    }

    public function setAdmission(?string $admission): void
    {
        $this->admission = $admission;
    }

    public function getAdmission(): ?string
    {
        return $this->admission;
    }

    public function setUpdatedAt(DateTimeInterface $updatedAt): void
    {
        $this->updatedAt = $updatedAt;
    }

    public function getUpdatedAt(): DateTimeInterface
    {
        return $this->updatedAt;
    }

    public function setIsHighlight(bool $highlight): void
    {
        $this->isHighlight = $highlight;
    }

    public function getIsHighlight(): bool
    {
        return $this->isHighlight;
    }

    public function setIsTipp(bool $tipp): void
    {
        $this->isTipp = $tipp;
    }

    public function getIsTipp(): bool
    {
        return $this->isTipp;
    }

    public function setBeginsAt(?DateTimeInterface $beginsAt): void
    {
        $this->beginsAt = $beginsAt;
    }

    public function getBeginsAt(): ?DateTimeInterface
    {
        return $this->beginsAt;
    }

    public function setTicketUri(?string $ticketUri): void
    {
        $this->ticketUri = $ticketUri;
    }

    public function getTicketUri(): ?string
    {
        return $this->ticketUri;
    }

    public function setTicketContact(?string $ticketContact): void
    {
        $this->ticketContact = $ticketContact;
    }

    public function getTicketContact(): ?string
    {
        return $this->ticketContact;
    }

    public function setLink(?string $link): void
    {
        $this->link = $link;
    }

    public function getLink(): ?string
    {
        return $this->link;
    }

    public function getBeginsAtSpecificTime(): bool
    {
        if ($this->beginsAt === null) {
            return false;
        }

        return (int) $this->beginsAt->format('H') > 0;
    }

    public function setEndsAt(?DateTimeInterface $endsAt): void
    {
        $this->endsAt = $endsAt;
    }

    public function getEndsAt(): ?DateTimeInterface
    {
        return $this->endsAt;
    }

    public function setLocation(Address $location = null): void
    {
        $this->location = $location;
    }

    public function getLocation(): ?Address
    {
        return $this->location;
    }

    public function setOrganizer(Address $organizer = null): void
    {
        $this->organizer = $organizer;
    }

    public function getOrganizer(): ?Address
    {
        return $this->organizer;
    }

    /** @param Collection<int, Category> $categories */
    public function setCategories(Collection $categories): void
    {
        $this->categories = $categories;
    }

    /** @return Collection<int, Category> */
    public function getCategories(): Collection
    {
        return $this->categories;
    }

    /** @param Collection<int, EventDate> $eventDates */
    public function setEventDates(Collection $eventDates): void
    {
        $this->eventDates = $eventDates;
    }

    /** @return Collection<int, EventDate> */
    public function getEventDates(): Collection
    {
        return $this->eventDates;
    }

    /** @param Collection<int, EventTag> $eventTags */
    public function setEventTags(Collection $eventTags): void
    {
        $this->eventTags = $eventTags;
    }

    /** @return Collection<int, EventTag> */
    public function getEventTags(): Collection
    {
        return $this->eventTags;
    }

    public function getAdditionalSearchString(): string
    {
        return $this->additionalSearchString;
    }

    public function setAdditionalSearchString(?string $additionalSearchString): void
    {
        $this->additionalSearchString = $additionalSearchString ?? '';
    }

    public function getUpcomingEventDates(): array
    {
        $midnightTimestamp = $this->now
            ->setTime(0, 0, 0)
            ->getTimestamp();

        // TODO Also return collection instead of array to have uniform return types
        return array_filter(
            $this->eventDates->toArray(),
            static function (EventDate $date) use ($midnightTimestamp) {
                return $date->getBeginsAt() === null
                    || $date->getBeginsAt()->getTimestamp() >= $midnightTimestamp;
            }
        );
    }

    public function getAnnotatedEventDates(): array
    {
        // TODO Also return collection instead of array to have uniform return types
        return array_filter(
            $this->eventDates->toArray(),
            function (EventDate $date) {
                return !empty($date->getNote());
            }
        );
    }

    /**
     * Get event dates in groups.
     *
     * Each event date record represents one single day
     * which may be grouped into a date group in case
     * a bunch of days represents multiple days (like a "week").
     *
     * @return array
     */
    public function getUpcomingEventDateGroups(): array
    {
        // TODO Also return collection instead of array to have uniform return types
        $groups = [];

        /** @var EventDate|null $groupStartDate */
        $groupStartDate = null;

        /** @var EventDate|null $groupDate */
        $groupDate = null;

        /** @var EventDate|null $currentDate */
        $currentDate = null;

        /** @var EventDate $currentDate */
        foreach ($this->eventDates->toArray() as $currentDate) {
            if ($currentDate->getBeginsAt() === null) {
                continue;
            }

            if ($currentDate->getIsInThePast()) {
                continue;
            }

            if (!isset($previousDate)) {
                // Initialize on a new group search and skip current iteration.
                $groupStartDate = $currentDate;
                $previousDate = $currentDate;
                continue;
            }

            $interval = $currentDate->getBeginsAt()->diff($previousDate->getBeginsAt());
            if ($groupStartDate !== null && $previousDate !== null && $interval->format('%d') > 1) {
                // On a difference bigger than one day, a group ends.
                $groupDate = new EventDate();
                $groupDate->setBeginsAt($groupStartDate->getBeginsAt());
                $groupDate->setEndsAt($previousDate->getEndsAt());
                $groups[] = $groupDate;

                // Reset previous date to ensure new initialization on next iteration.
                $groupStartDate = $previousDate = $groupDate = null;
                continue;
            }

            // Assign previous date for next iteration.
            $previousDate = $currentDate;
        }

        /*
         * There is either an unclosed group date or the last iteration does
         * not count its current date in, hence it must be added manually.
         */
        if ($groupDate !== null && $currentDate !== null) {
            $groupDate = new EventDate;
            $groupDate->setBeginsAt($currentDate->getBeginsAt());
        }
        if ($groupDate !== null && $currentDate !== null) {
            $groupDate->setEndsAt($currentDate->getEndsAt());
        }
        $groups[] = $groupDate;

        return $groups;
    }

    public function getEndsOnSameDay(): bool
    {
        if ($this->beginsAt === null || $this->endsAt === null) {
            return true;
        }

        return $this->beginsAt->format('Y-m-d') === $this->endsAt->format('Y-m-d');
    }

    /** @param Collection<int, Medium> $media */
    public function setMedia(Collection $media): void
    {
        $this->media = $media;
    }

    /** @return  Collection<int, Medium> */
    public function getMedia(): Collection
    {
        return $this->media;
    }

    public function setAttributes(array $attributes): void
    {
        $this->attributes = $attributes;
    }

    public function getAttributes(): array
    {
        $attributes = (array) $this->attributes;

        if ($this->getGuestCardAccepted()) {
            $key = array_search(EventConstants::ATTRIBUTE_GUEST_CARD, $attributes['features'], true);
            unset($attributes['features'][$key]);
        }

        return $attributes;
    }

    public function setSourceName(?string $sourceName): void
    {
        $this->sourceName = $sourceName;
    }

    public function getSourceName(): ?string
    {
        return $this->sourceName;
    }

    public function getScope(): int
    {
        return (int) $this->scope;
    }

    public function setScope(int $scope): void
    {
        if (EventScope::validValue($scope)) {
            $this->scope = $scope;
        }
    }

    /**
     * The guest card attribute is imported as a regular attribute but needs to be displayed
     * separately. This is a helper method for that.
     *
     * @return bool
     */
    public function getGuestCardAccepted(): bool
    {
        return is_array($this->attributes)
            && array_key_exists('features', $this->attributes)
            && is_array($this->attributes['features'])
            && in_array(EventConstants::ATTRIBUTE_GUEST_CARD, $this->attributes['features'], true);
    }

    /**
     * @param Collection<int, Article> $cities
     */
    public function setCities(Collection $cities): void
    {
        $this->cities = $cities;
    }

    /**
     * @return Collection<int, Article>
     */
    public function getCities(): Collection
    {
        return $this->cities;
    }

    public function getCity(): ?Article
    {
        return $this->cities->first() ?: null;
    }

    public function isHidden(): bool
    {
        return $this->hidden;
    }

    public function applyRecordConfiguration(RecordConfiguration $configuration): void
    {
        $this->hidden = (bool) ($configuration->getConfiguration()['hidden'] ?? false);
        foreach ($this->eventDates as $date) {
            $date->setHidden($this->hidden);
        }
    }

    public function isOnDemand(): bool
    {
        return $this->beginsAt === null || $this->endsAt === null;
    }

    public function isExpired(): bool
    {
        if ($this->isOnDemand()) {
            return false;
        }

        $now = new DateTime();
        return $this->endsAt < $now
            && $this->beginsAt->format('Y-m-d H:i:s') !== $this->endsAt->format('Y-m-d H:i:s');
    }
}
