// ========================================================================
// VirtualGamepad — WebSocket 客户端 + 手柄输入管理器
// ========================================================================

import * as cc from 'cc';

// ==================== 配置常量 ====================

export const VIRTUAL_GAMEPAD_GAME_ID = "rexueqiangshou";
export const VIRTUAL_GAMEPAD_PLAYER_ID = "player1";
export const VIRTUAL_GAMEPAD_DEVELOPMENT_WS_URL = "ws://10.40.92.42:3000";
export const VIRTUAL_GAMEPAD_PRODUCTION_WS_URL = "wss://hyp.huyingpai.com/huyingpai/lewo/ws";
export const VIRTUAL_GAMEPAD_H5_URL = "https://docp.daoran.tv/vconsole/index.html";
export const VIRTUAL_GAMEPAD_DEVELOPMENT_WXACODE_URL = "https://hyp.huyingpai.com/API_USER_CS/wxacode/unlimited";
export const VIRTUAL_GAMEPAD_PRODUCTION_WXACODE_URL = "https://hyp.huyingpai.com/API_USER/wxacode/unlimited";
export const VIRTUAL_GAMEPAD_WXACODE_PAGE = "pages/gamepad/gamepad";
export const VIRTUAL_GAMEPAD_WXACODE_PROJECT_CODE = "lewo";
export const VIRTUAL_GAMEPAD_MAX_MEMBERS = 2;
export const VIRTUAL_GAMEPAD_STOP_TIMEOUT_MS = 500;

export type VirtualGamepadEnvironment = "development" | "production";
export const VIRTUAL_GAMEPAD_DEFAULT_ENVIRONMENT: VirtualGamepadEnvironment = "production";
export type VirtualGamepadQrMode = "h5" | "json";
export const VIRTUAL_GAMEPAD_QR_MODE: VirtualGamepadQrMode = "h5";

declare const global: any;

function getRuntimeValue(key: string): string {
    let runtimeRoot: any = typeof window != "undefined" ? window : (typeof global != "undefined" ? global : null);
    if (runtimeRoot && runtimeRoot[key]) return String(runtimeRoot[key]);
    try {
        if (cc && cc.sys && cc.sys.localStorage) {
            return cc.sys.localStorage.getItem(key) || "";
        }
    } catch (e) { /* ignore */ }
    return "";
}

function getWsUrl(): string {
    return getRuntimeValue("RXQS_GAMEPAD_WS_URL") ||
        (getEnvironment() == "production" ? VIRTUAL_GAMEPAD_PRODUCTION_WS_URL : VIRTUAL_GAMEPAD_DEVELOPMENT_WS_URL);
}
function getWxacodeUrl(): string {
    return getRuntimeValue("RXQS_GAMEPAD_WXACODE_URL") ||
        (getEnvironment() == "production" ? VIRTUAL_GAMEPAD_PRODUCTION_WXACODE_URL : VIRTUAL_GAMEPAD_DEVELOPMENT_WXACODE_URL);
}
function getEnvironment(): VirtualGamepadEnvironment {
    let env = getRuntimeValue("RXQS_GAMEPAD_ENVIRONMENT");
    if (env == "development" || env == "production") return env;
    return VIRTUAL_GAMEPAD_DEFAULT_ENVIRONMENT;
}
function getH5Url(): string { return getRuntimeValue("RXQS_GAMEPAD_H5_URL") || VIRTUAL_GAMEPAD_H5_URL; }
function getRoomId(): string { return getRuntimeValue("RXQS_GAMEPAD_ROOM_ID"); }
function getToken(): string { return getRuntimeValue("RXQS_GAMEPAD_TOKEN"); }
function getQrMode(): VirtualGamepadQrMode {
    let mode = getRuntimeValue("RXQS_GAMEPAD_QR_MODE");
    return mode == "json" ? "json" : VIRTUAL_GAMEPAD_QR_MODE;
}

// ==================== Client ====================

export type VirtualGamepadConnectionState = "idle" | "connecting" | "open" | "joined" | "closed" | "error";

interface ClientOptions {
    wsUrl: string;
    roomId: string;
    deviceId: string;
    game: string;
    userId: string;
    userType: "xiaomi" | "guest";
    guestId?: string;
    token?: string;
    maxMembers: number;
}

class Client {
    private socket: WebSocket | null = null;
    private options: ClientOptions | null = null;
    private manualClose: boolean = false;
    private reconnectCount: number = 0;
    private heartbeatTimer: any = -1;
    private reconnectTimer: any = -1;
    private readonly maxReconnectCount: number = 3;
    private readonly heartbeatMs: number = 10000;

    public onStateChange: ((state: VirtualGamepadConnectionState, reason?: string) => void) | null = null;
    public onMessage: ((message: any) => void) | null = null;

    public connect(options: ClientOptions) {
        this.options = options;
        this.manualClose = false;
        this.clearReconnect();
        this.openSocket();
    }

    public close() {
        this.manualClose = true;
        this.clearHeartbeat();
        this.clearReconnect();
        if (this.socket) {
            this.socket.onopen = null;
            this.socket.onmessage = null;
            this.socket.onclose = null;
            this.socket.onerror = null;
            try { this.socket.close(); } catch (e) { /* ignore */ }
        }
        this.socket = null;
        this.emitState("closed");
    }

    public send(message: any): boolean {
        if (!this.socket || this.socket.readyState != WebSocket.OPEN) return false;
        try { this.socket.send(JSON.stringify(message)); return true; } catch (e) { return false; }
    }

    private openSocket() {
        if (!this.options || !this.options.wsUrl) return;
        this.clearHeartbeat();
        this.emitState("connecting");
        let opts = this.options;
        let query: Record<string, string> = {
            roomId: opts.roomId, deviceId: opts.deviceId, role: "game",
            userId: opts.userId, userType: opts.userType, maxMembers: String(opts.maxMembers)
        };
        if (opts.guestId) query.guestId = opts.guestId;
        if (opts.token) query.token = opts.token;
        let url = this.appendParams(opts.wsUrl, query);
        try {
            this.socket = new WebSocket(url);
            this.socket.onopen = this.handleOpen.bind(this);
            this.socket.onmessage = this.handleMessage.bind(this);
            this.socket.onclose = this.handleClose.bind(this);
            this.socket.onerror = this.handleError.bind(this);
        } catch (e) {
            this.handleError(e);
        }
    }

    private handleOpen() {
        if (!this.options) return;
        this.reconnectCount = 0;
        this.emitState("open");
        let msg: any = {
            type: "join", roomId: this.options.roomId, role: "game",
            deviceId: this.options.deviceId, userId: this.options.userId,
            userType: this.options.userType, maxMembers: this.options.maxMembers, game: this.options.game
        };
        if (this.options.guestId) msg.guestId = this.options.guestId;
        if (this.options.token) msg.token = this.options.token;
        console.log("[VirtualGamepad][WSS][Send] join", { type: msg.type, roomId: msg.roomId, role: msg.role, deviceId: msg.deviceId, userId: msg.userId, userType: msg.userType, guestId: msg.guestId || "", maxMembers: msg.maxMembers, game: msg.game, hasToken: !!msg.token });
        this.send(msg);
        this.startHeartbeat();
    }

    private handleMessage(event: MessageEvent) {
        let data = event && event.data;
        let message: any = null;
        try { message = typeof data == "string" ? JSON.parse(data) : data; } catch (e) { return; }
        if (!message) return;
        if (message.type != "pong") console.log("[VirtualGamepad][WSS][Receive]", message);
        if (message.type == "joined" || message.type == "welcome") this.emitState("joined");
        if (this.onMessage) this.onMessage(message);
    }

    private handleClose(event?: any) {
        this.clearHeartbeat();
        this.socket = null;
        this.emitState("closed", event && event.reason);
        if (!this.manualClose) this.scheduleReconnect();
    }

    private handleError(event?: any) {
        this.emitState("error", event && event.message);
        if (!this.manualClose && (!this.socket || this.socket.readyState == WebSocket.CLOSED)) this.scheduleReconnect();
    }

    private startHeartbeat() {
        this.clearHeartbeat();
        this.heartbeatTimer = setInterval(() => {
            if (!this.options) return;
            this.send({ type: "ping", roomId: this.options.roomId, deviceId: this.options.deviceId, role: "game", timestamp: Date.now(), ts: Date.now() });
        }, this.heartbeatMs);
    }

    private scheduleReconnect() {
        if (this.reconnectCount >= this.maxReconnectCount) return;
        this.clearReconnect();
        this.reconnectCount++;
        this.reconnectTimer = setTimeout(() => this.openSocket(), 1200 * this.reconnectCount);
    }

    private appendParams(url: string, params: Record<string, string>): string {
        let q = Object.keys(params).map(k => encodeURIComponent(k) + "=" + encodeURIComponent(params[k] || "")).join("&");
        return url + (url.indexOf("?") >= 0 ? "&" : "?") + q;
    }

    private emitState(state: VirtualGamepadConnectionState, reason?: string) {
        if (this.onStateChange) this.onStateChange(state, reason);
    }

    private clearHeartbeat() {
        if (this.heartbeatTimer != -1) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = -1; }
    }
    private clearReconnect() {
        if (this.reconnectTimer != -1) { clearTimeout(this.reconnectTimer); this.reconnectTimer = -1; }
    }
}

// ==================== Manager ====================

export type VirtualGamepadGameState = "ready" | "playing" | "paused" | "ended";
export type VirtualGamepadUserType = "xiaomi" | "guest";
export type VirtualGamepadSunCodeState = "idle" | "loading" | "ready" | "error";

export interface VirtualGamepadIdentity {
    userId: string;
    userType: VirtualGamepadUserType;
    guestId?: string;
}

export interface VirtualGamepadMoveInput {
    x: number;
    y: number;
    seq?: number;
    ts?: number;
    raw?: any;
    holdMode?: boolean;
    stopTimeoutMs?: number;
    side?: string;
    direction?: string;
    action?: string;
}

export type VirtualGamepadControlType = "stick" | "button" | "shoulder" | "trigger" | "back";

export interface VirtualGamepadControlInput {
    type: VirtualGamepadControlType;
    action: string;
    pressed: boolean;
    side?: string;
    key?: string;
    direction?: string;
    x?: number;
    y?: number;
    playerId?: string;
    seq?: number;
    ts?: number;
    raw?: any;
}

export interface VirtualGamepadStatus {
    connectionState: VirtualGamepadConnectionState;
    controllerConnected: boolean;
    roomId: string;
    deviceId: string;
    userId: string;
    userType: VirtualGamepadUserType;
    identityText: string;
    hasToken: boolean;
    maxMembers: number;
    sunCodeState: VirtualGamepadSunCodeState;
    sunCodeDataUrl: string;
    sunCodeError?: string;
    h5Url: string;
    qrPayload: string;
    statusText: string;
    lastError?: string;
}

type MoveListener = (input: VirtualGamepadMoveInput) => void;
type StatusListener = (status: VirtualGamepadStatus) => void;
type ControlListener = (input: VirtualGamepadControlInput) => void;

interface ListenerMap {
    move: MoveListener;
    status: StatusListener;
    button: ControlListener;
    shoulder: ControlListener;
    trigger: ControlListener;
    back: ControlListener;
}

export default class VirtualGamepadManager {
    private static _instance: VirtualGamepadManager | null = null;

    public static getInstance(): VirtualGamepadManager {
        if (!VirtualGamepadManager._instance) VirtualGamepadManager._instance = new VirtualGamepadManager();
        return VirtualGamepadManager._instance;
    }

    private client: Client | null = null;
    private roomId: string = "";
    private deviceId: string = "";
    private identity: VirtualGamepadIdentity = { userId: "", userType: "guest" };
    private started: boolean = false;
    private connectionState: VirtualGamepadConnectionState = "idle";
    private controllerConnected: boolean = false;
    private lastError: string = "";
    private lastInputSeq: number = -1;
    private gameState: VirtualGamepadGameState = "ready";
    private sunCodeState: VirtualGamepadSunCodeState = "idle";
    private sunCodeDataUrl: string = "";
    private sunCodeError: string = "";
    private sunCodeRequestSerial: number = 0;
    private _listeners = new Map<string, Function[]>();

    private constructor() {
        this.deviceId = this.getOrCreateDeviceId();
        this.identity = this.resolveIdentity();
        this.roomId = getRoomId() || this.identity.userId || this.createRoomId();
    }

    public start() {
        if (this.started) { this.emit("status", this.getStatus()); return; }
        this.started = true;
        this.controllerConnected = false;
        this.lastInputSeq = -1;
        this.lastError = "";
        this.identity = this.resolveIdentity();
        let configuredRoomId = getRoomId();
        this.roomId = configuredRoomId || this.identity.userId || this.createRoomId();
        if (configuredRoomId) console.warn("[VirtualGamepad][Identity] roomId uses runtime override", { roomId: configuredRoomId, configKey: "RXQS_GAMEPAD_ROOM_ID" });
        let token = getToken();
        this.client = new Client();
        this.client.onStateChange = this.handleClientState.bind(this);
        this.client.onMessage = this.handleMessage.bind(this);
        this.client.connect({
            wsUrl: getWsUrl(), roomId: this.roomId, deviceId: this.deviceId,
            game: VIRTUAL_GAMEPAD_GAME_ID, userId: this.identity.userId,
            userType: this.identity.userType, guestId: this.identity.guestId,
            token: token, maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS
        });
        console.log("[VirtualGamepad][Identity] connect", { environment: getEnvironment(), userType: this.identity.userType, userId: this.maskUserId(this.identity.userId), roomId: this.roomId, hasToken: !!token, maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS });
        this.requestSunCode();
        this.emit("status", this.getStatus());
    }

    public stop() {
        this.started = false;
        this.controllerConnected = false;
        this.lastInputSeq = -1;
        this.sunCodeRequestSerial++;
        if (this.client) { this.client.close(); this.client = null; }
        this.connectionState = "closed";
        this.emit("status", this.getStatus());
    }

    public setGameState(state: VirtualGamepadGameState) { this.gameState = state; }

    public getStatus(): VirtualGamepadStatus {
        return {
            connectionState: this.connectionState,
            controllerConnected: this.controllerConnected,
            roomId: this.roomId,
            deviceId: this.deviceId,
            userId: this.identity.userId,
            userType: this.identity.userType,
            identityText: this.getIdentityText(),
            hasToken: !!getToken(),
            maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS,
            sunCodeState: this.sunCodeState,
            sunCodeDataUrl: this.sunCodeDataUrl,
            sunCodeError: this.sunCodeError,
            h5Url: this.getH5Url(),
            qrPayload: this.getQrPayload(),
            statusText: this.getStatusText(),
            lastError: this.lastError
        };
    }

    public getQrPayload(): string {
        if (getQrMode() == "json") {
            return JSON.stringify({ wsUrl: getWsUrl(), roomId: this.roomId, maxMembers: VIRTUAL_GAMEPAD_MAX_MEMBERS, token: getToken() });
        }
        return this.getH5Url();
    }

    public getH5Url(): string {
        return this.appendParams(getH5Url(), {
            game: VIRTUAL_GAMEPAD_GAME_ID, roomId: this.roomId, wsUrl: getWsUrl(),
            playerId: VIRTUAL_GAMEPAD_PLAYER_ID, maxMembers: String(VIRTUAL_GAMEPAD_MAX_MEMBERS), token: getToken()
        });
    }

    public on<K extends keyof ListenerMap>(type: K, listener: ListenerMap[K]) {
        if (!listener) return;
        let list = this._listeners.get(type);
        if (!list) { list = []; this._listeners.set(type, list); }
        if (list.indexOf(listener) < 0) list.push(listener);
    }
    public off<K extends keyof ListenerMap>(type: K, listener: ListenerMap[K]) {
        let list = this._listeners.get(type);
        if (!list) return;
        let i = list.indexOf(listener);
        if (i >= 0) list.splice(i, 1);
    }

    private handleClientState(state: VirtualGamepadConnectionState, reason?: string) {
        this.connectionState = state;
        if (state == "error") this.lastError = reason || "连接失败";
        if (state == "closed" || state == "error") { this.controllerConnected = false; this.emit("move", { x: 0, y: 0 }); }
        this.emit("status", this.getStatus());
    }

    private handleMessage(message: any) {
        if (!this.isCurrentRoomMessage(message)) {
            console.warn("[VirtualGamepad][WSS] ignored message from another room", { expectedRoomId: this.roomId, receivedRoomId: message && message.roomId, type: message && message.type });
            return;
        }
        if (message.type == "error") { this.lastError = message.message || message.error || "手柄服务返回错误"; this.connectionState = "error"; this.emit("status", this.getStatus()); return; }
        if (message.type == "pong") return;
        if (message.type == "room_state") { this.handleRoomState(message); return; }
        if (this.isControllerJoinMessage(message)) { console.log("[VirtualGamepad][WSS][Presence] controller joined", message); this.setControllerConnected(true); return; }
        if (this.isControllerLeaveMessage(message)) { console.log("[VirtualGamepad][WSS][Presence] controller left", message); this.setControllerConnected(false); this.emit("move", { x: 0, y: 0, raw: message }); return; }
        if (message.type == "input" || message.action) this.handleInput(message);
    }

    private handleInput(message: any) {
        let payload = message.input || message;
        let playerId = this.getMessagePlayerId(message) || this.getMessagePlayerId(payload);
        if (playerId && playerId != VIRTUAL_GAMEPAD_PLAYER_ID) return;
        let seq = typeof payload.seq == "number" ? payload.seq : message.seq;
        if (typeof seq == "number") { if (seq <= this.lastInputSeq) return; this.lastInputSeq = seq; }
        this.setControllerConnected(true);

        // 按 input.type 分发
        if (payload && this.dispatchControl(message, payload)) return;

        // 旧格式兼容：move / stop
        if (payload.action == "move" || payload.action == "stop") {
            this.emit("move", {
                x: this.clampAxis(payload.action == "stop" ? 0 : Number(payload.x) || 0),
                y: this.clampAxis(payload.action == "stop" ? 0 : Number(payload.y) || 0),
                seq, ts: this.getMessageTimestamp(payload) || this.getMessageTimestamp(message), raw: message
            });
        }
    }

    /** 按 input.type 分发到对应事件，返回 true 表示已处理 */
    private dispatchControl(message: any, input: any): boolean {
        if (!input || !input.type) return false;
        let t = String(input.type).toLowerCase();
        let key = this.normalizeKey(input.key);
        let pressed = input.pressed !== false && input.action != "release" && input.action != "up" && input.action != "stop";
        let action = (input && input.action ? String(input.action).toLowerCase() : "press");

        // 摇杆
        if (t == "joystick" || t == "joystick_stick") {
            let side = this.normalizeKey(input.side || "left");
            this.emit("move", {
                x: pressed ? this.clampAxis(Number(input.x) || 0) : 0,
                y: pressed ? this.clampAxis(Number(input.y) || 0) : 0,
                seq: input.seq || message.seq, ts: this.getMessageTimestamp(input) || this.getMessageTimestamp(message),
                raw: message, holdMode: true, side, direction: input.direction || "", action
            });
            return true;
        }

        // 按键
        if (t == "button") {
            let et = (key == "back") ? "back" as const : "button" as const;
            this.emit(et, this.buildInput(message, { type: et, action, pressed, key }));
            return true;
        }

        // 肩键 / 返回 / 扳机
        if (t == "shoulder" || t == "back" || t == "trigger") {
            this.emit(t as any, this.buildInput(message, { type: t as any, action, pressed, key }));
            return true;
        }

        return false;
    }

    private buildInput(message: any, data: any): VirtualGamepadControlInput {
        let rawInput = message && message.input ? message.input : message;
        return {
            type: data.type, action: data.action, pressed: data.pressed,
            side: data.side, key: data.key, direction: data.direction,
            x: typeof data.x == "number" ? data.x : undefined,
            y: typeof data.y == "number" ? data.y : undefined,
            playerId: this.getMessagePlayerId(message) || this.getMessagePlayerId(rawInput) || VIRTUAL_GAMEPAD_PLAYER_ID,
            seq: rawInput && typeof rawInput.seq == "number" ? rawInput.seq : message.seq,
            ts: this.getMessageTimestamp(rawInput) || this.getMessageTimestamp(message),
            raw: message
        };
    }

    private normalizeKey(key: any): string { return String(key || "").toLowerCase(); }
    private isCurrentRoomMessage(message: any): boolean { return !message || !message.roomId || message.roomId == this.roomId; }

    private isControllerJoinMessage(message: any): boolean {
        if (!message || message.role == "game") return false;
        if (message.type == "device_join") return this.isTargetPlayerMessage(message);
        if (message.role == "controller" && (message.type == "join" || message.type == "joined")) return true;
        return ["peer_joined", "player_joined", "controller_joined", "controller_connected"].indexOf(message.type) >= 0;
    }

    private isControllerLeaveMessage(message: any): boolean {
        if (!message) return false;
        if (message.type == "device_leave") return this.isTargetPlayerMessage(message);
        return ["peer_left", "player_left", "controller_left", "controller_disconnected", "leave"].indexOf(message.type) >= 0;
    }

    private handleRoomState(message: any) {
        let items = message.slots || message.devices;
        if (items && items.length) { this.setControllerConnected(this.hasPlayerOneInList(items)); return; }
        if (typeof message.playerCount == "number") { this.setControllerConnected(message.playerCount > 0); return; }
        if (message.players && message.players[VIRTUAL_GAMEPAD_PLAYER_ID]) { this.setControllerConnected(true); return; }
        if (message.devices && message.devices[VIRTUAL_GAMEPAD_PLAYER_ID]) this.setControllerConnected(true);
    }

    private isTargetPlayerMessage(message: any): boolean { let pid = this.getMessagePlayerId(message); return !pid || pid == VIRTUAL_GAMEPAD_PLAYER_ID; }

    private getMessagePlayerId(message: any): string {
        if (!message) return "";
        if (message.playerId) return String(message.playerId);
        if (message.from == "player1" || message.from == "player2") return String(message.from);
        if (message.role == "player1" || message.role == "player2") return String(message.role);
        return "";
    }

    private hasPlayerOneInList(list: any): boolean {
        if (!list || !list.length) return false;
        return list.some((item: any) => item && item.role == VIRTUAL_GAMEPAD_PLAYER_ID && item.occupied !== false);
    }

    private getMessageTimestamp(message: any): number { return typeof message.ts == "number" ? message.ts : typeof message.timestamp == "number" ? message.timestamp : 0; }
    private setControllerConnected(connected: boolean) {
        if (this.controllerConnected == connected) return;
        this.controllerConnected = connected;
        if (!connected) this.emit("move", { x: 0, y: 0 });
        this.emit("status", this.getStatus());
    }

    private emit(type: string, ...args: any[]) {
        let list = this._listeners.get(type);
        if (list) { for (let i = list.length - 1; i >= 0; i--) list[i](...args); }
    }

    private getStatusText(): string {
        if (this.controllerConnected) return "虚拟手柄已连接";
        if (this.connectionState == "connecting") return "正在连接手柄服务";
        if (this.connectionState == "open" || this.connectionState == "joined") return "等待手柄扫码连接";
        if (this.connectionState == "error") return this.lastError || "手柄服务连接失败";
        return "虚拟手柄未连接";
    }

    private appendParams(url: string, params: Record<string, string>): string {
        let q = Object.keys(params).map(k => encodeURIComponent(k) + "=" + encodeURIComponent(params[k] || "")).join("&");
        let hi = url.indexOf("#");
        if (hi >= 0) { let p = url.substring(0, hi), h = url.substring(hi + 1); return p + "#" + h + (h.indexOf("?") >= 0 ? "&" : "?") + q; }
        return url + (url.indexOf("?") >= 0 ? "&" : "?") + q;
    }
    private clampAxis(v: number): number { return v > 1 ? 1 : v < -1 ? -1 : v; }

    private getOrCreateDeviceId(): string {
        let key = "rxqs_virtual_gamepad_device_id";
        try {
            let ls = cc.sys && cc.sys.localStorage;
            if (ls) { let s = ls.getItem(key); if (s) return s; let c = this.createId("game"); ls.setItem(key, c); return c; }
        } catch (e) { /* ignore */ }
        return this.createId("game");
    }

    private requestSunCode() {
        let serial = ++this.sunCodeRequestSerial;
        this.sunCodeState = "loading";
        this.sunCodeDataUrl = "";
        this.sunCodeError = "";
        this.emit("status", this.getStatus());
        let xhr = new XMLHttpRequest();
        xhr.timeout = 10000;
        let url = getWxacodeUrl();
        xhr.open("POST", url, true);
        xhr.responseType = "arraybuffer";
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.onload = () => {
            if (serial != this.sunCodeRequestSerial || !this.started) return;
            if (xhr.status < 200 || xhr.status >= 300 || !xhr.response) { this.failSunCode("太阳码接口返回 " + xhr.status, serial); return; }
            let bytes = new Uint8Array(xhr.response as ArrayBuffer);
            let mime = this.detectImageMimeType(bytes);
            if (!mime) { this.failSunCode("太阳码接口未返回有效图片", serial); return; }
            this.sunCodeDataUrl = "data:" + mime + ";base64," + this.encodeBase64(bytes);
            this.sunCodeState = "ready";
            this.sunCodeError = "";
            console.log("[VirtualGamepad][SunCode] ready", { environment: getEnvironment(), url, roomId: this.roomId, page: VIRTUAL_GAMEPAD_WXACODE_PAGE, projectCode: VIRTUAL_GAMEPAD_WXACODE_PROJECT_CODE, byteLength: bytes.length, mimeType: mime });
            this.emit("status", this.getStatus());
        };
        xhr.onerror = () => this.failSunCode("太阳码接口网络错误", serial);
        xhr.ontimeout = () => this.failSunCode("太阳码接口请求超时", serial);
        xhr.send(JSON.stringify({ scene: this.roomId, page: VIRTUAL_GAMEPAD_WXACODE_PAGE, projectCode: VIRTUAL_GAMEPAD_WXACODE_PROJECT_CODE }));
    }

    private failSunCode(msg: string, serial: number) {
        if (serial != this.sunCodeRequestSerial || !this.started) return;
        this.sunCodeState = "error";
        this.sunCodeDataUrl = "";
        this.sunCodeError = msg;
        console.warn("[VirtualGamepad][SunCode] unavailable", { roomId: this.roomId, message: msg });
        this.emit("status", this.getStatus());
    }

    private detectImageMimeType(bytes: Uint8Array): string {
        if (bytes.length >= 3 && bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff) return "image/jpeg";
        if (bytes.length >= 8 && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4e && bytes[3] == 0x47) return "image/png";
        return "";
    }

    private encodeBase64(bytes: Uint8Array): string {
        let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        let out = "";
        for (let i = 0; i < bytes.length; i += 3) {
            let a = bytes[i], b = i + 1 < bytes.length ? bytes[i + 1] : 0, c = i + 2 < bytes.length ? bytes[i + 2] : 0;
            out += chars.charAt(a >> 2) + chars.charAt(((a & 3) << 4) | (b >> 4));
            out += i + 1 < bytes.length ? chars.charAt(((b & 15) << 2) | (c >> 6)) : "=";
            out += i + 2 < bytes.length ? chars.charAt(c & 63) : "=";
        }
        return out;
    }

    private resolveIdentity(): VirtualGamepadIdentity {
        let guestId = this.getOrCreateGuestId();
        return { userId: guestId, userType: "guest", guestId };
    }

    private getOrCreateGuestId(): string {
        let key = "rxqs_virtual_gamepad_guest_id";
        try {
            let ls = cc.sys && cc.sys.localStorage;
            if (ls) { let s = ls.getItem(key); if (s) return s; let c = this.createId("guest"); ls.setItem(key, c); return c; }
        } catch (e) { /* ignore */ }
        return this.createId("guest");
    }

    private getIdentityText(): string { let l = this.identity.userType == "xiaomi" ? "小米用户" : "游客"; return l + " " + this.maskUserId(this.identity.userId); }
    private maskUserId(id: string): string { let v = String(id || ""); return v.length <= 8 ? v : v.substring(0, 4) + "..." + v.substring(v.length - 4); }
    private createRoomId(): string { return this.createId("rxqs"); }
    private createId(prefix: string): string { return prefix + "_" + Date.now().toString(36) + "_" + Math.floor(Math.random() * 100000).toString(36); }
}