/**
 * mp-shim.ts — RageMP compatibility layer for FiveM (client-side).
 *
 * Exposes a global `mp` object matching the RageMP client API so that ported
 * client feature files compile and run with minimal changes.
 *
 * Key differences from the server shim:
 *  - No player pool (there's only the local player)
 *  - Native calls go through FiveM's global native functions
 *  - mp.game.* wraps native namespaces
 *  - Browsers are replaced by FiveM NUI
 *  - mp.events.addProc is handled by src/client/core/rpc/index.ts
 */

import { rpc } from './rpc/index';

// ---------------------------------------------------------------------------
// Well-known RageMP client events → FiveM equivalents
// ---------------------------------------------------------------------------
const RAGEMP_TO_FIVEM: Record<string, string> = {
    render:                'FRAME_STARTED',   // PARTIAL — see note below
    entityStreamIn:        'entityCreating',  // PARTIAL
    entityStreamOut:       'entityRemoved',   // PARTIAL
    playerEnterVehicle:    'enteredVehicle',  // via baseevents
    playerExitVehicle:     'leftVehicle',     // via baseevents
    playerSpawn:           'playerSpawned',
};

// FiveM doesn't have a 'render' equivalent exactly — FRAME_STARTED doesn't
// exist either. For per-tick logic we use a persistent thread below.
const tickHandlers: Array<() => void> = [];

setTick(() => {
    for (const handler of tickHandlers) {
        try { handler(); } catch (e) { console.error('[mp-shim] tick handler error:', e); }
    }
});

// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------
const events = {
    // Register a client-side event handler.
    // - 'render' → runs every tick
    // - Net events sent from server → AddEventHandler + RegisterNetEvent
    // - Game events (playerEnterVehicle etc.) → mapped to FiveM baseevents
    add(eventOrMap: string | Record<string, Function>, handler?: Function): void {
        if (typeof eventOrMap === 'object') {
            for (const [name, fn] of Object.entries(eventOrMap)) {
                events.add(name, fn);
            }
            return;
        }

        const name = eventOrMap as string;
        const fn = handler!;

        if (name === 'render') {
            tickHandlers.push(fn as () => void);
            return;
        }

        if (name in RAGEMP_TO_FIVEM) {
            AddEventHandler(RAGEMP_TO_FIVEM[name], fn);
            return;
        }

        // Assume it's a server→client net event
        RegisterNetEvent(name);
        AddEventHandler(name, fn);
    },

    // RageMP client proc — handled by custom RPC module
    addProc(name: string, handler: (...args: any[]) => any): void {
        rpc.registerClientProc(name, handler);
    },

    // Client → Server event (replaces mp.events.callRemote)
    callRemote(eventName: string, ...args: any[]): void {
        TriggerServerEvent(eventName, ...args);
    },

    // Local client event emit
    call(eventName: string, ...args: any[]): void {
        TriggerEvent(eventName, ...args);
    },
};

// ---------------------------------------------------------------------------
// Local player
// ---------------------------------------------------------------------------
const localPlayer = {
    get handle(): number { return PlayerPedId(); },

    // Component variations (clothing)
    setComponentVariation(componentId: number, drawableId: number, textureId: number, paletteId = 0): void {
        SetPedComponentVariation(PlayerPedId(), componentId, drawableId, textureId, paletteId);
    },
    getComponentVariation(componentId: number): [number, number, number] {
        return [
            GetPedDrawableVariation(PlayerPedId(), componentId),
            GetPedTextureVariation(PlayerPedId(), componentId),
            GetPedPaletteVariation(PlayerPedId(), componentId),
        ];
    },
    setPropIndex(propId: number, drawableId: number, textureId: number, attach: boolean): void {
        SetPedPropIndex(PlayerPedId(), propId, drawableId, textureId, attach);
    },
    clearProp(propId: number): void {
        ClearPedProp(PlayerPedId(), propId);
    },

    // Head blend / facial features (character creator)
    setHeadBlendData(
        shapeFirst: number, shapeSecond: number, shapeThird: number,
        skinFirst: number, skinSecond: number, skinThird: number,
        shapeMix: number, skinMix: number, thirdMix: number, isParent: boolean,
    ): void {
        SetPedHeadBlendData(PlayerPedId(), shapeFirst, shapeSecond, shapeThird, skinFirst, skinSecond, skinThird, shapeMix, skinMix, thirdMix, isParent);
    },
    setHeadOverlay(overlayId: number, index: number, opacity: number, firstColor?: number, secondColor?: number): void {
        SetPedHeadOverlay(PlayerPedId(), overlayId, index, opacity);
        if (firstColor !== undefined) SetPedHeadOverlayColor(PlayerPedId(), overlayId, 1, firstColor, secondColor ?? 0);
    },
    setFaceFeature(featureId: number, scale: number): void {
        SetPedFaceFeature(PlayerPedId(), featureId, scale);
    },
    setHairColor(colorId: number, highlightColorId: number): void {
        SetPedHairColor(PlayerPedId(), colorId, highlightColorId);
    },
    setEyeColor(index: number): void {
        SetPedEyeColor(PlayerPedId(), index);
    },

    // Config flags
    setConfigFlag(flag: number, value: boolean): void {
        SetPedConfigFlag(PlayerPedId(), flag, value);
    },

    // Position / movement
    get position(): { x: number; y: number; z: number } {
        const [x, y, z] = GetEntityCoords(PlayerPedId(), true);
        return { x, y, z };
    },

    isInVehicle(): boolean {
        return IsPedInAnyVehicle(PlayerPedId(), false);
    },

    getCurrentVehicle(): number | null {
        const v = GetVehiclePedIsIn(PlayerPedId(), false);
        return v !== 0 ? v : null;
    },

    // STUB — animation playback proxied to game.invoke or native wrappers
    playAnimation(dict: string, name: string, blendInSpeed: number, blendOutSpeed: number, duration: number, flag: number, startOffset: number): void {
        TaskPlayAnim(PlayerPedId(), dict, name, blendInSpeed, blendOutSpeed, duration, flag, startOffset, false, false, false);
    },
    stopAnimation(): void {
        ClearPedTasks(PlayerPedId());
    },
};

// ---------------------------------------------------------------------------
// mp.game — wraps GTA native namespaces
// ---------------------------------------------------------------------------
const game = {
    audio: {
        setAudioFlag(flag: string, value: boolean): void {
            SetAudioFlag(flag, value);
        },
        playSound(soundId: number, soundName: string, soundSetName: string, isPedPersonalSound: boolean, p4: number, p5: boolean): void {
            PlaySound(soundId, soundName, soundSetName, isPedPersonalSound, p4, p5);
        },
        playSoundFrontend(soundId: number, audioName: string, audioRef: string, p3: boolean): void {
            PlaySoundFrontend(soundId, audioName, audioRef, p3);
        },
    },

    vehicle: {
        set defaultEngineBehaviour(value: boolean) {
            // SetVehicleDefaultEngineBehaviour doesn't exist; handled via config flags
        },
        setResetUnoccupiedSteerAngle(value: boolean): void {
            // STUB — no direct FiveM equivalent
        },
    },

    // Catch-all for raw native hash invocations (mp.game.invoke)
    invoke(hash: string, ...args: any[]): any {
        return (Citizen as any).invokeNative(hash, ...args);
    },
};

// ---------------------------------------------------------------------------
// mp.gui — chat and cursor
// ---------------------------------------------------------------------------
const gui = {
    chat: {
        activate(value: boolean): void {
            // FiveM chat activation is handled client-side via NUI events
            TriggerEvent('chat:visible', value);
        },
        show(value: boolean): void {
            TriggerEvent('chat:visible', value);
        },
        push(message: string): void {
            TriggerEvent('chat:addMessage', { args: [message] });
        },
    },
    cursor: {
        show(value: boolean, doLockCursor: boolean): void {
            SetCursorLocation(0.5, 0.5);
            // Full NUI cursor management handled per-feature
        },
    },
};

// ---------------------------------------------------------------------------
// mp.browsers — replaced by FiveM NUI
// ---------------------------------------------------------------------------
const browsers = {
    /**
     * FiveM NUI replaces RageMP CEF browsers.
     * The UI page is declared in fxmanifest.lua; only one exists at a time.
     * Returns a stub object whose .execute() sends NUI messages.
     */
    new(_url: string): { execute: (js: string) => void; active: boolean } {
        return {
            active: true,
            execute(js: string) {
                // Evaluate JS inside the NUI frame — use sparingly
                SendNUIMessage({ type: '__eval__', code: js });
            },
        };
    },
};

// ---------------------------------------------------------------------------
// mp.nui — FiveM-native NUI helpers (no RageMP equivalent; new API)
// ---------------------------------------------------------------------------
export const nui = {
    send(type: string, data?: any): void {
        SendNUIMessage({ type, ...(data ?? {}) });
    },
    focus(hasFocus: boolean, hasCursor: boolean): void {
        SetNuiFocus(hasFocus, hasCursor);
    },
    onCallback(name: string, handler: (data: any, cb: (result: any) => void) => void): void {
        RegisterNuiCallback(name, handler);
    },
};

// ---------------------------------------------------------------------------
// Assemble global mp object
// ---------------------------------------------------------------------------
(global as any).mp = {
    events,
    players: {
        local: localPlayer,
    },
    game,
    gui,
    browsers,
    nui,
};
