//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
#pragma once

namespace Js
{
    typedef void* WeakMapId;

    class JavascriptWeakMap : public DynamicObject
    {
    private:
        // WeakMapKeyMap is the data that is kept alive by the key object itself so
        // that the lifetime of the WeakMap mapping from key to value is tied to the
        // lifetime of the key object. The WeakMap itself contains only a weak
        // reference to this data for the purpose of removing all references in the
        // Clear() and Finalize() methods, as well as enumeration in the debugger.
        //
        // Currently the WeakMapKeyMap object is stored in an internal property slot
        // on the key object. This is a problem though because making an object the
        // key of a WeakMap will then change the object's type and invalidate
        // caching and JIT assumptions.
        //
        // One alternative idea to using an internal property slot to hold the
        // WeakMapKeyMap on the key object is to use the arrayOrFlags field on
        // DynamicObject. E.g. subclass JavascriptArray with a version that is
        // both an array and has the extra data on it, allowing it to be placed in
        // the arrayOrFlags field. There are problems with this approach though:
        //
        //   1. arrayOrFlags is tied to sensitive performance optimizations and
        //      changing it will be met with difficulty in trying to maintain
        //      current benchmark performance
        //
        //   2. Not all objects are DynamicObject, so there are still keys that
        //      will not have the arrayOrFlags field (e.g. HostDispatch). Perhaps
        //      such objects should not be permitted to be keys on WeakMap?
        //
        // Regardless of this idea, the ideal would be to have InternalPropertyIds
        // not affect the type of an object. That is, ideally we would have a way
        // to add and remove InternalPropertyIds from an object without affecting
        // its type and therefore without invalidating cache and JIT assumptions.
        //
        typedef JsUtil::BaseDictionary<WeakMapId, Var, Recycler, PowerOf2SizePolicy, RecyclerPointerComparer> WeakMapKeyMap;
        typedef JsUtil::WeaklyReferencedKeyDictionary<DynamicObject, bool, RecyclerPointerComparer<const DynamicObject*>> KeySet;

        Field(KeySet) keySet;

        WeakMapKeyMap* GetWeakMapKeyMapFromKey(DynamicObject* key) const;
        WeakMapKeyMap* AddWeakMapKeyMapToKey(DynamicObject* key);

        WeakMapId GetWeakMapId() const { return (void*)(((uintptr_t)this) | 1); }
        static JavascriptWeakMap* GetWeakMapFromId(WeakMapId id) { return reinterpret_cast<JavascriptWeakMap*>((uintptr_t)id & (~1)); }

        bool KeyMapGet(WeakMapKeyMap* map, Var* value) const;

        DEFINE_VTABLE_CTOR_MEMBER_INIT(JavascriptWeakMap, DynamicObject, keySet);
        DEFINE_MARSHAL_OBJECT_TO_SCRIPT_CONTEXT(JavascriptWeakMap);

    public:
        JavascriptWeakMap(DynamicType* type);

        static bool Is(Var aValue);
        static JavascriptWeakMap* FromVar(Var aValue);

        void Clear();
        bool Delete(DynamicObject* key);
        bool Get(DynamicObject* key, Var* value) const;
        bool Has(DynamicObject* key) const;
        void Set(DynamicObject* key, Var value);

        virtual void Finalize(bool isShutdown) override { Clear(); }
        virtual void Dispose(bool isShutdown) override { }

        virtual BOOL GetDiagTypeString(StringBuilder<ArenaAllocator>* stringBuilder, ScriptContext* requestContext) override;

        class EntryInfo
        {
        public:
            static FunctionInfo NewInstance;
            static FunctionInfo Delete;
            static FunctionInfo Get;
            static FunctionInfo Has;
            static FunctionInfo Set;
        };

        static Var NewInstance(RecyclableObject* function, CallInfo callInfo, ...);
        static Var EntryDelete(RecyclableObject* function, CallInfo callInfo, ...);
        static Var EntryGet(RecyclableObject* function, CallInfo callInfo, ...);
        static Var EntryHas(RecyclableObject* function, CallInfo callInfo, ...);
        static Var EntrySet(RecyclableObject* function, CallInfo callInfo, ...);

    public:
        // For diagnostics and heap enum provide size and allow enumeration of key value pairs
        int Size() { keySet.Clean(); return keySet.Count(); }
        template <typename Fn>
        void Map(Fn fn)
        {
            return keySet.Map([&](DynamicObject* key, bool, const RecyclerWeakReference<DynamicObject>*)
            {
                Var value = nullptr;
                WeakMapKeyMap* keyMap = GetWeakMapKeyMapFromKey(key);

                // It may be the case that a CustomExternalObject (CEO) was reset, removing its WeakMapKeyMap.
                // In this case it can still be in the keySet. The keyMap may be null because of the reset,
                // but it could be reinstated if the CEO was added to another WeakMap. Thus it could also be the case that
                // this WeakMap's ID is not in the WeakMapKeyMap returned from the key object. Ignore the
                // CEO key object in these two cases.
                if (keyMap != nullptr && keyMap->ContainsKey(GetWeakMapId()))
                {
                    KeyMapGet(keyMap, &value);
                    fn(key, value);
                }
            });
        }

#if ENABLE_TTD
    public:
        virtual void MarkVisitKindSpecificPtrs(TTD::SnapshotExtractor* extractor) override;

        virtual TTD::NSSnapObjects::SnapObjectType GetSnapTag_TTD() const override;
        virtual void ExtractSnapObjectDataInto(TTD::NSSnapObjects::SnapObject* objData, TTD::SlabAllocator& alloc) override;
#endif
    };
}
