<?php

namespace apexl\Io\modules\component\entities;

use apexl\Io\includes\Entity;
use apexl\Io\includes\System;
use apexl\Io\modules\component\entities\operators\componentOperator;
use apexl\Io\modules\component\services\componentBucket;
use apexl\Io\modules\component\services\propertyBucket;
use Exception;
use ReflectionClass;
use Stringable;

/**
 * @property int $id
 * @property int $pid
 * @property string $name
 * @property string $type
 * @property string $classes
 */
class componentEntity extends Entity implements Stringable
{
    private const TABLE = 'components';

    protected propertyBucket $props;
    protected componentBucket $components;
    protected componentBucket $supportComponents;

    protected array $classArray = [];
    protected array $columns = [];

    public function __construct()
    {
        parent::__construct(self::TABLE);
        $this->setOperator(new componentOperator(self::TABLE));

        $this->setName($this->getComponentName());
        $this->setRequiredData([
            'type',
        ]);

        $this->setType();

        $this->id = sprintf(
            '%s_%s',
            $this->getComponentName(),
            substr(sha1(microtime()), 0, 16)
        );

        $this->props = System::makeRegisteredService(propertyBucket::class);
        $this->components = System::makeRegisteredService(componentBucket::class);
        $this->supportComponents = System::makeRegisteredService(componentBucket::class);
    }

    public function setName($name): static
    {
        $this->data['name'] = $name;

        return $this;
    }

    /**
     * Easy get name of component method.
     * @return string
     */
    public function getComponentName(): string
    {
        return (new ReflectionClass($this))->getShortName();
    }

    /**
     * Type is required, so we know what entity class to use when loading. Default is component (the standard entity)
     */
    public function setType(string $type = 'component'): static
    {
        $this->data['type'] = $type;

        return $this;
    }

    public function setComponentId($id): static
    {
        $this->data['componentId'] = $id;

        return $this;
    }

    public function getName()
    {
        return $this->data['name'];
    }

    public function setID($id): static
    {
        $this->data['id'] = $id;

        return $this;
    }

    public function setDataSource($src): static
    {
        $this->addProperty('dataSrc', $src);

        return $this;
    }

    public function addProperty(
        string $name,
        mixed $value,
        array|string|null $classes = null,
        mixed $metaData = null,
    ): static {
        $this->props->set(
            $name,
            propertyEntity::factory($name, $value, $classes, $metaData),
        );

        return $this;
    }

    public function addProperties(array $props): static
    {
        if (!$props) {
            /**
             * Empty? assume we want to reset props
             * @Note This will not trigger a delete method. It will simply remove any properties in the loaded
             *       entity, until it is loaded again
             */
            $this->props->clearAll();

            return $this;
        }

        foreach ($props as $prop) {
            $this->addProperty(
                $prop['name'],
                $prop['value'],
                ($prop['classes'] ?? null),
                ($prop['metadata'] ?? null),
            );
        }

        return $this;
    }

    public function getComponents(): componentBucket
    {
        return $this->components;
    }

    public function resetComponents(): static
    {
        $this->components->clearAll();

        return $this;
    }

    public function resetClasses(): static
    {
        $this->classArray = [];
        $this->classes = '';

        return $this;
    }

    public function resetColumns(): static
    {
        $this->columns = [];

        return $this;
    }

    /**
     * As overrides are annoying, separate columns into their own overrideable storage before converting to classes.
     * @param $size
     * @param $cols
     * @return $this
     */
    public function setColWidth($size, $cols): static
    {
        $this->columns[$size] = $cols;
        $this->addClassInternal();

        return $this;
    }

    protected function addClassInternal($class = '')
    {
        $this->classes = '';
        if (!empty($class) && !in_array(trim((string) $class), $this->classArray)) {
            $this->classArray[] = trim((string) $class);
        }
        if (!empty($this->columns)) {
            //always do this when a class changes, so we dont have to scan
            // all components and manually trigger
            $cols = [];
            foreach ($this->columns as $col => $number) {
                $cols[] = 'col-'.$col.'-'.$number;
            }
            $this->classes = implode(' ', $cols);
        }
        //clean any blanks.
        array_filter($this->classArray);
        $this->classes .= empty(trim($this->classes)) ? implode(' ', $this->classArray) : ' '.implode(
                ' ',
                $this->classArray
            );
    }

    /**
     * At some point we managed to mess up the class creation and management, so add an override here to be processed
     * when the component render is called.
     */
    public function addClass(string $class): static
    {
        //check for multiples, split them out if we have them.
        if (str_contains(trim($class), ' ')) {
            $classes = explode(' ', $class);
            foreach ($classes as $class) {
                $this->addClassInternal($class);
            }
        } else {
            $this->addClassInternal($class);
        }

        return $this;
    }

    public function removeProperty(string $name): static
    {
        $this->props->clear($name);

        return $this;
    }


    public function addComponent(componentEntity $componentEntity): static
    {
        $this->components->set($componentEntity->id, $componentEntity);

        return $this;
    }

    /**
     * Bulk add components
     * @Note for now this overwrites any components already assigned.
     */
    public function addComponents(array $components): static
    {
        $this->components = componentBucket::fromArray($components);

        return $this;
    }

    public function addSupportComponent(componentEntity $componentEntity): static
    {
        $this->supportComponents->set($componentEntity->id, $componentEntity);

        return $this;
    }

    /**
     * Override base entity setData method as we need to set props and components as well.
     */
    public function setData(array $entity): static
    {
        parent::setData($entity);

        $this->props = propertyBucket::fromArray($entity['props'] ?? []);
        $this->components = componentBucket::fromArray($entity['components'] ?? []);
        $this->supportComponents = componentBucket::fromArray($entity['supportComponents'] ?? []);

        return $this;
    }

    public function __set($name, $value)
    {
        switch (strtolower((string) $name)) {
            //Allow us to shortcut the addProp method.
            case 'props':
                $value = is_array($value) ? (object) $value : $value;
                $classes = null;
                $metadata = null;
                if (is_array($value)) {
                    $classes = $value->classes ?? null;
                    $metadata = $value->metadata ?? null;
                }

                $property = propertyEntity::factory($name, $value, $classes, $metadata);
                $this->props->set($name, $property);
                break;
            default:
                parent::__set($name, $value);
        }
    }

    /**
     * Allow us to store with base entity data, prevents the need to call a method while passing the result
     * of another method to the operator
     * @throws Exception
     */
    public function store(): Entity
    {
        $this->operator->store([
            'entity' => $this->data,
            'props' => $this->props,
            'components' => $this->components,
        ]);

        return $this;
    }

    /**
     * Override the base toString method as we need to do some processing here.
     * Allows us to recursively process nested component entities.
     */
    public function __toString(): string
    {
        return json_encode($this->prepareString());
    }

    protected function prepareString(): object
    {
        //get data and build json Structure.
        $component = (object) $this->getData();
        $component->props = (object) []; //always pass at least an empty object
        //handle Properties
        foreach ($this->props as $name => $prop) {
            $component->props->$name = $prop->value;
        }

        //Handle Nested Components
        foreach ($this->components as $nestedComponent) {
            $component->components[] = $nestedComponent->prepareString();
        }

        //Handle Nested Support Components (Secondary page components related to the parent)
        foreach ($this->supportComponents as $nestedComponent) {
            $component->supportComponents[] = $nestedComponent->prepareString();
        }

        return $component;
    }

    /**
     * Clone the component entity.
     * @return componentEntity
     */
    public function clone(): componentEntity
    {
        return clone $this;
    }
}
