#include "camera.h"

#if SPICE64

#include <d3d9.h>
#include "mf_wrappers.h"
#include "avs/game.h"
#include "external/rapidjson/document.h"
#include "external/rapidjson/pointer.h"
#include "external/rapidjson/prettywriter.h"
#include "games/iidx/iidx.h"
#include "games/io.h"
#include "launcher/options.h"
#include "misc/eamuse.h"
#include "util/detour.h"
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "util/utils.h"

static std::filesystem::path dll_path;
static HMODULE iidx_module;
static uintptr_t addr_hook_a = 0;
static uintptr_t addr_hook_b = 0;
static uintptr_t addr_textures = 0;
static uintptr_t addr_device_offset = 0;

typedef void **(__fastcall *camera_hook_a_t)(PBYTE);
typedef LPDIRECT3DTEXTURE9 (*camera_hook_b_t)(void*, int);
static camera_hook_a_t camera_hook_a_orig = nullptr;
static camera_hook_b_t camera_hook_b_orig = nullptr;
auto static hook_a_init = std::once_flag {};

static LPDIRECT3DDEVICE9EX device;
static LPDIRECT3DTEXTURE9 *texture_a = nullptr;
static LPDIRECT3DTEXTURE9 *texture_b = nullptr;

struct PredefinedHook {
    std::string     pe_identifier;
    uintptr_t       hook_a;
    uintptr_t       hook_b;
    uintptr_t       hook_textures;
    uintptr_t       hook_device_offset;
};

PredefinedHook g_predefinedHooks[] =
{
    // 27 003
    { "5f713b52_6d4090", 0x005971b0, 0x005fb2c0, 0x04e49898, 0x000000d0 },
    // 27 010
    { "5f713946_7f52b0", 0x006b89e0, 0x0071c950, 0x04fd08b8, 0x000000d0 },
};

const DWORD   g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks);

namespace games::iidx {

    static IIDXLocalCamera *top_camera = nullptr;       // camera id #0
    static IIDXLocalCamera *front_camera = nullptr;     // camera id #1
    std::vector<IIDXLocalCamera*> LOCAL_CAMERA_LIST = {};
    static IDirect3DDeviceManager9 *s_pD3DManager = nullptr;
    std::filesystem::path CAMERA_CONFIG_PATH;
    bool CAMERA_READY = false;

    bool parse_cmd_params() {
        const auto remove_spaces = [](const char& c) {
            return c == ' ';
        };

        log_misc("iidx:camhook", "Using user-supplied addresses (-iidxcamhookoffset)");

        auto s = TDJ_CAMERA_OVERRIDE.value();
        s.erase(std::remove_if(s.begin(), s.end(), remove_spaces), s.end());

        if (sscanf(
                s.c_str(),
                "%i,%i,%i,%i",
                (int*)&addr_hook_a, (int*)&addr_hook_b, (int*)&addr_textures, (int*)&addr_device_offset) == 4) {

            return true;

        } else {
            log_fatal("iidx:camhook", "failed to parse -iidxcamhookoffset");
            return false;
        }
    }

    bool find_camera_hooks() {
        std::string dll_name = avs::game::DLL_NAME;
        dll_path = MODULE_PATH / dll_name; 

        iidx_module = libutils::try_module(dll_path);
        if (!iidx_module) {
            log_warning("iidx:camhook", "failed to hook game DLL");
            return FALSE;
        }

        uint32_t time_date_stamp = 0;
        uint32_t address_of_entry_point = 0;

        // there are three different ways we try to hook the camera entry points
        // 1) user-provided override via cmd line
        // 2) predefined offsets using PE header matching (needed for iidx27 and few others)
        // 3) signature match (should work for most iidx28-31)

        // method 1: user provided
        if (TDJ_CAMERA_OVERRIDE.has_value() && parse_cmd_params()) {
            return TRUE;
        }

        // method 2: predefined offsets
        get_pe_identifier(dll_path, &time_date_stamp, &address_of_entry_point);
        auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point);
        log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe);

        for (DWORD i = 0; i < g_predefinedHooksLength; i++) {
            if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) {
                log_misc("iidx:camhook", "Found predefined addresses");
                addr_hook_a        = g_predefinedHooks[i].hook_a;
                addr_hook_b        = g_predefinedHooks[i].hook_b;
                addr_textures      = g_predefinedHooks[i].hook_textures;
                addr_device_offset = g_predefinedHooks[i].hook_device_offset;
                return TRUE;
            }
        }

        // method 3: signature match
        log_info("iidx:camhook", "Did not find predefined hook address, try signature match");

        // --- addr_hook_a ---
        uint8_t *addr_hook_a_ptr = reinterpret_cast<uint8_t *>(find_pattern(
            iidx_module,
            "488BC4565741564883EC5048C740D8FEFFFFFF",
            "XXXXXXXXXXXXXXXXXXX",
            0, 0));

        if (addr_hook_a_ptr == nullptr) {
            log_warning("iidx:camhook", "failed to find hook: addr_hook_a");
            log_warning("iidx:camhook", "hint: this feature REQUIRES a compatible DLL!");
            return FALSE;
        }

        addr_hook_a = (uint64_t) (addr_hook_a_ptr - (uint8_t*) iidx_module);

        // addr_hook_b
        uint8_t* addr_hook_b_ptr = reinterpret_cast<uint8_t *>(find_pattern(
            iidx_module,
            "E8000000004885C07439E8",
            "X????XXXXXX",
            0, 0));

        if (addr_hook_b_ptr == nullptr) {
            log_warning("iidx:camhook", "failed to find hook: addr_hook_b");
            return FALSE;
        }

        // displace with the content of wildcard bytes
        int32_t disp_b = *((int32_t *) (addr_hook_b_ptr + 1));

        addr_hook_b = (addr_hook_b_ptr - (uint8_t*) iidx_module) + disp_b + 5;

        // --- addr_textures ---
        uint8_t* addr_textures_ptr = reinterpret_cast<uint8_t *>(find_pattern(
            iidx_module,
            "E800000000488BC8488BD34883C420",
            "X????XXXXXXXXXX",
            0, 0));
        
        if (addr_textures_ptr == nullptr) {
            log_warning("iidx:camhook", "failed to find hook: addr_textures (part 1)");
            return FALSE;
        }

        // displace with the content of wildcard bytes
        int32_t disp_textures = *((int32_t *) (addr_textures_ptr + 1));
        addr_textures_ptr += disp_textures + 5;

        uint32_t search_from = (addr_textures_ptr - (uint8_t*) iidx_module);

        addr_textures_ptr = reinterpret_cast<uint8_t *>(find_pattern_from(
            iidx_module,
            "488D0D",
            "XXX",
            0, 0, search_from));

        if (addr_textures_ptr == nullptr) {
            log_warning("iidx:camhook", "failed to find hook: addr_textures (part 2)");
            return FALSE;
        }

        // displace again with the next integer
        disp_textures = *((int32_t *) (addr_textures_ptr + 3));

        addr_textures = (addr_textures_ptr - (uint8_t*) iidx_module) + disp_textures + 7;

        // addr_device_offset
        search_from = (addr_hook_a_ptr - (uint8_t*) iidx_module);
        uint8_t *addr_device_ptr = reinterpret_cast<uint8_t *>(find_pattern_from(
                iidx_module,
                "488B89",
                "XXX",
                3, 0, search_from));
        addr_device_offset = *addr_device_ptr;

        if (addr_textures_ptr == nullptr) {
            log_warning("iidx:camhook", "failed to find hook: addr_textures (part 3)");
            return FALSE;
        }

        return TRUE;
    }

    static void **__fastcall camera_hook_a(PBYTE a1) {
        std::call_once(hook_a_init, [&]{           
            device = *reinterpret_cast<LPDIRECT3DDEVICE9EX*>(a1 + addr_device_offset);
            auto const textures = *reinterpret_cast<LPDIRECT3DTEXTURE9**>((uint8_t*)iidx_module + addr_textures);

            texture_a = textures;
            texture_b = textures + 2;

            init_local_camera();
        });
        return camera_hook_a_orig(a1);
    }

    static LPDIRECT3DTEXTURE9 camera_hook_b(void* a1, int idx) {
        if (idx == 0 && top_camera) {
            return top_camera->GetTexture();
        }
        if (idx == 1 && front_camera) {
            return front_camera->GetTexture();
        }
        return camera_hook_b_orig(a1, idx);
    }

    bool create_hooks() {
        auto *hook_a_ptr = reinterpret_cast<uint16_t *>(
                reinterpret_cast<intptr_t>(iidx_module) + addr_hook_a);

        if (!detour::trampoline(
                    reinterpret_cast<camera_hook_a_t>(hook_a_ptr),
                    camera_hook_a,
                    &camera_hook_a_orig)) {
            log_warning("iidx:camhook", "failed to trampoline hook_a");
            return FALSE;
        }

        auto *hook_b_ptr = reinterpret_cast<uint16_t *>(
                reinterpret_cast<intptr_t>(iidx_module) + addr_hook_b);

        if (!detour::trampoline(
                    reinterpret_cast<camera_hook_b_t>(hook_b_ptr),
                    camera_hook_b,
                    &camera_hook_b_orig)) {
            log_warning("iidx:camhook", "failed to trampoline hook_b");
            return FALSE;
        }

        return TRUE;
    }

    HRESULT create_d3d_device_manager() {
        UINT resetToken = 0;
        IDirect3DDeviceManager9 *pD3DManager = nullptr;

        HRESULT hr = DXVA2CreateDirect3DDeviceManager9(&resetToken, &pD3DManager);
        if (FAILED(hr)) { goto done; }

        hr = pD3DManager->ResetDevice(device, resetToken);
        if (FAILED(hr)) { goto done; }

        s_pD3DManager = pD3DManager;
        (s_pD3DManager)->AddRef();

    done:
        if (SUCCEEDED(hr)) {
            log_misc("iidx:camhook", "Created DeviceManager for DXVA");
        } else {
            log_warning("iidx:camhook", "Cannot create DXVA DeviceManager: {}", hr);
        }

        SafeRelease(&pD3DManager);
        return hr;
    }

    bool init_local_camera() {
        HRESULT hr = S_OK;
        IMFAttributes *pAttributes = nullptr;
        IMFActivate **ppDevices = nullptr;
        uint32_t numDevices;

        // Initialize an attribute store to specify enumeration parameters.
        hr = WrappedMFCreateAttributes(&pAttributes, 1);

        // Ask for source type = video capture devices.
        if (SUCCEEDED(hr)) {
            hr = pAttributes->SetGUID(
                MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, 
                MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID
            );
        }
        
        // Enumerate devices.
        if (SUCCEEDED(hr)) {
            hr = WrappedMFEnumDeviceSources(pAttributes, &ppDevices, &numDevices);
        }

        log_info("iidx:camhook", "MFEnumDeviceSources returned {} device(s)", numDevices);
        if (numDevices > 0) {
            hr = create_d3d_device_manager();
        }

        if (SUCCEEDED(hr)) {

            // get options
            std::string top_camera_id("");
            std::string front_camera_id("");
            auto options = games::get_options(eamuse_get_game());
            if (options->at(launcher::Options::IIDXCamHookTopId).is_active()) {
                top_camera_id = options->at(launcher::Options::IIDXCamHookTopId).value_text();
            }
            if (options->at(launcher::Options::IIDXCamHookFrontId).is_active()) {
                front_camera_id = options->at(launcher::Options::IIDXCamHookFrontId).value_text();
            }
            int preferred_top_index = -1;
            int preferred_front_index = -1;

            log_info("iidx:camhook", "-------- Listing all cameras from MFEnumDeviceSources --------");

            // print camera names first for user
            for (size_t i = 0; i < numDevices; i++) {
                // get camera activation
                auto pActivate = ppDevices[i];

                // print friendly name
                PWCHAR friendly = nullptr;
                UINT32 friendly_len = 0;
                hr = pActivate->GetAllocatedString(
                    MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME,
                    &friendly,
                    &friendly_len);
                if (SUCCEEDED(hr) && friendly != nullptr) {
                    log_info("iidx:camhook", "Camera {} Name: {}", i, ws2s(friendly));
                    CoTaskMemFree(friendly);
                    friendly = nullptr;
                }

                // get symlink name
                PWCHAR symlink = nullptr;
                UINT32 symlink_len = 0;
                hr = pActivate->GetAllocatedString(
                    MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
                    &symlink,
                    &symlink_len);
                if (FAILED(hr) || symlink == nullptr) {
                    log_info("iidx:camhook", "Camera {}: failed to get vidcap symlink name", i);
                    continue;
                }

                const auto symlink_s = ws2s(symlink);
                log_info("iidx:camhook", "Camera {}   ID: {}", i, symlink_s);

                if (!top_camera_id.empty() && (symlink_s.find(top_camera_id) != std::string::npos)) {
                    preferred_top_index = i;
                } else if (!front_camera_id.empty() && (symlink_s.find(front_camera_id) != std::string::npos)) {
                    preferred_front_index = i;
                }

                if (symlink != nullptr) {
                    CoTaskMemFree(symlink);
                    symlink = nullptr;
                }
            }

            log_info("iidx:camhook", "--------    End of cameras from MFEnumDeviceSources   --------");

            // pick out preferred top/front cameras

            log_misc("iidx:camhook", "Adding user-preferred cameras");
            for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
                auto pActivate = ppDevices[i];
                if ((int)i == preferred_top_index && top_camera == nullptr) {
                    log_misc("iidx:camhook", "Adding user-preferred top camera {} / '{}'", i, top_camera_id);
                    top_camera = add_top_camera(pActivate);
                } else if ((int)i == preferred_front_index && front_camera == nullptr) {
                    log_misc("iidx:camhook", "Adding user-preferred front camera {} / '{}'", i, front_camera_id);
                    front_camera = add_front_camera(pActivate);
                }
            }

            // fill out the rest of cameras
            log_misc("iidx:camhook", "Adding other cameras");
            for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
                auto pActivate = ppDevices[i];

                if ((int)i == preferred_top_index || (int)i == preferred_front_index) {
                    // we already tried these above; don't use them here
                    continue;
                }

                if (!FLIP_CAMS) {
                    // top camera first, then front
                    if (top_camera == nullptr) {
                        top_camera = add_top_camera(pActivate);
                    } else if (front_camera == nullptr) {
                        front_camera = add_front_camera(pActivate);
                    }
                } else {
                    // front first, then top
                    if (front_camera == nullptr) {
                        front_camera = add_front_camera(pActivate);
                    } else if (top_camera == nullptr) {
                        top_camera = add_top_camera(pActivate);
                    }
                }
            }
        }

        if (LOCAL_CAMERA_LIST.size() == 0) {
            goto done;
        }

        camera_config_load();
        if (top_camera) {
            top_camera->StartCapture();
        }
        if (front_camera) {
            front_camera->StartCapture();
        }

        CAMERA_READY = true;

    done:
        SafeRelease(&pAttributes);

        for (DWORD i = 0; i < numDevices; i++) {
            SafeRelease(&ppDevices[i]);
        }
        CoTaskMemFree(ppDevices);

        return SUCCEEDED(hr);
    }

    IIDXLocalCamera* add_top_camera(IMFActivate* pActivate) {
        auto top_camera = new IIDXLocalCamera("top", TDJ_CAMERA_PREFER_16_9, pActivate, s_pD3DManager, device, texture_a);
        if (top_camera->m_initialized) {
            LOCAL_CAMERA_LIST.push_back(top_camera);
        } else {
            top_camera->Release();
            top_camera = nullptr;
        }
        return top_camera;
    }

    IIDXLocalCamera* add_front_camera(IMFActivate* pActivate) {
        auto front_camera = new IIDXLocalCamera("front", TDJ_CAMERA_PREFER_16_9, pActivate, s_pD3DManager, device, texture_b);
        if (front_camera->m_initialized) {
            LOCAL_CAMERA_LIST.push_back(front_camera);
        } else {
            front_camera->Release();
            front_camera = nullptr;
        }
        return front_camera;
    }

    bool init_camera_hooks() {
        bool result = find_camera_hooks();
        if (result) {
            log_misc(
                "iidx:camhook",
                "found hooks:\n    hook_a        0x{:x}\n    hook_b        0x{:x}\n    textures      0x{:x}\n    device_offset 0x{:x}",
                addr_hook_a, addr_hook_b, addr_textures, addr_device_offset);
            result = create_hooks();
            if (result) {
                init_mf_library();
            }
        }
        
        return result;
    }

    void camera_release() {
        if (top_camera) {
            top_camera->Release();
            top_camera = nullptr;
        }
        if (front_camera) {
            front_camera->Release();
            front_camera = nullptr;
        }
        SafeRelease(&s_pD3DManager);
    }

    bool camera_config_load() {
        CAMERA_CONFIG_PATH =
            fileutils::get_config_file_path("iidx::camhook", "spicetools_camera_control.json");

        try {
            // read config file
            std::string config = fileutils::text_read(CAMERA_CONFIG_PATH);
            if (!config.empty()) {
                // parse document
                rapidjson::Document doc;
                doc.Parse(config.c_str());

                // check parse error
                auto error = doc.GetParseError();
                if (error) {
                    log_warning("iidx:camhook", "config parse error: {}", error);
                    return false;
                }

                // verify root is a dict
                if (!doc.IsObject()) {
                    log_warning("iidx:camhook", "config not found");
                    return false;
                }

                const std::string root("/sp2x_cameras");
                auto numCameras = LOCAL_CAMERA_LIST.size();
                for (size_t i = 0; i < numCameras; i++) {
                    auto *camera = LOCAL_CAMERA_LIST.at(i);
                    auto symLink = camera->GetSymLink();

                    const auto cameraNode = rapidjson::Pointer(root + "/" + symLink).Get(doc);
                    if (cameraNode && cameraNode->IsObject()) {
                        log_info("iidx:camhook", "Parsing config for: {}", symLink);

                        // Media type
                        auto mediaTypePointer = rapidjson::Pointer(root + "/" + symLink + "/MediaType").Get(doc);
                        if (mediaTypePointer && mediaTypePointer->IsString()) {
                            std::string mediaType = mediaTypePointer->GetString();
                            if (mediaType.length() > 0) {
                                camera->m_selectedMediaTypeDescription = mediaType;
                                camera->m_useAutoMediaType = false;
                            } else {
                                camera->m_useAutoMediaType = true;
                            }
                        }

                        // Draw mode
                        auto drawModePointer = rapidjson::Pointer(root + "/" + symLink + "/DrawMode").Get(doc);
                        if (drawModePointer && drawModePointer->IsString()) {
                            std::string drawModeString = drawModePointer->GetString();
                            for (int j = 0; j < DRAW_MODE_SIZE; j++) {
                                if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
                                    camera->m_drawMode = (LocalCameraDrawMode) j;
                                    break;
                                }
                            }
                        }

                        // Flip
                        auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
                        if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
                            camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
                        }

                        auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
                        if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
                            camera->m_flipVertical = flipVerticalPointer->GetBool();
                        }

                        // Allow manual control
                        auto manualControlPointer = rapidjson::Pointer(root + "/" + symLink + "/AllowManualControl").Get(doc);
                        if (manualControlPointer && manualControlPointer->IsBool() && manualControlPointer->GetBool()) {
                            camera->m_allowManualControl = true;
                        }

                        // Camera control
                        // if m_allowManualControl is false, SetCameraControlProp will fail, but run through this code
                        // anyway to exercise parsing code
                        for (int propIndex = 0; propIndex < CAMERA_CONTROL_PROP_SIZE; propIndex++) {
                            std::string label = CAMERA_CONTROL_LABELS[propIndex];
                            CameraControlProp prop = {};
                            camera->GetCameraControlProp(propIndex, &prop);
                            auto valuePointer = rapidjson::Pointer(root + "/" + symLink + "/" + label + "/value").Get(doc);
                            auto flagsPointer = rapidjson::Pointer(root + "/" + symLink + "/" + label + "/flags").Get(doc);
                            if (valuePointer && valuePointer->IsInt() && flagsPointer && flagsPointer->IsInt()) {
                                prop.value = (long)valuePointer->GetInt();
                                prop.valueFlags = (long)flagsPointer->GetInt();
                                camera->SetCameraControlProp(propIndex, prop.value, prop.valueFlags);
                            }

                        }
                        log_misc("iidx:camhook", "  >> done");
                    } else {
                        log_misc("iidx:camhook", "No previous config for: {}", symLink);
                    }
                }
            }
        } catch (const std::exception& e) {
            log_warning("iidx:camhook", "exception occurred while config: {}", e.what());
            return false;
        }

        return true;
    }

    bool camera_config_save() {
        log_info("iidx:camhook", "saving config");

        // create document
        rapidjson::Document doc;
        std::string config = fileutils::text_read(CAMERA_CONFIG_PATH);
        if (!config.empty()) {
            doc.Parse(config.c_str());
            log_misc("iidx:camhook", "existing config file found");
        }
        if (!doc.IsObject()) {
            log_misc("iidx:camhook", "clearing out config file");
            doc.SetObject();
        }

        auto numCameras = LOCAL_CAMERA_LIST.size();
        for (size_t i = 0; i < numCameras; i++) {
            auto *camera = LOCAL_CAMERA_LIST.at(i);
            auto symLink = camera->GetSymLink();
            std::string root("/sp2x_cameras/" + symLink + "/");

            // Media type
            if (camera->m_useAutoMediaType) {
                rapidjson::Pointer(root + "MediaType").Set(doc, "");
            } else {
                rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
            }

            // Draw Mode
            rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);

            // Flip
            rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
            rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);

            // Manual control
            rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);

            // Camera control
            for (int propIndex = 0; propIndex < CAMERA_CONTROL_PROP_SIZE; propIndex++) {
                std::string label = CAMERA_CONTROL_LABELS[propIndex];
                CameraControlProp prop = {};
                camera->GetCameraControlProp(propIndex, &prop);
                rapidjson::Pointer(root + label + "/value").Set(doc, (int)prop.value);
                rapidjson::Pointer(root + label + "/flags").Set(doc, (int)prop.valueFlags);
            }
        }

        // build JSON
        rapidjson::StringBuffer buffer;
        rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
        doc.Accept(writer);

        // save to file
        if (fileutils::write_config_file("iidx::camhook", CAMERA_CONFIG_PATH, buffer.GetString())) {
        } else {
            log_warning("iidx:camhook", "unable to save config file");
        }

        return true;
    }

    bool camera_config_reset() {
        return false;
    }
}

#endif
