<?php

namespace apexl\Io\modules\component\entities;

use apexl\Io\includes\Entity;
use apexl\Io\includes\Hook;
use apexl\Io\modules\component\entities\operators\componentOperator;
use Exception;
use ReflectionClass;

/**
 * @property int $id
 * @property ?int $pid
 * @property string $name
 * @property string $type
 * @property string $classes
 *
 * @mixin componentOperator
 */
class componentEntity extends Entity
{
    public const HOOK__COMPONENT_CLASS = 'component:class';
    public const HOOK__COMPONENT_STYLE = 'component:style';

    protected object $props;
    protected array $components;
    protected array $supportComponents;
    protected string $componentId;

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

    protected string $name;

    public function __construct()
    {
        parent::__construct('components');
        $this->setOperator(new componentOperator('components'));
        $this->setName($this->getComponentName());
        $this->setRequiredData([
            'type',
        ]);
        $this->setType();
        $this->id = sprintf('%s_%s', $this->getComponentName(), substr(sha1(microtime()), 0, 16));
        $this->setComponentId($this->id);
        $this->props = (object) [];

        $this->classArray = Hook::processHook(
            self::HOOK__COMPONENT_CLASS,
            $this->classArray,
            $this->getComponentName(),
            $this->componentId
        );

        $this->styleArray = Hook::processHook(
            self::HOOK__COMPONENT_STYLE,
            $this->styleArray,
            $this->getComponentName(),
            $this->componentId
        );
    }

    /**
     * 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'): componentEntity
    {
        $this->data["type"] = $type;

        return $this;
    }

    public function setComponentId(string $id): componentEntity
    {
        $this->componentId = $id;

        return $this;
    }

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

        return $this;
    }

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

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

        return $this;
    }

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

        return $this;
    }

    public function setRefreshTag($tag): componentEntity
    {
        $this->addProperty("refreshTag", $tag);

        return $this;
    }

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

    public function resetComponents(): componentEntity
    {
        $this->components = [];

        return $this;
    }

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

        return $this;
    }

    public function resetColumns(): componentEntity
    {
        $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): componentEntity
    {
        $this->columns[$size] = $cols;

        $this->columnClasses();

        return $this;
    }

    protected function columnClasses(): void
    {
        $this->classArray = array_filter(
            $this->classArray,
            fn(string $class): bool => substr($class, 0, 4) !== 'col-',
        );

        foreach ($this->columns as $col => $number) {
            $this->addClass('col-' . $col . '-' . $number);
        }

    }

    /**
     * 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.
     * @param  string|array  $class
     */
    public function addClass($class): componentEntity
    {
        foreach ((array) $class as $_class) {

            $_class = explode(' ', $_class);
            if (count($_class) > 1) {
                $this->addClass($_class);
            }

            $_class = trim(array_pop($_class));

            if ($_class && !in_array($_class, $this->classArray)) {
                $this->classArray[] = $_class;
            }
        }

        return $this;
    }

    /**
     * @throws Exception
     */
    public function addProperties(array $props): void
    {
        if ($props !== []) {
            foreach ($props as $prop) {
                //validation for property names and values
                if (($prop['name'] ?? true) || ($prop['value'] ?? true)) {
                    throw new Exception("Property Name and Value must be set.");
                }
                $this->addProperty(
                    $prop['name'],
                    $prop['value'],
                    ($prop['classes'] ?? null),
                    ($prop['metadata'] ?? null)
                );
            }
        } else {
            //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 = (object) [];
        }
    }

    public function addProperty(string $name, $value, string $classes = '', string $metaData = ''): componentEntity
    {
        $property = new propertyEntity();
        $property->setName($name)
            ->setValue($value)
            ->setClasses($classes)
            ->setMetadata($metaData);

        $this->props->$name = $property;

        return $this;
    }

    public function removeProperty($name): componentEntity
    {
        if (isset($this->props->$name)) {
            unset($this->props->$name);
        }

        return $this;
    }

    public function addComponent(componentEntity $componentEntity): componentEntity
    {
        $this->components[$componentEntity->id] = $componentEntity;

        return $this;
    }

    //bulk add components - note, for now this overwrites any components already assigned.
    public function addComponents(array $components): componentEntity
    {
        $this->components = $components;

        return $this;
    }

    public function addSupportComponent(componentEntity $componentEntity): componentEntity
    {
        $this->supportComponents[$componentEntity->id] = $componentEntity;

        return $this;
    }

    /**
     * Override base entity setData method as we need to set props and components as well.
     */
    public function setData($entity): componentEntity
    {
        $this->data = empty($entity['data']) ? [] : json_decode(json_encode($entity['data']), true);
        $this->props = $entity['props'] ?? (object) [];
        $this->components = $entity['components'] ?? [];
        $this->supportComponents = $entity['supportComponents'] ?? [];

        return $this;
    }

    /**
     * Override @param $name string
     * @param $value string|array|object
     * @return $this
     * @see \apexl\entityCore\Entity
     * Magic PHP __set function to set provided field value
     */
    public function __set($name, $value)
    {
        switch (strtolower($name)) {
            //Allow us to shortcut the addProp method.
            case 'props':
                $property = (new propertyEntity())->setName($name);
                $value = is_array($value) ? (object) $value : $value;
                if (is_object($value)) {
                    $property->setValue($value->value);
                    $property->setClasses(($value->classes ?? null));
                    $property->setMetadata($value->metadata ?? null);
                } else {
                    $property->setValue($value);
                }
                $this->props->$name = $property;
                break;
            default:
                return $this->setField($name, $value);
        }

        return $this;
    }

    /**
     * Allow us to store with base entity data, prevents the need o call a method while passing the result of another method to the operator
     */
    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.
     * @return false|string
     */
    public function __toString()
    {
        return json_encode($this->prepareString()); //return generated json
    }

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

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

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

        return $component;
    }

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

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

    /**
     * @param  string|array  $style
     */
    public function addStyle($style): componentEntity
    {
        foreach ((array) $style as $_style) {

            $_style = explode(';', $_style);
            if (count($_style) > 1) {
                $this->addStyle($_style);
            }

            $_style = trim($_style[0]);

            if ($_style && !in_array($_style, $this->styleArray)) {
                $this->styleArray[] = $_style;
            }
        }

        return $this;
    }
}
