/*
 * Copyright (c) 2021 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 "adapter/preview/osal/fetch_manager.h"

#include <memory>
#include <mutex>

#include "curl/curl.h"

#include "adapter/preview/osal/http_constant.h"
#include "base/log/log.h"
#include "base/utils/singleton.h"

#define ACE_CURL_EASY_SET_OPTION(handle, opt, data)                                                 \
    do {                                                                                            \
        CURLcode result = curl_easy_setopt(handle, opt, data);                                      \
        if (result != CURLE_OK) {                                                                   \
            LOGW("Failed to set option: %{public}s, %{public}s", #opt, curl_easy_strerror(result)); \
            return false;                                                                           \
        }                                                                                           \
    } while (0)

namespace OHOS::Ace {

class FetchManagerImpl final : public FetchManager, public Singleton<FetchManagerImpl> {
    DECLARE_SINGLETON(FetchManagerImpl);
    ACE_DISALLOW_MOVE(FetchManagerImpl);

public:
    bool Fetch(const RequestData requestData, const int32_t callbackId, ResponseData& responseData) override
    {
        if (!Initialize()) {
            return false;
        }

        std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> handle(curl_easy_init(), &curl_easy_cleanup);
        if (!handle) {
            return false;
        }

        struct curl_slist* header = nullptr;
        if (!requestData.GetHeader().empty()) {
            for (auto&& [key, value] : requestData.GetHeader()) {
                header = curl_slist_append(header, (key + ":" + value).c_str());
            }
            ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_HTTPHEADER, header);
        }

        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_TIMEOUT_MS, HttpConstant::TIME_OUT);
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_CONNECTTIMEOUT_MS, HttpConstant::TIME_OUT);
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_BUFFERSIZE, HttpConstant::BUFFER_SIZE);

        std::string responseBody;
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_WRITEFUNCTION, OnWritingMemoryBody);
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_WRITEDATA, &responseBody);

        std::string responseHeader;
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_HEADERFUNCTION, OnWritingMemoryHeader);
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_HEADERDATA, &responseHeader);

        // Some servers don't like requests that are made without a user-agent field, so we provide one
        ACE_CURL_EASY_SET_OPTION(handle.get(), CURLOPT_USERAGENT, "libcurl-agent/1.0");

        std::string method = requestData.GetMethod();
        if (method.empty()) {
            method = "GET";
        }
        if (method == HttpConstant::HTTP_METHOD_HEAD || method == HttpConstant::HTTP_METHOD_OPTIONS ||
            method == HttpConstant::HTTP_METHOD_DELETE || method == HttpConstant::HTTP_METHOD_TRACE ||
            method == HttpConstant::HTTP_METHOD_GET) {
            SetOptionForGet(requestData, handle.get());
        } else if (method == HttpConstant::HTTP_METHOD_POST || method == HttpConstant::HTTP_METHOD_PUT) {
            SetOptionForPost(requestData, handle.get());
        } else {
            responseData.SetCode(HttpConstant::ERROR);
            responseData.SetData(responseBody);
            return true;
        }

        CURLcode result = curl_easy_perform(handle.get());
        if (result != CURLE_OK) {
            LOGW("Failed to fetch, url: %{private}s, %{public}s", requestData.GetUrl().c_str(),
                curl_easy_strerror(result));
            responseData.SetCode(HttpConstant::ERROR);
            responseData.SetData(responseBody);
            return true;
        }

        char* ct = nullptr;
        curl_easy_getinfo(handle.get(), CURLINFO_CONTENT_TYPE, &ct);

        int32_t responseCode = HttpConstant::ERROR;
        curl_easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &responseCode);
        responseData.SetCode(responseCode);
        responseData.SetData(responseBody);
        responseData.SetHeaders(responseHeader);
        if (header != nullptr) {
            curl_slist_free_all(header);
        }
        return true;
    }

    bool SetOptionForGet(const RequestData requestData, CURL* curl) const
    {
        // refer to function buildConnectionWithParam() in HttpFetchImpl.java
        std::string url = requestData.GetUrl();
        if (requestData.GetData() != "") {
            std::size_t index = url.find(HttpConstant::URL_PARAM_SEPARATOR);
            if (index != std::string::npos) {
                std::string param = url.substr(index + 1);

                std::string encodeIn = param + HttpConstant::URL_PARAM_DELIMITER + requestData.GetData();
                char* encodeOut = curl_easy_escape(curl, encodeIn.c_str(), 0);
                if (encodeOut != nullptr) {
                    url = url.substr(0, index + 1) + encodeOut;
                    curl_free(encodeOut);
                }
            } else {
                char* encodeOut = curl_easy_escape(curl, requestData.GetData().c_str(), 0);
                if (encodeOut != nullptr) {
                    url = url + HttpConstant::URL_PARAM_SEPARATOR + encodeOut;
                    curl_free(encodeOut);
                }
            }
        }
        ACE_CURL_EASY_SET_OPTION(curl, CURLOPT_URL, url.c_str());
        return true;
    }

    bool SetOptionForPost(const RequestData requestData, CURL* curl) const
    {
        // refer to function buildConnectionWithStream() in HttpFetchImpl.java
        std::string url = requestData.GetUrl();
        ACE_CURL_EASY_SET_OPTION(curl, CURLOPT_URL, url.c_str());
        ACE_CURL_EASY_SET_OPTION(curl, CURLOPT_POSTFIELDS, requestData.GetData().c_str());
        return true;
    }

private:
    static size_t OnWritingMemoryBody(const void* data, size_t size, size_t memBytes, void* userData)
    {
        ((std::string*)userData)->append((char*)data, 0, size * memBytes);
        return size * memBytes;
    }
    static size_t OnWritingMemoryHeader(const void* data, size_t size, size_t memBytes, void* userData)
    {
        ((std::string*)userData)->append((char*)data, 0, size * memBytes);
        return size * memBytes;
    }

    bool Initialize()
    {
        if (initialized_) {
            return true;
        }

        std::lock_guard<std::mutex> lock(mutex_);
        if (initialized_) {
            return true;
        }
        if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
            LOGE("Failed to initialize 'curl'");
            return false;
        }
        initialized_ = true;
        return true;
    }

    std::mutex mutex_;
    bool initialized_ = false;
};

FetchManagerImpl::FetchManagerImpl() = default;

FetchManagerImpl::~FetchManagerImpl()
{
    curl_global_cleanup();
}

SINGLETON_INSTANCE_IMPL(FetchManagerImpl);

FetchManager& FetchManager::GetInstance()
{
    return Singleton<FetchManagerImpl>::GetInstance();
}

} // namespace OHOS::Ace