/*
 * Copyright (c) 2023 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 "interfaces/napi/kits/utils/napi_utils.h"
#include "interfaces/napi/kits/display_sync/js_display_sync.h"


namespace OHOS::Ace::Napi {
constexpr size_t STR_MAX_BUFFER_SIZE = 1024;
constexpr size_t CALLBACK_OJECT_NUM = 1;
constexpr size_t ARGC_NUM_SIZE = 2;

napi_value NapiGetUndefined(napi_env env)
{
    napi_value result = nullptr;
    napi_get_undefined(env, &result);
    return result;
}

void NapiPrintErrorInfo(napi_env env)
{
    const napi_extended_error_info *error_info;
    napi_get_last_error_info(env, &error_info);
    LOGE("JsDisplaySync ErrorInfo: %{public}s", error_info->error_message);
    return;
}

bool ParseJsValue(napi_env env, napi_value jsObject, const std::string& name, int32_t& data)
{
    napi_value value = nullptr;
    napi_get_named_property(env, jsObject, name.c_str(), &value);
    napi_valuetype type = napi_undefined;
    napi_typeof(env, value, &type);
    if (type == napi_number) {
        napi_get_value_int32(env, value, &data);
        return true;
    } else {
        return false;
    }
    return true;
}

static size_t ParseArgs(
    napi_env& env, napi_callback_info& info, napi_value& thisVar, napi_value& cb, CallbackType& callbackType)
{
    size_t argc = ARGC_NUM_SIZE;
    napi_value argv[ARGC_NUM_SIZE] = { 0 };
    napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr);
    NAPI_ASSERT_BASE(env, argc > 0, "too few parameter", 0);

    napi_valuetype napiType;
    NAPI_CALL_BASE(env, napi_typeof(env, argv[0], &napiType), 0);
    NAPI_ASSERT_BASE(env, napiType == napi_string, "parameter 1 should be string", 0);
    char type[STR_MAX_BUFFER_SIZE] = { 0 };
    size_t len = 0;
    napi_get_value_string_utf8(env, argv[0], type, STR_MAX_BUFFER_SIZE, &len);
    NAPI_ASSERT_BASE(env, len < STR_MAX_BUFFER_SIZE, "condition string too long", 0);
    NAPI_ASSERT_BASE(
        env, (strcmp("frame", type) == 0), "type mismatch('frame')", 0);
    if (strcmp("frame", type) == 0) {
        callbackType = CallbackType::ONFRAME;
    } else {
        callbackType = CallbackType::UNKNOW;
    }

    if (argc <= 1) {
        return argc;
    }

    NAPI_CALL_BASE(env, napi_typeof(env, argv[1], &napiType), 0);
    NAPI_ASSERT_BASE(env, napiType == napi_function, "type mismatch for parameter 2", 0);
    cb = argv[1];
    return argc;
}

DisplaySync* GetDisplaySync(napi_env env, napi_callback_info info)
{
    DisplaySync* displaySync = nullptr;
    napi_value thisVar = nullptr;
    napi_get_cb_info(env, info, nullptr, nullptr, &thisVar, nullptr);
    napi_unwrap_s(env, thisVar, &DISPLAY_SYNC_LISTENER_TYPE_TAG, reinterpret_cast<void**>(&displaySync));
    return displaySync;
}

void CreateTimeInfoJsObject(const napi_env env, RefPtr<DisplaySyncData> displaySyncData,
                            napi_value& intervalInfo)
{
    napi_status status = napi_create_object(env, &intervalInfo);
    if (status != napi_ok) {
        NapiPrintErrorInfo(env);
        return;
    }

    napi_value timestamp;
    napi_value targetTimestamp;
    napi_create_int64(env, displaySyncData->timestamp_, &timestamp);
    napi_create_int64(env, displaySyncData->targetTimestamp_, &targetTimestamp);
    auto resultTimestamp = napi_set_named_property(env, intervalInfo, "timestamp", timestamp);
    auto resultTargetTimestamp = napi_set_named_property(env, intervalInfo, "targetTimestamp", targetTimestamp);
    if (resultTimestamp != napi_ok || resultTargetTimestamp != napi_ok) {
        NapiPrintErrorInfo(env);
        return;
    }
}

napi_value ParseExpectedFrameRateRange(napi_env env, napi_callback_info info, FrameRateRange& frameRateRange)
{
    size_t argc = 1;
    napi_value argv[1];
    napi_value thisVar = nullptr;
    napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr);
    if (argc != 1) {
        NapiThrow(env, "The number of parameters is incorrect.", ERROR_CODE_PARAM_INVALID);
        return NapiGetUndefined(env);
    }

    napi_value nativeObj = argv[0];
    if (nativeObj == nullptr) {
        NapiThrow(env, "The nativeObj is nullptr.", ERROR_CODE_PARAM_INVALID);
        return NapiGetUndefined(env);
    }

    int32_t minFPS = 0;
    int32_t maxFPS = 0;
    int32_t expectedFPS = 0;
    ParseJsValue(env, nativeObj, "min", minFPS);
    ParseJsValue(env, nativeObj, "max", maxFPS);
    ParseJsValue(env, nativeObj, "expected", expectedFPS);

    frameRateRange.Set(minFPS, maxFPS, expectedFPS);
    if (!frameRateRange.IsValid()) {
        NapiThrow(env, "ExpectedFrameRateRange Error", ERROR_CODE_PARAM_INVALID);
        return NapiGetUndefined(env);
    }
    return NapiGetUndefined(env);
}

napi_value JSSetExpectedFrameRateRange(napi_env env, napi_callback_info info)
{
    FrameRateRange frameRateRange;
    ParseExpectedFrameRateRange(env, info, frameRateRange);

    auto displaySync = GetDisplaySync(env, info);
    if (!displaySync) {
        TAG_LOGW(AceLogTag::ACE_DISPLAY_SYNC, "JSSetExpectedFrameRateRange: cannot find displaySync.");
        return NapiGetUndefined(env);
    }
    RefPtr<UIDisplaySync> uiDisplaySync = displaySync->GetUIDisplaySync();
    if (!uiDisplaySync) {
        TAG_LOGW(AceLogTag::ACE_DISPLAY_SYNC, "JSSetExpectedFrameRateRange: cannot get uiDisplaySync.");
        return NapiGetUndefined(env);
    }

    uiDisplaySync->SetExpectedFrameRateRange(frameRateRange);
    TAG_LOGD(AceLogTag::ACE_DISPLAY_SYNC, "Id: %{public}" PRIu64 " SetExpectedFrameRateRange"
        "{%{public}d, %{public}d, %{public}d}", uiDisplaySync->GetId(), frameRateRange.min_, frameRateRange.max_,
        frameRateRange.preferred_);
    return NapiGetUndefined(env);
}

napi_value JSStart(napi_env env, napi_callback_info info)
{
    auto displaySync = GetDisplaySync(env, info);
    if (!displaySync) {
        TAG_LOGW(AceLogTag::ACE_DISPLAY_SYNC, "JSStart: cannot find displaySync.");
        return NapiGetUndefined(env);
    }
    RefPtr<UIDisplaySync> uiDisplaySync = displaySync->GetUIDisplaySync();
    if (!uiDisplaySync) {
        TAG_LOGW(AceLogTag::ACE_DISPLAY_SYNC, "JSStart: cannot get uiDisplaySync when starting.");
        return NapiGetUndefined(env);
    }

    uiDisplaySync->AddToPipelineOnContainer();
    TAG_LOGD(AceLogTag::ACE_DISPLAY_SYNC, "Id: %{public}" PRIu64 " Start", uiDisplaySync->GetId());
    return NapiGetUndefined(env);
}

napi_value JSStop(napi_env env, napi_callback_info info)
{
    auto displaySync = GetDisplaySync(env, info);
    if (!displaySync) {
        TAG_LOGW(AceLogTag::ACE_DISPLAY_SYNC, "JSStop: cannot find displaySync.");
        return NapiGetUndefined(env);
    }
    RefPtr<UIDisplaySync> uiDisplaySync = displaySync->GetUIDisplaySync();
    if (!uiDisplaySync) {
        TAG_LOGW(AceLogTag::ACE_DISPLAY_SYNC, "JSStop: cannot get uiDisplaySync when stopping.");
        return NapiGetUndefined(env);
    }

    uiDisplaySync->DelFromPipelineOnContainer();
    TAG_LOGD(AceLogTag::ACE_DISPLAY_SYNC, "Id: %{public}" PRIu64 " Stop", uiDisplaySync->GetId());
    return NapiGetUndefined(env);
}

void DisplaySync::Initialize(napi_env env, napi_value thisVar)
{
    napi_handle_scope scope = nullptr;
    napi_open_handle_scope(env, &scope);
    if (!scope) {
        return;
    }
    napi_create_reference(env, thisVar, 1, &thisVarRef_);
    napi_close_handle_scope(env, scope);
}

void DisplaySync::NapiSerializer(napi_env& env, napi_value& jsDisplaySync)
{
    napi_status status = napi_create_object(env, &jsDisplaySync);
    if (status != napi_ok) {
        NapiPrintErrorInfo(env);
        return;
    }

    napi_wrap_s(
        env, jsDisplaySync, this,
        [](napi_env env, void* data, void* hint) {
            DisplaySync* displaySync = static_cast<DisplaySync*>(data);
            if (displaySync) {
                displaySync->Destroy(env);
                delete displaySync;
            }
        },
        nullptr, &DISPLAY_SYNC_LISTENER_TYPE_TAG, nullptr);
}

void DisplaySync::RegisterOnFrameCallback(napi_value cb, napi_ref& onFrameRef,
    CallbackType callbackType, napi_env env)
{
    if (onFrameRef) {
        return;
    }
    napi_create_reference(env, cb, 1, &onFrameRef);

    GetUIDisplaySync()->RegisterOnFrameWithData([env, onFrameRef] (RefPtr<DisplaySyncData> displaySyncData) {
        napi_handle_scope innerScope = nullptr;
        napi_status status = napi_open_handle_scope(env, &innerScope);
        if (status != napi_ok) {
            NapiPrintErrorInfo(env);
            return;
        }

        napi_value ret = nullptr;
        napi_value onframe = nullptr;
        auto result = napi_get_reference_value(env, onFrameRef, &onframe);
        if (result != napi_ok || onframe == nullptr) {
            NapiPrintErrorInfo(env);
            napi_close_handle_scope(env, innerScope);
            return;
        }

        napi_value intervalInfo = nullptr;
        CreateTimeInfoJsObject(env, displaySyncData, intervalInfo);
        napi_value args[CALLBACK_OJECT_NUM] = { intervalInfo };
        napi_call_function(env, nullptr, onframe, CALLBACK_OJECT_NUM, args, &ret);
        napi_close_handle_scope(env, innerScope);
    });
}

void DisplaySync::UnregisterOnFrameCallback(napi_env env, size_t argc, napi_ref& onFrameRef)
{
    if (argc >= 1) {
        napi_delete_reference(env, onFrameRef);
        onFrameRef = nullptr;
        GetUIDisplaySync()->UnregisterOnFrame();
    }
    return;
}

void DisplaySync::Destroy(napi_env env)
{
    if (onFrameRef_ != nullptr) {
        napi_delete_reference(env, onFrameRef_);
    }

    if (thisVarRef_ != nullptr) {
        napi_delete_reference(env, thisVarRef_);
    }
}

napi_value JSOnFrame_On(napi_env env, napi_callback_info info)
{
    napi_value thisVar = nullptr;
    napi_value cb = nullptr;
    CallbackType callbackType = CallbackType::UNKNOW;
    size_t argc = ParseArgs(env, info, thisVar, cb, callbackType);
    NAPI_ASSERT(env, (argc == ARGC_NUM_SIZE && thisVar != nullptr && cb != nullptr), "Invalid arguments");

    DisplaySync* displaySync = GetDisplaySync(env, info);
    if (!displaySync) {
        return NapiGetUndefined(env);
    }

    if (callbackType == CallbackType::ONFRAME) {
        displaySync->RegisterOnFrameCallback(cb, displaySync->onFrameRef_, callbackType, env);
    }
    return NapiGetUndefined(env);
}

napi_value JSOnFrame_Off(napi_env env, napi_callback_info info)
{
    napi_value thisVar = nullptr;
    napi_value cb = nullptr;
    CallbackType callbackType = CallbackType::UNKNOW;
    size_t argc = ParseArgs(env, info, thisVar, cb, callbackType);
    DisplaySync* displaySync = GetDisplaySync(env, info);
    if (!displaySync) {
        return NapiGetUndefined(env);
    }
    if (callbackType == CallbackType::ONFRAME) {
        displaySync->UnregisterOnFrameCallback(env, argc, displaySync->onFrameRef_);
    }
    return NapiGetUndefined(env);
}

static napi_value JSCreate(napi_env env, napi_callback_info info)
{
    auto uiDisplaySync = AceType::MakeRefPtr<UIDisplaySync>();
    DisplaySync* displaySync = new DisplaySync(uiDisplaySync);

    napi_value jsDisplaySync = nullptr;
    displaySync->NapiSerializer(env, jsDisplaySync);
    if (!jsDisplaySync) {
        delete displaySync;
        return nullptr;
    }

    napi_property_descriptor resultFuncs[] = {
        DECLARE_NAPI_FUNCTION("setExpectedFrameRateRange", JSSetExpectedFrameRateRange),
        DECLARE_NAPI_FUNCTION("on", JSOnFrame_On),
        DECLARE_NAPI_FUNCTION("off", JSOnFrame_Off),
        DECLARE_NAPI_FUNCTION("start", JSStart),
        DECLARE_NAPI_FUNCTION("stop", JSStop),
    };

    TAG_LOGD(AceLogTag::ACE_DISPLAY_SYNC, "Create UIDisplaySync Id: %{public}" PRIu64 "",
        uiDisplaySync->GetId());
    NAPI_CALL(env, napi_define_properties(
        env, jsDisplaySync, sizeof(resultFuncs) / sizeof(resultFuncs[0]), resultFuncs));
    return jsDisplaySync;
}

static napi_value DisplaySyncExport(napi_env env, napi_value exports)
{
    napi_property_descriptor displaySyncDesc[] = {
        DECLARE_NAPI_FUNCTION("create", JSCreate),
    };
    NAPI_CALL(env, napi_define_properties(
        env, exports, sizeof(displaySyncDesc) / sizeof(displaySyncDesc[0]), displaySyncDesc));
    return exports;
}

static napi_module displaySyncModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = DisplaySyncExport,
    .nm_modname = "graphics.displaySync",
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void DisplaySyncRegister()
{
    napi_module_register(&displaySyncModule);
}
} // namespace OHOS::Ace::Napi