//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
#include "RuntimeLibraryPch.h"
#include "JavascriptExceptionMetadata.h"

namespace Js {
    SimplePropertyDescriptor const ExceptionMetadataPropertyDescriptors[6] =
    {
        SimplePropertyDescriptor(NO_WRITE_BARRIER_TAG(BuiltInPropertyRecords::exception), PropertyWritable),
        SimplePropertyDescriptor(NO_WRITE_BARRIER_TAG(BuiltInPropertyRecords::source), PropertyWritable),
        SimplePropertyDescriptor(NO_WRITE_BARRIER_TAG(BuiltInPropertyRecords::line), PropertyWritable),
        SimplePropertyDescriptor(NO_WRITE_BARRIER_TAG(BuiltInPropertyRecords::column), PropertyWritable),
        SimplePropertyDescriptor(NO_WRITE_BARRIER_TAG(BuiltInPropertyRecords::length), PropertyWritable),
        SimplePropertyDescriptor(NO_WRITE_BARRIER_TAG(BuiltInPropertyRecords::url), PropertyWritable)
    };


    SimpleTypeHandler<6> JavascriptExceptionMetadata::ExceptionMetadataTypeHandler(NO_WRITE_BARRIER_TAG(ExceptionMetadataPropertyDescriptors));

    Var JavascriptExceptionMetadata::CreateMetadataVar(ScriptContext * scriptContext) {
        DynamicType* exceptionMetadataType = DynamicType::New(scriptContext, TypeIds_Object,
            scriptContext->GetLibrary()->GetNull(), nullptr, &JavascriptExceptionMetadata::ExceptionMetadataTypeHandler, true, true);
        return DynamicObject::New(scriptContext->GetRecycler(), exceptionMetadataType);
    }

    void JavascriptExceptionMetadata::PopulateMetadataFromCompileException(Var metadata, Var exception, ScriptContext * scriptContext) {
        Js::Var var;

        var = Js::JavascriptOperators::ToNumber(
            Js::JavascriptOperators::OP_GetProperty(exception, Js::PropertyIds::line, scriptContext),
            scriptContext);
        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::line, var, scriptContext);

        var = Js::JavascriptOperators::ToNumber(
            Js::JavascriptOperators::OP_GetProperty(exception, Js::PropertyIds::column, scriptContext),
            scriptContext);
        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::column, var, scriptContext);

        var = Js::JavascriptOperators::ToNumber(
            Js::JavascriptOperators::OP_GetProperty(exception, Js::PropertyIds::length, scriptContext),
            scriptContext);
        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::length, var, scriptContext);

        var = Js::JavascriptConversion::ToString(
            Js::JavascriptOperators::OP_GetProperty(exception, Js::PropertyIds::source, scriptContext),
            scriptContext);
        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::source, var, scriptContext);

        var = Js::JavascriptConversion::ToString(
            Js::JavascriptOperators::OP_GetProperty(exception, Js::PropertyIds::url, scriptContext),
            scriptContext);
        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::url, var, scriptContext);
    }

    bool JavascriptExceptionMetadata::PopulateMetadataFromException(Var metadata, JavascriptExceptionObject * recordedException, ScriptContext * scriptContext) {
        uint32 offset = recordedException->GetByteCodeOffset();
        FunctionBody * functionBody = recordedException->GetFunctionBody();
        ULONG line;
        LONG column;
        if (functionBody->GetUtf8SourceInfo()->GetIsLibraryCode() ||
            !functionBody->GetLineCharOffset(offset, &line, &column)) {
            line = 0;
            column = 0;
        }

        Js::Utf8SourceInfo* sourceInfo = functionBody->GetUtf8SourceInfo();
        sourceInfo->EnsureLineOffsetCache();
        LineOffsetCache *cache = sourceInfo->GetLineOffsetCache();

        if (line >= cache->GetLineCount())
        {
            return false;
        }

        uint32 nextLine;
        if (UInt32Math::Add(line, 1, &nextLine))
        {
            // Overflowed
            return false;
        }

        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::line,
            Js::JavascriptNumber::New(line, scriptContext), scriptContext);

        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::column,
            Js::JavascriptNumber::New(column, scriptContext), scriptContext);

        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::length,
            Js::JavascriptNumber::New(0, scriptContext), scriptContext);

        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::url,
            Js::JavascriptString::NewCopySz(functionBody->GetSourceName(), scriptContext), scriptContext);

        LPCUTF8 functionSource = sourceInfo->GetSource(_u("Jsrt::JsExperimentalGetAndClearExceptionWithMetadata"));

        charcount_t startByteOffset = 0;
        charcount_t endByteOffset = 0;
        charcount_t startCharOffset = 0;
        charcount_t endCharOffset = 0;

        
        startCharOffset = cache->GetCharacterOffsetForLine(line, &startByteOffset);

        if (nextLine >= cache->GetLineCount())
        {
            endByteOffset = functionBody->LengthInBytes() + functionBody->StartInDocument();
            endCharOffset = functionBody->LengthInChars() + functionBody->StartOffset();
        }
        else
        {
            endCharOffset = cache->GetCharacterOffsetForLine(nextLine, &endByteOffset);

            // The offsets above point to the start of the following line,
            // while we need to find the end of the current line.
            // To do so, just step back over the preceeding newline character(s)
            if (functionSource[endByteOffset - 1] == _u('\n'))
            {
                endCharOffset--;
                endByteOffset--;
                // This may have been \r\n
                if (endByteOffset > 0 && functionSource[endByteOffset - 1] == _u('\r'))
                {
                    endCharOffset--;
                    endByteOffset--;
                }
            }
            else
            {
                utf8::DecodeOptions options = utf8::doAllowThreeByteSurrogates;
                LPCUTF8 potentialNewlineStart = functionSource + endByteOffset - 3;
                char16 decodedCharacter = utf8::Decode(potentialNewlineStart, functionSource + endByteOffset, options);
                if (decodedCharacter == 0x2028 || decodedCharacter == 0x2029)
                {
                    endCharOffset--;
                    endByteOffset -= 3;
                }
                else if (functionSource[endByteOffset - 1] == _u('\r'))
                {
                    endCharOffset--;
                    endByteOffset--;
                }
                else
                {
                    AssertMsg(FALSE, "Line ending logic out of sync between Js::JavascriptExceptionMetadata and Js::LineOffsetCache::GetCharacterOffsetForLine");
                    return false;
                }
            }
        }

        LPCUTF8 functionStart = functionSource + startByteOffset;

        Js::BufferStringBuilder builder(endCharOffset - startCharOffset, scriptContext);
        utf8::DecodeOptions options = sourceInfo->IsCesu8() ? utf8::doAllowThreeByteSurrogates : utf8::doDefault;
        utf8::DecodeUnitsInto(builder.DangerousGetWritableBuffer(), functionStart, functionSource + endByteOffset, options);

        Js::JavascriptOperators::OP_SetProperty(metadata, Js::PropertyIds::source,
            builder.ToString(), scriptContext);

        return true;
    }
}
