#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include "FindMyMouse.h"
#include "WinHookEventIDs.h"
#include <thread>
#include <common/utils/logger_helper.h>
#include <common/utils/color.h>
#include <common/utils/string_utils.h>
#include <common/utils/EventWaiter.h>
#include <common/interop/shared_constants.h>

namespace
{
    const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
    const wchar_t JSON_KEY_VALUE[] = L"value";
    const wchar_t JSON_KEY_ACTIVATION_METHOD[] = L"activation_method";
    const wchar_t JSON_KEY_INCLUDE_WIN_KEY[] = L"include_win_key";
    const wchar_t JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE[] = L"do_not_activate_on_game_mode";
    const wchar_t JSON_KEY_BACKGROUND_COLOR[] = L"background_color";
    const wchar_t JSON_KEY_SPOTLIGHT_COLOR[] = L"spotlight_color";
    const wchar_t JSON_KEY_OVERLAY_OPACITY[] = L"overlay_opacity"; // legacy only (migrated into color alpha)
    const wchar_t JSON_KEY_SPOTLIGHT_RADIUS[] = L"spotlight_radius";
    const wchar_t JSON_KEY_ANIMATION_DURATION_MS[] = L"animation_duration_ms";
    const wchar_t JSON_KEY_SPOTLIGHT_INITIAL_ZOOM[] = L"spotlight_initial_zoom";
    const wchar_t JSON_KEY_EXCLUDED_APPS[] = L"excluded_apps";
    const wchar_t JSON_KEY_SHAKING_MINIMUM_DISTANCE[] = L"shaking_minimum_distance";
    const wchar_t JSON_KEY_SHAKING_INTERVAL_MS[] = L"shaking_interval_ms";
    const wchar_t JSON_KEY_SHAKING_FACTOR[] = L"shaking_factor";
    const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
}

extern "C" IMAGE_DOS_HEADER __ImageBase;

HMODULE m_hModule;

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    m_hModule = hModule;
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Trace::RegisterProvider();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        Trace::UnregisterProvider();
        break;
    }
    return TRUE;
}

// The PowerToy name that will be shown in the settings.
const static wchar_t* MODULE_NAME = L"FindMyMouse";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"Focus the mouse pointer";

// Implement the PowerToy Module Interface and all the required methods.
class FindMyMouse : public PowertoyModuleIface
{
private:
    // The PowerToy state.
    bool m_enabled = false;

    // Hotkey to invoke the module
    HotkeyEx m_hotkey;

    // Find My Mouse specific settings
    FindMyMouseSettings m_findMyMouseSettings;

    // Event-driven trigger support
    EventWaiter m_triggerEventWaiter;

    // Load initial settings from the persisted values.
    void init_settings();

    // Helper function to extract the settings
    void parse_settings(PowerToysSettings::PowerToyValues& settings);

public:
    // Constructor
    FindMyMouse()
    {
        LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::findMyMouseLoggerName);
        init_settings();
    };

    // Destroy the powertoy and free memory
    virtual void destroy() override
    {
        // Ensure threads/handles are cleaned up before destruction
        disable();
        delete this;
    }

    // Return the localized display name of the powertoy
    virtual const wchar_t* get_name() override
    {
        return MODULE_NAME;
    }

    // Return the non localized key of the powertoy, this will be cached by the runner
    virtual const wchar_t* get_key() override
    {
        return MODULE_NAME;
    }

    // Return the configured status for the gpo policy for the module
    virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
    {
        return powertoys_gpo::getConfiguredFindMyMouseEnabledValue();
    }

    // Return JSON with the configuration options.
    virtual bool get_config(wchar_t* buffer, int* buffer_size) override
    {
        HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);

        // Create a Settings object.
        PowerToysSettings::Settings settings(hinstance, get_name());
        settings.set_description(MODULE_DESC);

        return settings.serialize_to_buffer(buffer, buffer_size);
    }

    // Signal from the Settings editor to call a custom action.
    // This can be used to spawn more complex editors.
    virtual void call_custom_action(const wchar_t* action) override
    {
    }

    // Called by the runner to pass the updated settings values as a serialized JSON.
    virtual void set_config(const wchar_t* config) override
    {
        try
        {
            // Parse the input JSON string.
            PowerToysSettings::PowerToyValues values =
                PowerToysSettings::PowerToyValues::from_json_string(config, get_key());

            parse_settings(values);

            FindMyMouseApplySettings(m_findMyMouseSettings);
        }
        catch (std::exception&)
        {
            // Improper JSON.
        }
    }

    // Enable the powertoy
    virtual void enable()
    {
        m_enabled = true;
        Trace::EnableFindMyMouse(true);
        std::thread([=]() { FindMyMouseMain(m_hModule, m_findMyMouseSettings); }).detach();

        // Start listening for external trigger event so we can invoke the same logic as the hotkey.
        m_triggerEventWaiter.start(CommonSharedConstants::FIND_MY_MOUSE_TRIGGER_EVENT, [this](DWORD) {
            OnHotkeyEx();
        });
    }

    // Disable the powertoy
    virtual void disable()
    {
        m_enabled = false;
        Trace::EnableFindMyMouse(false);
        FindMyMouseDisable();

        m_triggerEventWaiter.stop();
    }

    // Returns if the powertoys is enabled
    virtual bool is_enabled() override
    {
        return m_enabled;
    }

    virtual std::optional<HotkeyEx> GetHotkeyEx() override
    {
        Logger::trace("GetHotkeyEx()");
        if (m_findMyMouseSettings.activationMethod == FindMyMouseActivationMethod::Shortcut)
        {
            return m_hotkey;
        }

        return std::nullopt;
    }

    virtual void OnHotkeyEx() override
    {
        Logger::trace("OnHotkeyEx()");
        HWND hwnd = GetSonarHwnd();
        if (hwnd != nullptr)
        {
            PostMessageW(hwnd, WM_PRIV_SHORTCUT, NULL, NULL);
        }
    }
};

// Load the settings file.
void FindMyMouse::init_settings()
{
    try
    {
        // Load and parse the settings file for this PowerToy.
        PowerToysSettings::PowerToyValues settings =
            PowerToysSettings::PowerToyValues::load_from_settings_file(FindMyMouse::get_key());
        parse_settings(settings);
    }
    catch (std::exception&)
    {
        // Error while loading from the settings file. Let default values stay as they are.
    }
}

inline static uint8_t LegacyOpacityToAlpha(int overlayOpacityPercent)
{
    if (overlayOpacityPercent < 0)
    {
        return 255; // fallback: fully opaque
    }

    if (overlayOpacityPercent > 100)
    {
        overlayOpacityPercent = 100;
    }

    // Round to nearest integer (0–255)
    return static_cast<uint8_t>((overlayOpacityPercent * 255 + 50) / 100);
}

void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
{
    auto settingsObject = settings.get_raw_json();
    FindMyMouseSettings findMyMouseSettings;

    if (!settingsObject.GetView().Size())
    {
        Logger::info("Find My Mouse settings are empty");
        m_findMyMouseSettings = findMyMouseSettings;
        return;
    }

    // Early exit if no properties object exists
    if (!settingsObject.HasKey(JSON_KEY_PROPERTIES))
    {
        Logger::info("Find My Mouse settings have no properties");
        m_findMyMouseSettings = findMyMouseSettings;
        return;
    }

    auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);

    // Parse Activation Method
    if (properties.HasKey(JSON_KEY_ACTIVATION_METHOD))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_METHOD);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value < static_cast<int>(FindMyMouseActivationMethod::EnumElements) && value >= 0)
            {
                std::wstring version = (std::wstring)settingsObject.GetNamedString(L"version");
                if (version == L"1.0" && value == 1)
                {
                    findMyMouseSettings.activationMethod = FindMyMouseActivationMethod::ShakeMouse;
                }
                else
                {
                    findMyMouseSettings.activationMethod = static_cast<FindMyMouseActivationMethod>(value);
                }
            }
            else
            {
                throw std::runtime_error("Invalid Activation Method value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
        }
    }

    // Parse Include Win Key
    if (properties.HasKey(JSON_KEY_INCLUDE_WIN_KEY))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
            findMyMouseSettings.includeWinKey = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
        }
        catch (...)
        {
            Logger::warn("Failed to get 'include windows key with ctrl' setting");
        }
    }

    // Parse Do Not Activate On Game Mode
    if (properties.HasKey(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
            findMyMouseSettings.doNotActivateOnGameMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
        }
        catch (...)
        {
            Logger::warn("Failed to get 'do not activate on game mode' setting");
        }
    }

    // Colors + legacy overlay opacity migration
    // Desired behavior:
    //  - Old schema: colors stored as RGB (no alpha) + separate overlay_opacity (0-100). We should migrate by applying that opacity as alpha.
    //  - New schema: colors stored as ARGB (alpha embedded). Ignore overlay_opacity even if still present.
    int legacyOverlayOpacity = -1;
    bool backgroundColorHadExplicitAlpha = false;
    bool spotlightColorHadExplicitAlpha = false;

    // Parse Legacy Overlay Opacity (may not exist in newer settings)
    if (properties.HasKey(JSON_KEY_OVERLAY_OPACITY))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_OVERLAY_OPACITY);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0 && value <= 100)
            {
                legacyOverlayOpacity = value;
            }
        }
        catch (...)
        {
            // overlay_opacity may have invalid data
        }
    }

    // Parse Background Color
    if (properties.HasKey(JSON_KEY_BACKGROUND_COLOR))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_BACKGROUND_COLOR);
            auto backgroundColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
            uint8_t a = 255, r, g, b;
            bool parsed = false;
            if (checkValidARGB(backgroundColorStr, &a, &r, &g, &b))
            {
                parsed = true;
                backgroundColorHadExplicitAlpha = true; // New schema with alpha present
            }
            else if (checkValidRGB(backgroundColorStr, &r, &g, &b))
            {
                a = LegacyOpacityToAlpha(legacyOverlayOpacity);
                parsed = true; // Old schema (no alpha component)
            }
            if (parsed)
            {
                findMyMouseSettings.backgroundColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
            }
            else
            {
                Logger::error("Background color value is invalid. Will use default");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize background color from settings. Will use default value");
        }
    }

    // Parse Spotlight Color
    if (properties.HasKey(JSON_KEY_SPOTLIGHT_COLOR))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_COLOR);
            auto spotlightColorStr = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
            uint8_t a = 255, r, g, b;
            bool parsed = false;
            if (checkValidARGB(spotlightColorStr, &a, &r, &g, &b))
            {
                parsed = true;
                spotlightColorHadExplicitAlpha = true;
            }
            else if (checkValidRGB(spotlightColorStr, &r, &g, &b))
            {
                a = LegacyOpacityToAlpha(legacyOverlayOpacity);
                parsed = true;
            }
            if (parsed)
            {
                findMyMouseSettings.spotlightColor = winrt::Windows::UI::ColorHelper::FromArgb(a, r, g, b);
            }
            else
            {
                Logger::error("Spotlight color value is invalid. Will use default");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize spotlight color from settings. Will use default value");
        }
    }

    // Parse Spotlight Radius
    if (properties.HasKey(JSON_KEY_SPOTLIGHT_RADIUS))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_RADIUS);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0)
            {
                findMyMouseSettings.spotlightRadius = value;
            }
            else
            {
                throw std::runtime_error("Invalid Spotlight Radius value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Spotlight Radius from settings. Will use default value");
        }
    }

    // Parse Animation Duration
    if (properties.HasKey(JSON_KEY_ANIMATION_DURATION_MS))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ANIMATION_DURATION_MS);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0)
            {
                findMyMouseSettings.animationDurationMs = value;
            }
            else
            {
                throw std::runtime_error("Invalid Animation Duration value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Animation Duration from settings. Will use default value");
        }
    }

    // Parse Spotlight Initial Zoom
    if (properties.HasKey(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SPOTLIGHT_INITIAL_ZOOM);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0)
            {
                findMyMouseSettings.spotlightInitialZoom = value;
            }
            else
            {
                throw std::runtime_error("Invalid Spotlight Initial Zoom value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Spotlight Initial Zoom from settings. Will use default value");
        }
    }

    // Parse Excluded Apps
    if (properties.HasKey(JSON_KEY_EXCLUDED_APPS))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_EXCLUDED_APPS);
            std::wstring apps = jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE).c_str();
            std::vector<std::wstring> excludedApps;
            auto excludedUppercase = apps;
            CharUpperBuffW(excludedUppercase.data(), static_cast<DWORD>(excludedUppercase.length()));
            std::wstring_view view(excludedUppercase);
            view = left_trim<wchar_t>(trim<wchar_t>(view));

            while (!view.empty())
            {
                auto pos = (std::min)(view.find_first_of(L"\r\n"), view.length());
                excludedApps.emplace_back(view.substr(0, pos));
                view.remove_prefix(pos);
                view = left_trim<wchar_t>(trim<wchar_t>(view));
            }

            findMyMouseSettings.excludedApps = excludedApps;
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Excluded Apps from settings. Will use default value");
        }
    }

    // Parse Shaking Minimum Distance
    if (properties.HasKey(JSON_KEY_SHAKING_MINIMUM_DISTANCE))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_MINIMUM_DISTANCE);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0)
            {
                findMyMouseSettings.shakeMinimumDistance = value;
            }
            else
            {
                throw std::runtime_error("Invalid Shaking Minimum Distance value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Shaking Minimum Distance from settings. Will use default value");
        }
    }

    // Parse Shaking Interval Milliseconds
    if (properties.HasKey(JSON_KEY_SHAKING_INTERVAL_MS))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_INTERVAL_MS);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0)
            {
                findMyMouseSettings.shakeIntervalMs = value;
            }
            else
            {
                throw std::runtime_error("Invalid Shaking Interval Milliseconds value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Shaking Interval Milliseconds from settings. Will use default value");
        }
    }

    // Parse Shaking Factor
    if (properties.HasKey(JSON_KEY_SHAKING_FACTOR))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_SHAKING_FACTOR);
            int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
            if (value >= 0)
            {
                findMyMouseSettings.shakeFactor = value;
            }
            else
            {
                throw std::runtime_error("Invalid Shaking Factor value");
            }
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Shaking Factor from settings. Will use default value");
        }
    }

    // Parse HotKey
    if (properties.HasKey(JSON_KEY_ACTIVATION_SHORTCUT))
    {
        try
        {
            auto jsonPropertiesObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
            auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
            m_hotkey = HotkeyEx();
            if (hotkey.win_pressed())
            {
                m_hotkey.modifiersMask |= MOD_WIN;
            }

            if (hotkey.ctrl_pressed())
            {
                m_hotkey.modifiersMask |= MOD_CONTROL;
            }

            if (hotkey.shift_pressed())
            {
                m_hotkey.modifiersMask |= MOD_SHIFT;
            }

            if (hotkey.alt_pressed())
            {
                m_hotkey.modifiersMask |= MOD_ALT;
            }

            m_hotkey.vkCode = static_cast<WORD>(hotkey.get_code());
        }
        catch (...)
        {
            Logger::warn("Failed to initialize Activation Shortcut from settings. Will use default value");
        }
    }

    if (!m_hotkey.modifiersMask)
    {
        Logger::info("Using default Activation Shortcut");
        m_hotkey.modifiersMask = MOD_SHIFT | MOD_WIN;
        m_hotkey.vkCode = 0x46; // F key
    }

    m_findMyMouseSettings = findMyMouseSettings;
}

extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
    return new FindMyMouse();
}
