/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef ds_InlineMap_h
#define ds_InlineMap_h

#include "js/HashTable.h"

namespace js {

/*
 * A type can only be used as an InlineMap key if zero is an invalid key value
 * (and thus may be used as a tombstone value by InlineMap).
 */
template <typename T> struct ZeroIsReserved         { static const bool result = false; };
template <typename T> struct ZeroIsReserved<T *>    { static const bool result = true; };

template <typename K, typename V, size_t InlineElems>
class InlineMap
{
  public:
    typedef HashMap<K, V, DefaultHasher<K>, SystemAllocPolicy> WordMap;

    struct InlineElem
    {
        K key;
        V value;
    };

  private:
    typedef typename WordMap::Ptr       WordMapPtr;
    typedef typename WordMap::AddPtr    WordMapAddPtr;
    typedef typename WordMap::Range     WordMapRange;

    size_t          inlNext;
    size_t          inlCount;
    InlineElem      inl[InlineElems];
    WordMap         map;

    void checkStaticInvariants() {
        JS_STATIC_ASSERT(ZeroIsReserved<K>::result);
    }

    bool usingMap() const {
        return inlNext > InlineElems;
    }

    bool switchToMap() {
        JS_ASSERT(inlNext == InlineElems);

        if (map.initialized()) {
            map.clear();
        } else {
            if (!map.init(count()))
                return false;
            JS_ASSERT(map.initialized());
        }

        for (InlineElem *it = inl, *end = inl + inlNext; it != end; ++it) {
            if (it->key && !map.putNew(it->key, it->value))
                return false;
        }

        inlNext = InlineElems + 1;
        JS_ASSERT(map.count() == inlCount);
        JS_ASSERT(usingMap());
        return true;
    }

    JS_NEVER_INLINE
    bool switchAndAdd(const K &key, const V &value) {
        if (!switchToMap())
            return false;

        return map.putNew(key, value);
    }

  public:
    explicit InlineMap()
      : inlNext(0), inlCount(0) {
        checkStaticInvariants(); /* Force the template to instantiate the static invariants. */
    }

    class Entry
    {
        friend class InlineMap;
        const K &key_;
        V &value_;

        Entry(const K &key, V &value) : key_(key), value_(value) {}

      public:
        const K &key() { return key_; }
        V &value() { return value_; }
    }; /* class Entry */

    class Ptr
    {
        friend class InlineMap;

        WordMapPtr  mapPtr;
        InlineElem  *inlPtr;
        bool        isInlinePtr;

        typedef Ptr ******* ConvertibleToBool;

        explicit Ptr(WordMapPtr p) : mapPtr(p), isInlinePtr(false) {}
        explicit Ptr(InlineElem *ie) : inlPtr(ie), isInlinePtr(true) {}
        void operator==(const Ptr &other);

      public:
        /* Leaves Ptr uninitialized. */
        Ptr() {
#ifdef DEBUG
            inlPtr = (InlineElem *) 0xbad;
            isInlinePtr = true;
#endif
        }

        /* Default copy constructor works for this structure. */

        bool found() const {
            return isInlinePtr ? bool(inlPtr) : mapPtr.found();
        }

        operator ConvertibleToBool() const {
            return ConvertibleToBool(found());
        }

        K &key() {
            JS_ASSERT(found());
            return isInlinePtr ? inlPtr->key : mapPtr->key;
        }

        V &value() {
            JS_ASSERT(found());
            return isInlinePtr ? inlPtr->value : mapPtr->value;
        }
    }; /* class Ptr */

    class AddPtr
    {
        friend class InlineMap;

        WordMapAddPtr   mapAddPtr;
        InlineElem      *inlAddPtr;
        bool            isInlinePtr;
        /* Indicates whether inlAddPtr is a found result or an add pointer. */
        bool            inlPtrFound;

        AddPtr(InlineElem *ptr, bool found)
          : inlAddPtr(ptr), isInlinePtr(true), inlPtrFound(found)
        {}

        AddPtr(const WordMapAddPtr &p) : mapAddPtr(p), isInlinePtr(false) {}

        void operator==(const AddPtr &other);

        typedef AddPtr ******* ConvertibleToBool;

      public:
        AddPtr() {}

        bool found() const {
            return isInlinePtr ? inlPtrFound : mapAddPtr.found();
        }

        operator ConvertibleToBool() const {
            return found() ? ConvertibleToBool(1) : ConvertibleToBool(0);
        }

        V &value() {
            JS_ASSERT(found());
            if (isInlinePtr)
                return inlAddPtr->value;
            return mapAddPtr->value;
        }
    }; /* class AddPtr */

    size_t count() {
        return usingMap() ? map.count() : inlCount;
    }

    bool empty() const {
        return usingMap() ? map.empty() : !inlCount;
    }

    void clear() {
        inlNext = 0;
        inlCount = 0;
    }

    bool isMap() const {
        return usingMap();
    }

    const WordMap &asMap() const {
        JS_ASSERT(isMap());
        return map;
    }

    const InlineElem *asInline() const {
        JS_ASSERT(!isMap());
        return inl;
    }

    const InlineElem *inlineEnd() const {
        JS_ASSERT(!isMap());
        return inl + inlNext;
    }

    JS_ALWAYS_INLINE
    Ptr lookup(const K &key) {
        if (usingMap())
            return Ptr(map.lookup(key));

        for (InlineElem *it = inl, *end = inl + inlNext; it != end; ++it) {
            if (it->key == key)
                return Ptr(it);
        }

        return Ptr(NULL);
    }

    JS_ALWAYS_INLINE
    AddPtr lookupForAdd(const K &key) {
        if (usingMap())
            return AddPtr(map.lookupForAdd(key));

        for (InlineElem *it = inl, *end = inl + inlNext; it != end; ++it) {
            if (it->key == key)
                return AddPtr(it, true);
        }

        /*
         * The add pointer that's returned here may indicate the limit entry of
         * the linear space, in which case the |add| operation will initialize
         * the map if necessary and add the entry there.
         */
        return AddPtr(inl + inlNext, false);
    }

    JS_ALWAYS_INLINE
    bool add(AddPtr &p, const K &key, const V &value) {
        JS_ASSERT(!p);

        if (p.isInlinePtr) {
            InlineElem *addPtr = p.inlAddPtr;
            JS_ASSERT(addPtr == inl + inlNext);

            /* Switching to map mode before we add this pointer. */
            if (addPtr == inl + InlineElems)
                return switchAndAdd(key, value);

            JS_ASSERT(!p.found());
            JS_ASSERT(uintptr_t(inl + inlNext) == uintptr_t(p.inlAddPtr));
            p.inlAddPtr->key = key;
            p.inlAddPtr->value = value;
            ++inlCount;
            ++inlNext;
            return true;
        }

        return map.add(p.mapAddPtr, key, value);
    }

    JS_ALWAYS_INLINE
    bool put(const K &key, const V &value) {
        AddPtr p = lookupForAdd(key);
        if (p) {
            p.value() = value;
            return true;
        }
        return add(p, key, value);
    }

    void remove(Ptr p) {
        JS_ASSERT(p);
        if (p.isInlinePtr) {
            JS_ASSERT(inlCount > 0);
            JS_ASSERT(p.inlPtr->key != NULL);
            p.inlPtr->key = NULL;
            --inlCount;
            return;
        }
        JS_ASSERT(map.initialized() && usingMap());
        map.remove(p.mapPtr);
    }

    void remove(const K &key) {
        if (Ptr p = lookup(key))
            remove(p);
    }

    class Range
    {
        friend class InlineMap;

        WordMapRange    mapRange;
        InlineElem      *cur;
        InlineElem      *end;
        bool            isInline;

        explicit Range(WordMapRange r)
          : cur(NULL), end(NULL), /* Avoid GCC 4.3.3 over-warning. */
            isInline(false) {
            mapRange = r;
            JS_ASSERT(!isInlineRange());
        }

        Range(const InlineElem *begin, const InlineElem *end_)
          : cur(const_cast<InlineElem *>(begin)),
            end(const_cast<InlineElem *>(end_)),
            isInline(true) {
            advancePastNulls(cur);
            JS_ASSERT(isInlineRange());
        }

        bool checkInlineRangeInvariants() const {
            JS_ASSERT(uintptr_t(cur) <= uintptr_t(end));
            JS_ASSERT_IF(cur != end, cur->key != NULL);
            return true;
        }

        bool isInlineRange() const {
            JS_ASSERT_IF(isInline, checkInlineRangeInvariants());
            return isInline;
        }

        void advancePastNulls(InlineElem *begin) {
            InlineElem *newCur = begin;
            while (newCur < end && NULL == newCur->key)
                ++newCur;
            JS_ASSERT(uintptr_t(newCur) <= uintptr_t(end));
            cur = newCur;
        }

        void bumpCurPtr() {
            JS_ASSERT(isInlineRange());
            advancePastNulls(cur + 1);
        }

        void operator==(const Range &other);

      public:
        bool empty() const {
            return isInlineRange() ? cur == end : mapRange.empty();
        }

        Entry front() {
            JS_ASSERT(!empty());
            if (isInlineRange())
                return Entry(cur->key, cur->value);
            return Entry(mapRange.front().key, mapRange.front().value);
        }

        void popFront() {
            JS_ASSERT(!empty());
            if (isInlineRange())
                bumpCurPtr();
            else
                mapRange.popFront();
        }
    }; /* class Range */

    Range all() const {
        return usingMap() ? Range(map.all()) : Range(inl, inl + inlNext);
    }
}; /* class InlineMap */

} /* namespace js */

#endif /* ds_InlineMap_h */
