/*
 * Copyright (C) 2013 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "MapData.h"

#include "CopiedAllocator.h"
#include "CopyVisitorInlines.h"
#include "ExceptionHelpers.h"
#include "JSCJSValueInlines.h"
#include "SlotVisitorInlines.h"

#include <wtf/CryptographicallyRandomNumber.h>
#include <wtf/MathExtras.h>


namespace JSC {

const ClassInfo MapData::s_info = { "MapData", 0, 0, 0, CREATE_METHOD_TABLE(MapData) };

static const int32_t minimumMapSize = 8;

MapData::MapData(VM& vm)
    : Base(vm, vm.mapDataStructure.get())
    , m_capacity(0)
    , m_size(0)
    , m_deletedCount(0)
    , m_iteratorCount(0)
    , m_entries(0)
{
}

MapData::Entry* MapData::find(CallFrame* callFrame, KeyType key)
{
    if (key.value.isString()) {
        auto iter = m_stringKeyedTable.find(asString(key.value)->value(callFrame).impl());
        if (iter == m_stringKeyedTable.end())
            return 0;
        return &m_entries[iter->value];
    }
    if (key.value.isCell()) {
        auto iter = m_cellKeyedTable.find(key.value.asCell());
        if (iter == m_cellKeyedTable.end())
            return 0;
        return &m_entries[iter->value];
    }

    auto iter = m_valueKeyedTable.find(JSValue::encode(key.value));
    if (iter == m_valueKeyedTable.end())
        return 0;
    return &m_entries[iter->value];
}

bool MapData::contains(CallFrame* callFrame, KeyType key)
{
    return find(callFrame, key);
}

template <typename Map, typename Key> MapData::Entry* MapData::add(CallFrame* callFrame, Map& map, Key key, KeyType keyValue)
{
    typename Map::iterator location = map.find(key);
    if (location != map.end())
        return &m_entries[location->value];
    
    if (!ensureSpaceForAppend(callFrame))
        return 0;

    auto result = map.add(key, m_size);
    RELEASE_ASSERT(result.isNewEntry);
    Entry* entry = &m_entries[m_size++];
    new (entry) Entry();
    entry->key.set(callFrame->vm(), this, keyValue.value);
    return entry;
}

void MapData::set(CallFrame* callFrame, KeyType key, JSValue value)
{
    Entry* location = add(callFrame, key);
    if (!location)
        return;
    location->value.set(callFrame->vm(), this, value);
}
    
MapData::Entry* MapData::add(CallFrame* callFrame, KeyType key)
{
    if (key.value.isString())
        return add(callFrame, m_stringKeyedTable, asString(key.value)->value(callFrame).impl(), key);
    if (key.value.isCell())
        return add(callFrame, m_cellKeyedTable, key.value.asCell(), key);
    return add(callFrame, m_valueKeyedTable, JSValue::encode(key.value), key);
}

JSValue MapData::get(CallFrame* callFrame, KeyType key)
{
    if (Entry* entry = find(callFrame, key))
        return entry->value.get();
    return JSValue();
}

bool MapData::remove(CallFrame* callFrame, KeyType key)
{
    int32_t location;
    if (key.value.isString()) {
        auto iter = m_stringKeyedTable.find(asString(key.value)->value(callFrame).impl());
        if (iter == m_stringKeyedTable.end())
            return false;
        location = iter->value;
        m_stringKeyedTable.remove(iter);
    } else if (key.value.isCell()) {
        auto iter = m_cellKeyedTable.find(key.value.asCell());
        if (iter == m_cellKeyedTable.end())
            return false;
        location = iter->value;
        m_cellKeyedTable.remove(iter);
    } else {
        auto iter = m_valueKeyedTable.find(JSValue::encode(key.value));
        if (iter == m_valueKeyedTable.end())
            return false;
        location = iter->value;
        m_valueKeyedTable.remove(iter);
    }
    m_entries[location].key.clear();
    m_entries[location].value.clear();
    m_deletedCount++;
    return true;
}

void MapData::replaceAndPackBackingStore(Entry* destination, int32_t newCapacity)
{
    ASSERT(shouldPack());
    int32_t newEnd = 0;
    RELEASE_ASSERT(newCapacity > 0);
    for (int32_t i = 0; i < m_size; i++) {
        Entry& entry = m_entries[i];
        if (!entry.key)
            continue;
        ASSERT(newEnd < newCapacity);
        destination[newEnd] = entry;

        // We overwrite the old entry with a forwarding index for the new entry,
        // so that we can fix up our hash tables below without doing additional
        // hash lookups
        entry.value.setWithoutWriteBarrier(jsNumber(newEnd));
        newEnd++;
    }

    // Fixup for the hashmaps
    for (auto ptr = m_valueKeyedTable.begin(); ptr != m_valueKeyedTable.end(); ++ptr)
        ptr->value = m_entries[ptr->value].value.get().asInt32();
    for (auto ptr = m_stringKeyedTable.begin(); ptr != m_stringKeyedTable.end(); ++ptr)
        ptr->value = m_entries[ptr->value].value.get().asInt32();

    ASSERT((m_size - newEnd) == m_deletedCount);
    m_deletedCount = 0;

    m_capacity = newCapacity;
    m_size = newEnd;
    m_entries = destination;

}

void MapData::replaceBackingStore(Entry* destination, int32_t newCapacity)
{
    ASSERT(!shouldPack());
    RELEASE_ASSERT(newCapacity > 0);
    ASSERT(newCapacity >= m_capacity);
    memcpy(destination, m_entries, sizeof(Entry) * m_size);
    m_capacity = newCapacity;
    m_entries = destination;
}

CheckedBoolean MapData::ensureSpaceForAppend(CallFrame* callFrame)
{
    if (m_capacity > m_size)
        return true;

    size_t requiredSize = std::max(m_capacity + (m_capacity / 2) + 1, minimumMapSize);
    void* newStorage = 0;
    DeferGC defer(*callFrame->heap());
    if (!callFrame->heap()->tryAllocateStorage(this, requiredSize * sizeof(Entry), &newStorage)) {
        throwOutOfMemoryError(callFrame);
        return false;
    }
    Entry* newEntries = static_cast<Entry*>(newStorage);
    if (shouldPack())
        replaceAndPackBackingStore(newEntries, requiredSize);
    else
        replaceBackingStore(newEntries, requiredSize);
    Heap::writeBarrier(this);
    return true;
}

void MapData::visitChildren(JSCell* cell, SlotVisitor& visitor)
{
    Base::visitChildren(cell, visitor);
    MapData* thisObject = jsCast<MapData*>(cell);
    Entry* entries = thisObject->m_entries;
    if (!entries)
        return;
    size_t size = thisObject->m_size;
    if (thisObject->m_deletedCount) {
        for (size_t i = 0; i < size; i++) {
            if (!entries[i].key)
                continue;
            visitor.append(&entries[i].key);
            visitor.append(&entries[i].value);
        }
    } else
        visitor.appendValues(&entries[0].key, size * (sizeof(Entry) / sizeof(WriteBarrier<Unknown>)));

    visitor.copyLater(thisObject, MapBackingStoreCopyToken, entries, thisObject->capacityInBytes());
}

void MapData::copyBackingStore(JSCell* cell, CopyVisitor& visitor, CopyToken token)
{
    MapData* thisObject = jsCast<MapData*>(cell);
    if (token == MapBackingStoreCopyToken && visitor.checkIfShouldCopy(thisObject->m_entries)) {
        Entry* oldEntries = thisObject->m_entries;
        Entry* newEntries = static_cast<Entry*>(visitor.allocateNewSpace(thisObject->capacityInBytes()));
        if (thisObject->shouldPack())
            thisObject->replaceAndPackBackingStore(newEntries, thisObject->m_capacity);
        else
            thisObject->replaceBackingStore(newEntries, thisObject->m_capacity);
        visitor.didCopy(oldEntries, thisObject->capacityInBytes());
    }
    Base::copyBackingStore(cell, visitor, token);
}

void MapData::destroy(JSCell* cell)
{
    static_cast<MapData*>(cell)->~MapData();
}

}
