<?php


namespace AutoAdmin\Bundle\Content\Translation;

use AutoAdmin\Bundle\Content\Entity\ContentTranslation;
use AutoAdmin\Bundle\Content\Entity\Locale;
use AutoAdmin\Bundle\Content\Model\LocaleInterface;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Content\Model\TranslationInterface;
use ReflectionClass;
use Symfony\Component\PropertyAccess\PropertyAccessor;

class EntityTranslator
{
    /**
     * @var TranslatableInterface[]
     */
    private array $managed = [];
    private ?LocaleInterface $locale = null;

    /**
     * @var LocaleInterface[]
     */
    private array $fallbackLocales = [];
    private ?PropertyAccessor $propertyAccessor = null;

    public function setPropertyAccessor(PropertyAccessor $propertyAccessor): void
    {
        $this->propertyAccessor = $propertyAccessor;
    }

    public function translate(TranslatableInterface $entity, string $field, ?LocaleInterface $locale = null)
    {
        if ($locale === null) {
            $locale = $this->getLocale();
        }

        $fieldToCheck = explode('.', $field)[0];// first segment
        $translation = $this->getTranslation($entity, $locale);
        $fieldExistsInEntity = $this->fieldExistsInEntity($translation, $fieldToCheck);

        // try in translation object first then in default
        $obj = ($translation && $fieldExistsInEntity) ? $translation : $entity;

        return $this->getPropertyValue($obj, $field, $locale);
    }

    /**
     * Get object property value while handling locale
     * if 'autofill_parameters' config is running across translatable objects, use appropriate locale object
     * (load proper trans before generating segments)
     *
     * @param object $object
     * @param $field
     * @param Locale $locale
     * @return mixed|null
     */
    private function getPropertyValue(object $object, $field, Locale $locale): mixed
    {
        $altered = $this->forcePropertyPathLocale($object, $field, $locale);

        // now the path is using the wanted locale, get path value
        $val = $this->propertyAccessor->getValue($object, $field);

        $this->revertPropertyPathLocale($altered);

        return $val;
    }

    /*
     * Here, set right locale on each field path segments (usually used by 'autofill_parameters')
     * eg: category.parent.slug will need locale on 1, and 2 segments
     */
    private function forcePropertyPathLocale(object $object, $field, Locale $locale): array
    {
        $altered = [];
        $fieldSteps = explode('.', $field);

        if ($object instanceof TranslatableInterface) {
            $altered[] = [$object, $object->getCurrentTranslation()->getLocale()];
        }

        $lastNode = $object;
        foreach ($fieldSteps as $fieldStep) {
            if (empty($lastNode)) {
                break;
            }

            if (
                ($fieldNode = $this->propertyAccessor->getValue($lastNode, $fieldStep))
                && $fieldNode instanceof TranslatableInterface
                && $fieldNode->getCurrentTranslation()->getLocale()->getCode() != $locale->getCode()
            ) {
                $altered[] = [$fieldNode, $fieldNode->getCurrentTranslation()->getLocale()];
                $this->initializeTranslation($fieldNode, $locale);
            }
            $lastNode = $fieldNode;
        }

        return $altered;
    }

    /*
     * Set previous locale back
     */
    private function revertPropertyPathLocale(array $altered)
    {
        // set locale back on objects
        foreach ($altered as [$item, $loc]) {
            $this->initializeTranslation($item, $loc);
        }
    }

    public function getLocale(): ?LocaleInterface
    {
        return $this->locale;
    }

    public function setLocale(string|LocaleInterface $locale): void
    {
        if (is_string($locale)) {
            $locale = new Locale($locale);
        }

        $this->locale = $locale;

        $this->flush();
    }

    public function getTranslation(TranslatableInterface $entity, LocaleInterface $locale): ?TranslationInterface
    {
        $translations = $entity->getTranslations();

        foreach ($translations as $translation) {
            if ($translation->getLocale()->getCode() === $locale->getCode()) {
                return $translation;
            }
        }

        return null;
    }

    private function flush(): void
    {
        foreach ($this->managed as $entity) {
            $this->initializeCurrentTranslation($entity);
        }
    }

    public function initializeCurrentTranslation(TranslatableInterface $entity): string|LocaleInterface|null
    {
        $currentLocale = $this->getLocale();
        $this->initializeTranslation($entity, $currentLocale);

        return $currentLocale;
    }

    /**
     * Load localized translation in translatable.currentTranslation using given locale
     *
     * @param TranslatableInterface $entity
     * @param LocaleInterface|null $locale
     * @return bool
     */
    public function initializeTranslation(TranslatableInterface $entity, LocaleInterface $locale = null): bool
    {
        if ($locale === null) {
            $locale = (new Locale('fr'));
        }
        $this->managed[$this->getId($entity)] = $entity;

        $translation = $this->getTranslation($entity, $locale);
        $entity->setCurrentTranslation($translation);

        return $translation !== null;
    }

    private function getId(TranslatableInterface $translatable): string
    {
        return spl_object_hash($translatable);
    }

    public function detach(TranslatableInterface $translatable): void
    {
        unset($this->managed[$this->getId($translatable)]);
    }


    public function fieldExistsInEntity($entity, string $field): bool
    {
        $reflection = new ReflectionClass($entity);
        $properties = $reflection->getProperties();
        $propertyNames = [];
        foreach ($properties as $property) {
            $propertyNames[] = $property->getName();
        }

        return in_array($field, $propertyNames);
    }

    private function initializeFallbackTranslation(TranslatableInterface $entity): ?string
    {
        $fallbackLocales = $this->getFallbackLocales();
        foreach ($fallbackLocales as $fallback) {
            if ($this->initializeTranslation($entity, $fallback)) {
                return $fallback;
            }
        }

        return null;
    }

    /**
     * @return string[]
     */
    public function getFallbackLocales(): array
    {
        return $this->fallbackLocales;
    }

    /**
     * @param LocaleInterface[] $fallbackLocales
     */
    public function setFallbackLocales(array $fallbackLocales): void
    {
        $this->fallbackLocales = $fallbackLocales;
    }

    public function getTranslatableFieldsFromClass(string $class): array
    {
        $classTranslation = $class.'Translation';
        if (!class_exists($classTranslation)) {
            return [];
        }

        $reflection = new ReflectionClass($classTranslation);
        $properties = $reflection->getProperties();

        $props = [];

        foreach ($properties as $key => $property) {
            if ($property->class === $classTranslation || $property->class === ContentTranslation::class) {
                $props[$key] = $property;
            }
        }

        return $props;
    }
}
