<?php

declare(strict_types=1);

namespace BeyondSEO\Domain\Base\Repo\RC\Traits;

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

use BeyondSEODeps\DDD\Domain\Base\Entities\DefaultObject;
use BeyondSEODeps\DDD\Domain\Base\Entities\Entity;
use BeyondSEODeps\DDD\Domain\Base\Entities\EntitySet;
use BeyondSEODeps\DDD\Domain\Base\Entities\LazyLoad\LazyLoadRepo;
use BeyondSEODeps\DDD\Domain\Base\Entities\ObjectSet;
use BeyondSEODeps\DDD\Domain\Base\Entities\ValueObject;
use BeyondSEODeps\DDD\Infrastructure\Exceptions\InternalErrorException;
use BeyondSEODeps\DDD\Infrastructure\Reflection\ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionUnionType;

/**
 * Extends an entity with ArgusLoad capabilities. It makes out of an entity an ArgusRepo Entity.
 * It was a specific design decision to make Argus Repository extending Domain entities in order to reduce overhead
 * since Argus Repository entities are thought to be extension of domain entities with basically the same properties but extended through ArgusLoad capabilities
 */
trait RCTrait
{
    use RCLoadTrait;

    /** @var bool Used for easy identification of current class beeing an Argus Repo Class */

    public static bool $isArgusEntity = true;

    /** @var string private caching for cacheKey (we want to avoid to call multiple times uniqueKey during loading operations */
    private string $cacheKey;

    /**
     * copies all public properties from entity to this Argus Object
     * operates recursively and creates if possible Argus Repo Objects from Entities
     * @param Entity|ValueObject $entity
     * @return static
     * @throws InternalErrorException
     * @throws ReflectionException
     */
    public function fromEntity(Entity|ValueObject &$entity): static
    {
        $propertyNamesToSkip = [
            'children' => true,
            'parent' => true,
            'elementsByUniqueKey' => true,
            'objectType' => true
        ];
        if ($parent = $entity->getParent()) {
            $this->setParent($parent);
        }
        if (!($this instanceof $entity)) {
            throw new InternalErrorException(
                'Argus Entity can be imported only from the same Class, e.g. Entities\Domain > Argus\Domain'
            );
        }
        $entityReflectionClass = $entity::getReflectionClass();
        foreach ($entity->getProperties() as $property) {
            $propertyName = $property->getName();
            if (isset($propertyNamesToSkip[$propertyName])) {
                continue;
            }
            if (!isset($entity->$propertyName)) {
                continue;
            }
            $typeName = null;
            $type = $property->getType();
            if ($type instanceof ReflectionNamedType) {
                $typeName = $type->getName();
            }
            // treating ObjectSets
            if ($propertyName == 'elements' && $this instanceof ObjectSet) {
                /** @var Entity $element */
                foreach ($entity->elements as $element) {
                    $rcClassName = $element::getRepoClass(LazyLoadRepo::RC);
                    if ($rcClassName) {
                        /** @var RCTrait $rcInstance */
                        $rcInstance = new $rcClassName();
                        $rcInstance->fromEntity($element);
                        $rcInstance->setParent($this);
                        $this->add($rcInstance);
                    } else {
                        $element->setParent($this);
                        $this->add($element);
                    }
                }
                continue;
            }

            $typeIsBuiltin = false;
            if ($type instanceof ReflectionUnionType) {
                foreach ($type->getTypes() as $unionType) {
                    if (!$unionType->isBuiltin()) {
                        $typeIsBuiltin = false;
                    }
                }
            } elseif ($type !== null) {
                $typeIsBuiltin = $type->isBuiltin();
            }

            if (!($type && !$typeIsBuiltin && $typeName)) {
                $this->$propertyName = $entity->$propertyName;
            } elseif (is_a($typeName, DefaultObject::class, true)) {
                $propertyIsParent = $entityReflectionClass->isLazyLoadedPropertyToBeAddedAsParent($propertyName);
                if ($propertyIsParent){
                    $this->$propertyName = $entity->$propertyName;
                    continue;
                }
                $rcClassName = $entity->getRepoClassForProperty(LazyLoadRepo::ARGUS, $propertyName);
                if (!$rcClassName && is_a($type->getName(),DefaultObject::class,true)) {
                    /** @var DefaultObject $typeClassName */
                    $typeClassName = $type->getName();
                    $rcClassName = $typeClassName::getRepoClass(LazyLoadRepo::RC);
                }
                //$propertyClass = $rcClassName = str_replace('\\Entities\\', '\\Repo\\Argus\\', $property->getType());
                if ($rcClassName && class_exists($rcClassName)) {
                    $rcInstance = new $rcClassName();
                    $propertyInstnace = $entity->$propertyName;
                    $propertyParent = $propertyInstnace->getParent();
                    if ($propertyParent) {
                        $rcInstance->setParent($propertyParent);
                    }
                    $rcInstance->fromEntity($entity->$propertyName);
                    $this->$propertyName = $rcInstance;
                    // if property is child of entity, we add $rcInstance also als our child
                    $parent = $entity->$propertyName->getParent();
                } else {
                    $this->$propertyName = $entity->$propertyName;
                }
                // we reflect parent child relationships from entity
                if ($entity->$propertyName->getParent() === $entity) {
                    $this->addChildren($this->$propertyName);
                }
            } else {
                $this->$propertyName = $entity->$propertyName;
            }
        }
        // if a parent for this location has not been set, we use the one from the entity passed.
        // this usually happens on the root level of the fromEntity call
        if ($this->parent === null && $entity->parent) {
            $this->parent = $entity->parent;
        }
        return $this;
    }


    /**
     * copies all public properties from current Argus Repo Instance to corresponding OrmEntity instance
     * operates recursively and creates if possible Entities from Argus Repo Objects
     * @param Entity $entiy
     * @return void
     * @throws ReflectionException
     */
    public function toEntity(
        array $callPath = [], EntitySet|Entity|ValueObject|null &$entityInstance = null
    ): EntitySet|Entity|ValueObject|null {
        if (isset($callPath[spl_object_id($this)])) {
            return null;
        }
        $propertyNamesToSkip = ['children' => true, 'parent' => true, 'elementsByUniqueKey' => true];

        $callPath[spl_object_id($this)] = true;

        /** @var ReflectionClass $parentRelectionClass */
        $parentRelectionClass = static::getReflectionClass()->getParentClass();
        $parentClassName = $parentRelectionClass->getName();

        $entity = $entityInstance ?? new $parentClassName();
        if ($parent = $this->getParent()) {
            $entity->setParent($parent);
        }
        foreach ($parentRelectionClass->getProperties() as $property) {
            $propertyName = $property->getName();
            if (isset($propertyNamesToSkip[$propertyName])) {
                continue;
            }
            if (!isset($this->$propertyName)) {
                continue;
            }
            if ($propertyName == 'objectType') {
                continue;
            }

            // treating ObjectSets
            if ($propertyName == 'elements' && $this instanceof ObjectSet) {
                /** @var Entity $element */
                foreach ($this->elements as $element) {
                    if (method_exists($element, 'toEntity')) {
                        $alreadyPresentElement = $entity->getByUniqueKey($element->uniqueKey());
                        if ($alreadyPresentElement) {
                            $element->toEntity($callPath, $alreadyPresentElement);
                        } else {
                            $entityInstance = $element->toEntity($callPath);
                            $entity->add($entityInstance);
                        }
                    } else {
                        $entity->add($element);
                    }
                }
                continue;
            }
            $typeIsBuiltin = false;
            $type = $property->getType();
            if ($type instanceof ReflectionUnionType) {
                foreach ($type->getTypes() as $unionType) {
                    if (!$unionType->isBuiltin()) {
                        $typeIsBuiltin = false;
                    }
                }
            } elseif ($type !== null) {
                $typeIsBuiltin = $type->isBuiltin();
            }
            if ($typeIsBuiltin || $type === null) {
                $entity->$propertyName = $this->$propertyName;
            } elseif ($this->$propertyName instanceof Entity || $this->$propertyName instanceof ValueObject) {
                if (method_exists($this->$propertyName, 'toEntity')) {
                    $entityInstance = $this->$propertyName->toEntity($callPath);
                    if ($entityInstance) {
                        $entity->$propertyName = $entityInstance;
                    } else {
                        $entity->$propertyName = $this->$propertyName;
                    }
                } else {
                    $entity->$propertyName = $this->$propertyName;
                }
                // we reflect parent child relationships on entity
                if (($this->$propertyName->parent ?? null) && spl_object_id(
                        $this->$propertyName->parent
                    ) == spl_object_id($this)) {
                    $entity->addChildren($entity->$propertyName);
                }
            } else {
                $entity->$propertyName = $this->$propertyName;
            }
        }

        return $entity;
    }

    /**
     * returns a cache key used for storing the object in cache, this function considers dynamically also children for creating the cache key
     * @return string
     */
    public function cacheKey(): string
    {
        if (isset($this->cacheKey)) {
            return $this->cacheKey;
        }

        $key = $this->uniqueKey(); //. '_' . ($this->shall_be_loaded || $this->autoload || $this->loaded);
        //if (!($this->children && count($this->children))) return $key;
        //foreach ($this->children as $child) $key .= '_' . $child->cacheKey();
        $this->cacheKey = $key;
        return $key;
    }

    public static function uniqueKeyStatic( string|int|null $id = null): string
    {
        $parentRelectionClass = static::getReflectionClass()->getParentClass();
        $parentClassName = $parentRelectionClass->getName();
        return $parentClassName . ($id ? '_' . $id : '');
    }
}
