/*
 * Copyright (c) 2020-2021, Andreas Kling <andreas@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#pragma once

#include <LibJS/Export.h>
#include <LibJS/Runtime/Environment.h>
#include <LibJS/Runtime/EnvironmentCoordinate.h>
#include <LibJS/Runtime/PropertyKey.h>
#include <LibJS/Runtime/Value.h>

namespace JS {

Reference make_private_reference(VM&, Value base_value, Utf16FlyString const& private_identifier);

class JS_API Reference {
public:
    enum class BaseType : u8 {
        Unresolvable,
        Value,
        Environment,
    };

    Reference(BaseType type, PropertyKey name, Strict strict)
        : m_name(move(name))
        , m_base_type(type)
        , m_strict(strict)
    {
    }

    Reference(Value base, PropertyKey name, Optional<Value> this_value, Strict strict)
        : m_name(move(name))
        , m_base_value(base)
        , m_this_value(this_value)
        , m_base_type(BaseType::Value)
        , m_strict(strict)
    {
    }

    Reference(Environment& base, Utf16FlyString referenced_name, Strict strict, Optional<EnvironmentCoordinate> environment_coordinate = {})
        : m_name(move(referenced_name))
        , m_base_environment(&base)
        , m_environment_coordinate(move(environment_coordinate))
        , m_base_type(BaseType::Environment)
        , m_strict(strict)
    {
    }

    Reference(Value base, PrivateName name)
        : m_name(move(name))
        , m_base_value(base)
        , m_base_type(BaseType::Value)
        , m_strict(Strict::Yes)
    {
    }

    Value base() const
    {
        VERIFY(m_base_type == BaseType::Value);
        return m_base_value;
    }

    Environment& base_environment() const
    {
        VERIFY(m_base_type == BaseType::Environment);
        return *m_base_environment;
    }

    PropertyKey const& name() const { return m_name.get<PropertyKey>(); }
    PrivateName const& private_name() const { return m_name.get<PrivateName>(); }
    bool is_strict() const { return m_strict == Strict::Yes; }

    // 6.2.4.2 IsUnresolvableReference ( V ), https://tc39.es/ecma262/#sec-isunresolvablereference
    bool is_unresolvable() const { return m_base_type == BaseType::Unresolvable; }

    // 6.2.4.1 IsPropertyReference ( V ), https://tc39.es/ecma262/#sec-ispropertyreference
    bool is_property_reference() const
    {
        if (is_unresolvable())
            return false;
        if (m_base_type == BaseType::Environment)
            return false;
        return true;
    }

    // 6.2.4.7 GetThisValue ( V ), https://tc39.es/ecma262/#sec-getthisvalue
    Value get_this_value() const
    {
        VERIFY(is_property_reference());
        if (is_super_reference())
            return m_this_value.value();
        return m_base_value;
    }

    // 6.2.4.3 IsSuperReference ( V ), https://tc39.es/ecma262/#sec-issuperreference
    bool is_super_reference() const
    {
        return m_this_value.has_value();
    }

    // 6.2.4.4 IsPrivateReference ( V ), https://tc39.es/ecma262/#sec-isprivatereference
    bool is_private_reference() const
    {
        return m_name.has<PrivateName>();
    }

    // Note: Non-standard helper.
    bool is_environment_reference() const
    {
        return m_base_type == BaseType::Environment;
    }

    ThrowCompletionOr<void> initialize_referenced_binding(VM&, Value value, Environment::InitializeBindingHint hint = Environment::InitializeBindingHint::Normal) const;

    ThrowCompletionOr<void> put_value(VM&, Value);
    ThrowCompletionOr<Value> get_value(VM&) const;
    ThrowCompletionOr<bool> delete_(VM&);

    bool is_valid_reference() const { return true; }

    Optional<EnvironmentCoordinate> environment_coordinate() const { return m_environment_coordinate; }

    void visit_edges(Cell::Visitor& visitor)
    {
        if (m_base_type == BaseType::Value) {
            visitor.visit(m_base_value);
        } else if (m_base_type == BaseType::Environment) {
            visitor.visit(m_base_environment);
        }
        m_name.visit(
            [&](PropertyKey const& key) { key.visit_edges(visitor); },
            [&](PrivateName const&) { /* no GC pointers */ });
        if (m_this_value.has_value())
            visitor.visit(*m_this_value);
    }

private:
    Completion throw_reference_error(VM&) const;

    Variant<PropertyKey, PrivateName> m_name;
    union {
        Value m_base_value {};
        mutable Environment* m_base_environment;
    };
    Optional<Value> m_this_value;
    Optional<EnvironmentCoordinate> m_environment_coordinate;
    BaseType m_base_type { BaseType::Unresolvable };
    Strict m_strict { Strict::No };
};

}
