/*############################################################################
  # Copyright (C) Intel Corporation
  #
  # SPDX-License-Identifier: MIT
  ############################################################################*/

#include "src/windows/mfx_dispatcher.h"
#include "src/windows/mfx_dispatcher_log.h"
#include "src/windows/mfx_load_dll.h"

#include <assert.h>

#include <string.h>
#include <windows.h>

#include <algorithm>
#include "src/windows/mfx_dxva2_device.h"
#include "src/windows/mfx_vector.h"
#include "src/windows/mfxvideo++.h"
#include "vpl/mfxadapter.h"

#if _MSC_VER
    #pragma warning(disable : 4355)
#endif

MFX_DISP_HANDLE::MFX_DISP_HANDLE(const mfxVersion requiredVersion)
        : _mfxSession(),
          apiVersion(requiredVersion) {
    actualApiVersion.Version = 0;
    implType                 = MFX_LIB_SOFTWARE;
    impl                     = MFX_IMPL_SOFTWARE;
    loadStatus               = MFX_ERR_NOT_FOUND;
    dispVersion.Major        = MFX_DISPATCHER_VERSION_MAJOR;
    dispVersion.Minor        = MFX_DISPATCHER_VERSION_MINOR;
    storageID                = 0;
    implInterface            = MFX_IMPL_HARDWARE_ANY;

    hModule = (mfxModuleHandle)0;

} // MFX_DISP_HANDLE::MFX_DISP_HANDLE(const mfxVersion requiredVersion)

MFX_DISP_HANDLE::~MFX_DISP_HANDLE(void) {
    Close();

} // MFX_DISP_HANDLE::~MFX_DISP_HANDLE(void)

mfxStatus MFX_DISP_HANDLE::Close(void) {
    mfxStatus mfxRes;

    mfxRes = UnLoadSelectedDLL();

    // need to reset dispatcher state after unloading dll
    if (MFX_ERR_NONE == mfxRes) {
        implType                          = MFX_LIB_SOFTWARE;
        impl                              = MFX_IMPL_SOFTWARE;
        loadStatus                        = MFX_ERR_NOT_FOUND;
        dispVersion.Major                 = MFX_DISPATCHER_VERSION_MAJOR;
        dispVersion.Minor                 = MFX_DISPATCHER_VERSION_MINOR;
        *static_cast<_mfxSession *>(this) = _mfxSession();
        hModule                           = (mfxModuleHandle)0;
    }

    return mfxRes;

} // mfxStatus MFX_DISP_HANDLE::Close(void)

mfxStatus MFX_DISP_HANDLE::LoadSelectedDLL(const wchar_t *pPath,
                                           eMfxImplType reqImplType,
                                           mfxIMPL reqImpl,
                                           mfxIMPL reqImplInterface,
                                           mfxInitParam &par,
                                           mfxInitializationParam &vplParam,
                                           bool bCloneSession) {
    mfxStatus mfxRes = MFX_ERR_NONE;

    // check error(s)
    if ((MFX_LIB_SOFTWARE != reqImplType) && (MFX_LIB_HARDWARE != reqImplType)) {
        DISPATCHER_LOG_ERROR(
            (("implType == %s, should be either MFX_LIB_SOFTWARE or MFX_LIB_HARDWARE\n"),
             DispatcherLog_GetMFXImplString(reqImplType).c_str()));
        loadStatus = MFX_ERR_ABORTED;
        return loadStatus;
    }
    // only exact types of implementation is allowed
    if ((MFX_IMPL_SOFTWARE != reqImpl) && (MFX_IMPL_HARDWARE != reqImpl) &&
        (MFX_IMPL_HARDWARE2 != reqImpl) && (MFX_IMPL_HARDWARE3 != reqImpl) &&
        (MFX_IMPL_HARDWARE4 != reqImpl)) {
        DISPATCHER_LOG_ERROR((("invalid implementation impl == %s\n"),
                              DispatcherLog_GetMFXImplString(impl).c_str()));
        loadStatus = MFX_ERR_ABORTED;
        return loadStatus;
    }

    // close the handle before initialization
    Close();

    // save the library's type
    this->implType      = reqImplType;
    this->impl          = reqImpl;
    this->implInterface = reqImplInterface;

    {
        assert(hModule == (mfxModuleHandle)0);
        DISPATCHER_LOG_BLOCK(("invoking LoadLibrary(%S)\n", pPath));

        // load the DLL into the memory
        hModule = MFX::mfx_dll_load(pPath);

        if (hModule) {
            int i;

            DISPATCHER_LOG_OPERATION({
                wchar_t modulePath[1024];
                GetModuleFileNameW((HMODULE)hModule,
                                   modulePath,
                                   sizeof(modulePath) / sizeof(modulePath[0]));
                DISPATCHER_LOG_INFO((("loaded module %S\n"), modulePath))
            });

            // load video functions: pointers to exposed functions
            for (i = 0; i < eVideoFuncTotal; i += 1) {
                mfxFunctionPointer pProc =
                    (mfxFunctionPointer)MFX::mfx_dll_get_addr(hModule, APIFunc[i].pName);
                if (pProc) {
                    // function exists in the library,
                    // save the pointer.
                    callTable[i] = pProc;
                }
                else {
                    // The library doesn't contain the function
                    DISPATCHER_LOG_WRN((("Can't find API function \"%s\"\n"), APIFunc[i].pName));
                    if (apiVersion.Version >= APIFunc[i].apiVersion.Version) {
                        DISPATCHER_LOG_ERROR((("\"%s\" is required for API %u.%u\n"),
                                              APIFunc[i].pName,
                                              apiVersion.Major,
                                              apiVersion.Minor));
                        mfxRes = MFX_ERR_UNSUPPORTED;
                        break;
                    }
                }
            }

            // if version >= 2.0, load these functions as well
            if (apiVersion.Major >= 2) {
                for (i = 0; i < eVideoFunc2Total; i += 1) {
                    mfxFunctionPointer pProc =
                        (mfxFunctionPointer)MFX::mfx_dll_get_addr(hModule, APIVideoFunc2[i].pName);
                    if (pProc) {
                        // function exists in the library,
                        // save the pointer.
                        callVideoTable2[i] = pProc;
                    }
                    else {
                        // The library doesn't contain the function
                        DISPATCHER_LOG_WRN(
                            (("Can't find API function \"%s\"\n"), APIVideoFunc2[i].pName));
                        if (apiVersion.Version >= APIVideoFunc2[i].apiVersion.Version) {
                            DISPATCHER_LOG_ERROR((("\"%s\" is required for API %u.%u\n"),
                                                  APIVideoFunc2[i].pName,
                                                  apiVersion.Major,
                                                  apiVersion.Minor));
                            mfxRes = MFX_ERR_UNSUPPORTED;
                            break;
                        }
                    }
                }
            }
        }
        else {
            DISPATCHER_LOG_WRN((("can't find DLL: GetLastErr()=0x%x\n"), GetLastError()))
            mfxRes = MFX_ERR_UNSUPPORTED;
        }
    }

    if (bCloneSession == true) {
        loadStatus = mfxRes;
        return mfxRes;
    }

    // initialize the loaded DLL
    if (MFX_ERR_NONE == mfxRes) {
        mfxVersion version(apiVersion);

        if (version.Major >= 2) {
            // for API >= 2.0 call MFXInitialize instead of MFXInitEx
            int tableIndex           = eMFXInitialize;
            mfxFunctionPointer pFunc = callVideoTable2[tableIndex];

            // filled-in mfxInitializationParam must be provided for MFXInitialize path
            mfxRes =
                (*(mfxStatus(MFX_CDECL *)(mfxInitializationParam, mfxSession *))pFunc)(vplParam,
                                                                                       &session);
        }
        else {
            // Call old-style MFXInit init for older libraries
            bool callOldInit =
                !callTable[eMFXInitEx]; // if true call eMFXInit, if false - eMFXInitEx
            int tableIndex           = (callOldInit) ? eMFXInit : eMFXInitEx;
            mfxFunctionPointer pFunc = callTable[tableIndex];

            if (callOldInit) {
                DISPATCHER_LOG_BLOCK(("MFXInit(%s,ver=%u.%u,session=0x%p)\n",
                                      DispatcherLog_GetMFXImplString(impl | implInterface).c_str(),
                                      apiVersion.Major,
                                      apiVersion.Minor,
                                      &session));

                mfxRes = (*(mfxStatus(MFX_CDECL *)(mfxIMPL, mfxVersion *, mfxSession *))pFunc)(
                    impl | implInterface,
                    &version,
                    &session);
            }
            else {
                DISPATCHER_LOG_BLOCK(("MFXInitEx(%s,ver=%u.%u,ExtThreads=%d,session=0x%p)\n",
                                      DispatcherLog_GetMFXImplString(impl | implInterface).c_str(),
                                      apiVersion.Major,
                                      apiVersion.Minor,
                                      par.ExternalThreads,
                                      &session));

                mfxInitParam initPar = par;
                // adjusting user parameters
                initPar.Implementation = impl | implInterface;
                initPar.Version        = version;
                mfxRes =
                    (*(mfxStatus(MFX_CDECL *)(mfxInitParam, mfxSession *))pFunc)(initPar, &session);
            }
        }

        if (MFX_ERR_NONE != mfxRes) {
            DISPATCHER_LOG_WRN((("library can't be load. MFXInit returned %s \n"),
                                DispatcherLog_GetMFXStatusString(mfxRes)))
        }
        else {
            mfxRes = MFXQueryVersion((mfxSession)this, &actualApiVersion);

            if (MFX_ERR_NONE != mfxRes) {
                DISPATCHER_LOG_ERROR(
                    (("MFXQueryVersion returned: %d, skipped this library\n"), mfxRes))
            }
            else {
                DISPATCHER_LOG_INFO((("MFXQueryVersion returned API: %d.%d\n"),
                                     actualApiVersion.Major,
                                     actualApiVersion.Minor))
                //special hook for applications that uses sink api to get loaded library path
                DISPATCHER_LOG_LIBRARY(("%p", hModule));
                DISPATCHER_LOG_INFO(("library loaded successfully\n"))
            }
        }
    }

    loadStatus = mfxRes;
    return mfxRes;

} // mfxStatus MFX_DISP_HANDLE::LoadSelectedDLL(const wchar_t* pPath, eMfxImplType reqImplType, mfxIMPL reqImpl, mfxIMPL reqImplInterface, mfxInitParam& par, mfxInitializationParam &vplParam)

mfxStatus MFX_DISP_HANDLE::UnLoadSelectedDLL(void) {
    mfxStatus mfxRes = MFX_ERR_NONE;

    // close the loaded DLL
    if (session) {
        /* check whether it is audio session or video */
        int tableIndex = eMFXClose;
        mfxFunctionPointer pFunc;
        pFunc = callTable[tableIndex];

        mfxRes = (*(mfxStatus(MFX_CDECL *)(mfxSession))pFunc)(session);
        if (MFX_ERR_NONE == mfxRes) {
            session = (mfxSession)0;
        }

        DISPATCHER_LOG_INFO((("MFXClose(0x%x) returned %d\n"), session, mfxRes));
        // actually, the return value is required to pass outside only.
    }

    // it is possible, that there is an active child session.
    // can't unload library in that case.
    if ((MFX_ERR_UNDEFINED_BEHAVIOR != mfxRes) && (hModule)) {
        // unload the library.
        if (!MFX::mfx_dll_free(hModule)) {
            mfxRes = MFX_ERR_UNDEFINED_BEHAVIOR;
        }
        hModule = (mfxModuleHandle)0;
    }

    return mfxRes;

} // mfxStatus MFX_DISP_HANDLE::UnLoadSelectedDLL(void)

MFX_DISP_HANDLE_EX::MFX_DISP_HANDLE_EX(const mfxVersion requiredVersion)
        : MFX_DISP_HANDLE(requiredVersion),
          mediaAdapterType(MFX_MEDIA_UNKNOWN) {}

#if (defined(_WIN64) || defined(_WIN32)) && (MFX_VERSION >= 1031)
static mfxStatus InitDummySession(mfxU32 adapter_n, MFXVideoSession &dummy_session) {
    mfxInitParam initPar;
    memset(&initPar, 0, sizeof(initPar));

    initPar.Version.Major = 1;
    initPar.Version.Minor = 0;

    switch (adapter_n) {
        case 0:
            initPar.Implementation = MFX_IMPL_HARDWARE;
            break;
        case 1:
            initPar.Implementation = MFX_IMPL_HARDWARE2;
            break;
        case 2:
            initPar.Implementation = MFX_IMPL_HARDWARE3;
            break;
        case 3:
            initPar.Implementation = MFX_IMPL_HARDWARE4;
            break;

        default:
            // try searching on all display adapters
            initPar.Implementation = MFX_IMPL_HARDWARE_ANY;
            break;
    }

    initPar.Implementation |= MFX_IMPL_VIA_D3D11;

    return dummy_session.InitEx(initPar);
}

static inline bool is_iGPU(const mfxAdapterInfo &adapter_info) {
    return adapter_info.Platform.MediaAdapterType == MFX_MEDIA_INTEGRATED;
}

static inline bool is_dGPU(const mfxAdapterInfo &adapter_info) {
    return adapter_info.Platform.MediaAdapterType == MFX_MEDIA_DISCRETE;
}

// This function implies that iGPU has higher priority
static inline mfxI32 iGPU_priority(const void *ll, const void *rr) {
    const mfxAdapterInfo &l = *(reinterpret_cast<const mfxAdapterInfo *>(ll));
    const mfxAdapterInfo &r = *(reinterpret_cast<const mfxAdapterInfo *>(rr));

    if ((is_iGPU(l) && is_iGPU(r)) || (is_dGPU(l) && is_dGPU(r)))
        return 0;

    if (is_iGPU(l) && is_dGPU(r))
        return -1;

    // The only combination left is_dGPU(l) && is_iGPU(r))
    return 1;
}

static void RearrangeInPriorityOrder(const mfxComponentInfo &info,
                                     MFX::MFXVector<mfxAdapterInfo> &vec) {
    (void)info;
    {
        // Move iGPU to top priority
        qsort(vec.data(), vec.size(), sizeof(mfxAdapterInfo), &iGPU_priority);
    }
}

static mfxStatus PrepareAdaptersInfo(const mfxComponentInfo *info,
                                     MFX::MFXVector<mfxAdapterInfo> &vec,
                                     mfxAdaptersInfo &adapters) {
    // No suitable adapters on system to handle user's workload
    if (vec.empty()) {
        adapters.NumActual = 0;
        return MFX_ERR_NOT_FOUND;
    }

    if (info) {
        RearrangeInPriorityOrder(*info, vec);
    }

    mfxU32 num_to_copy = (std::min)(mfxU32(vec.size()), adapters.NumAlloc);
    for (mfxU32 i = 0; i < num_to_copy; ++i) {
        adapters.Adapters[i] = vec[i];
    }

    adapters.NumActual = num_to_copy;

    if (vec.size() > adapters.NumAlloc) {
        return MFX_WRN_OUT_OF_RANGE;
    }

    return MFX_ERR_NONE;
}

static inline bool QueryAdapterInfo(mfxU32 adapter_n, mfxU32 &VendorID, mfxU32 &DeviceID) {
    MFX::DXVA2Device dxvaDevice;

    if (!dxvaDevice.InitDXGI1(adapter_n))
        return false;

    VendorID = dxvaDevice.GetVendorID();
    DeviceID = dxvaDevice.GetDeviceID();

    return true;
}

static inline mfxU32 MakeVersion(mfxU16 major, mfxU16 minor) {
    return major * 1000 + minor;
}

mfxStatus MFXQueryAdaptersDecode(mfxBitstream *bitstream,
                                 mfxU32 codec_id,
                                 mfxAdaptersInfo *adapters) {
    if (!adapters || !bitstream)
        return MFX_ERR_NULL_PTR;

    MFX::MFXVector<mfxAdapterInfo> obtained_info;

    mfxU32 adapter_n = 0, VendorID, DeviceID;

    mfxComponentInfo input_info;
    memset(&input_info, 0, sizeof(input_info));
    input_info.Type                     = mfxComponentType::MFX_COMPONENT_DECODE;
    input_info.Requirements.mfx.CodecId = codec_id;

    for (;;) {
        if (!QueryAdapterInfo(adapter_n, VendorID, DeviceID))
            break;

        ++adapter_n;

        if (VendorID != INTEL_VENDOR_ID)
            continue;

        // Check if requested capabilities are supported
        MFXVideoSession dummy_session;

        mfxStatus sts = InitDummySession(adapter_n - 1, dummy_session);
        if (sts != MFX_ERR_NONE) {
            continue;
        }

        mfxVideoParam stream_params, out;
        memset(&out, 0, sizeof(out));
        memset(&stream_params, 0, sizeof(stream_params));
        out.mfx.CodecId = stream_params.mfx.CodecId = codec_id;

        sts = MFXVideoDECODE_DecodeHeader(dummy_session.operator mfxSession(),
                                          bitstream,
                                          &stream_params);

        if (sts != MFX_ERR_NONE) {
            continue;
        }

        sts = MFXVideoDECODE_Query(dummy_session.operator mfxSession(), &stream_params, &out);

        if (sts !=
            MFX_ERR_NONE) // skip MFX_ERR_UNSUPPORTED as well as MFX_WRN_INCOMPATIBLE_VIDEO_PARAM
            continue;

        mfxAdapterInfo info;
        memset(&info, 0, sizeof(info));

        //WA for initialization when application built w/ new API, but lib w/ old one.
        mfxVersion apiVersion;
        sts = dummy_session.QueryVersion(&apiVersion);
        if (sts != MFX_ERR_NONE)
            continue;

        mfxU32 version = MakeVersion(apiVersion.Major, apiVersion.Minor);

        if (version >= 1019) {
            sts = MFXVideoCORE_QueryPlatform(dummy_session.operator mfxSession(), &info.Platform);

            if (sts != MFX_ERR_NONE) {
                continue;
            }
        }
        else {
            // for API versions greater than 1.19 Device id is set inside QueryPlatform call
            info.Platform.DeviceId = static_cast<mfxU16>(DeviceID);
        }

        info.Number = adapter_n - 1;

        obtained_info.push_back(info);
    }

    return PrepareAdaptersInfo(&input_info, obtained_info, *adapters);
}

mfxStatus MFXQueryAdapters(mfxComponentInfo *input_info, mfxAdaptersInfo *adapters) {
    if (!adapters)
        return MFX_ERR_NULL_PTR;

    MFX::MFXVector<mfxAdapterInfo> obtained_info;
    //obtained_info.reserve(adapters->NumAdaptersAlloc);

    mfxU32 adapter_n = 0, VendorID, DeviceID;

    for (;;) {
        if (!QueryAdapterInfo(adapter_n, VendorID, DeviceID))
            break;

        ++adapter_n;

        if (VendorID != INTEL_VENDOR_ID)
            continue;

        // Check if requested capabilities are supported
        MFXVideoSession dummy_session;

        mfxStatus sts = InitDummySession(adapter_n - 1, dummy_session);
        if (sts != MFX_ERR_NONE) {
            continue;
        }

        // If input_info is NULL just return all Intel adapters and information about them
        if (input_info) {
            mfxVideoParam out;
            memset(&out, 0, sizeof(out));

            switch (input_info->Type) {
                case mfxComponentType::MFX_COMPONENT_ENCODE: {
                    out.mfx.CodecId = input_info->Requirements.mfx.CodecId;

                    sts = MFXVideoENCODE_Query(dummy_session.operator mfxSession(),
                                               &input_info->Requirements,
                                               &out);
                } break;
                case mfxComponentType::MFX_COMPONENT_DECODE: {
                    out.mfx.CodecId = input_info->Requirements.mfx.CodecId;

                    sts = MFXVideoDECODE_Query(dummy_session.operator mfxSession(),
                                               &input_info->Requirements,
                                               &out);
                } break;
                case mfxComponentType::MFX_COMPONENT_VPP: {
                    sts = MFXVideoVPP_Query(dummy_session.operator mfxSession(),
                                            &input_info->Requirements,
                                            &out);
                } break;
                default:
                    sts = MFX_ERR_UNSUPPORTED;
            }
        }

        if (sts !=
            MFX_ERR_NONE) // skip MFX_ERR_UNSUPPORTED as well as MFX_WRN_INCOMPATIBLE_VIDEO_PARAM
            continue;

        mfxAdapterInfo info;
        memset(&info, 0, sizeof(info));

        //WA for initialization when application built w/ new API, but lib w/ old one.
        mfxVersion apiVersion;
        sts = dummy_session.QueryVersion(&apiVersion);
        if (sts != MFX_ERR_NONE)
            continue;

        mfxU32 version = MakeVersion(apiVersion.Major, apiVersion.Minor);

        if (version >= 1019) {
            sts = MFXVideoCORE_QueryPlatform(dummy_session.operator mfxSession(), &info.Platform);

            if (sts != MFX_ERR_NONE) {
                continue;
            }
        }
        else {
            // for API versions greater than 1.19 Device id is set inside QueryPlatform call
            info.Platform.DeviceId = static_cast<mfxU16>(DeviceID);
        }

        info.Number = adapter_n - 1;

        obtained_info.push_back(info);
    }

    return PrepareAdaptersInfo(input_info, obtained_info, *adapters);
}

mfxStatus MFXQueryAdaptersNumber(mfxU32 *num_adapters) {
    if (!num_adapters)
        return MFX_ERR_NULL_PTR;

    mfxU32 intel_adapter_count = 0, VendorID, DeviceID;

    for (mfxU32 cur_adapter = 0;; ++cur_adapter) {
        if (!QueryAdapterInfo(cur_adapter, VendorID, DeviceID))
            break;

        if (VendorID == INTEL_VENDOR_ID)
            ++intel_adapter_count;
    }

    *num_adapters = intel_adapter_count;

    return MFX_ERR_NONE;
}

#endif // (defined(_WIN64) || defined(_WIN32)) && (MFX_VERSION >= 1031)
