<?php

namespace Laminas\Code\Generator;

use Laminas\Code\Reflection\PropertyReflection;

use function array_reduce;
use function get_class;
use function gettype;
use function is_bool;
use function is_object;
use function method_exists;
use function sprintf;
use function str_replace;
use function strtolower;

class PropertyGenerator extends AbstractMemberGenerator
{
    public const FLAG_CONSTANT = 0x08;
    public const FLAG_READONLY = 0x80;

    protected bool $isConst = false;

    protected ?TypeGenerator $type = null;

    protected ?PropertyValueGenerator $defaultValue = null;

    private bool $omitDefaultValue = false;

    /**
     * @param  PropertyValueGenerator|string|array|null  $defaultValue
     * @param  int|int[]                                 $flags
     */
    public function __construct(
        ?string $name = null,
        $defaultValue = null,
        $flags = self::FLAG_PUBLIC,
        ?TypeGenerator $type = null
    ) {
        parent::__construct();

        if (null !== $name) {
            $this->setName($name);
        }
        if (null !== $defaultValue) {
            $this->setDefaultValue($defaultValue);
        }
        if ($flags !== self::FLAG_PUBLIC) {
            $this->setFlags($flags);
        }

        $this->type = $type;
    }

    /** @return static */
    public static function fromReflection(PropertyReflection $reflectionProperty)
    {
        $property = new static();

        $property->setName($reflectionProperty->getName());

        $allDefaultProperties = $reflectionProperty->getDeclaringClass()->getDefaultProperties();

        $defaultValue = $allDefaultProperties[$reflectionProperty->getName()] ?? null;
        $property->setDefaultValue($defaultValue);
        if ($defaultValue === null) {
            $property->omitDefaultValue = true;
        }

        if ($reflectionProperty->getDocComment() != '') {
            $property->setDocBlock(DocBlockGenerator::fromReflection($reflectionProperty->getDocBlock()));
        }

        if ($reflectionProperty->isStatic()) {
            $property->setStatic(true);
        }

        if (method_exists($reflectionProperty, 'isReadonly') && $reflectionProperty->isReadonly()) {
            $property->setReadonly(true);
        }

        if ($reflectionProperty->isPrivate()) {
            $property->setVisibility(self::VISIBILITY_PRIVATE);
        } elseif ($reflectionProperty->isProtected()) {
            $property->setVisibility(self::VISIBILITY_PROTECTED);
        } else {
            $property->setVisibility(self::VISIBILITY_PUBLIC);
        }

        $property->setType(TypeGenerator::fromReflectionType(
            $reflectionProperty->getType(),
            $reflectionProperty->getDeclaringClass()
        ));

        $property->setSourceDirty(false);

        return $property;
    }

    /**
     * Generate from array
     *
     * @configkey name               string   [required] Class Name
     * @configkey const              bool
     * @configkey defaultvalue       null|bool|string|int|float|array|ValueGenerator
     * @configkey flags              int
     * @configkey abstract           bool
     * @configkey final              bool
     * @configkey static             bool
     * @configkey visibility         string
     * @configkey omitdefaultvalue   bool
     * @configkey readonly           bool
     * @configkey type               null|TypeGenerator
     * @param  array  $array
     * @return static
     * @throws Exception\InvalidArgumentException
     */
    public static function fromArray(array $array)
    {
        if (! isset($array['name'])) {
            throw new Exception\InvalidArgumentException(
                'Property generator requires that a name is provided for this object'
            );
        }

        $property = new static($array['name']);
        foreach ($array as $name => $value) {
            // normalize key
            switch (strtolower(str_replace(['.', '-', '_'], '', $name))) {
                case 'const':
                    $property->setConst($value);
                    break;
                case 'defaultvalue':
                    $property->setDefaultValue($value);
                    break;
                case 'docblock':
                    $docBlock = $value instanceof DocBlockGenerator ? $value : DocBlockGenerator::fromArray($value);
                    $property->setDocBlock($docBlock);
                    break;
                case 'flags':
                    $property->setFlags($value);
                    break;
                case 'abstract':
                    $property->setAbstract($value);
                    break;
                case 'final':
                    $property->setFinal($value);
                    break;
                case 'static':
                    $property->setStatic($value);
                    break;
                case 'visibility':
                    $property->setVisibility($value);
                    break;
                case 'omitdefaultvalue':
                    $property->omitDefaultValue($value);
                    break;
                case 'readonly':
                    if (! is_bool($value)) {
                        throw new Exception\InvalidArgumentException(sprintf(
                            '%s is expecting boolean on key %s. Got %s',
                            __METHOD__,
                            $name,
                            is_object($value)
                                ? get_class($value)
                                : gettype($value)
                        ));
                    }

                    $property->setReadonly($value);
                    break;
                case 'type':
                    if (! $value instanceof TypeGenerator) {
                        throw new Exception\InvalidArgumentException(sprintf(
                            '%s is expecting %s on key %s. Got %s',
                            __METHOD__,
                            TypeGenerator::class,
                            $name,
                            is_object($value) ? get_class($value) : gettype($value)
                        ));
                    }
                    $property->setType($value);
                    break;
            }
        }

        return $property;
    }

    /**
     * @param  bool  $const
     * @return PropertyGenerator
     */
    public function setConst($const)
    {
        if (true === $const) {
            $this->setFlags(self::FLAG_CONSTANT);

            return $this;
        }

        $this->removeFlag(self::FLAG_CONSTANT);

        return $this;
    }

    /**
     * @return bool
     */
    public function isConst()
    {
        return (bool) ($this->flags & self::FLAG_CONSTANT);
    }

    public function setReadonly(bool $readonly): self
    {
        if (true === $readonly) {
            $this->setFlags(self::FLAG_READONLY);

            return $this;
        }

        $this->removeFlag(self::FLAG_READONLY);

        return $this;
    }

    public function isReadonly(): bool
    {
        return (bool) ($this->flags & self::FLAG_READONLY);
    }

    /**
     * {@inheritDoc}
     */
    public function setFlags($flags)
    {
        $flags = array_reduce((array) $flags, static function (int $a, int $b): int {
            return $a | $b;
        }, 0);

        if ($flags & self::FLAG_READONLY && $flags & self::FLAG_STATIC) {
            throw new Exception\RuntimeException('Modifier "readonly" in combination with "static" not permitted.');
        }

        if ($flags & self::FLAG_READONLY && $flags & self::FLAG_CONSTANT) {
            throw new Exception\RuntimeException('Modifier "readonly" in combination with "constant" not permitted.');
        }

        return parent::setFlags($flags);
    }

    /**
     * @return ?PropertyValueGenerator
     */
    public function getDefaultValue()
    {
        return $this->defaultValue;
    }

    /**
     * @param  PropertyValueGenerator|mixed  $defaultValue
     * @param  string                        $defaultValueType
     * @param  string                        $defaultValueOutputMode
     * @return static
     */
    public function setDefaultValue(
        $defaultValue,
        $defaultValueType = PropertyValueGenerator::TYPE_AUTO,
        $defaultValueOutputMode = PropertyValueGenerator::OUTPUT_MULTIPLE_LINE
    ) {
        if (! $defaultValue instanceof PropertyValueGenerator) {
            $defaultValue = new PropertyValueGenerator($defaultValue, $defaultValueType, $defaultValueOutputMode);
        }

        $this->defaultValue = $defaultValue;

        return $this;
    }

    /**
     * @return string
     * @psalm-return non-empty-string
     * @throws Exception\RuntimeException
     */
    public function generate()
    {
        $name         = $this->getName();
        $defaultValue = $this->getDefaultValue();

        $output = '';

        if (($docBlock = $this->getDocBlock()) !== null) {
            $docBlock->setIndentation('    ');
            $output .= $docBlock->generate();
        }

        if ($this->isConst()) {
            if ($defaultValue !== null && ! $defaultValue->isValidConstantType()) {
                throw new Exception\RuntimeException(sprintf(
                    'The property %s is said to be '
                    . 'constant but does not have a valid constant value.',
                    $this->name
                ));
            }

            return $output
                   . $this->indentation
                   . ($this->isFinal() ? 'final ' : '')
                   . $this->getVisibility()
                   . ' const '
                   . $name . ' = '
                   . ($defaultValue !== null ? $defaultValue->generate() : 'null;');
        }

        $type    = $this->type;
        $output .= $this->indentation
                   . $this->getVisibility()
                   . ($this->isReadonly() ? ' readonly' : '')
                   . ($this->isStatic() ? ' static' : '')
                   . ($type ? ' ' . $type->generate() : '')
                   . ' $' . $name;

        if ($this->omitDefaultValue) {
            return $output . ';';
        }

        return $output . ' = ' . ($defaultValue !== null ? $defaultValue->generate() : 'null;');
    }

    /**
     * @return PropertyGenerator
     */
    public function omitDefaultValue(bool $omit = true)
    {
        $this->omitDefaultValue = $omit;

        return $this;
    }

    public function getType(): ?TypeGenerator
    {
        return $this->type;
    }

    public function setType(?TypeGenerator $type): void
    {
        $this->type = $type;
    }
}
