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

#include "RuntimeLibraryPch.h"

namespace Js
{

const charcount_t JSONStringifier::MaxGapLength = 10;

JSONStringifier::JSONStringifier(_In_ ScriptContext* scriptContext) :
    scriptContext(scriptContext),
    replacerFunction(nullptr),
    totalStringLength(0),
    indentLength(0),
    gapLength(0),
    gap(nullptr),
    propertyList(nullptr)
{
}

void
JSONStringifier::SetStringGap(_In_ JavascriptString* spaceString)
{
    this->gapLength = min(MaxGapLength, spaceString->GetLength());
    if (this->gapLength != 0)
    {
        this->gap = RecyclerNewArrayLeaf(this->scriptContext->GetRecycler(), char16, this->gapLength);
        wmemcpy_s(gap, this->gapLength, spaceString->GetString(), this->gapLength);
    }
}

void
JSONStringifier::SetNumericGap(charcount_t spaceCount)
{
    this->gapLength = spaceCount;
    if (this->gapLength != 0)
    {
        this->gap = RecyclerNewArrayLeaf(this->scriptContext->GetRecycler(), char16, this->gapLength);
        wmemset(gap, _u(' '), this->gapLength);
    }
}

void
JSONStringifier::ReadSpace(_In_opt_ Var space)
{
    if (space != nullptr)
    {
        switch (JavascriptOperators::GetTypeId(space))
        {
        case TypeIds_Integer:
            this->SetNumericGap(static_cast<charcount_t>(max(0, min(static_cast<int>(MaxGapLength), TaggedInt::ToInt32(space)))));

            break;
        case TypeIds_Number:
        case TypeIds_NumberObject:
        case TypeIds_Int64Number:
        case TypeIds_UInt64Number:
        {
            double numericSpace = JavascriptConversion::ToInteger(space, this->scriptContext);
            if (numericSpace > 0)
            {
                SetNumericGap(numericSpace < static_cast<double>(MaxGapLength) ? static_cast<charcount_t>(numericSpace) : MaxGapLength);
            }
            break;
        }
        case TypeIds_String:
            this->SetStringGap(UnsafeVarTo<JavascriptString>(space));
            break;
        case TypeIds_StringObject:
            this->SetStringGap(JavascriptConversion::ToString(space, this->scriptContext));
            break;
        default:
            break;
        }
    }
}

void
JSONStringifier::AddToPropertyList(_In_ Var item, _Inout_ BVSparse<Recycler>* propertyBV)
{
    JavascriptString* propertyName = nullptr;
    switch (JavascriptOperators::GetTypeId(item))
    {
    case TypeIds_Integer:
        propertyName = this->scriptContext->GetIntegerString(item);
        break;
    case TypeIds_String:
        propertyName = UnsafeVarTo<JavascriptString>(item);
        break;
    case TypeIds_Number:
    case TypeIds_NumberObject:
    case TypeIds_Int64Number:
    case TypeIds_UInt64Number:
    case TypeIds_StringObject:
        propertyName = JavascriptConversion::ToString(item, this->scriptContext);
        break;
    default:
        break;
    }

    if (propertyName != nullptr)
    {
        PropertyRecord const* propertyRecord;
        scriptContext->GetOrAddPropertyRecord(propertyName, &propertyRecord);
        if(!propertyBV->TestAndSet(propertyRecord->GetPropertyId()))
        {
            PropertyListElement elem;
            elem.propertyName = propertyName;
            elem.propertyRecord = propertyRecord;
            this->propertyList->Push(elem);
        }
    }
}


void
JSONStringifier::ReadReplacer(_In_opt_ Var replacer)
{
    if (replacer != nullptr)
    {
        RecyclableObject* replacerObj = JavascriptOperators::TryFromVar<RecyclableObject>(replacer);
        if (replacerObj && JavascriptOperators::IsObject(replacerObj))
        {
            if (JavascriptConversion::IsCallable(replacerObj))
            {
                this->replacerFunction = replacerObj;
            }
            else if (JavascriptOperators::IsArray(replacerObj))
            {
                Recycler* recycler = this->scriptContext->GetRecycler();

                BVSparse<Recycler> propertyListBV(recycler);
                this->propertyList = RecyclerNew(recycler, PropertyList, recycler);
                JavascriptArray* propertyArray = JavascriptArray::TryVarToNonES5Array(replacer);
                if (propertyArray != nullptr)
                {
                    uint32 length = propertyArray->GetLength();
                    for (uint32 i = 0; i < length; i++)
                    {
                        Var item = propertyArray->DirectGetItem(i);
                        this->AddToPropertyList(item, &propertyListBV);
                    }
                }
                else
                {
                    int64 length = JavascriptConversion::ToLength(JavascriptOperators::OP_GetLength(replacerObj, this->scriptContext), this->scriptContext);
                    // ToLength always returns positive length
                    Assert(length >= 0);
                    for (uint64 i = 0; i < static_cast<uint64>(length); i++)
                    {
                        Var item = nullptr;
                        if (JavascriptOperators::GetItem(replacerObj, i, &item, scriptContext))
                        {
                            this->AddToPropertyList(item, &propertyListBV);
                        }
                    }
                }
                // PropertyList is an SList, which only has push/pop, so need to reverse list for it to be in order
                this->propertyList->Reverse();
            }
        }
    }
}

LazyJSONString*
JSONStringifier::Stringify(_In_ ScriptContext* scriptContext, _In_ Var value, _In_opt_ Var replacer, _In_opt_ Var space)
{
    Recycler* recycler = scriptContext->GetRecycler();
    JavascriptLibrary* library = scriptContext->GetLibrary();

    if (scriptContext->Cache()->toJSONCache == nullptr)
    {
        scriptContext->Cache()->toJSONCache = ScriptContextPolymorphicInlineCache::New(32, library);
    }

    JSONProperty* prop = RecyclerNewStruct(recycler, JSONProperty);
    JSONObjectStack objStack = { 0 };

    JSONStringifier stringifier(scriptContext);

    stringifier.ReadReplacer(replacer);
    stringifier.ReadSpace(space);

    DynamicObject* wrapper = nullptr;
    if (stringifier.HasReplacerFunction())
    {
        // ReplacerFunction takes wrapper object as a parameter, so we need to materialize it (otherwise it isn't needed)
        wrapper = library->CreateObject();
        PropertyId propertyId = scriptContext->GetEmptyStringPropertyId();
        JavascriptOperators::InitProperty(wrapper, propertyId, value);
    }

    stringifier.ReadProperty(
        library->GetEmptyString(),
        wrapper,
        prop,
        value,
        &objStack);

    if (prop->type == JSONContentType::Undefined)
    {
        return nullptr;
    }
    else
    {
        return RecyclerNew(
            recycler,
            LazyJSONString,
            prop,
            stringifier.totalStringLength,
            stringifier.GetGap(),
            stringifier.GetGapLength(),
            library->GetStringTypeStatic());
    }
}

_Ret_notnull_ Var
JSONStringifier::ReadValue(_In_ JavascriptString* key, _In_opt_ const PropertyRecord* propertyRecord, _In_ RecyclableObject* holder)
{
    Var value = nullptr;
    PropertyValueInfo info;

    if (propertyRecord == nullptr)
    {
        key->GetPropertyRecord(&propertyRecord);
    }

    if (propertyRecord->IsNumeric())
    {
        JavascriptOperators::GetItem(holder, propertyRecord->GetNumericValue(), &value, this->scriptContext);
    }
    else
    {
        PropertyString* propertyString = JavascriptOperators::TryFromVar<PropertyString>(key);
        if (propertyString != nullptr)
        {
            PropertyValueInfo::SetCacheInfo(&info, propertyString, propertyString->GetLdElemInlineCache(), false);
            if (propertyString->TryGetPropertyFromCache<false /* ownPropertyOnly */, false /* OutputExistence */>(holder, holder, &value, this->scriptContext, &info))
            {
                return value;
            }
        }
        JavascriptOperators::GetProperty(holder, propertyRecord->GetPropertyId(), &value, this->scriptContext, &info);
    }

    return value;
}

// Convert NumberObject, StringObject, or BooleanObject to their primitive types,
// since stringification converts these to their primitive values.
// These types are used in cases where user does something like new Object("hello")
Var
JSONStringifier::TryConvertPrimitiveObject(_In_ RecyclableObject* value)
{
    TypeId id = JavascriptOperators::GetTypeId(value);
    if (TypeIds_NumberObject == id)
    {
        return JavascriptNumber::ToVarNoCheck(JavascriptConversion::ToNumber(value, this->scriptContext), this->scriptContext);
    }
    else if (TypeIds_StringObject == id)
    {
        return JavascriptConversion::ToString(value, this->scriptContext);
    }
    else if (TypeIds_BooleanObject == id)
    {
        return (UnsafeVarTo<JavascriptBooleanObject>(value)->GetValue() != FALSE)
            ? this->scriptContext->GetLibrary()->GetTrue()
            : this->scriptContext->GetLibrary()->GetFalse();
    }
    return nullptr;
}

Var
JSONStringifier::ToJSON(_In_ JavascriptString* key, _In_ RecyclableObject* valueObject)
{
    Var toJSON = nullptr;
    PolymorphicInlineCache* cache = this->scriptContext->Cache()->toJSONCache;
    PropertyValueInfo info;
    PropertyValueInfo::SetCacheInfo(&info, cache, false);

    // The vast majority of objects don't have custom toJSON, so check the missing cache first.
    // We can check all of the others afterward.
    if (CacheOperators::TryGetProperty<
        false,  // CheckLocal
        false,  // CheckProto
        false,  // CheckAccessor
        true,   // CheckMissing
        true,   // CheckPolymorphicInlineCache
        false,  // CheckTypePropertyCache
        false,  // IsInlineCacheAvailable
        true,   // IsPolymorphicInlineCacheAvailable
        false,  // ReturnOperationInfo
        false>  // OutputExistence
        (valueObject,
            false,
            valueObject,
            PropertyIds::toJSON,
            &toJSON,
            this->scriptContext,
            nullptr,
            &info))
    {
        // Any cache hit means the property is missing, so don't bother to do anything else
        return nullptr;
    }

    if (!CacheOperators::TryGetProperty<
        true,   // CheckLocal
        true,   // CheckProto
        true,   // CheckAccessor
        false,  // CheckMissing
        true,   // CheckPolymorphicInlineCache
        true,   // CheckTypePropertyCache
        false,  // IsInlineCacheAvailable
        true,   // IsPolymorphicInlineCacheAvailable
        false,  // ReturnOperationInfo
        false>  // OutputExistence
        (valueObject,
            false,
            valueObject,
            PropertyIds::toJSON,
            &toJSON,
            this->scriptContext,
            nullptr,
            &info))
    {
        if (!JavascriptOperators::GetProperty(valueObject, PropertyIds::toJSON, &toJSON, this->scriptContext, &info))
        {
            return nullptr;
        }
    }
    if (JavascriptConversion::IsCallable(toJSON))
    {
        RecyclableObject* func = UnsafeVarTo<RecyclableObject>(toJSON);
        Var values[2];
        Arguments args(2, values);
        args.Values[0] = valueObject;
        args.Values[1] = key;
        BEGIN_SAFE_REENTRANT_CALL(this->scriptContext->GetThreadContext())
        {
            return JavascriptFunction::CallFunction<true>(func, func->GetEntryPoint(), args);
        }
        END_SAFE_REENTRANT_CALL;
    }
    return nullptr;
}

uint32
JSONStringifier::ReadArrayLength(_In_ RecyclableObject* value)
{
    JavascriptArray* arr = JavascriptArray::TryVarToNonES5Array(value);
    if (arr != nullptr)
    {
        return arr->GetLength();
    }

    int64 len = JavascriptConversion::ToLength(JavascriptOperators::OP_GetLength(value, this->scriptContext), this->scriptContext);
    if (len >= MaxCharCount)
    {
        // If the length goes more than MaxCharCount we will eventually fail (as OOM) in ConcatStringBuilder - so failing early.
        JavascriptError::ThrowRangeError(this->scriptContext, JSERR_OutOfBoundString);
    }
    return static_cast<uint32>(len);
}

void
JSONStringifier::ReadArrayElement(uint32 index, _In_ RecyclableObject* arr, _Out_ JSONProperty* prop, _In_ JSONObjectStack* objectStack)
{
    Var value = nullptr;
    JavascriptArray* jsArray = JavascriptArray::TryVarToNonES5Array(arr);
    if (jsArray && !jsArray->IsCrossSiteObject())
    {
        value = jsArray->DirectGetItem(index);
    }
    else
    {
        JavascriptOperators::GetItem(arr, index, &value, this->scriptContext);
    }
    JavascriptString* indexString = this->scriptContext->GetIntegerString(index);
    this->ReadProperty(indexString, arr, prop, value, objectStack);
}

JSONArray*
JSONStringifier::ReadArray(_In_ RecyclableObject* arr, _In_ JSONObjectStack* objectStack)
{
    if (objectStack->Has(arr))
    {
        JavascriptError::ThrowTypeError(this->scriptContext, JSERR_JSONSerializeCircular);
    }
    JSONObjectStack stack = { 0 };
    stack.next = objectStack;
    stack.object = arr;

    // Increase indentation
    const charcount_t stepbackLength = this->indentLength;
    if (this->gapLength != 0)
    {
        this->indentLength = UInt32Math::Add(this->indentLength, this->gapLength);
    }

    const uint32 arrayLength = this->ReadArrayLength(arr);

    const size_t arraySize = AllocSizeMath::Mul(arrayLength, sizeof(JSONProperty));
    JSONArray * jsonArray = RecyclerNewPlusZ(this->scriptContext->GetRecycler(), arraySize, JSONArray);

    jsonArray->length = arrayLength;
    for (uint32 i = 0; i < arrayLength; ++i)
    {
        this->ReadArrayElement(i, arr, &jsonArray->arr[i], &stack);
        if (jsonArray->arr[i].type == JSONContentType::Undefined)
        {
            // We append null string in case of Undefined
            this->SetNullProperty(&jsonArray->arr[i]);
        }
    }

    this->CalculateStringifiedLength(arrayLength, stepbackLength);

    // Restore indentataion
    this->indentLength = stepbackLength;

    return jsonArray;
}

void
JSONStringifier::AppendObjectElement(
    _In_ JavascriptString* propertyName,
    _In_ JSONObject* jsonObject,
    _In_ JSONObjectProperty* prop)
{
    // Undefined result is not concatenated
    if (prop->propertyValue.type != JSONContentType::Undefined)
    {
        // Increase length for the name of the property
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, CalculateStringElementLength(propertyName));
        // Increment length for concatenation of ":"
        UInt32Math::Inc(this->totalStringLength);
        if (this->gapLength != 0)
        {
            // If gap is specified, a space is appended
            UInt32Math::Inc(this->totalStringLength);
        }

        jsonObject->Push(*prop);
    }
}

void
JSONStringifier::ReadObjectElement(
    _In_ JavascriptString* propertyName,
    _In_ uint32 numericIndex,
    _In_ RecyclableObject* obj,
    _In_ JSONObject* jsonObject,
    _In_ JSONObjectStack* objectStack)
{
    JSONObjectProperty prop;
    prop.propertyName = propertyName;

    Var value = JavascriptOperators::GetItem(obj, numericIndex, this->scriptContext);

    this->ReadProperty(propertyName, obj, &prop.propertyValue, value, objectStack);

    this->AppendObjectElement(propertyName, jsonObject, &prop);
}

void
JSONStringifier::ReadObjectElement(
    _In_ JavascriptString* propertyName,
    _In_opt_ PropertyRecord const* propertyRecord,
    _In_ RecyclableObject* obj,
    _In_ JSONObject* jsonObject,
    _In_ JSONObjectStack* objectStack)
{
    JSONObjectProperty prop;
    prop.propertyName = propertyName;

    Var value = this->ReadValue(propertyName, propertyRecord, obj);

    this->ReadProperty(propertyName, obj, &prop.propertyValue, value, objectStack);

    this->AppendObjectElement(propertyName, jsonObject, &prop);
}

// Calculates how many additional characters are needed for printing the Object/Array structure
// This includes commas, brackets, and gap (if any)
void
JSONStringifier::CalculateStringifiedLength(uint32 propertyCount, charcount_t stepbackLength)
{
    if (propertyCount == 0)
    {
        // Empty object is always represented as "{}" (and arrays are "[]")
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, 2);
    }
    else if (this->gapLength == 0)
    {
        // Concatenate the comma separators, one for all but the last property
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, propertyCount - 1);
        // Wrap the object with curly braces
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, 2);
    }
    else
    {
        // Separator is the concatenation of comma, line feed, and indent
        // Therefore length is 2 + indentLength
        charcount_t separatorLength = UInt32Math::Add(this->indentLength, 2);
        // Separators are inserted after all but the last property
        charcount_t totalSeparatorsLength = UInt32Math::Mul(separatorLength, propertyCount - 1);
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, totalSeparatorsLength);

        // Properties are wrapped by open bracket, line feed, indent, line feed, stepback, and close bracket
        // That amounts to an additional length of 4 + indentLength + stepbackLength
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, 4);
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, stepbackLength);
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, this->indentLength);
    }
}

void
JSONStringifier::ReadProxy(_In_ JavascriptProxy* proxyObject, _In_ JSONObject* jsonObject, _In_ JSONObjectStack* stack)
{
    JavascriptArray* ownPropertyNames = proxyObject->PropertyKeysTrap(JavascriptProxy::KeysTrapKind::GetOwnPropertyNamesKind, this->scriptContext);

    // filter enumerable keys
    uint32 resultLength = ownPropertyNames->GetLength();
    for (uint32 i = 0; i < resultLength; i++)
    {
        Var element = ownPropertyNames->DirectGetItem(i);

        // Array should only have string elements, but let's check to be safe
        JavascriptString* propertyName = JavascriptOperators::TryFromVar<JavascriptString>(element);
        Assert(propertyName);
        if (propertyName != nullptr)
        {
            PropertyDescriptor propertyDescriptor;
            PropertyRecord const* propertyRecord;
            JavascriptConversion::ToPropertyKey(propertyName, scriptContext, &propertyRecord, nullptr);
            if (JavascriptOperators::GetOwnPropertyDescriptor(proxyObject, propertyRecord->GetPropertyId(), scriptContext, &propertyDescriptor))
            {
                if (propertyDescriptor.IsEnumerable())
                {
                    this->ReadObjectElement(propertyName, propertyRecord, proxyObject, jsonObject, stack);
                }
            }
        }
    }
}

JSONObject*
JSONStringifier::ReadObject(_In_ RecyclableObject* obj, _In_ JSONObjectStack* objectStack)
{
    if (objectStack->Has(obj))
    {
        JavascriptError::ThrowTypeError(this->scriptContext, JSERR_JSONSerializeCircular);
    }
    JSONObjectStack stack = { 0 };
    stack.next = objectStack;
    stack.object = obj;

    // Increase indentation
    const charcount_t stepbackLength = this->indentLength;
    if (this->gapLength != 0)
    {
        this->indentLength = UInt32Math::Add(this->indentLength, this->gapLength);
    }

    Recycler* recycler = this->scriptContext->GetRecycler();
    JSONObject* jsonObject = RecyclerNew(recycler, JSONObject, recycler);

    // If we are given a property list, we should only read the listed properties
    if (this->propertyList != nullptr)
    {
        FOREACH_SLIST_ENTRY(PropertyListElement, entry, this->propertyList)
        {
            this->ReadObjectElement(entry.propertyName, entry.propertyRecord, obj, jsonObject, &stack);
        }
        NEXT_SLIST_ENTRY;
    }
    else
    {
        // Enumerating proxies is different than normal objects, so enumerate them separately
        JavascriptProxy* proxyObject = JavascriptOperators::TryFromVar<JavascriptProxy>(obj);
        if (proxyObject != nullptr)
        {
            this->ReadProxy(proxyObject, jsonObject, &stack);
        }
        else
        {
            JavascriptStaticEnumerator enumerator;
            EnumeratorCache* cache = this->scriptContext->GetLibrary()->GetStringifyCache(obj->GetType());
            if (obj->GetEnumerator(&enumerator, EnumeratorFlags::SnapShotSemantics | EnumeratorFlags::EphemeralReference | EnumeratorFlags::UseCache, this->scriptContext, cache))
            {
                JavascriptString* propertyName = nullptr;
                PropertyId nextKey = Constants::NoProperty;
                while ((propertyName = enumerator.MoveAndGetNext(nextKey)) != nullptr)
                {
                    const uint32 numericIndex = enumerator.GetCurrentItemIndex();
                    if (numericIndex != Constants::InvalidSourceIndex)
                    {
                        this->ReadObjectElement(propertyName, numericIndex, obj, jsonObject, &stack);
                    }
                    else
                    {
                        PropertyRecord const * propertyRecord = nullptr;
                        if (nextKey == Constants::NoProperty)
                        {
                            this->scriptContext->GetOrAddPropertyRecord(propertyName, &propertyRecord);
                            nextKey = propertyRecord->GetPropertyId();
                        }
                        this->ReadObjectElement(propertyName, propertyRecord, obj, jsonObject, &stack);
                    }
                }
            }
        }
    }
    // JSONObject is an SList, which only has push/pop, so need to reverse list for it to be in order
    jsonObject->Reverse();
    const uint propertyCount = jsonObject->Count();

    this->CalculateStringifiedLength(propertyCount, stepbackLength);

    // Restore indent level
    this->indentLength = stepbackLength;

    return jsonObject;
}

Var
JSONStringifier::CallReplacerFunction(_In_opt_ RecyclableObject* holder, _In_ JavascriptString* key, _In_ Var value)
{
    Var values[3];
    Arguments args(0, values);

    args.Info.Count = 3;
    args.Values[0] = holder;
    args.Values[1] = key;
    args.Values[2] = value;

    BEGIN_SAFE_REENTRANT_CALL(this->scriptContext->GetThreadContext())
    {
        return JavascriptFunction::CallFunction<true>(this->replacerFunction, this->replacerFunction->GetEntryPoint(), args);
    }
    END_SAFE_REENTRANT_CALL;
}

void
JSONStringifier::SetNullProperty(_Out_ JSONProperty* prop)
{
    prop->type = JSONContentType::Null;
    this->totalStringLength = UInt32Math::Add(this->totalStringLength, Constants::NullStringLength);
}

void
JSONStringifier::SetNumericProperty(double value, _In_ Var valueVar, _Out_ JSONProperty* prop)
{
    if (NumberUtilities::IsFinite(value))
    {
        prop->type = JSONContentType::Number;
        prop->numericValue.value = valueVar;
        prop->numericValue.string = JavascriptConversion::ToString(valueVar, this->scriptContext);
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, prop->numericValue.string->GetLength());
    }
    else
    {
        this->SetNullProperty(prop);
    }
}

// Strings may need to be escaped, so the length requires some calculation
charcount_t
JSONStringifier::CalculateStringElementLength(_In_ JavascriptString* str)
{
    const charcount_t strLength = str->GetLength();

    // To avoid overflow checks, use uint64 for intermediate size calculation. We cannot overflow uint64,
    // as stringification will be at most a 5x expansion of the original string (which has max length INT_MAX - 1)
    //
    // All JSON strings are enclosed in double quotes, so add 2 to the length to account for them
    uint64 escapedStrLength = strLength + 2;
    const char16* bufferStart = str->GetString();
    for (const char16* index = str->GetString(); index < bufferStart + strLength; ++index)
    {
        char16 currentCharacter = *index;

        // Some characters may require an escape sequence. We can use the escapeMapCount table
        // to determine how many extra characters are needed
        if (currentCharacter < _countof(LazyJSONString::escapeMapCount))
        {
            escapedStrLength += LazyJSONString::escapeMapCount[currentCharacter];
        }
        else if (utf8::IsLowSurrogateChar(currentCharacter))
        {
            // Lone trailing-surrogate code units should be escaped.
            // They will always need 5 extra characters for the escape sequence, ie: \udbff
            escapedStrLength += 5;
        }
        else if (utf8::IsHighSurrogateChar(currentCharacter))
        {
            if (index + 1 < bufferStart + strLength && utf8::IsLowSurrogateChar(*(index + 1)))
            {
                // Regular surrogate pairs are handled normally - skip the trailing-surrogate code unit.
                index++;
            }
            else
            {
                // High-surrogate code unit not followed by a trailing-surrogate code unit should be escaped.
                escapedStrLength += 5;
            }
        }
    }
    if (escapedStrLength > UINT32_MAX)
    {
        Js::Throw::OutOfMemory();
    }
    return static_cast<charcount_t>(escapedStrLength);
}

void
JSONStringifier::ReadData(_In_ RecyclableObject* valueObj, _Out_ JSONProperty* prop)
{
    TypeId typeId = JavascriptOperators::GetTypeId(valueObj);

    switch (typeId)
    {
    case TypeIds_Null:
        this->SetNullProperty(prop);
        return;

    case TypeIds_Boolean:
        if (UnsafeVarTo<JavascriptBoolean>(valueObj)->GetValue() != FALSE)
        {
            prop->type = JSONContentType::True;
            this->totalStringLength = UInt32Math::Add(this->totalStringLength, Constants::TrueStringLength);
        }
        else
        {
            prop->type = JSONContentType::False;
            this->totalStringLength = UInt32Math::Add(this->totalStringLength, Constants::FalseStringLength);
        }
        return;

    case TypeIds_Int64Number:
        this->SetNumericProperty(static_cast<double>(UnsafeVarTo<JavascriptInt64Number>(valueObj)->GetValue()), valueObj, prop);
        return;

    case TypeIds_UInt64Number:
        this->SetNumericProperty(static_cast<double>(UnsafeVarTo<JavascriptUInt64Number>(valueObj)->GetValue()), valueObj, prop);
        return;

#if !FLOATVAR
    case TypeIds_Number:
        this->SetNumericProperty(JavascriptNumber::GetValue(valueObj), valueObj, prop);
        return;
#endif

    case TypeIds_String:
        prop->stringValue = UnsafeVarTo<JavascriptString>(valueObj);
        prop->type = JSONContentType::String;
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, CalculateStringElementLength(prop->stringValue));
        return;

    case TypeIds_Symbol:
        // Treat symbols as undefined
    case TypeIds_Undefined:
        // If undefined is in an object, it is not included in the string, and if it is an array element it will show up as "null"
        // Given that, we can't caluclate the string length here and instead we do it at the usage points
        prop->type = JSONContentType::Undefined;
        return;

    default:
        Assert(UNREACHED);
        prop->type = JSONContentType::Undefined;
        return;
    }
}

void
JSONStringifier::ReadData(_In_ Var value, _Out_ JSONProperty* prop)
{
    RecyclableObject* valueObj = JavascriptOperators::TryFromVar<RecyclableObject>(value);
    if (valueObj != nullptr)
    {
        ReadData(valueObj, prop);
        return;
    }

    // If value isn't a RecyclableObject, it must be a tagged number

    TypeId typeId = JavascriptOperators::GetTypeId(value);
    switch (typeId)
    {
    case TypeIds_Integer:
    {
        prop->type = JSONContentType::Number;
        prop->numericValue.value = value;
        prop->numericValue.string = this->scriptContext->GetIntegerString(value);
        this->totalStringLength = UInt32Math::Add(this->totalStringLength, prop->numericValue.string->GetLength());
        return;
    }
#if FLOATVAR
    case TypeIds_Number:
        this->SetNumericProperty(JavascriptNumber::GetValue(value), value, prop);
        return;
#endif
    default:
        Assume(UNREACHED);
        prop->type = JSONContentType::Undefined;
        return;
    }
}

void
RecheckValue(_In_ Var value, _Out_ RecyclableObject** valueObj, _Out_ bool* isObject)
{
    *valueObj = JavascriptOperators::TryFromVar<RecyclableObject>(value);
    if (*valueObj != nullptr)
    {
        *isObject = JavascriptOperators::IsObject(value) != FALSE;
    }
    else
    {
        *isObject = false;
    }
}

void
JSONStringifier::ReadProperty(
    _In_ JavascriptString* key,
    _In_opt_ RecyclableObject* holder,
    _Out_ JSONProperty* prop,
    _In_ Var value,
    _In_ JSONObjectStack* objectStack)
{
    PROBE_STACK(this->scriptContext, Constants::MinStackDefault);

    // Save these to avoid having to recheck conditions unless value is modified by ToJSON or a replacer function
    RecyclableObject* valueObj = JavascriptOperators::TryFromVar<RecyclableObject>(value);
    bool isObject = false;
    if (valueObj)
    {
        isObject = JavascriptOperators::IsObject(valueObj) != FALSE;
        if (isObject)
        {
            // If value is an object, we must first check if it has a ToJSON method
            Var toJSONValue = this->ToJSON(key, valueObj);
            if (toJSONValue != nullptr)
            {
                value = toJSONValue;
                RecheckValue(toJSONValue, &valueObj, &isObject);
            }
        }
    }

    // If user provided a replacer function, we need to call it
    if (this->replacerFunction != nullptr)
    {
        value = this->CallReplacerFunction(holder, key, value);
        RecheckValue(value, &valueObj, &isObject);
    }

    if (valueObj != nullptr)
    {
        // Callable JS objects are undefined, but we still stringify callable host objects
        // Host object case is for compat with old implementation, but isn't defined in the
        // spec, so we should consider removing it
        if (JavascriptConversion::IsCallable(valueObj) && JavascriptOperators::IsJsNativeObject(valueObj))
        {
            prop->type = JSONContentType::Undefined;
            return;
        }

        if (JavascriptOperators::IsArray(valueObj))
        {
            prop->type = JSONContentType::Array;
            prop->arr = this->ReadArray(valueObj, objectStack);
            return;
        }

        Var primitive = this->TryConvertPrimitiveObject(valueObj);
        if (primitive != nullptr)
        {
            value = primitive;
            RecheckValue(value, &valueObj, &isObject);
        }

        if (isObject)
        {
            prop->type = JSONContentType::Object;
            prop->obj = this->ReadObject(valueObj, objectStack);
            return;
        }

        if (valueObj != nullptr)
        {
            this->ReadData(valueObj, prop);
            return;
        }
    }
    this->ReadData(value, prop);
    return;
}

} // namespace Js
