<?php

namespace Velis\Dto\Traits;

use Exception;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionProperty;
use Velis\Dto\BaseDto;
use Velis\Dto\RelationTransformer\RelationTransformer;
use Velis\Dto\Utility\Optional;
use Velis\Dto\Utility\PropertyUtility;
use Velis\Model\DataObject;

/**
 * This trait is responsible for loading relations from the model and converting them into proper DTOs.
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
trait RelationTrait
{
    /**
     * @throws Exception
     */
    protected function loadRelations(): void
    {
        if (!$this->model instanceof DataObject) {
            return;
        }

        $classReflection = new ReflectionClass($this);

        // Iterate through DTO's properties and get its mappers.
        foreach ($classReflection->getProperties() as $property) {
            // Mappers should implement Mappable interface.
            $mappers = $property->getAttributes(RelationTransformer::class, ReflectionAttribute::IS_INSTANCEOF);

            if (count($mappers) > 1) {
                throw new Exception('Only one mapper can be assigned to a property.');
            }

            // If there is no mapper assigned to a property, skip it, leaving as null.
            if (count($mappers) <= 0) {
                continue;
            }

            // If there is a mapper assigned to a property, load related data and transform to the specified DTO.
            /** @var RelationTransformer $mapper */
            $mapper = $mappers[0]->newInstance();
            $relatedData = $mapper->map($this->model);
            $this->setPropertyValue($property, $relatedData);
        }
    }

    /**
     * Set the value to the property.
     * @param BaseDto|BaseDto[]|null $value
     */
    private function setPropertyValue(ReflectionProperty $property, array|BaseDto|null $value): void
    {
        if (!is_null($value)) {
            $property->setValue($this, $value);
            return;
        }

        $this->setEmptyValue($property);
    }

    /**
     * We have to return all the declared properties from our DTO, so we have to initialize all of them
     * - even an empty property.
     * So we have to set an empty array or null - depends on the default property type.
     */
    private function setEmptyValue(ReflectionProperty $property): void
    {
        if (PropertyUtility::isOptional($property)) {
            $property->setValue($this, Optional::set());
            return;
        }

        if (PropertyUtility::hasType($property, 'array') && !$property->getType()->allowsNull()) {
            $property->setValue($this, []);
            return;
        }

        $property->setValue($this, null);
    }
}
