// enable Windows 8 touch injection types; the functions are loaded dynamically
#define _WIN32_WINNT 0x0602

#include <atomic>
#include <mutex>

#include <windows.h>
#include <commctrl.h>

#include "inject.h"
#include "inject_internal.h"
#include "transform.h"

#include "hooks/graphics/graphics.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"

namespace nativetouch::inject {

    constexpr DWORD INJECTION_RETRY_DELAY_MS = 1;
    constexpr POINTER_FLAGS CONTACT_DOWN_FLAGS =
        POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
    constexpr POINTER_FLAGS CONTACT_UPDATE_FLAGS =
        POINTER_FLAG_UPDATE | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;

    // Windows 8 APIs are resolved dynamically to preserve older-OS compatibility
    static decltype(RegisterTouchWindow) *RegisterTouchWindow_orig = nullptr;
    static decltype(InitializeTouchInjection) *InitializeTouchInjection_ptr = nullptr;
    static decltype(InjectTouchInput) *InjectTouchInput_ptr = nullptr;

    // state for the single synthetic touch contact
    struct ContactState {
        ContactOwner owner = ContactOwner::None;
        HWND input_window = nullptr;
        POINT position {};
        UINT_PTR timer_id = 0;

        bool is_active() const {
            return owner != ContactOwner::None;
        }
    };

    // tracks an injector-owned contact without matching unrelated hardware touches
    struct SyntheticTouchIdentity {
        POINT down_position {};
        HANDLE source = nullptr;
        DWORD id = 0;
        bool identified = false;
        bool pending = false;
        bool transform_coordinates = false;
        std::atomic<HWND> transform_window { nullptr };

        void begin(POINT position, HWND window, bool transform_returned_coordinates) {
            down_position = position;
            source = nullptr;
            id = 0;
            identified = false;
            pending = true;
            transform_coordinates = transform_returned_coordinates;
            transform_window.store(window, std::memory_order_release);
        }

        void reset(HWND expected_window) {
            source = nullptr;
            id = 0;
            identified = false;
            pending = false;
            transform_coordinates = false;
            transform_window.compare_exchange_strong(
                expected_window, nullptr, std::memory_order_acq_rel);
        }

        HWND get_transform_window() const {
            return transform_window.load(std::memory_order_acquire);
        }

        bool matches(PTOUCHINPUT point) {
            // InjectTouchInput provides no application marker in TOUCHINPUT. Claim the first
            // matching DOWN, then use the source and ID assigned by Windows for this contact.
            if (!identified) {
                // correlate the pending injection's pixel position
                constexpr LONG POSITION_TOLERANCE = 200;
                const auto delta_x = point->x - down_position.x * 100;
                const auto delta_y = point->y - down_position.y * 100;
                if (!pending || !(point->dwFlags & TOUCHEVENTF_DOWN) ||
                    delta_x < -POSITION_TOLERANCE || delta_x > POSITION_TOLERANCE ||
                    delta_y < -POSITION_TOLERANCE || delta_y > POSITION_TOLERANCE) {
                    return false;
                }

                // save the identity that remains stable through this contact's MOVE and UP.
                source = point->hSource;
                id = point->dwID;
                identified = true;
            }

            // require both the provider and contact identities to match.
            return point->hSource == source && point->dwID == id;
        }

        bool should_transform_coordinates() const {
            return transform_coordinates;
        }
    };

    static ContactState contact_state;
    static SyntheticTouchIdentity synthetic_touch_identity;

    // main game window that receives WM_TOUCH forwarded from the dedicated TDJ subscreen
    static std::atomic<HWND> touch_delivery_window { nullptr };

    // window whose UI thread owns synthetic contact state and synthetic touch requests;
    // normally the active touch window, while dedicated TDJ uses the subscreen window
    static std::atomic<HWND> injection_window { nullptr };

    // cross-thread game-loop refreshes are coalesced so they cannot flood the window queue
    static std::atomic<bool> contact_refresh_pending { false };

    static std::once_flag initialization_once;
    static int window_subclass_token;

    // asks the touch window thread to send an UPDATE when the game loop runs elsewhere
    static UINT contact_refresh_message;

    // submit one synthetic contact frame to Windows touch injection
    static bool inject_touch_frame(
            POINT position, POINTER_FLAGS pointer_flags, bool retry_if_not_ready = false) {
        POINTER_TOUCH_INFO contact {};
        contact.pointerInfo.pointerType = PT_TOUCH;
        contact.pointerInfo.pointerId = 0;
        contact.pointerInfo.pointerFlags = pointer_flags;
        contact.pointerInfo.ptPixelLocation = position;
        contact.touchFlags = TOUCH_FLAG_NONE;

        BOOL result = InjectTouchInput_ptr(1, &contact);
        if (result) {
            return true;
        }

        // drop ordinary UPDATE frames when Windows is still processing the prior frame
        const auto error = GetLastError();
        if (error == ERROR_NOT_READY &&
            (pointer_flags & POINTER_FLAG_UPDATE) && !retry_if_not_ready) {
            return true;
        }

        // retry required frames once after a brief delay
        if (error == ERROR_NOT_READY) {
            Sleep(INJECTION_RETRY_DELAY_MS);
            result = InjectTouchInput_ptr(1, &contact);
        }
        if (result) {
            return true;
        }

        static bool error_logged = false;
        if (!error_logged) {
            error_logged = true;
            log_warning("touch::native", "failed to inject synthetic touch input: {}", GetLastError());
        }
        return false;
    }

    // rewrite only the contact created by this injector into game touch coordinates
    bool transform_touch_input(PTOUCHINPUT point) {
        const auto transform_window = synthetic_touch_identity.get_transform_window();
        if (transform_window == nullptr) {
            return false;
        }

        if (!synthetic_touch_identity.matches(point)) {
            // this contact is not owned by this injector; leave it unchanged
            return false;
        }

        if (synthetic_touch_identity.should_transform_coordinates()) {
            // Windows receives the physical position; the game receives the mapped position
            POINT position { point->x / 100, point->y / 100 };
            if (transform::screen_to_game(transform_window, &position)) {
                point->x = position.x * 100;
                point->y = position.y * 100;
            }
        }

        if (point->dwFlags & TOUCHEVENTF_UP) {
            synthetic_touch_identity.reset(transform_window);
        }

        return true;
    }

    bool contact_is_active() {
        return contact_state.is_active();
    }

    bool contact_is_owned_by(ContactOwner owner, HWND window) {
        return contact_state.is_active() &&
            contact_state.owner == owner &&
            contact_state.input_window == window;
    }

    bool begin_contact(
            ContactOwner owner,
            HWND window,
            POINT position,
            bool transform_returned_coordinates) {
        contact_state.owner = owner;
        contact_state.input_window = window;
        contact_state.position = position;
        contact_state.timer_id = 0;
        synthetic_touch_identity.begin(position, window, transform_returned_coordinates);

        if (inject_touch_frame(position, CONTACT_DOWN_FLAGS)) {
            return true;
        }

        contact_state = {};
        synthetic_touch_identity.reset(window);
        return false;
    }

    bool update_contact(ContactOwner owner, HWND window, POINT position) {
        if (!contact_is_owned_by(owner, window) ||
            !inject_touch_frame(position, CONTACT_UPDATE_FLAGS)) {
            return false;
        }

        contact_state.position = position;
        return true;
    }

    // contact state belongs to the injection window thread; this helper must run there
    static void refresh_contact_lifetime_on_window_thread(HWND window) {
        if (!contact_is_owned_by(contact_state.owner, window)) {
            return;
        }

        inject_touch_frame(contact_state.position, CONTACT_UPDATE_FLAGS, true);
    }

    // PAN calls this from ac_io_update to keep a stationary contact alive. Contact state is
    // owned by the touch window thread, so same-thread calls refresh immediately. Calls from
    // another thread post one coalesced private message for the window thread to handle.
    void refresh_contact_lifetime() {
        const auto window = injection_window.load(std::memory_order_acquire);
        if (window == nullptr) {
            return;
        }

        // preserve synchronous game-loop timing when it already runs on the window thread
        const auto window_thread = GetWindowThreadProcessId(window, nullptr);
        if (window_thread == GetCurrentThreadId()) {
            refresh_contact_lifetime_on_window_thread(window);
            return;
        }

        // otherwise marshal one outstanding refresh instead of sharing contact state across threads
        if (contact_refresh_message == 0 ||
            contact_refresh_pending.exchange(true, std::memory_order_acq_rel)) {
            return;
        }
        if (!PostMessageW(window, contact_refresh_message, 0, 0)) {
            contact_refresh_pending.store(false, std::memory_order_release);
        }
    }

    void set_contact_timer(
            ContactOwner owner, HWND window, UINT_PTR timer_id) {
        if (contact_is_owned_by(owner, window)) {
            contact_state.timer_id = timer_id;
        }
    }

    // end whichever producer currently owns the single synthetic contact
    bool release_active_contact() {
        if (!contact_state.is_active()) {
            return true;
        }

        const auto input_window = contact_state.input_window;
        if (input_window != nullptr && contact_state.timer_id != 0) {
            KillTimer(input_window, contact_state.timer_id);
        }

        const auto result = inject_touch_frame(contact_state.position, POINTER_FLAG_UP);
        contact_state = {};

        // release mouse capture if this contact owns it
        if (input_window != nullptr && GetCapture() == input_window) {
            ReleaseCapture();
        }
        return result;
    }

    // translate primary mouse messages on a touch window into one touch contact
    static LRESULT CALLBACK touch_window_subclass_proc(
            HWND window, UINT message, WPARAM w_param, LPARAM l_param,
            UINT_PTR subclass_id, DWORD_PTR) {

        // consume marshalled lifetime refreshes on the contact-owning window thread
        if (contact_refresh_message != 0 && message == contact_refresh_message) {
            contact_refresh_pending.store(false, std::memory_order_release);
            refresh_contact_lifetime_on_window_thread(window);
            return 0;
        }

        // IIDX handles touch on its main window even when input belongs to the subscreen
        // forward the touch message there
        if (message == WM_TOUCH &&
            transform::is_tdj_dedicated_subscreen(window)) {
            const auto delivery_window =
                touch_delivery_window.load(std::memory_order_acquire);
            if (delivery_window != nullptr) {
                return SendMessageW(delivery_window, message, w_param, l_param);
            }
        }

        if (handle_synthetic_message(window, message, w_param, l_param) ||
            handle_mouse_message(window, message, w_param)) {
            return 0;
        }

        if (message == WM_NCDESTROY) {
            if (contact_state.input_window == window) {
                release_active_contact();
            }
            HWND expected_window = window;
            injection_window.compare_exchange_strong(
                expected_window, nullptr, std::memory_order_acq_rel);
            contact_refresh_pending.store(false, std::memory_order_release);
            touch_delivery_window.store(nullptr, std::memory_order_release);
            RemoveWindowSubclass(window, touch_window_subclass_proc, subclass_id);
        }

        return DefSubclassProc(window, message, w_param, l_param);
    }

    // attach mouse injection without replacing the window's existing procedure
    static void attach_window_impl(HWND window, bool register_touch) {
        if (!initialize_touch_injection()) {
            if (register_touch) {
                log_warning(
                    "touch::native",
                    "touch injection unavailable; touch window was not registered");
            }
            return;
        }

        // if requested, register for touch messages (for cases where the game didn't call it)
        if (register_touch &&
            (RegisterTouchWindow_orig == nullptr ||
             !RegisterTouchWindow_orig(window, TWF_WANTPALM))) {
            log_warning(
                "touch::native", "failed to register mouse touch window: {}", GetLastError());
            return;
        }

        // set window subclass to intercept messages
        if (!SetWindowSubclass(
                window,
                touch_window_subclass_proc,
                reinterpret_cast<UINT_PTR>(&window_subclass_token),
                0)) {
            log_warning(
                "touch::native",
                "failed to attach mouse touch injection to window: {}",
                GetLastError());
            return;
        }

        // publish the UI-thread target for synthetic touch requests
        if (!GRAPHICS_IIDX_WSUB || window == TDJ_SUBSCREEN_WINDOW) {
            injection_window.store(window, std::memory_order_release);
        }
        log_misc(
            "touch::native", "mouse touch injection attached to window {}", fmt::ptr(window));
    }

    HWND get_injection_window() {
        return injection_window.load(std::memory_order_acquire);
    }

    void attach_window(HWND window) {
        // attach window, but don't register for Windows touch messages
        attach_window_impl(window, false);
    }

    void register_and_attach_window(HWND window) {
        // attach window and register for Windows touch messages
        attach_window_impl(window, true);
    }

    // preserve native registration and attach mouse injection to the touch window
    static BOOL WINAPI RegisterTouchWindowHook(HWND window, ULONG flags) {

        // for TDJ in windowed subscreen mode, the main game window is
        // target for delivering touch messages, not the subscreen
        if (GRAPHICS_IIDX_WSUB && window != TDJ_SUBSCREEN_WINDOW) {
            touch_delivery_window.store(window, std::memory_order_release);
            return TRUE;
        }

        // call original
        const auto result = RegisterTouchWindow_orig(window, flags);

        if (result) {
            // attach but don't register for touch messages
            // (we're already in the middle of RegisterTouchWindow as a result
            // of the game calling it)
            attach_window(window);
        }

        return result;
    }

    static void clear_touch_injection_functions() {
        InitializeTouchInjection_ptr = nullptr;
        InjectTouchInput_ptr = nullptr;
    }

    // load and initialize Windows 8 touch injection without a static API dependency
    bool initialize_touch_injection() {
        std::call_once(initialization_once, [] {
            initialize_synthetic_touch();
            contact_refresh_message =
                RegisterWindowMessageW(L"spice2x.native_touch.refresh_contact");
            if (contact_refresh_message == 0) {
                log_warning(
                    "touch::native", "failed to register contact refresh message: {}", GetLastError());
            }

            // load all APIs from user32 without adding static imports
            //
            // note that these are expected to be present in Windows 8 and above;
            // however, on WINE, touch implementation remains in Windows 7 era
            // and therefore the hooks below will fail, hence the fallback to
            // legacy wintouchemu code
            const auto user32 = libutils::load_library("user32.dll");
            InitializeTouchInjection_ptr = libutils::try_proc<decltype(InitializeTouchInjection_ptr)>(
                user32, "InitializeTouchInjection");
            if (InitializeTouchInjection_ptr == nullptr) {
                log_warning(
                    "touch::native", "InitializeTouchInjection unavailable; mouse touch injection disabled");
                return;
            }

            InjectTouchInput_ptr = libutils::try_proc<decltype(InjectTouchInput_ptr)>(
                user32, "InjectTouchInput");
            if (InjectTouchInput_ptr == nullptr) {
                clear_touch_injection_functions();
                log_warning(
                    "touch::native", "InjectTouchInput unavailable; mouse touch injection disabled");
                return;
            }

            // reserve one synthetic contact and disable Windows' visual touch feedback
            if (!InitializeTouchInjection_ptr(1, TOUCH_FEEDBACK_NONE)) {
                log_warning(
                    "touch::native", "failed to initialize mouse touch injection: {}", GetLastError());
                clear_touch_injection_functions();
                return;
            }

            log_misc("touch::native", "mouse touch injection initialized");
        });
        return InitializeTouchInjection_ptr != nullptr && InjectTouchInput_ptr != nullptr;
    }

    // install injection support for touch windows registered by the game module
    bool hook_available(HMODULE module) {
        if (detour::iat_find("RegisterTouchWindow", module) == nullptr) {
            log_warning("touch::native", "RegisterTouchWindow unavailable");
            return false;
        }
        return true;
    }

    bool hook(HMODULE module) {
        if (!initialize_touch_injection()) {
            return false;
        }

        RegisterTouchWindow_orig = detour::iat_try(
            "RegisterTouchWindow", RegisterTouchWindowHook, module);
        if (RegisterTouchWindow_orig == nullptr) {
            log_warning("touch::native", "failed to hook RegisterTouchWindow");
            return false;
        }

        log_misc("touch::native", "RegisterTouchWindow hooked");
        return true;
    }
}
