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

namespace Js
{
    template <typename TLoadCallback>
    class JsrtSourceHolderPolicy
    {
    };

#ifdef _WIN32  // JsSerializedScriptLoadSourceCallback is WIN32 only
    template <>
    class JsrtSourceHolderPolicy<JsSerializedScriptLoadSourceCallback>
    {
    public:
        typedef WCHAR TLoadCharType;

        // Helper function for converting a Unicode script to utf8.
        // If heapAlloc is true the returned buffer must be freed with HeapDelete.
        // Otherwise scriptContext must be provided and GCed object is
        // returned.
        static void ScriptToUtf8(_When_(heapAlloc, _In_opt_) _When_(!heapAlloc, _In_) Js::ScriptContext *scriptContext,
            _In_z_ const WCHAR *script, _Outptr_result_buffer_(*utf8Length) utf8char_t **utf8Script, _Out_ size_t *utf8Length,
            _Out_ size_t *scriptLength, _Out_opt_ size_t *utf8AllocLength, _In_ bool heapAlloc)
        {
            Assert(utf8Script != nullptr);
            Assert(utf8Length != nullptr);
            Assert(scriptLength != nullptr);

            *utf8Script = nullptr;
            *utf8Length = 0;
            *scriptLength = 0;

            if (utf8AllocLength != nullptr)
            {
                *utf8AllocLength = 0;
            }

            size_t length = wcslen(script);
            if (length > UINT_MAX)
            {
                Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
            }

            // `length` should not be bigger than MAXLONG
            // UINT_MAX / 3 < MAXLONG
            size_t cbUtf8Buffer = ((UINT_MAX / 3) - 1 > length) ? (length + 1) * 3 : UINT_MAX;
            if (cbUtf8Buffer >= UINT_MAX)
            {
                Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
            }

            if (!heapAlloc)
            {
                Assert(scriptContext != nullptr);
                *utf8Script = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), utf8char_t, cbUtf8Buffer);
            }
            else
            {
                *utf8Script = HeapNewArray(utf8char_t, cbUtf8Buffer);
            }

            *utf8Length = utf8::EncodeIntoAndNullTerminate<utf8::Utf8EncodingKind::TrueUtf8>(*utf8Script, cbUtf8Buffer, script, static_cast<charcount_t>(length));
            *scriptLength = length;

            if (utf8AllocLength != nullptr)
            {
                *utf8AllocLength = cbUtf8Buffer;
            }
        }

        static void FreeMappedSource(utf8char_t const * source, size_t allocLength)
        {
            HeapDeleteArray(allocLength, source);
        }
    };
#endif  // _WIN32

    template <typename TLoadCallback, typename TUnloadCallback>
    void JsrtSourceHolder<TLoadCallback, TUnloadCallback>::EnsureSource(MapRequestFor requestedFor, const WCHAR* reasonString)
    {
        if (this->mappedSource != nullptr)
        {
            return;
        }

        Assert(scriptLoadCallback != nullptr);
        Assert(this->mappedSource == nullptr);

        const typename JsrtSourceHolderPolicy<TLoadCallback>::TLoadCharType *source = nullptr;
        size_t sourceLength = 0;
        utf8char_t *utf8Source = nullptr;
        size_t utf8Length = 0;
        size_t utf8AllocLength = 0;

        if (!scriptLoadCallback(sourceContext, &source))
        {
            // Assume out of memory
            Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
        }

        JsrtSourceHolderPolicy<TLoadCallback>::ScriptToUtf8(nullptr, source, &utf8Source, &utf8Length, &sourceLength, &utf8AllocLength, true);

        this->mappedSource = utf8Source;
        this->mappedSourceByteLength = utf8Length;
        this->mappedAllocLength = utf8AllocLength;

        this->scriptLoadCallback = nullptr;

#if ENABLE_DEBUG_CONFIG_OPTIONS
        AssertMsg(reasonString != nullptr, "Reason string for why we are mapping the source was not provided.");
        JS_ETW(EventWriteJSCRIPT_SOURCEMAPPING(reasonString, (ushort)requestedFor));
#endif
    }

    template <typename TLoadCallback, typename TUnloadCallback>
    void JsrtSourceHolder<TLoadCallback, TUnloadCallback>::Finalize(bool isShutdown)
    {
        Unload();
    }

    template <typename TLoadCallback, typename TUnloadCallback>
    void JsrtSourceHolder<TLoadCallback, TUnloadCallback>::Unload()
    {
        if (scriptUnloadCallback == nullptr)
        {
            return;
        }

        scriptUnloadCallback(sourceContext);

        if (this->mappedSource != nullptr)
        {
            JsrtSourceHolderPolicy<TLoadCallback>::FreeMappedSource(
                this->mappedSource, this->mappedAllocLength);
            this->mappedSource = nullptr;
        }

        // Don't allow load or unload again after told to unload.
        scriptLoadCallback = nullptr;
        scriptUnloadCallback = nullptr;
        sourceContext = NULL;
    }

#ifndef NTBUILD // ChakraCore Only
    template <>
    class JsrtSourceHolderPolicy<JsSerializedLoadScriptCallback>
    {
    public:
        typedef JsValueRef TLoadCharType;

        static void ScriptToUtf8(_When_(heapAlloc, _In_opt_) _When_(!heapAlloc, _In_) Js::ScriptContext *scriptContext,
            _In_z_ const byte *script_, bool isUtf8, size_t length, _Outptr_result_buffer_(*utf8Length) utf8char_t **utf8Script, _Out_ size_t *utf8Length,
            _Out_ size_t *scriptLength, _Out_opt_ size_t *utf8AllocLength, _In_ bool heapAlloc)
        {
            if (isUtf8)
            {
                *utf8Script = (utf8char_t*)script_;
                *utf8Length = length;
                *scriptLength = length; // xplat-todo: incorrect for utf8

                if (utf8AllocLength)
                {
                    *utf8AllocLength = 0;
                }
            }
            else
            {
                const WCHAR *script = (const WCHAR*) script_;
                Assert(utf8Script != nullptr);
                Assert(utf8Length != nullptr);
                Assert(scriptLength != nullptr);

                *utf8Script = nullptr;
                *utf8Length = 0;
                *scriptLength = 0;

                if (utf8AllocLength != nullptr)
                {
                    *utf8AllocLength = 0;
                }

                size_t script_length = wcslen(script);
                if (script_length > UINT_MAX)
                {
                    Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
                }

                // `script_length` should not be bigger than MAXLONG
                // UINT_MAX / 3 < MAXLONG
                size_t cbUtf8Buffer = ((UINT_MAX / 3) - 1 > script_length) ? (script_length + 1) * 3 : UINT_MAX;
                if (cbUtf8Buffer >= UINT_MAX)
                {
                    Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
                }

                if (!heapAlloc)
                {
                    Assert(scriptContext != nullptr);
                    *utf8Script = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), utf8char_t, cbUtf8Buffer);
                }
                else
                {
                    *utf8Script = HeapNewArray(utf8char_t, cbUtf8Buffer);
                }

                *utf8Length = utf8::EncodeIntoAndNullTerminate<utf8::Utf8EncodingKind::TrueUtf8>(*utf8Script, cbUtf8Buffer, script, static_cast<charcount_t>(script_length));
                *scriptLength = script_length;

                if (utf8AllocLength != nullptr)
                {
                    *utf8AllocLength = cbUtf8Buffer;
                }
            }
        }

        static void FreeMappedSource(utf8char_t const * source, size_t allocLength)
        {
            if (allocLength)
            {
                HeapDeleteArray(allocLength, source);
            }
        }
    };

    template <>
    void JsrtSourceHolder<JsSerializedLoadScriptCallback,
        JsSerializedScriptUnloadCallback>::EnsureSource(MapRequestFor requestedFor,
        const WCHAR* reasonString)
    {
        if (this->mappedSource != nullptr)
        {
            return;
        }

        Assert(scriptLoadCallback != nullptr);
        Assert(this->mappedSource == nullptr);

        JsValueRef scriptVal;
        JsParseScriptAttributes attributes = JsParseScriptAttributeNone;
        size_t sourceLength = 0;
        utf8char_t *utf8Source = nullptr;
        size_t utf8Length = 0;
        size_t utf8AllocLength = 0;

        Js::ScriptContext* scriptContext = JsrtContext::GetCurrent()->GetScriptContext();
        BEGIN_LEAVE_SCRIPT(scriptContext)
        if (!scriptLoadCallback(sourceContext, &scriptVal, &attributes))
        {
            // Assume out of memory
            Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
        }
        END_LEAVE_SCRIPT(scriptContext);

        bool isExternalArray = Js::ExternalArrayBuffer::Is(scriptVal),
             isString = false;
        bool isUtf8   = !(attributes & JsParseScriptAttributeArrayBufferIsUtf16Encoded);

        if (!isExternalArray)
        {
            isString = Js::JavascriptString::Is(scriptVal);
            if (!isString)
            {
                Js::JavascriptError::ThrowOutOfMemoryError(nullptr);
                return;
            }
        }

        const byte* script = isExternalArray ?
            ((Js::ExternalArrayBuffer*)(scriptVal))->GetBuffer() :
            (const byte*)((Js::JavascriptString*)(scriptVal))->GetSz();
        const size_t cb = isExternalArray ?
            ((Js::ExternalArrayBuffer*)(scriptVal))->GetByteLength() :
            ((Js::JavascriptString*)(scriptVal))->GetLength();

        JsrtSourceHolderPolicy<JsSerializedLoadScriptCallback>::ScriptToUtf8(nullptr,
            script, isUtf8, cb, &utf8Source, &utf8Length, &sourceLength,
            &utf8AllocLength, true);

        this->mappedScriptValue = scriptVal;
        this->mappedSource = utf8Source;
        this->mappedSourceByteLength = utf8Length;
        this->mappedAllocLength = utf8AllocLength;

        this->scriptLoadCallback = nullptr;

#if ENABLE_DEBUG_CONFIG_OPTIONS
        AssertMsg(reasonString != nullptr, "Reason string for why we are mapping the source was not provided.");
        JS_ETW(EventWriteJSCRIPT_SOURCEMAPPING(reasonString, (ushort)requestedFor));
#endif
    }

    template <>
    void JsrtSourceHolder<JsSerializedLoadScriptCallback, JsSerializedScriptUnloadCallback>::Finalize(bool isShutdown)
    {
        if (this->mappedSource != nullptr)
        {
            JsrtSourceHolderPolicy<JsSerializedLoadScriptCallback>::FreeMappedSource(
                this->mappedSource, this->mappedAllocLength);
            this->mappedSource = nullptr;
        }
        this->mappedScriptValue = nullptr;

        // Don't allow load or unload again after told to unload.
        scriptLoadCallback = nullptr;
        scriptUnloadCallback = nullptr;
        sourceContext = NULL;
    }

template class JsrtSourceHolder<JsSerializedLoadScriptCallback, JsSerializedScriptUnloadCallback>;

#endif // NTBUILD

#ifdef _WIN32
template class JsrtSourceHolder<JsSerializedScriptLoadSourceCallback, JsSerializedScriptUnloadCallback>;
#endif // _WIN32
};
