<?php

namespace ipl\Stdlib;

use Generator;
use OutOfBoundsException;
use Traversable;

/**
 * Trait for property access, mutation, and array access
 */
trait Properties
{
    /** @var array<string, mixed> */
    private array $properties = [];

    /**
     * Get whether this class has any properties
     *
     * @return bool
     */
    public function hasProperties(): bool
    {
        return ! empty($this->properties);
    }

    /**
     * Get whether a property with the given key exists
     *
     * @param string $key
     *
     * @return bool
     */
    public function hasProperty(string $key): bool
    {
        return array_key_exists($key, $this->properties);
    }

    /**
     * Set the given properties
     *
     * @param array<string, mixed> $properties
     *
     * @return $this
     */
    public function setProperties(array $properties): static
    {
        foreach ($properties as $key => $value) {
            $this->setProperty($key, $value);
        }

        return $this;
    }

    /**
     * Get the property by the given key
     *
     * @param string $key
     *
     * @return mixed
     *
     * @throws OutOfBoundsException If the property by the given key does not exist
     */
    protected function getProperty(string $key): mixed
    {
        if (array_key_exists($key, $this->properties)) {
            return $this->properties[$key];
        }

        throw new OutOfBoundsException("Can't access property '$key'. Property does not exist");
    }

    /**
     * Set a property with the given key and value
     *
     * @param string $key
     * @param mixed  $value
     *
     * @return $this
     */
    protected function setProperty(string $key, mixed $value): static
    {
        $this->properties[$key] = $value;

        return $this;
    }

    /**
     * Iterate over all existing properties
     *
     * @return Generator<string, mixed>
     */
    public function getIterator(): Traversable
    {
        foreach ($this->properties as $key => $value) {
            yield $key => $value;
        }
    }

    /**
     * Check whether an offset exists
     *
     * @param mixed $offset
     *
     * @return bool
     */
    public function offsetExists(mixed $offset): bool
    {
        return isset($this->properties[$offset]);
    }

    /**
     * Get the value for an offset
     *
     * @param mixed $offset
     *
     * @return mixed
     */
    public function offsetGet(mixed $offset): mixed
    {
        return $this->getProperty($offset);
    }

    /**
     * Set the value for an offset
     *
     * @param mixed $offset
     * @param mixed $value
     *
     * @return void
     */
    public function offsetSet(mixed $offset, mixed $value): void
    {
        $this->setProperty($offset, $value);
    }

    /**
     * Unset the value for an offset
     *
     * @param mixed $offset
     *
     * @return void
     */
    public function offsetUnset(mixed $offset): void
    {
        unset($this->properties[$offset]);
    }

    /**
     * Get the value of a non-public property
     *
     * This is a PHP magic method which is implicitly called upon access to non-public properties,
     * e.g. `$value = $object->property;`.
     * Do not call this method directly.
     *
     * @param string $key
     *
     * @return mixed
     */
    public function __get(string $key): mixed
    {
        return $this->getProperty($key);
    }

    /**
     * Set the value of a non-public property
     *
     * This is a PHP magic method which is implicitly called upon access to non-public properties,
     * e.g. `$object->property = $value;`.
     * Do not call this method directly.
     *
     * @param string $key
     * @param mixed  $value
     *
     * @return void
     */
    public function __set(string $key, mixed $value): void
    {
        $this->setProperty($key, $value);
    }

    /**
     * Check whether a non-public property is defined and not null
     *
     * This is a PHP magic method which is implicitly called upon access to non-public properties,
     * e.g. `isset($object->property);`.
     * Do not call this method directly.
     *
     * @param string $key
     *
     * @return bool
     */
    public function __isset(string $key): bool
    {
        return $this->offsetExists($key);
    }

    /**
     * Unset the value of a non-public property
     *
     * This is a PHP magic method which is implicitly called upon access to non-public properties,
     * e.g. `unset($object->property);`. This method does nothing if the property does not exist.
     * Do not call this method directly.
     *
     * @param string $key
     *
     * @return void
     */
    public function __unset(string $key): void
    {
        $this->offsetUnset($key);
    }
}
