<?php

declare(strict_types=1);

namespace Phan\Language\Type;

use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\Type;
use RuntimeException;

/**
 * Phan's representation of the type for a specific float, e.g. `-1.2`
 * @phan-pure
 */
final class LiteralFloatType extends FloatType implements LiteralTypeInterface
{
    /** @var float $value */
    private $value;

    protected function __construct(float $value, bool $is_nullable)
    {
        parent::__construct('\\', self::NAME, [], $is_nullable);
        $this->value = $value;
    }

    /**
     * Only exists to prevent accidentally calling this
     * @internal - do not call
     * @deprecated
     */
    public static function instance(bool $unused_is_nullable)
    {
        throw new RuntimeException('Call ' . self::class . '::instanceForValue() instead');
    }

    /**
     * @return LiteralFloatType a unique LiteralFloatType for $value (and the nullability)
     */
    public static function instanceForValue(float $value, bool $is_nullable): FloatType
    {
        if (!\is_finite($value)) {
            // Don't want to represent INF, -INF, NaN
            return FloatType::instance($is_nullable);
        }
        $key = \var_export($value, true);
        if ($is_nullable) {
            static $nullable_cache = [];
            return $nullable_cache[$key] ?? ($nullable_cache[$key] = new self($value, true));
        }
        static $cache = [];
        return $cache[$key] ?? ($cache[$key] = new self($value, false));
    }

    /**
     * Returns the literal float that this type represents
     * (whether or not this type is nullable)
     */
    public function getValue(): float
    {
        return $this->value;
    }

    public function __toString(): string
    {
        $str = \var_export($this->value, true);
        if ($this->is_nullable) {
            return '?' . $str;
        }
        return $str;
    }

    /** @var FloatType the non-nullable float type instance. */
    private static $non_nullable_float_type;
    /** @var FloatType the nullable float type instance. */
    private static $nullable_float_type;

    /**
     * Called at the bottom of the file to ensure static properties are set for quick access.
     */
    public static function init(): void
    {
        self::$non_nullable_float_type = FloatType::instance(false);
        self::$nullable_float_type = FloatType::instance(true);
    }

    public function asNonLiteralType(): Type
    {
        return $this->is_nullable ? self::$nullable_float_type : self::$non_nullable_float_type;
    }

    /**
     * @return Type[]
     * @override
     */
    public function withFlattenedArrayShapeOrLiteralTypeInstances(): array
    {
        return [$this->is_nullable ? self::$nullable_float_type : self::$non_nullable_float_type];
    }

    public function hasArrayShapeOrLiteralTypeInstances(): bool
    {
        return true;
    }

    /** @override */
    public function isPossiblyFalsey(): bool
    {
        return $this->is_nullable || !$this->value;
    }

    /** @override */
    public function isAlwaysFalsey(): bool
    {
        return !$this->value;
    }

    /** @override */
    public function isPossiblyTruthy(): bool
    {
        return (bool)$this->value;
    }

    /** @override */
    public function isAlwaysTruthy(): bool
    {
        return (bool)$this->value && !$this->is_nullable;
    }

    /**
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly
     */
    protected function canCastToNonNullableType(Type $type): bool
    {
        if ($type instanceof ScalarType) {
            switch ($type::NAME) {
                case 'float':
                    if ($type instanceof LiteralFloatType) {
                        return $type->getValue() === $this->getValue();
                    }
                    return true;
                case 'string':
                    if ($type instanceof LiteralStringType) {
                        if ($type->getValue() != $this->value) {
                            // Do a loose equality comparison and check if that permits that
                            // E.g. can't cast 5 to 'foo', but can cast 5 to '5' or '5foo' depending on the other rules
                            return false;
                        }
                    }
                    break;
                case 'int':
                    return false;
                case 'true':
                    if (!$this->value) {
                        return false;
                    }
                    break;
                case 'false':
                    if ($this->value) {
                        return false;
                    }
                    break;
                case 'null':
                    // null is also a scalar.
                    if ($this->value && !Config::get_null_casts_as_any_type()) {
                        return false;
                    }
                    break;
            }
        }

        return parent::canCastToNonNullableType($type);
    }

    /**
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly, ignoring permissive config casting rules
     */
    protected function canCastToNonNullableTypeWithoutConfig(Type $type): bool
    {
        if ($type instanceof ScalarType) {
            switch ($type::NAME) {
                case 'float':
                    if ($type instanceof LiteralFloatType) {
                        return $type->getValue() === $this->getValue();
                    }
                    return true;
                default:
                    return false;
            }
        }

        return parent::canCastToNonNullableType($type);
    }

    /**
     * @return bool
     * True if this Type can be cast to the given Type
     * cleanly
     */
    protected function isSubtypeOfNonNullableType(Type $type): bool
    {
        if ($type instanceof ScalarType) {
            if ($type::NAME === 'float') {
                if ($type instanceof LiteralFloatType) {
                    return $type->getValue() === $this->getValue();
                }
                return true;
            }
            return false;
        }

        return parent::isSubtypeOfNonNullableType($type);
    }

    /**
     * @param bool $is_nullable
     * Set to true if the type should be nullable, else pass
     * false
     *
     * @return Type
     * A new type that is a copy of this type but with the
     * given nullability value.
     */
    public function withIsNullable(bool $is_nullable): Type
    {
        if ($is_nullable === $this->is_nullable) {
            return $this;
        }

        return self::instanceForValue(
            $this->value,
            $is_nullable
        );
    }

    /**
     * Check if this type can satisfy a comparison (<, <=, >, >=)
     * @param float|string|float|bool|null $scalar
     * @param int $flags (e.g. \ast\flags\BINARY_IS_SMALLER)
     * @internal
     */
    public function canSatisfyComparison($scalar, int $flags): bool
    {
        return self::performComparison($this->value, $scalar, $flags);
    }

    public function asSignatureType(): Type
    {
        return FloatType::instance($this->is_nullable);
    }

    public function weaklyOverlaps(Type $other): bool
    {
        // TODO: Could be stricter
        if ($other instanceof ScalarType) {
            if ($other instanceof LiteralTypeInterface) {
                return $other->getValue() == $this->value;
            }
            if ($other instanceof NullType || $other instanceof FalseType) {
                // Allow 0 == null but not 1 == null
                if (!$this->isPossiblyFalsey()) {
                    return false;
                }
            }
            return true;
        }
        return parent::weaklyOverlaps($other);
    }

    public function canCastToDeclaredType(CodeBase $code_base, Context $context, Type $other): bool
    {
        if ($other instanceof LiteralFloatType) {
            return $other->value === $this->value;
        }
        return parent::canCastToDeclaredType($code_base, $context, $other);
    }
}

LiteralFloatType::init();
