<?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\ArticleAdapterInterface;
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\OpeningTimes\KitchenTime;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasCanonicalUrl;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasCanonicalUrlInterface;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasClient;
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\HasExternalIds;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasSeoInformation;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\HasSeoInformationInterface;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\SavesRawJson;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\SortableRelationsTrait;
use Newland\Toubiz\Sync\Neos\Domain\Model\Traits\UrlIdentifierTrait;
use Newland\Toubiz\Sync\Neos\Enum\ArticleType;
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 Newland\Toubiz\Sync\Neos\Utility\StringSanitize;
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"}),
 *      @ORM\Index(name="newland_toubiz_sync_neos_article_original_id", columns={"originalid"}),
 *      @ORM\Index(name="newland_toubiz_sync_neos_article_language_grouping", columns={"languageGrouping"}),
 *      @ORM\Index(name="newland_toubiz_sync_neos_article_filtering", columns={
 *          "client", "maintype", "language", "hidden"
 *      }),
 * })
 */
class Article extends AbstractEntity implements
    CustomUuidGeneration,
    HasCanonicalUrlInterface,
    HasSeoInformationInterface,
    RecordConfigurationSubject
{
    use HasCanonicalUrl;
    use HasClient;
    use HasDetailUri;
    use HasExternalIds;
    use HasSeoInformation;
    use SavesRawJson;
    use SortableRelationsTrait;
    use TranslatableEntity;
    use UrlIdentifierTrait;

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

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

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

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

    /**
     * @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="LAZY"
     * )
     * @var Collection<int, Address>
     */
    protected $addresses;

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

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

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

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

    /**
     * @ORM\ManyToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Medium", inversedBy="articles", fetch="LAZY")
     * @var Collection<int, 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<int, File>
     */
    protected $files;

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

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

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

    /**
     * @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\OneToMany(
     *     targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Uri",
     *     mappedBy="articleAsBookingUri",
     *     orphanRemoval=true,
     *     cascade={"persist", "remove"}
     * )
     * @var Collection<int, Uri>
     */
    protected $bookingUris;

    /**
     * @ORM\ManyToMany(
     *     targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\StarClassification",
     *     inversedBy="articles",
     * )
     * @var Collection<int, StarClassification>
     */
    protected $starClassifications;

    /**
     * @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)
     * @Flow\IgnoreValidation()
     * @var RelatedLists|null
     */
    protected $relatedLists;

    /**
     * The two next properties are mappings for a self referencing Many-To-Many Relationship
     * Each City(Article) can have multiple Articles(POI, Gastro, Etc) and every Article(POI, Gastro, Etc)
     * can have multiple cities.
     *
     * the property cities maps the city article as a location
     * the property articlesBelongingToCity maps all articles for a given city
     *
     * Why do we need the explicit JoinTable annotation? If we don't use an explicit declaration of the join table
     * and leave it to Doctrine, it will create a join table with only 1 field called neos_article.
     *
     * Why do we need it on both properties? There is a test for this, and the requirement came
     * exactly because that test failed,
     * we need to explicitly declare on both properties because if the relationship starts on the
     * articlesBelongingToCity property, it would again
     * create a join table with only 1 field called neos_articles and break.
     *
     * So by declaring it like this we ensure the join table is constructed correctly
     * including in the testing environment.
     *
     * @var Collection<int, Article>
     * @ORM\ManyToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Article", inversedBy="articlesBelongingToCity")
     * @ORM\JoinTable(name="newland_toubiz_sync_neos_domain_model_article_locations_join",
     *      joinColumns={@ORM\JoinColumn(name="location", referencedColumnName="persistence_object_identifier")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="article", referencedColumnName="persistence_object_identifier")}
     *   )
     */
    protected $cities;

    /**
     * @var Collection<int, Article>|null
     * @ORM\ManyToMany(targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\Article", mappedBy="cities")
     * @ORM\JoinTable(
     *      name="newland_toubiz_sync_neos_domain_model_article_locations_join",
     *      joinColumns={@ORM\JoinColumn(name="article", referencedColumnName="persistence_object_identifier")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="location", referencedColumnName="persistence_object_identifier")}
     *   )
     */
    protected $articlesBelongingToCity;

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

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

    /**
     * @var int
     */
    protected $randomSorting = 0;

    /**
     * @var string|null
     */
    protected $sourceSystem;

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

    /**
     * @var Collection<int, KitchenTime>
     * @ORM\OneToMany(
     *     targetEntity="Newland\Toubiz\Sync\Neos\Domain\Model\OpeningTimes\KitchenTime",
     *     mappedBy="article",
     *     fetch="LAZY",
     *     cascade={"REMOVE"}
     * )
     * @ORM\OrderBy({ "sorting" = "ASC" })
     */
    protected $kitchenTimes;

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

    public function __construct()
    {
        $this->addresses = new ArrayCollection();
        $this->categories = new ArrayCollection();
        $this->files = new ArrayCollection();
        $this->media = new ArrayCollection();
        $this->attributes = new ArrayCollection();
        $this->articlesBelongingToCity = new ArrayCollection();
        $this->eventsBelongingToCity = new ArrayCollection();
        $this->cities = new ArrayCollection();
        $this->starClassifications = new ArrayCollection();
        $this->bookingUris = new ArrayCollection();
        $this->kitchenTimes = new ArrayCollection();
        $this->awards = new ArrayCollection();
    }

    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 getHumanReadableMainType(): ?string
    {
        return ArticleType::$map[$this->mainType] ?? null;
    }

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

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

    public function setProcessedName(string $name): void
    {
        $process = StringSanitize::sanitize($name);
        $this->processedName = $process;
    }

    public function getProcessedName(): string
    {
        return $this->processedName;
    }

    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;
    }

    /** @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;
    }

    public function getMainCategory(): ?Category
    {
        if ($this->mainCategory) {
            return $this->mainCategory;
        }

        $categoriesSorted = $this->getCategoriesSorted();

        return $categoriesSorted[0] ?? null;
    }

    public function setMainCategory(?Category $category): void
    {
        $this->mainCategory = $category;
    }

    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 setCategorySorting(array $categorySorting): void
    {
        $this->setRelationSortingForField('categories', $categorySorting);
        $this->categoriesSorted = null;
    }

    /** @param Collection<int, Address> $addresses */
    public function setAddresses(Collection $addresses): void
    {
        $this->addresses = $addresses;
    }

    /** @return Collection<int, Address> */
    public function getAddresses(): Collection
    {
        return $this->addresses;
    }

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

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

    /** @param Collection<int, Attribute> $attributes */
    public function setAttributes(Collection $attributes): void
    {
        $this->attributes = $attributes;
    }

    /** @return Collection<int, Attribute> */
    public function getAttributes(): Collection
    {
        return $this->attributes;
    }

    /** @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 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 getImagesSorted(): array
    {
        return array_filter(
            $this->getMediaSorted(),
            function (Medium $medium) {
                return $medium->getType() === 'image';
            }
        );
    }

    public function getVideosSorted(): array
    {
        return array_filter(
            $this->getMediaSorted(),
            function (Medium $medium) {
                return $medium->getType() === 'video';
            }
        );
    }

    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;
    }

    /** @param Collection<int, File> $files */
    public function setFiles(Collection $files): void
    {
        $this->files = $files;
    }

    /** @return Collection<int, File> */
    public function getFiles(): Collection
    {
        return $this->files;
    }

    /** @return Collection<int, Uri> */
    public function getBookingUris(): Collection
    {
        return $this->bookingUris;
    }

    /** @param Collection<int, Uri> $bookingUris */
    public function setBookingUris(Collection $bookingUris): void
    {
        $this->bookingUris = $bookingUris;
    }

    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;
    }

    /** @return Collection<int, StarClassification> */
    public function getStarClassifications(): Collection
    {
        return $this->starClassifications;
    }

    /** @param Collection<int, StarClassification> $starClassifications */
    public function setStarClassifications(Collection $starClassifications): void
    {
        $this->starClassifications = $starClassifications;
    }


    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 !== null && $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
    {
        return $this->sourceSystem === ArticleAdapterInterface::SOURCE_OUTDOORACTIVE;
    }

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

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

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

        // Outdooractive adapter uses prefixed original id.
        return str_replace(TourAttributes::DATA_SOURCE_OUTDOOR_ACTIVE . '_', '', $this->originalId);
    }

    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
     */
    public function getFirstAttribute(string $name, $default = null)
    {
        $value = $default;

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

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

        return $value;
    }

    /**
     * @return array<string, AttributeCollection>
     */
    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);
    }

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

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

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

    public function setHidden(bool $hidden): void
    {
        $this->hidden = $hidden;
    }

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

    public function getRandomSorting(): int
    {
        return $this->randomSorting;
    }

    public function setRandomSorting(int $randomSorting): void
    {
        $this->randomSorting = $randomSorting;
    }

    public function getSourceSystem(): ?string
    {
        return $this->sourceSystem;
    }

    public function setSourceSystem(string $sourceSystem): void
    {
        $this->sourceSystem = $sourceSystem;
    }

    public function getSourceApiVersion(): ?string
    {
        return $this->sourceApiVersion;
    }

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

    /**
     * @return Collection<int, KitchenTime>
     */
    public function getKitchenTimes(): Collection
    {
        return $this->kitchenTimes;
    }

    /**
     * @param Collection<int, KitchenTime> $kitchenTimes
     */
    public function setKitchenTimes(Collection $kitchenTimes): void
    {
        $this->kitchenTimes = $kitchenTimes;
    }

    /**
     * @return Collection<int, Award>
     */
    public function getAwards(): Collection
    {
        return $this->awards;
    }

    /**
     * @return Collection<int, Award>
     */
    public function getAwardsForList(): Collection
    {
        return $this->awards->filter(
            function (Award $award) {
                return $award->getShowInList();
            }
        );
    }

    /**
     * @param Collection<int, Award> $awards
     */
    public function setAwards(Collection $awards): void
    {
        $this->awards = $awards;
    }

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

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