// Copyright 2014 PDFium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com

#include "../../include/javascript/IJavaScript.h"
#include "../../include/javascript/JS_Context.h"
#include "../../include/javascript/JS_Define.h"
#include "../../include/javascript/JS_EventHandler.h"
#include "../../include/javascript/JS_GlobalData.h"
#include "../../include/javascript/JS_Object.h"
#include "../../include/javascript/JS_Value.h"
#include "../../include/javascript/JavaScript.h"
#include "../../include/javascript/global.h"
#include "../../include/fpdfxfa/fpdfxfa_app.h"
#include "../../include/javascript/resource.h"

/* ---------------------------- global ---------------------------- */

// Helper class for compile-time calculation of hash values in order to
// avoid having global object initializers.
template <unsigned ACC, wchar_t... Ns>
struct CHash;

// Only needed to hash single-character strings.
template <wchar_t N>
struct CHash<N> {
  static const unsigned value = N;
};

template <unsigned ACC, wchar_t N>
struct CHash<ACC, N> {
  static const unsigned value = (ACC * 1313LLU + N) & 0xFFFFFFFF;
};

template <unsigned ACC, wchar_t N, wchar_t... Ns>
struct CHash<ACC, N, Ns...> {
  static const unsigned value = CHash<CHash<ACC, N>::value, Ns...>::value;
};

extern const unsigned int JSCONST_nStringHash =
  CHash<'s','t','r','i','n','g'>::value;
extern const unsigned int JSCONST_nNumberHash =
  CHash<'n','u','m','b','e','r'>::value;
extern const unsigned int JSCONST_nBoolHash =
  CHash<'b','o','o','l','e','a','n'>::value;
extern const unsigned int JSCONST_nDateHash =
  CHash<'d','a','t','e'>::value;
extern const unsigned int JSCONST_nObjectHash =
  CHash<'o','b','j','e','c','t'>::value;
extern const unsigned int JSCONST_nFXobjHash =
  CHash<'f','x','o','b','j'>::value;
extern const unsigned int JSCONST_nNullHash =
  CHash<'n','u','l','l'>::value;
extern const unsigned int JSCONST_nUndefHash =
  CHash<'u','n','d','e','f','i','n','e','d'>::value;

#ifdef _DEBUG
class HashVerify
{
public:
  HashVerify();
} g_hashVerify;

HashVerify::HashVerify()
{
  ASSERT(JSCONST_nStringHash ==
    JS_CalcHash(VALUE_NAME_STRING,wcslen(VALUE_NAME_STRING)));
  ASSERT(JSCONST_nNumberHash ==
    JS_CalcHash(VALUE_NAME_NUMBER,wcslen(VALUE_NAME_NUMBER)));
  ASSERT(JSCONST_nBoolHash ==
    JS_CalcHash(VALUE_NAME_BOOLEAN,wcslen(VALUE_NAME_BOOLEAN)));
  ASSERT(JSCONST_nDateHash ==
    JS_CalcHash(VALUE_NAME_DATE,wcslen(VALUE_NAME_DATE)));
  ASSERT(JSCONST_nObjectHash ==
    JS_CalcHash(VALUE_NAME_OBJECT,wcslen(VALUE_NAME_OBJECT)));
  ASSERT(JSCONST_nFXobjHash ==
    JS_CalcHash(VALUE_NAME_FXOBJ,wcslen(VALUE_NAME_FXOBJ)));
  ASSERT(JSCONST_nNullHash ==
    JS_CalcHash(VALUE_NAME_NULL,wcslen(VALUE_NAME_NULL)));
  ASSERT(JSCONST_nUndefHash ==
    JS_CalcHash(VALUE_NAME_UNDEFINED,wcslen(VALUE_NAME_UNDEFINED)));
}
#endif


BEGIN_JS_STATIC_CONST(CJS_Global)
END_JS_STATIC_CONST()

BEGIN_JS_STATIC_PROP(CJS_Global)
END_JS_STATIC_PROP()

BEGIN_JS_STATIC_METHOD(CJS_Global)
    JS_STATIC_METHOD_ENTRY(setPersistent)
END_JS_STATIC_METHOD()

IMPLEMENT_SPECIAL_JS_CLASS(CJS_Global, global_alternate, global);

FX_BOOL CJS_Global::InitInstance(IFXJS_Context* cc)
{
    CJS_Context* pContext = (CJS_Context*)cc;
    ASSERT(pContext != NULL);

    global_alternate* pGlobal = (global_alternate*)GetEmbedObject();
    ASSERT(pGlobal != NULL);

    pGlobal->Initial(pContext->GetReaderApp());

    return TRUE;
};

global_alternate::global_alternate(CJS_Object* pJSObject)
    : CJS_EmbedObj(pJSObject),
    m_pApp(NULL)
{
}

global_alternate::~global_alternate(void)
{
	DestroyGlobalPersisitentVariables();
	CPDFXFA_App::GetInstance()->GetRuntimeFactory()->ReleaseGlobalData();
}

void global_alternate::Initial(CPDFDoc_Environment* pApp)
{
	m_pApp = pApp;
	m_pGlobalData = CPDFXFA_App::GetInstance()->GetRuntimeFactory()->NewGlobalData(pApp);
	UpdateGlobalPersistentVariables();
}

FX_BOOL global_alternate::QueryProperty(const FX_WCHAR* propname)
{
    return CFX_WideString(propname) != L"setPersistent";
}

FX_BOOL global_alternate::DelProperty(IFXJS_Context* cc, const FX_WCHAR* propname, CFX_WideString& sError)
{
    js_global_data* pData = NULL;
    CFX_ByteString sPropName = CFX_ByteString::FromUnicode(propname);

    if (m_mapGlobal.Lookup(sPropName, (void*&)pData))
    {
        pData->bDeleted = TRUE;
        return TRUE;
    }

    return FALSE;
}

FX_BOOL global_alternate::DoProperty(IFXJS_Context* cc, const FX_WCHAR* propname, CJS_PropValue& vp, CFX_WideString& sError)
{
    if (vp.IsSetting())
    {
        CFX_ByteString sPropName = CFX_ByteString::FromUnicode(propname);
        switch (vp.GetType())
        {
        case VT_number:
            {
                double dData;
                vp >> dData;
                return SetGlobalVariables(sPropName, JS_GLOBALDATA_TYPE_NUMBER, dData, false, "", v8::Local<v8::Object>(), FALSE);
            }
        case VT_boolean:
            {
                bool bData;
                vp >> bData;
                return SetGlobalVariables(sPropName, JS_GLOBALDATA_TYPE_BOOLEAN, 0, bData, "", v8::Local<v8::Object>(), FALSE);
            }
        case VT_string:
            {
                CFX_ByteString sData;
                vp >> sData;
                return SetGlobalVariables(sPropName, JS_GLOBALDATA_TYPE_STRING, 0, false, sData, v8::Local<v8::Object>(), FALSE);
            }
        case VT_object:
            {
                JSObject pData;
                vp >> pData;
                return SetGlobalVariables(sPropName, JS_GLOBALDATA_TYPE_OBJECT, 0, false, "", pData, FALSE);
            }
        case VT_null:
            {
                return SetGlobalVariables(sPropName, JS_GLOBALDATA_TYPE_NULL, 0, false, "", v8::Local<v8::Object>(), FALSE);
            }
        case VT_undefined:
            {
                DelProperty(cc, propname, sError);
                return TRUE;
            }
        default:
            break;
        }
    }
    else
    {
        void* pVoid = nullptr;
        if (!m_mapGlobal.Lookup(CFX_ByteString::FromUnicode(propname), pVoid))
        {
            vp.SetNull();
            return TRUE;
        }
        if (!pVoid)
        {
            vp.SetNull();
            return TRUE;
        }
        js_global_data* pData = (js_global_data*)pVoid;
        if (pData->bDeleted)
            return TRUE;

        switch (pData->nType)
        {
            case JS_GLOBALDATA_TYPE_NUMBER:
                vp << pData->dData;
                return TRUE;
            case JS_GLOBALDATA_TYPE_BOOLEAN:
                vp << pData->bData;
                return TRUE;
            case JS_GLOBALDATA_TYPE_STRING:
                vp << pData->sData;
                return TRUE;
            case JS_GLOBALDATA_TYPE_OBJECT:
                {
                    v8::Local<v8::Object> obj = v8::Local<v8::Object>::New(vp.GetIsolate(),pData->pData);
                    vp << obj;
                    return TRUE;
                }
            case JS_GLOBALDATA_TYPE_NULL:
                vp.SetNull();
                return TRUE;
            default:
                break;
        }
    }
    return FALSE;
}

FX_BOOL global_alternate::setPersistent(IFXJS_Context* cc, const CJS_Parameters& params, CJS_Value& vRet, CFX_WideString& sError)
{
    CJS_Context* pContext = static_cast<CJS_Context*>(cc);
    if (params.size() != 2)
    {
        sError = JSGetStringFromID(pContext, IDS_STRING_JSPARAMERROR);
        return FALSE;
    }

    CFX_ByteString sName = params[0].ToCFXByteString();

    js_global_data* pData = NULL;
    if (m_mapGlobal.Lookup(sName, (void*&)pData))
    {
        if (pData && !pData->bDeleted)
        {
            pData->bPersistent = params[1].ToBool();
            return TRUE;
        }
    }

    sError = JSGetStringFromID(pContext, IDS_STRING_JSNOGLOBAL);
    return FALSE;
}

void global_alternate::UpdateGlobalPersistentVariables()
{
    ASSERT(m_pGlobalData != NULL);

    for (int i=0,sz=m_pGlobalData->GetSize(); i<sz; i++)
    {
        CJS_GlobalData_Element* pData = m_pGlobalData->GetAt(i);
        ASSERT(pData != NULL);

        switch (pData->data.nType)
        {
        case JS_GLOBALDATA_TYPE_NUMBER:
            SetGlobalVariables(pData->data.sKey, JS_GLOBALDATA_TYPE_NUMBER, pData->data.dData, false, "", v8::Local<v8::Object>(), pData->bPersistent == 1);
            JS_PutObjectNumber(NULL,(JSFXObject)(*m_pJSObject),
                               pData->data.sKey.UTF8Decode().c_str(), pData->data.dData);
            break;
        case JS_GLOBALDATA_TYPE_BOOLEAN:
            SetGlobalVariables(pData->data.sKey, JS_GLOBALDATA_TYPE_BOOLEAN, 0, (bool)(pData->data.bData == 1), "", v8::Local<v8::Object>(), pData->bPersistent == 1);
            JS_PutObjectBoolean(NULL,(JSFXObject)(*m_pJSObject),
                                pData->data.sKey.UTF8Decode().c_str(), (bool)(pData->data.bData == 1));
            break;
        case JS_GLOBALDATA_TYPE_STRING:
            SetGlobalVariables(pData->data.sKey, JS_GLOBALDATA_TYPE_STRING, 0, false, pData->data.sData, v8::Local<v8::Object>(), pData->bPersistent == 1);
            JS_PutObjectString(NULL, (JSFXObject)(*m_pJSObject),
                               pData->data.sKey.UTF8Decode().c_str(),
                               pData->data.sData.UTF8Decode().c_str());
            break;
        case JS_GLOBALDATA_TYPE_OBJECT:
            {
                IJS_Runtime* pRuntime = JS_GetRuntime((JSFXObject)(*m_pJSObject));
                v8::Local<v8::Object> pObj = JS_NewFxDynamicObj(pRuntime, NULL, -1);

                PutObjectProperty(pObj, &pData->data);

                SetGlobalVariables(pData->data.sKey, JS_GLOBALDATA_TYPE_OBJECT, 0, false, "",
                    (JSObject)pObj, pData->bPersistent == 1);
                JS_PutObjectObject(NULL,(JSFXObject)(*m_pJSObject),
                                   pData->data.sKey.UTF8Decode().c_str(), (JSObject)pObj);
            }
            break;
        case JS_GLOBALDATA_TYPE_NULL:
            SetGlobalVariables(pData->data.sKey, JS_GLOBALDATA_TYPE_NULL, 0, false, "", v8::Local<v8::Object>(), pData->bPersistent == 1);
            JS_PutObjectNull(NULL,(JSFXObject)(*m_pJSObject),
                             pData->data.sKey.UTF8Decode().c_str());
            break;
        }
    }
}

void global_alternate::CommitGlobalPersisitentVariables()
{
    ASSERT(m_pGlobalData != NULL);

    FX_POSITION  pos = m_mapGlobal.GetStartPosition();
    while (pos)
    {
        CFX_ByteString name;
        js_global_data* pData = NULL;
        m_mapGlobal.GetNextAssoc(pos, name, (void*&)pData);

        if (pData)
        {
            if (pData->bDeleted)
            {
                m_pGlobalData->DeleteGlobalVariable(name);
            }
            else
            {
                switch (pData->nType)
                {
                case JS_GLOBALDATA_TYPE_NUMBER:
                    m_pGlobalData->SetGlobalVariableNumber(name, pData->dData);
                    m_pGlobalData->SetGlobalVariablePersistent(name, pData->bPersistent);
                    break;
                case JS_GLOBALDATA_TYPE_BOOLEAN:
                    m_pGlobalData->SetGlobalVariableBoolean(name, pData->bData);
                    m_pGlobalData->SetGlobalVariablePersistent(name, pData->bPersistent);
                    break;
                case JS_GLOBALDATA_TYPE_STRING:
                    m_pGlobalData->SetGlobalVariableString(name, pData->sData);
                    m_pGlobalData->SetGlobalVariablePersistent(name, pData->bPersistent);
                    break;
                case JS_GLOBALDATA_TYPE_OBJECT:
                    //if (pData->pData)
                    {
                        CJS_GlobalVariableArray array;
                        v8::Local<v8::Object> obj = v8::Local<v8::Object>::New(GetJSObject()->GetIsolate(),pData->pData);
                        ObjectToArray(obj, array);
                        m_pGlobalData->SetGlobalVariableObject(name, array);
                        m_pGlobalData->SetGlobalVariablePersistent(name, pData->bPersistent);
                    }
                    break;
                case JS_GLOBALDATA_TYPE_NULL:
                    m_pGlobalData->SetGlobalVariableNull(name);
                    m_pGlobalData->SetGlobalVariablePersistent(name, pData->bPersistent);
                    break;
                }
            }
        }
    }
}

void global_alternate::ObjectToArray(v8::Local<v8::Object> pObj, CJS_GlobalVariableArray& array)
{
    v8::Local<v8::Context> context = pObj->CreationContext();
    v8::Isolate* isolate = context->GetIsolate();
    v8::Local<v8::Array> pKeyList = JS_GetObjectElementNames(isolate, pObj);
    int nObjElements = pKeyList->Length();

    for (int i=0; i<nObjElements; i++)
    {

        CFX_WideString ws = JS_ToString(isolate, JS_GetArrayElement(isolate, pKeyList, i));
        CFX_ByteString sKey = ws.UTF8Encode();

        v8::Local<v8::Value> v = JS_GetObjectElement(isolate, pObj, ws.c_str());
        FXJSVALUETYPE vt = GET_VALUE_TYPE(v);
        switch (vt)
        {
        case VT_number:
            {
                CJS_KeyValue* pObjElement = new CJS_KeyValue;
                pObjElement->nType = JS_GLOBALDATA_TYPE_NUMBER;
                pObjElement->sKey = sKey;
                pObjElement->dData = JS_ToNumber(isolate, v);
                array.Add(pObjElement);
            }
            break;
        case VT_boolean:
            {
                CJS_KeyValue* pObjElement = new CJS_KeyValue;
                pObjElement->nType = JS_GLOBALDATA_TYPE_BOOLEAN;
                pObjElement->sKey = sKey;
                pObjElement->dData = JS_ToBoolean(isolate, v);
                array.Add(pObjElement);
            }
            break;
        case VT_string:
            {
                CFX_ByteString sValue = CJS_Value(isolate, v, VT_string).ToCFXByteString();
                CJS_KeyValue* pObjElement = new CJS_KeyValue;
                pObjElement->nType = JS_GLOBALDATA_TYPE_STRING;
                pObjElement->sKey = sKey;
                pObjElement->sData = sValue;
                array.Add(pObjElement);
            }
            break;
        case VT_object:
            {
                CJS_KeyValue* pObjElement = new CJS_KeyValue;
                pObjElement->nType = JS_GLOBALDATA_TYPE_OBJECT;
                pObjElement->sKey = sKey;
                ObjectToArray(JS_ToObject(isolate, v), pObjElement->objData);
                array.Add(pObjElement);
            }
            break;
        case VT_null:
            {
                CJS_KeyValue* pObjElement = new CJS_KeyValue;
                pObjElement->nType = JS_GLOBALDATA_TYPE_NULL;
                pObjElement->sKey = sKey;
                array.Add(pObjElement);
            }
            break;
        default:
            break;
        }
    }
}

void global_alternate::PutObjectProperty(v8::Local<v8::Object> pObj, CJS_KeyValue* pData)
{
    ASSERT(pData != NULL);

    for (int i=0,sz=pData->objData.Count(); i<sz; i++)
    {
        CJS_KeyValue* pObjData = pData->objData.GetAt(i);
        ASSERT(pObjData != NULL);

        switch (pObjData->nType)
        {
        case JS_GLOBALDATA_TYPE_NUMBER:
            JS_PutObjectNumber(NULL,(JSObject)pObj, pObjData->sKey.UTF8Decode().c_str(), pObjData->dData);
            break;
        case JS_GLOBALDATA_TYPE_BOOLEAN:
            JS_PutObjectBoolean(NULL,(JSObject)pObj, pObjData->sKey.UTF8Decode().c_str(), (bool)(pObjData->bData == 1));
            break;
        case JS_GLOBALDATA_TYPE_STRING:
            JS_PutObjectString(NULL,(JSObject)pObj, pObjData->sKey.UTF8Decode().c_str(), pObjData->sData.UTF8Decode().c_str());
            break;
        case JS_GLOBALDATA_TYPE_OBJECT:
            {
                IJS_Runtime* pRuntime = JS_GetRuntime((JSFXObject)(*m_pJSObject));
                v8::Local<v8::Object> pNewObj = JS_NewFxDynamicObj(pRuntime, NULL, -1);
                PutObjectProperty(pNewObj, pObjData);
                JS_PutObjectObject(NULL, (JSObject)pObj, pObjData->sKey.UTF8Decode().c_str(), (JSObject)pNewObj);
            }
            break;
        case JS_GLOBALDATA_TYPE_NULL:
            JS_PutObjectNull(NULL,(JSObject)pObj, pObjData->sKey.UTF8Decode().c_str());
            break;
        }
    }
}

void global_alternate::DestroyGlobalPersisitentVariables()
{
    FX_POSITION  pos = m_mapGlobal.GetStartPosition();
    while (pos)
    {
        CFX_ByteString name;
        js_global_data* pData = NULL;
        m_mapGlobal.GetNextAssoc(pos, name, (void*&)pData);
        delete pData;
    }

    m_mapGlobal.RemoveAll();
}


FX_BOOL global_alternate::SetGlobalVariables(const FX_CHAR* propname, int nType,
                double dData, bool bData, const CFX_ByteString& sData, JSObject pData, bool bDefaultPersistent)
{
    if (propname == NULL) return FALSE;

    js_global_data* pTemp = NULL;
    m_mapGlobal.Lookup(propname, (void*&)pTemp);

    if (pTemp)
    {
        if (pTemp->bDeleted || pTemp->nType != nType)
        {
            pTemp->dData = 0;
            pTemp->bData = 0;
            pTemp->sData = "";
            pTemp->nType = nType;
        }

        pTemp->bDeleted = FALSE;

        switch (nType)
        {
        case JS_GLOBALDATA_TYPE_NUMBER:
            {
                pTemp->dData = dData;
            }
            break;
        case JS_GLOBALDATA_TYPE_BOOLEAN:
            {
                pTemp->bData = bData;
            }
            break;
        case JS_GLOBALDATA_TYPE_STRING:
            {
                pTemp->sData = sData;
            }
            break;
        case JS_GLOBALDATA_TYPE_OBJECT:
            {
                pTemp->pData.Reset(JS_GetRuntime(pData), pData);
            }
            break;
        case JS_GLOBALDATA_TYPE_NULL:
            break;
        default:
            return FALSE;
        }

        return TRUE;
    }

    js_global_data* pNewData = NULL;

    switch (nType)
    {
    case JS_GLOBALDATA_TYPE_NUMBER:
        {
            pNewData = new js_global_data;
            pNewData->nType = JS_GLOBALDATA_TYPE_NUMBER;
            pNewData->dData = dData;
            pNewData->bPersistent = bDefaultPersistent;
        }
        break;
    case JS_GLOBALDATA_TYPE_BOOLEAN:
        {
            pNewData = new js_global_data;
            pNewData->nType = JS_GLOBALDATA_TYPE_BOOLEAN;
            pNewData->bData = bData;
            pNewData->bPersistent = bDefaultPersistent;
        }
        break;
    case JS_GLOBALDATA_TYPE_STRING:
        {
            pNewData = new js_global_data;
            pNewData->nType = JS_GLOBALDATA_TYPE_STRING;
            pNewData->sData = sData;
            pNewData->bPersistent = bDefaultPersistent;
        }
        break;
    case JS_GLOBALDATA_TYPE_OBJECT:
        {
            pNewData = new js_global_data;
            pNewData->nType = JS_GLOBALDATA_TYPE_OBJECT;
            pNewData->pData.Reset(JS_GetRuntime(pData), pData);
            pNewData->bPersistent = bDefaultPersistent;
        }
        break;
    case JS_GLOBALDATA_TYPE_NULL:
        {
            pNewData = new js_global_data;
            pNewData->nType = JS_GLOBALDATA_TYPE_NULL;
            pNewData->bPersistent = bDefaultPersistent;
        }
        break;
    default:
        return FALSE;
    }

    m_mapGlobal.SetAt(propname, (void*)pNewData);

    return TRUE;
}

FXJSVALUETYPE GET_VALUE_TYPE(v8::Local<v8::Value> p)
{
  const unsigned int nHash = JS_CalcHash(JS_GetTypeof(p));

  if (nHash == JSCONST_nUndefHash)
    return VT_undefined;
  if (nHash == JSCONST_nNullHash)
    return VT_null;
  if (nHash == JSCONST_nStringHash)
    return VT_string;
  if (nHash == JSCONST_nNumberHash)
    return VT_number;
  if (nHash == JSCONST_nBoolHash)
    return VT_boolean;
  if (nHash == JSCONST_nDateHash)
    return VT_date;
  if (nHash == JSCONST_nObjectHash)
    return VT_object;
  if (nHash == JSCONST_nFXobjHash)
    return VT_fxobject;

  return VT_unknown;
}