<?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 Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Neos\Flow\Annotations as Flow;
use Newland\Toubiz\Api\ObjectAdapter\Attributes\PointOfInterestAttributes;
use Newland\Toubiz\Api\ObjectAdapter\Attributes\TourAttributes;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Api\Utility\ArrayUtility;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasDetailUri;
use Newland\Toubiz\Sync\Neos\Domain\Model\RelatedLists\RelatedLists;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\SortableRelationsTrait;
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\Service\AttributeCollection;
use Newland\Toubiz\Sync\Neos\Translation\TranslatableEntity;
use Ramsey\Uuid\UuidInterface;

/**
 * An article.
 *
 * This represents several article types, like a "point of interest",
 * a lodging, a tour, etc.
 *
 * @Flow\Entity
 * @ORM\Table(indexes={
 *      @ORM\Index(name="newland_toubiz_sync_neos_article_import_ident", columns={"language", "originalid"})
 * })
 */
class Article extends AbstractEntity implements CustomUuidGeneration
{
    use SortableRelationsTrait,
        TranslatableEntity,
        UrlIdentifierTrait,
        HasDetailUri;


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

    /**
     * @var int
     */
    protected $mainType;

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

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

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

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

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

    /**
     * @var Address|null
     * @ORM\ManyToOne(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Address", cascade={"persist"})
     */
    protected $mainAddress;

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

    /**
     * @ORM\OneToOne(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\CityData", inversedBy="city")
     * @var CityData|null
     */
    protected $cityData;

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

    /**
     * @Flow\Transient
     * @Flow\IgnoreValidation
     * @var Category[]
     */
    protected $categoriesSorted;

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

    /**
     * @Flow\Transient
     * @Flow\IgnoreValidation
     * @var Medium[]
     */
    protected $mediaSorted;

    /**
     * @ORM\ManyToOne(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Medium", cascade={"persist"})
     * @var Medium|null
     */
    protected $mainMedium;

    /**
     * @ORM\ManyToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\File", inversedBy="articles", fetch="LAZY",
     *     orphanRemoval=true, cascade={"persist", "remove"})
     * @var Collection<File>
     */
    protected $files;

    /**
     * @ORM\OneToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Attribute", mappedBy="article",
     *     orphanRemoval=true, fetch="EAGER", cascade={"persist", "remove"})
     * @var Collection<Attribute>
     */
    protected $attributes;

    /**
     * Transient property for simple caching.
     *
     * @Flow\Transient()
     * @Flow\IgnoreValidation()
     * @var AttributeCollection[]|null
     */
    private $flatAttributes;

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

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

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

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

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

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

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

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

    /**
     * URI for booking this article.
     * @ORM\Column(nullable=true)
     * @var string|null
     */
    protected $bookingUri;

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

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

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

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

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

    /**
     * Opening times as raw data as given by the source api.
     * Will be parsed by JavaScript.
     *
     * @ORM\Column(type="text", nullable=true)
     * @var string|null
     */
    protected $openingTimes;

    /**
     * The format of the opening times data.
     * There should be a JavaScript parser available for every format.
     *
     * @ORM\Column(nullable=true)
     * @var string|null
     */
    protected $openingTimesFormat;

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

    public function __construct()
    {
        $this->addresses = new ArrayCollection();
        $this->categories = new ArrayCollection();
        $this->files = new ArrayCollection();
        $this->media = new ArrayCollection();
        $this->attributes = new ArrayCollection();
    }

    public function initializeObject(): void
    {
        // Support legacy data until next full article sync.
        if ($this->mainMedium === null && !empty($this->getMediaSorted())) {
            $this->mainMedium = $this->mediaSorted[0];
        }
    }

    public function getMainType(): int
    {
        return $this->mainType;
    }

    public function setMainType(int $type): void
    {
        $this->mainType = $type;
    }

    public function getIsAttraction(): bool
    {
        return $this->mainType === ArticleConstants::TYPE_ATTRACTION;
    }

    public function getIsGastronomy(): bool
    {
        return $this->mainType === ArticleConstants::TYPE_GASTRONOMY;
    }

    public function getIsTour(): bool
    {
        return $this->mainType === ArticleConstants::TYPE_TOUR;
    }

    public function getIsLodging(): bool
    {
        return $this->mainType === ArticleConstants::TYPE_LODGING;
    }

    public function getIsDirectMarketer(): bool
    {
        return $this->mainType === ArticleConstants::TYPE_DIRECT_MARKETER;
    }

    public function getIsCity(): bool
    {
        return $this->mainType === ArticleConstants::TYPE_CITY;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }

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

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

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

    public function getAbstract(): ?string
    {
        return $this->abstract;
    }

    public function setCategories(Collection $categories): void
    {
        $this->categories = $categories;
    }

    public function getCategories(): Collection
    {
        return $this->categories;
    }

    public function getMainCategory(): ?Category
    {
        $categoriesSorted = $this->getCategoriesSorted();

        return $categoriesSorted[0] ?? null;
    }

    public function getCategoriesSorted(): array
    {
        // Sort and cache relations only on first call.
        if ($this->categoriesSorted === null) {
            $this->categoriesSorted = $this->getSortedRelations('categories', 'originalId');
        }

        return $this->categoriesSorted;
    }

    public function setAddresses(Collection $addresses): void
    {
        $this->addresses = $addresses;
    }

    public function getAddresses(): Collection
    {
        return $this->addresses;
    }

    public function getCityData(): ?CityData
    {
        return $this->cityData;
    }

    public function setCityData(?CityData $cityData): void
    {
        $this->cityData = $cityData;
    }

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

    public function getAttributes(): Collection
    {
        return $this->attributes;
    }

    public function setMedia(Collection $media): void
    {
        $this->media = $media;
    }

    public function getMedia(): Collection
    {
        return $this->media;
    }

    public function getMediaSorted(): array
    {
        // Sort and cache relations only on first call.
        if ($this->mediaSorted === null) {
            $this->mediaSorted = $this->getSortedRelations('media', 'originalId');
        }

        return $this->mediaSorted;
    }

    public function setMainMedium(?Medium $mainMedium): void
    {
        $this->mainMedium = $mainMedium;
    }

    public function getMainMedium(): ?Medium
    {
        return $this->mainMedium;
    }

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

    public function getFacebookUri(): ?string
    {
        return $this->facebookUri;
    }

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

    public function getTwitterUri(): ?string
    {
        return $this->twitterUri;
    }

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

    public function getInstagramUri(): ?string
    {
        return $this->instagramUri;
    }

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

    public function getYoutubeUri(): ?string
    {
        return $this->youtubeUri;
    }

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

    public function getFlickrUri(): ?string
    {
        return $this->flickrUri;
    }

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

    public function getWikipediaUri(): ?string
    {
        return $this->wikipediaUri;
    }

    public function setFiles(Collection $files): void
    {
        $this->files = $files;
    }

    public function getFiles(): Collection
    {
        return $this->files;
    }

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

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

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

    public function getAuthorName(): ?string
    {
        return $this->authorName;
    }

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

    public function getBookingUri(): ?string
    {
        return $this->bookingUri;
    }

    public function getClient(): ?string
    {
        return $this->client;
    }

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

    public function getOpeningTimes(): ?string
    {
        return $this->openingTimes;
    }

    public function setOpeningTimes(string $openingTimes): void
    {
        $this->openingTimes = $openingTimes;
    }

    public function getOpeningTimesFormat(): ?string
    {
        return $this->openingTimesFormat;
    }

    public function setOpeningTimesFormat(string $openingTimesFormat): void
    {
        $this->openingTimesFormat = $openingTimesFormat;
    }

    public function getStarClassification(): int
    {
        return $this->starClassification ?? 0;
    }

    public function setStarClassification(int $starClassification): void
    {
        $this->starClassification = $starClassification;
    }

    public function getAverageRating(): int
    {
        return $this->averageRating ?? 0;
    }

    public function setAverageRating(int $averageRating): void
    {
        $this->averageRating = $averageRating;
    }

    public function getNumberOfRatings(): int
    {
        return $this->numberOfRatings ?? 0;
    }

    public function setNumberOfRatings(int $numberOfRatings): void
    {
        $this->numberOfRatings = $numberOfRatings;
    }

    public function jsonSerialize(): array
    {
        $category = null;
        if ($this->categories->count() > 0) {
            $category = $this->getMainCategory();
        }

        $tour = null;
        if ($this->getIsTour() && $this->hasAttribute(TourAttributes::GEOMETRY)) {
            $tour = $this->getFirstAttribute(TourAttributes::GEOMETRY);
        }

        $image = null;
        if ($this->media->count() > 0) {
            $image = [
                'url' => $this->getMediaSorted()[0]->getSourceUri(),
                'alt' => $this->getMediaSorted()[0]->getTitle(),
            ];
        }

        $latitude = $this->latitude ?? ($this->mainAddress ? $this->mainAddress->getLatitude() : null);
        $longitude = $this->longitude ?? ($this->mainAddress ? $this->mainAddress->getLongitude() : null);

        return [
            'name' => $this->name,
            'lat' => $latitude,
            'lng' => $longitude,
            'category' => $category ? $category->getOriginalId() : null,
            'categoryTitle' => $category ? $category->getTitle() : null,
            'tour' => $tour,
            'image' => $image,
            'isTour' => $this->getIsTour(),
            'difficultyRating' => $this->getDifficultyRating(),
            'outdoorActiveTrackingId' => $this->getOutdoorActiveId(),
            'url' => null,
        ];
    }

    public function isOutdoorActiveTour(): bool
    {
        if (!$this->getIsTour() || !$this->hasAttribute(TourAttributes::DATA_SOURCE)) {
            return false;
        }

        $source = $this->getFirstAttribute(TourAttributes::DATA_SOURCE);
        return $source === TourAttributes::DATA_SOURCE_OUTDOOR_ACTIVE;
    }

    public function getOutdoorActiveId(): ?string
    {
        if (!$this->isOutdoorActiveTour()) {
            return null;
        }
        return  $this->getFirstAttribute(TourAttributes::DATA_SOURCE_ID);
    }

    public function getLatitude(): ?float
    {
        return $this->latitude;
    }

    public function setLatitude(?float $latitude): void
    {
        $this->latitude = $latitude;
    }

    public function getLongitude(): ?float
    {
        return $this->longitude;
    }

    public function setLongitude(?float $longitude): void
    {
        $this->longitude = $longitude;
    }

    public function getMainAddress(): ?Address
    {
        return $this->mainAddress;
    }

    public function setMainAddress(?Address $mainAddress): void
    {
        $this->mainAddress = $mainAddress;
    }

    /**
     * @return mixed|null
     */
    private function getDifficultyRating()
    {
        return $this->getFirstAttribute(TourAttributes::DIFFICULTY_RATING);
    }

    private function hasAttribute(string $name): bool
    {
        foreach ($this->attributes as $attribute) {
            if ($attribute->getName() === $name) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param string $name
     * @param mixed $default
     * @return mixed|null
     */
    private function getFirstAttribute(string $name, $default = null)
    {
        $value = $default;

        /** @var Attribute $attribute */
        $attributeObject = $this->attributes->filter(
            function (Attribute $attribute) use ($name) {
                return $attribute->getName() === $name;
            }
        )->first();

        if ($attributeObject) {
            $value = $attributeObject->getData();
        }

        return $value;
    }

    public function getFlatAttributes(): array
    {
        if (empty($this->flatAttributes)) {
            $this->generateFlatAttributes();
        }

        return (array) $this->flatAttributes;
    }

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

    public function getUpdatedAt(): ?\DateTime
    {
        return $this->updatedAt;
    }

    /**
     * @return string[][]
     */
    public function getPriceTable(): array
    {
        $table = $this->getFirstAttribute(PointOfInterestAttributes::PRICES);
        if (ArrayUtility::isTwoDimensionalArrayOfPrimitives($table)) {
            return $table;
        }
        return [];
    }

    public function getRelatedLists(): ?RelatedLists
    {
        return $this->relatedLists;
    }

    public function setRelatedLists(RelatedLists $relatedLists = null): void
    {
        $this->relatedLists = $relatedLists;
    }

    private function generateFlatAttributes(): void
    {
        $this->flatAttributes = [];

        /** @var Attribute $attribute */
        foreach ($this->attributes as $attribute) {
            if (!array_key_exists($attribute->getName(), $this->flatAttributes)) {
                $this->flatAttributes[$attribute->getName()] = new AttributeCollection();
            }
            $this->flatAttributes[$attribute->getName()]->add($attribute);
        }
    }

    public function getHasGeocoordinates(): bool
    {
        return ($this->hasLatitude() && $this->hasLongitude());
    }

    public function hasLatitude(): bool
    {
        return !empty($this->latitude);
    }

    public function hasLongitude(): bool
    {
        return !empty($this->longitude);
    }
}
