/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "ecmascript/js_api/js_api_lightweightset.h"

#include "ecmascript/containers/containers_errors.h"
#include "ecmascript/interpreter/interpreter.h"
#include "ecmascript/js_api/js_api_lightweightset_iterator.h"
#include "ecmascript/js_array.h"
#include "ecmascript/js_function.h"
#include "ecmascript/js_tagged_value.h"
#include "ecmascript/object_factory.h"

namespace panda::ecmascript {
using ContainerError = containers::ContainerError;
using ErrorFlag = containers::ErrorFlag;
bool JSAPILightWeightSet::Add(JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                              const JSHandle<JSTaggedValue> &value)
{
    uint32_t hashCode = obj->Hash(value.GetTaggedValue());
    JSHandle<TaggedArray> hashArray(thread, obj->GetHashes());
    JSHandle<TaggedArray> valueArray(thread, obj->GetValues());
    int32_t size = static_cast<int32_t>(obj->GetLength());
    int32_t index = obj->GetHashIndex(value, size);
    if (index >= 0) {
        return false;
    }
    index ^= JSAPILightWeightSet::HASH_REBELLION;
    if (index < size) {
        obj->AdjustArray(thread, hashArray, index, size, true);
        obj->AdjustArray(thread, valueArray, index, size, true);
    }
    uint32_t capacity = hashArray->GetLength();
    if (size + 1 >= static_cast<int32_t>(capacity)) {
        // need expanding
        uint32_t newCapacity = capacity << 1U;
        hashArray = thread->GetEcmaVM()->GetFactory()->CopyArray(hashArray, capacity, newCapacity);
        valueArray = thread->GetEcmaVM()->GetFactory()->CopyArray(valueArray, capacity, newCapacity);
        obj->SetHashes(thread, hashArray);
        obj->SetValues(thread, valueArray);
    }
    hashArray->Set(thread, index, JSTaggedValue(hashCode));
    valueArray->Set(thread, index, value.GetTaggedValue());
    size++;
    obj->SetLength(size);
    return true;
}

JSTaggedValue JSAPILightWeightSet::Get(const uint32_t index)
{
    TaggedArray *valueArray = TaggedArray::Cast(GetValues().GetTaggedObject());
    return valueArray->Get(index);
}

JSHandle<TaggedArray> JSAPILightWeightSet::CreateSlot(const JSThread *thread, const uint32_t capacity)
{
    ASSERT_PRINT(capacity > 0, "size must be a non-negative integer");
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    JSHandle<TaggedArray> taggedArray = factory->NewTaggedArray(capacity);
    for (uint32_t i = 0; i < capacity; i++) {
        taggedArray->Set(thread, i, JSTaggedValue::Hole());
    }
    return taggedArray;
}

int32_t JSAPILightWeightSet::GetHashIndex(const JSHandle<JSTaggedValue> &value, int32_t size)
{
    uint32_t hashCode = Hash(value.GetTaggedValue());
    int32_t index = BinarySearchHashes(hashCode, size);
    if (index < 0) {
        return index;
    }
    TaggedArray *valueArray = TaggedArray::Cast(GetValues().GetTaggedObject());
    if (index < size && (JSTaggedValue::SameValue(valueArray->Get(index), value.GetTaggedValue()))) {
        return index;
    }
    TaggedArray *hashArray = TaggedArray::Cast(GetHashes().GetTaggedObject());
    int32_t right = index;
    while (right < size && (hashArray->Get(right).GetNumber() == hashCode)) {
        if (JSTaggedValue::SameValue(valueArray->Get(right), value.GetTaggedValue())) {
            return right;
        }
        right++;
    }
    int32_t left = index - 1;
    while (left >= 0 && ((hashArray->Get(left).GetNumber() == hashCode))) {
        if (JSTaggedValue::SameValue(valueArray->Get(left), value.GetTaggedValue())) {
            return left;
        }
        left--;
    }
    return -right;
}

int32_t JSAPILightWeightSet::BinarySearchHashes(uint32_t hash, int32_t size)
{
    int32_t low = 0;
    int32_t high = size - 1;
    TaggedArray *hashArray = TaggedArray::Cast(GetHashes().GetTaggedObject());
    while (low <= high) {
        uint32_t mid = static_cast<uint32_t>(low + high) >> 1U;
        uint32_t midVal = (uint32_t)(hashArray->Get(mid).GetNumber());
        if (midVal < hash) {
            low = mid + 1;
        } else {
            if (midVal <= hash) {
                return mid;
            }
            high = mid - 1;
        }
    }
    return -(low + 1);
}

bool JSAPILightWeightSet::AddAll(JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                                 const JSHandle<JSTaggedValue> &value)
{
    bool changed = false;
    JSHandle<JSAPILightWeightSet> srcLightWeightSet = JSHandle<JSAPILightWeightSet>::Cast(value);
    uint32_t srcSize = srcLightWeightSet->GetSize();
    uint32_t size = obj->GetSize();
    obj->EnsureCapacity(thread, obj, size + srcSize);
    JSMutableHandle<JSTaggedValue> element(thread, JSTaggedValue::Undefined());
    for (uint32_t i = 0; i < srcSize; i++) {
        element.Update(srcLightWeightSet->GetValueAt(i));
        changed |= JSAPILightWeightSet::Add(thread, obj, element);
    }
    return changed;
}

void JSAPILightWeightSet::EnsureCapacity(const JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                                         uint32_t minimumCapacity)
{
    TaggedArray *hashes = TaggedArray::Cast(obj->GetValues().GetTaggedObject());
    uint32_t capacity = hashes->GetLength();
    uint32_t newCapacity = capacity;
    if (capacity > minimumCapacity) {
        return;
    }
    // adjust
    while (newCapacity <= minimumCapacity) {
        newCapacity = newCapacity << 1U;
    }
    obj->SizeCopy(thread, obj, capacity, newCapacity);
}

void JSAPILightWeightSet::SizeCopy(const JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                                   uint32_t capacity, uint32_t newCapacity)
{
    JSHandle<TaggedArray> hashArray(thread, obj->GetHashes());
    JSHandle<TaggedArray> valueArray(thread, obj->GetValues());
    hashArray = thread->GetEcmaVM()->GetFactory()->CopyArray(hashArray, capacity, newCapacity);
    valueArray = thread->GetEcmaVM()->GetFactory()->CopyArray(valueArray, capacity, newCapacity);

    obj->SetValues(thread, hashArray);
    obj->SetHashes(thread, valueArray);
}

bool JSAPILightWeightSet::IsEmpty()
{
    return GetLength() == 0;
}

JSTaggedValue JSAPILightWeightSet::GetValueAt(int32_t index)
{
    int32_t size = static_cast<int32_t>(GetLength());
    if (index < 0 || index >= size) {
        return JSTaggedValue::Undefined();
    }
    TaggedArray *values = TaggedArray::Cast(GetValues().GetTaggedObject());
    return values->Get(index);
}

JSTaggedValue JSAPILightWeightSet::GetHashAt(int32_t index)
{
    int32_t size = static_cast<int32_t>(GetLength());
    if (index < 0 || index >= size) {
        return JSTaggedValue::Undefined();
    }
    TaggedArray *values = TaggedArray::Cast(GetHashes().GetTaggedObject());
    return values->Get(index);
}

bool JSAPILightWeightSet::HasAll(const JSHandle<JSTaggedValue> &value)
{
    bool result = false;
    uint32_t relocate = 0;
    JSAPILightWeightSet *lightweightSet = JSAPILightWeightSet::Cast(value.GetTaggedValue().GetTaggedObject());
    uint32_t size = GetLength();
    uint32_t destSize = lightweightSet->GetLength();
    TaggedArray *hashes = TaggedArray::Cast(GetHashes().GetTaggedObject());
    TaggedArray *destHashes = TaggedArray::Cast(lightweightSet->GetHashes().GetTaggedObject());
    if (destSize > size) {
        return result;
    }
    for (uint32_t i = 0; i < destSize; i++) {
        uint32_t destHashCode = destHashes->Get(i).GetNumber();
        result = false;
        for (uint32_t j = relocate; j < size; j++) {
            uint32_t hashCode = hashes->Get(j).GetNumber();
            if (destHashCode == hashCode) {
                result = true;
                relocate = j + 1;
                break;
            }
        }
        if (!result) {
            break;
        }
    }
    return result;
}

bool JSAPILightWeightSet::Has(const JSHandle<JSTaggedValue> &value)
{
    uint32_t size = GetLength();
    int32_t index = GetHashIndex(value, size);
    if (index < 0) {
        return false;
    }
    return true;
}

bool JSAPILightWeightSet::HasHash(const JSHandle<JSTaggedValue> &hashCode)
{
    uint32_t size = GetLength();
    int32_t index = BinarySearchHashes(hashCode.GetTaggedValue().GetNumber(), size);
    if (index < 0) {
        return false;
    }
    return true;
}

bool JSAPILightWeightSet::Equal(JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                                const JSHandle<JSTaggedValue> &value)
{
    bool result = false;
    JSHandle<TaggedArray> destHashes(thread, obj->GetValues());
    uint32_t destSize = obj->GetLength();
    uint32_t srcSize = 0;
    JSMutableHandle<TaggedArray> srcHashes(thread, obj->GetHashes());
    if (value.GetTaggedValue().IsJSAPILightWeightSet()) {
        JSAPILightWeightSet *srcLightWeightSet = JSAPILightWeightSet::Cast(value.GetTaggedValue().GetTaggedObject());
        srcSize = srcLightWeightSet->GetLength();
        if (srcSize == 0 || destSize == 0) {
            return false;
        }
        srcHashes.Update(srcLightWeightSet->GetHashes());
    }
    if (value.GetTaggedValue().IsJSArray()) {
        srcHashes.Update(JSArray::ToTaggedArray(thread, value));
        srcSize = srcHashes->GetLength();
        if (srcSize == 0 || destSize == 0) {
            return false;
        }
    }
    if (srcSize != destSize) {
        return false;
    }
    for (uint32_t i = 0; i < destSize; i++) {
        JSTaggedValue compareValue = destHashes->Get(i);
        JSTaggedValue values = srcHashes->Get(i);
        if (compareValue.IsNumber() && values.IsNumber()) {
            result = JSTaggedValue::SameValueNumberic(compareValue, values);
        }
        if (compareValue.IsString() && values.IsString()) {
            result =
                JSTaggedValue::StringCompare(EcmaString::Cast(compareValue.GetTaggedObject()),
                                             EcmaString::Cast(values.GetTaggedObject()));
        }
        if (!result) {
            return result;
        }
    }
    return result;
}

void JSAPILightWeightSet::IncreaseCapacityTo(JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                                             int32_t minCapacity)
{
    uint32_t capacity = TaggedArray::Cast(obj->GetValues().GetTaggedObject())->GetLength();
    int32_t intCapacity = static_cast<int32_t>(capacity);
    if (minCapacity <= 0 || intCapacity >= minCapacity) {
        std::ostringstream oss;
        oss << "The value of \"minimumCapacity\" is out of range. It must be > " << intCapacity
            << ". Received value is: " << minCapacity;
        JSTaggedValue error = ContainerError::BusinessError(thread, ErrorFlag::RANGE_ERROR, oss.str().c_str());
        THROW_NEW_ERROR_AND_RETURN(thread, error);
    }
    JSHandle<TaggedArray> hashArray(thread, obj->GetHashes());
    JSHandle<TaggedArray> newElements =
        thread->GetEcmaVM()->GetFactory()->NewAndCopyTaggedArray(hashArray,
                                                                 static_cast<uint32_t>(minCapacity), capacity);
    obj->SetHashes(thread, newElements);
}

JSHandle<JSTaggedValue> JSAPILightWeightSet::GetIteratorObj(JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj,
                                                            IterationKind kind)
{
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    JSHandle<JSTaggedValue> iter =
        JSHandle<JSTaggedValue>::Cast(factory->NewJSAPILightWeightSetIterator(obj, kind));
    return iter;
}

JSTaggedValue JSAPILightWeightSet::ForEach(JSThread *thread, const JSHandle<JSTaggedValue> &thisHandle,
                                           const JSHandle<JSTaggedValue> &callbackFn,
                                           const JSHandle<JSTaggedValue> &thisArg)
{
    JSHandle<JSAPILightWeightSet> lightweightset = JSHandle<JSAPILightWeightSet>::Cast(thisHandle);
    uint32_t length = lightweightset->GetSize();
    JSHandle<JSTaggedValue> undefined = thread->GlobalConstants()->GetHandledUndefined();
    for (uint32_t k = 0; k < length; k++) {
        JSTaggedValue kValue = lightweightset->GetValueAt(k);
        EcmaRuntimeCallInfo *info =
            EcmaInterpreter::NewRuntimeCallInfo(thread, callbackFn, thisArg, undefined, 3); // 3:three args
        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, JSTaggedValue::Exception());
        info->SetCallArg(kValue, kValue, thisHandle.GetTaggedValue());
        JSTaggedValue funcResult = JSFunction::Call(info);
        RETURN_VALUE_IF_ABRUPT_COMPLETION(thread, funcResult);
        if (lightweightset->GetSize() != length) {  // prevent length change
            length = lightweightset->GetSize();
        }
    }
    return JSTaggedValue::Undefined();
}

int32_t JSAPILightWeightSet::GetIndexOf(JSHandle<JSTaggedValue> &value)
{
    uint32_t size = GetLength();
    int32_t index = GetHashIndex(value, size);
    return index;
}

JSTaggedValue JSAPILightWeightSet::Remove(JSThread *thread, JSHandle<JSTaggedValue> &value)
{
    uint32_t size = GetLength();
    TaggedArray *valueArray = TaggedArray::Cast(GetValues().GetTaggedObject());
    int32_t index = GetHashIndex(value, size);
    if (index < 0) {
        return JSTaggedValue::Undefined();
    }
    JSTaggedValue result = valueArray->Get(index);
    RemoveAt(thread, index);
    return result;
}

bool JSAPILightWeightSet::RemoveAt(JSThread *thread, int32_t index)
{
    uint32_t size = GetLength();
    if (index < 0 || index >= static_cast<int32_t>(size)) {
        return false;
    }
    JSHandle<TaggedArray> valueArray(thread, GetValues());
    JSHandle<TaggedArray> hashArray(thread, GetHashes());
    RemoveValue(thread, hashArray, static_cast<uint32_t>(index));
    RemoveValue(thread, valueArray, static_cast<uint32_t>(index));
    SetLength(size - 1);
    return true;
}

void JSAPILightWeightSet::RemoveValue(const JSThread *thread, JSHandle<TaggedArray> &taggedArray,
                                      uint32_t index)
{
    uint32_t len = GetLength();
    ASSERT(index < len);
    TaggedArray::RemoveElementByIndex(thread, taggedArray, index, len);
}

void JSAPILightWeightSet::AdjustArray(JSThread *thread, JSHandle<TaggedArray> srcArray, uint32_t fromIndex,
                                      uint32_t toIndex, bool direction)
{
    uint32_t size = GetLength();
    uint32_t idx = size - 1;
    if (direction) {
        while (fromIndex < toIndex) {
            JSTaggedValue value = srcArray->Get(idx);
            srcArray->Set(thread, idx + 1, value);
            idx--;
            fromIndex++;
        }
    } else {
        uint32_t moveSize = size - fromIndex;
        for (uint32_t i = 0; i < moveSize; i++) {
            if ((fromIndex + i) < size) {
                JSTaggedValue value = srcArray->Get(fromIndex + i);
                srcArray->Set(thread, toIndex + i, value);
            } else {
                srcArray->Set(thread, toIndex + i, JSTaggedValue::Hole());
            }
        }
    }
}

JSTaggedValue JSAPILightWeightSet::ToString(JSThread *thread, const JSHandle<JSAPILightWeightSet> &obj)
{
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    std::u16string sepStr = std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> {}.from_bytes(",");

    uint32_t length = obj->GetSize();
    JSHandle<TaggedArray> valueArray(thread, obj->GetValues());
    std::u16string concatStr;
    JSMutableHandle<JSTaggedValue> values(thread, JSTaggedValue::Undefined());
    for (uint32_t k = 0; k < length; k++) {
        std::u16string nextStr;
        values.Update(valueArray->Get(k));
        if (!values->IsUndefined() && !values->IsNull()) {
            JSHandle<EcmaString> nextStringHandle = JSTaggedValue::ToString(thread, values);
            RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
            nextStr = EcmaStringAccessor(nextStringHandle).ToU16String();
        }
        if (k > 0) {
            concatStr.append(sepStr);
            concatStr.append(nextStr);
            continue;
        }
        concatStr.append(nextStr);
    }
    char16_t *char16tData = concatStr.data();
    auto *uint16tData = reinterpret_cast<uint16_t *>(char16tData);
    int32_t u16strSize = concatStr.size();
    return factory->NewFromUtf16Literal(uint16tData, u16strSize).GetTaggedValue();
}

void JSAPILightWeightSet::Clear(JSThread *thread)
{
    TaggedArray *hashArray = TaggedArray::Cast(GetHashes().GetTaggedObject());
    TaggedArray *valueArray = TaggedArray::Cast(GetValues().GetTaggedObject());
    uint32_t size = GetLength();
    for (uint32_t index = 0; index < size; index++) {
        hashArray->Set(thread, index, JSTaggedValue::Hole());
        valueArray->Set(thread, index, JSTaggedValue::Hole());
    }
    SetLength(0);
}

uint32_t JSAPILightWeightSet::Hash(JSTaggedValue key)
{
    if (key.IsDouble() && key.GetDouble() == 0.0) {
        key = JSTaggedValue(0);
    }
    if (key.IsSymbol()) {
        auto symbolString = JSSymbol::Cast(key.GetTaggedObject());
        return symbolString->GetHashField();
    }
    if (key.IsString()) {
        auto keyString = EcmaString::Cast(key.GetTaggedObject());
        return EcmaStringAccessor(keyString).GetHashcode();
    }
    if (key.IsECMAObject()) {
        uint32_t hash = ECMAObject::Cast(key.GetTaggedObject())->GetHash();
        if (hash == 0) {
            hash = base::RandomGenerator::GenerateIdentityHash();
            ECMAObject::Cast(key.GetTaggedObject())->SetHash(hash);
        }
        return hash;
    }
    if (key.IsInt()) {
        uint32_t hash = key.GetInt();
        return hash;
    }
    uint64_t keyValue = key.GetRawData();
    return GetHash32(reinterpret_cast<uint8_t *>(&keyValue), sizeof(keyValue) / sizeof(uint8_t));
}
} // namespace panda::ecmascript