<?php
namespace Newland\Toubiz\Sync\Neos\Importer;

/*
 * 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 Neos\Flow\Annotations as Flow;
use Newland\Toubiz\Api\Constants\EntityListType;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleTypeCityAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleWithLocationDataInterface;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleWithSocialMediaLinksAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\Article\ArticleWithStarRatings;
use Newland\Toubiz\Api\ObjectAdapter\ArticleAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\FileAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\MediumAdapterInterface;
use Newland\Toubiz\Api\Service\WithUuidPredictionService;
use Newland\Toubiz\Api\Service\WithUuidPredictionServiceInterface;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Model\Category;
use Newland\Toubiz\Sync\Neos\Domain\Model\CityData;
use Newland\Toubiz\Sync\Neos\Domain\Model\Medium;
use Newland\Toubiz\Sync\Neos\Domain\Model\RelatedLists\EntityList;
use Newland\Toubiz\Sync\Neos\Domain\Model\RelatedLists\EntityListInterface;
use Newland\Toubiz\Sync\Neos\Domain\Model\RelatedLists\RelatedLists;
use Newland\Toubiz\Sync\Neos\Domain\Model\ZipCode;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;

/**
 * Article importer.
 *
 * @Flow\Scope("singleton")
 */
class ArticleImporter extends AbstractImporter
{
    use WithUuidPredictionService;
    use ClientAware;

    /**
     * @Flow\Inject()
     * @var ArticleRepository
     */
    protected $articleRepository;

    /**
     * @param ArticleAdapterInterface $data
     * @return void
     */
    public function remove(ArticleAdapterInterface $data): void
    {
        $article = $this->findArticle($data);

        if ($article !== null) {
            $this->articleRepository->remove($article);
            $this->persistenceManager->persistAll();
        }
    }

    /**
     * Import method.
     *
     * Persist given data by creating new objects or updating existing ones.
     *
     * @param ArticleAdapterInterface $data
     * @return Article
     */
    public function import($data): Article
    {
        $article = $this->findArticle($data);
        $persisted = $article !== null;

        if ($article === null) {
            $article = new Article();
        }

        $this->mapSimpleValues($data, $article);
        $this->mapRatingValues($data, $article);
        $article->setClient($this->client);
        $this->mapOpeningTimes($data, $article);

        $this->mapGeoCoordinates($data, $article);
        $this->mapAddresses($data, $article);
        $this->mapMainAddress($data, $article);
        $this->mapCategories($data, $article);
        $this->mapFiles($data, $article);
        $this->mapMedia($data, $article);
        $this->mapAttributes($data, $article);

        if ($data instanceof ArticleWithSocialMediaLinksAdapterInterface) {
            $this->mapSocialMediaLinks($data, $article);
        }

        if ($data instanceof ArticleTypeCityAdapterInterface) {
            $this->mapCityData($data, $article);
        }

        if ($data instanceof ArticleWithLocationDataInterface) {
            $this->mapLocationData($data, $article);
        }

        $article->setUpdatedAt(new \DateTime);

        if ($persisted) {
            $this->articleRepository->update($article);
        } else {
            $this->articleRepository->add($article);
        }

        return $article;
    }

    protected function mapSocialMediaLinks(ArticleWithSocialMediaLinksAdapterInterface $data, Article $article): void
    {
        $article->setFacebookUri($data->getFacebookUri());
        $article->setTwitterUri($data->getTwitterUri());
        $article->setInstagramUri($data->getInstagramUri());
        $article->setYoutubeUri($data->getYoutubeUri());
        $article->setWikipediaUri($data->getWikipediaUri());
        $article->setFlickrUri($data->getFlickrUri());
    }


    private function findArticle(ArticleAdapterInterface $data): ?Article
    {
        return $this->articleRepository->withLanguage(
            $data->getLanguage(),
            function () use ($data) {
                return $this->articleRepository->findOneByOriginalIdAndClient(
                    $data->getExternalId(),
                    $this->client
                );
            }
        );
    }

    protected function mapSimpleValues(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setOriginalId($data->getExternalId());
        $article->setMainType($data->getMainType());
        $article->setName($data->getName());
        $article->setProcessedName($data->getName());
        $article->setAbstract($data->getAbstract());
        $article->setDescription($data->getDescription());
        $article->setSourceName($data->getSourceName());
        $article->setAuthorName($data->getAuthorName());
        $article->setBookingUri($data->getBookingUri());
        $article->setDetailUri($data->getDetailUri());
        $article->setLanguage($data->getLanguage());
        $article->setUrlIdentifier($this->generateUrlIdentifierFromData($data));
    }

    protected function mapRatingValues(ArticleAdapterInterface $data, Article $article): void
    {
        if ($data instanceof ArticleWithStarRatings) {
            $this->updateCollection($article->getStarClassifications(), $data->getStarRatings(), function ($adapter) {
                $importer = new StarClassificationImporter();
                $importer->setLanguage($this->language);
                return $importer->import($adapter);
            });
        }

        $article->setAverageRating($data->getAverageRating() ?: 0);
        $article->setNumberOfRatings($data->getNumberOfRatings() ?: 0);
    }

    protected function mapGeoCoordinates(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setLatitude($data->getLatitude());
        $article->setLongitude($data->getLongitude());
    }

    protected function mapAddresses(ArticleAdapterInterface $data, Article $article): void
    {
        $this->updateCollection($article->getAddresses(), $data->getAddresses(), function ($adapter) {
            $addressImporter = new AddressImporter();
            $addressImporter->setLanguage($this->language);
            return $addressImporter->import($adapter);
        });
    }

    protected function mapMainAddress(ArticleAdapterInterface $data, Article $article): void
    {
        $article->setMainAddress(null);

        $mainAddress = null;
        $mainAddressAdapter = $data->getMainAddress();

        if ($mainAddressAdapter) {
            $addressImporter = new AddressImporter();
            $addressImporter->setLanguage($this->language);
            $mainAddress = $addressImporter->import($mainAddressAdapter);
        }

        $article->setMainAddress($mainAddress);
    }

    protected function mapCategories(ArticleAdapterInterface $data, Article $article): void
    {
        $importer = new CategoryImporter();
        $importer->setLanguage($data->getLanguage());
        [ $categories ] = $this->updateCollection(
            $article->getCategories(),
            $data->getCategories(),
            function ($adapter) use ($importer) {
                return $importer->import($adapter);
            }
        );

        $sorting = array_map(
            function (Category $category) {
                return $category->getOriginalId();
            },
            $categories->toArray()
        );

        $article->setRelationSortingForField('categories', $sorting);
    }

    protected function mapMedia(ArticleAdapterInterface $data, Article $article): void
    {
        $existing = [];
        if ($article->getMedia() !== null) {
            foreach ($article->getMedia() as $media) {
                $existing[$media->getOriginalId()] = $media;
            }
        }

        /** @var Collection $media */
        [ $media ] = $this->updateCollection(
            $article->getMedia(),
            $data->getMedia(),
            function (MediumAdapterInterface $adapter) use ($existing) {
                return (new MediumImporter)
                    ->import($adapter, $existing[$adapter->getExternalId()] ?? null);
            }
        );

        $sorting = array_map(function (Medium $medium) {
            return $medium->getOriginalId();
        }, $media->toArray());
        $article->setRelationSortingForField('media', $sorting);

        $mainMediumEntry = $data->getMainMedium();
        $mainMedium = null;
        if ($mainMediumEntry) {
            $existingMainMedium = null;
            foreach ($media as $medium) {
                if ($medium->getOriginalId() === $mainMediumEntry->getExternalId()) {
                    $existingMainMedium = $medium;
                    break;
                }
            }

            $mainMedium = (new MediumImporter())->import($mainMediumEntry, $existingMainMedium);
        }
        $article->setMainMedium($mainMedium);
    }

    protected function mapFiles(ArticleAdapterInterface $data, Article $article): void
    {
        $existing = [];
        foreach ($article->getFiles() as $file) {
            $existing[$file->getOriginalId()] = $file;
        }

        $this->updateCollection(
            $article->getFiles(),
            $data->getFiles(),
            function (FileAdapterInterface $adapter) use ($existing) {
                return (new FileImporter)
                    ->import($adapter, $existing[$adapter->getExternalId()] ?? null);
            }
        );
    }

    public function mapOpeningTimes(ArticleAdapterInterface $data, Article $article): void
    {
        if ($data->getOpeningTimesFormat()) {
            $article->setOpeningTimesFormat((string) $data->getOpeningTimesFormat());
            $article->setOpeningTimes($data->getOpeningTimes() ?? '');
        }
    }

    protected function mapAttributes(ArticleAdapterInterface $data, Article $article): void
    {
        $attributes = $article->getAttributes();
        $attributes->clear();

        $attributesData = $data->getAttributes();
        if ($attributesData) {
            foreach ($attributesData as $attributeData) {
                $importer = new AttributeImporter();
                $importer->setArticle($article);
                $attribute = $importer->import($attributeData);
                if (!$attributes->contains($attribute)) {
                    $attributes->add($attribute);
                }
            }
        }

        $article->setAttributes($attributes);
    }

    protected function mapRelatedLists(ArticleTypeCityAdapterInterface $data, Article $article): void
    {
        if ($data instanceof WithUuidPredictionServiceInterface) {
            $data->setUuidPredictionService($this->uuidPredictionService);
        }

        $relatedListsData = $data->getRelatedLists();

        if ($relatedListsData === null) {
            return;
        }

        $relatedLists = new RelatedLists();
        $poiHighlightsData = $relatedListsData->getHighlightsList();
        if ($poiHighlightsData) {
            $entityList = new EntityList();
            $entityList->setType(EntityListType::ARTICLES);
            $entityList->setTitle($poiHighlightsData->getTitle());
            $entityList->setIdentifiers($poiHighlightsData->getIdentifiers());

            $relatedLists->setPoiHighlights($entityList);
        }

        $entityLists = [];
        foreach ($relatedListsData->getEntityLists() as $entityListData) {
            $entityList = new EntityList();
            $entityList->setType($entityListData->getType());
            $entityList->setTitle($entityListData->getTitle());
            $entityList->setIdentifiers($entityListData->getIdentifiers());

            $entityLists[] = $entityList;
        }

        $relatedLists->setEntityLists($entityLists);

        $article->setRelatedLists($relatedLists);
    }

    private function mapCityData(ArticleTypeCityAdapterInterface $data, Article $article): void
    {
        $this->mapRelatedLists($data, $article);

        $cityData = $article->getCityData() ?? new CityData();
        $cityData->setCity($article);
        $cityData->setIdToubiz($data->getIdToubiz());
        $cityData->setIdTomas($data->getIdTomas());

        $zipCodeImporter = new ZipCodeImporter();
        $zipCodeImporter->setUuidPredictionService($this->uuidPredictionService);
        $this->updateCollection(
            $cityData->getZipCodes(),
            $data->getZipCodes(),
            function ($zipCode) use ($zipCodeImporter) {
                if (preg_match(ZipCode::VALID_PATTERN, $zipCode)) {
                    return $zipCodeImporter->import($zipCode);
                }
                return null;
            }
        );

        $cityData->setClaim($data->getClaim());
        $cityData->setFacts($data->getFacts());
        $cityData->setWebcamUrl($data->getWebcamUrl());
        $cityData->setWebcamDescription($data->getWebcamDescription());
        $cityData->setNews($data->getNews());

        $article->setCityData($cityData);
    }

    private function generateUrlIdentifierFromData(ArticleAdapterInterface $data): string
    {
        return implode(
            '-',
            [
                $data->getExternalId(),
                $this->client,
                $data->getMainType(),
                $data->getLanguage(),
            ]
        );
    }

    private function mapLocationData(ArticleWithLocationDataInterface $data, Article $article): void
    {
        $rawId = $data->getLocationId();
        $ids = $rawId ? explode('#', $rawId) : [];
        $language = $article->getLanguage();

        $this->updateCollection($article->getCities(), $ids, function ($id) use ($language) {
            return $this->articleRepository->withLanguage($language, function () use ($id) {
                return $this->articleRepository->findOneByOriginalId($id);
            });
        });
    }
}
