<?php

namespace Solspace\Freeform\Library\Attributes;

use Solspace\Freeform\Library\Helpers\IsolatedTwig;
use Solspace\Freeform\Library\Helpers\ReflectionHelper;
use Solspace\Freeform\Library\Helpers\StringHelper;
use Solspace\Freeform\Library\Serialization\Normalizers\CustomNormalizerInterface;
use Symfony\Component\Serializer\Annotation\Ignore;

class Attributes implements \Stringable, CustomNormalizerInterface, \Countable, \JsonSerializable, \IteratorAggregate
{
    public const STRATEGY_APPEND = 'append';
    public const STRATEGY_REMOVE = 'remove';
    public const STRATEGY_REPLACE = 'replace';

    private const EXCLUDED_ATTRIBUTES = ['tag'];
    private const EXCLUDED_TWIG_ATTRIBUTES = ['value', 'name', 'id'];

    private array $attributes = [];

    public function __construct(array $attributes = [])
    {
        $this->merge($attributes);
    }

    public function __toString(): string
    {
        $stringArray = [];

        foreach ($this->attributes as $key => $value) {
            if (empty($key) || \in_array($key, self::EXCLUDED_ATTRIBUTES, true)) {
                continue;
            }

            if (false === $value) {
                continue;
            }

            $key = htmlspecialchars($key, \ENT_QUOTES, 'UTF-8');

            if (true === $value || null === $value) {
                $stringArray[] = $key;

                continue;
            }

            $value = match (\gettype($value)) {
                'array' => implode(' ', (array) $value),
                'object' => implode(
                    ' ',
                    array_map(
                        static fn ($value, $key) => \sprintf('%s:%s', $value, $key),
                        array_keys((array) $value),
                        (array) $value
                    )
                ),
                default => $value,
            };

            $value = trim($value);
            $value = htmlspecialchars($value, \ENT_QUOTES, 'UTF-8');

            $stringArray[] = "{$key}=\"{$value}\"";
        }

        $stringArray = array_filter($stringArray);

        if (empty($stringArray)) {
            return '';
        }

        return ' '.implode(' ', $stringArray);
    }

    public function __clone(): void
    {
        $reflection = new \ReflectionClass($this);
        foreach ($reflection->getProperties() as $property) {
            $type = $property->getType();
            $typeName = $type?->getName();
            if (ReflectionHelper::isInstanceOf(self::class, $typeName)) {
                $this->{$property->getName()} = $this->{$property->getName()}->clone();
            }
        }
    }

    public function getTag(string $default = 'div'): ?string
    {
        return $this->attributes['tag'] ?? $default;
    }

    public static function fromArray(array $attributes): self
    {
        $instance = new self();

        foreach ($attributes as $key => $value) {
            $instance->set($key, $value);
        }

        return $instance;
    }

    public function get(string $name, mixed $default = null): mixed
    {
        return $this->attributes[$name] ?? $default;
    }

    public function set(string $key, mixed $value = null, string $strategy = self::STRATEGY_APPEND): self
    {
        preg_match('/^[=+-]/', $key, $matches);
        if ($matches) {
            $prefix = $matches[0];
            match ($prefix) {
                '=' => $strategy = self::STRATEGY_REPLACE,
                '+' => $strategy = self::STRATEGY_APPEND,
                '-' => $strategy = self::STRATEGY_REMOVE,
            };

            $key = substr($key, 1);
        }

        if (\is_array($value)) {
            $value = array_map(
                static function ($item) {
                    if (\is_string($item)) {
                        $item = trim($item);
                    }

                    return $item;
                },
                $value
            );
            $value = array_filter($value);
            $value = array_unique($value);
            $value = StringHelper::implodeRecursively(' ', $value);
        }

        if (\is_string($value)) {
            $value = trim($value);
        }

        switch ($strategy) {
            case self::STRATEGY_REPLACE:
                $this->attributes[$key] = $value;

                break;

            case self::STRATEGY_REMOVE:
                if (\is_string($value)) {
                    $removable = explode(' ', $value);
                } elseif (!\is_array($value)) {
                    $removable = [$value];
                } else {
                    $removable = $value;
                }

                $removable = array_map('trim', $removable);

                $attributes = explode(' ', $this->attributes[$key] ?? '');
                $attributes = array_filter($attributes, static fn ($attribute) => !\in_array($attribute, $removable, true));
                $this->attributes[$key] = implode(' ', $attributes);

                if (empty($this->attributes[$key])) {
                    unset($this->attributes[$key]);
                }

                break;

            case self::STRATEGY_APPEND:
            default:
                if (\array_key_exists($key, $this->attributes) && !\is_bool($value)) {
                    $this->attributes[$key] = trim($this->attributes[$key].' '.$value);
                    $values = explode(' ', $this->attributes[$key]);
                    $values = array_filter($values);
                    $values = array_unique($values);
                    $values = array_map('trim', $values);
                    $this->attributes[$key] = implode(' ', $values);
                } else {
                    $this->attributes[$key] = $value;
                }

                break;
        }

        return $this;
    }

    public function setIfEmpty(?string $key, mixed $value = null): self
    {
        if (!\array_key_exists($key, $this->attributes)) {
            $this->attributes[$key] = $value;
        }

        return $this;
    }

    public function replace(string $key, mixed $value = null): self
    {
        return $this->set($key, $value, self::STRATEGY_REPLACE);
    }

    public function append(string $key, mixed $value = null): self
    {
        return $this->set($key, $value);
    }

    public function merge(array|self $attributes): self
    {
        $reflection = new \ReflectionClass($this);

        if ($attributes instanceof self) {
            foreach ($attributes->attributes as $key => $value) {
                $this->set($key, $value);
            }

            foreach ($this->getSubAttributes() as $name => $nestedAttribute) {
                $item = $attributes->{$name} ?? null;
                if ($item instanceof self) {
                    $nestedAttribute->merge($item);
                }
            }

            return $this;
        }

        foreach ($reflection->getProperties() as $property) {
            if (!\array_key_exists($property->getName(), $attributes)) {
                continue;
            }

            if (!\is_array($attributes[$property->getName()])) {
                continue;
            }

            $type = $property->getType();
            if (!$type) {
                continue;
            }

            if (self::class !== $type->getName()) {
                continue;
            }

            $this->{$property->getName()}->merge($attributes[$property->getName()]);
            unset($attributes[$property->getName()]);
        }

        foreach ($attributes as $key => $value) {
            $this->set($key, $value);
        }

        return $this;
    }

    public function remove(string $key): self
    {
        unset($this->attributes[$key]);

        return $this;
    }

    public function clone(): self
    {
        return clone $this;
    }

    public function count(): int
    {
        return \count($this->attributes);
    }

    public function toArray(): array
    {
        $reflection = new \ReflectionClass($this);
        $array = $this->attributes;

        foreach ($reflection->getProperties() as $property) {
            $type = $property->getType();
            if ($type && self::class === $type->getName()) {
                $array[$property->getName()] = $this->{$property->getName()}->jsonSerialize();
            }
        }

        return $array;
    }

    public function toHtmlTagArray(?array $properties = null): array
    {
        $array = $this->attributes;
        foreach (self::EXCLUDED_ATTRIBUTES as $attribute) {
            unset($array[$attribute]);
        }

        if (!empty($properties)) {
            $twig = new IsolatedTwig();

            $replacements = [];
            foreach ($array as $key => $value) {
                $key = $twig->render($key, $properties);

                if (!empty($value) && !\in_array($key, self::EXCLUDED_TWIG_ATTRIBUTES, true)) {
                    $value = $twig->render($value, $properties);
                }

                $replacements[$key] = $value;
            }

            $array = $replacements;
        }

        foreach ($array as $key => $value) {
            if (null === $value) {
                $array[$key] = true;
            }
        }

        return $array;
    }

    #[Ignore]
    public function normalize(): array
    {
        return $this->toArray();
    }

    public function jsonSerialize(): object
    {
        return (object) $this->toArray();
    }

    public function getIterator(): \ArrayIterator
    {
        return new \ArrayIterator($this->attributes);
    }

    private function getSubAttributes(): array
    {
        $reflection = new \ReflectionClass($this);
        $attributes = [];

        foreach ($reflection->getProperties() as $property) {
            $type = $property->getType();
            if ($type && self::class === $type->getName()) {
                $attributes[$property->getName()] = $this->{$property->getName()};
            }
        }

        return $attributes;
    }
}
