import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../engine/debug/index.js";
import { Mathf } from "../engine/engine_math.js";
import { RoomEvents } from "../engine/engine_networking.js";
import { serializable } from "../engine/engine_serialization_decorator.js";
import * as utils from "../engine/engine_utils.js"
import { getParam } from "../engine/engine_utils.js";
import { getIconElement } from "../engine/webcomponents/icons.js";
import { Behaviour } from "./Component.js";

const viewParamName = "view";
const debug = utils.getParam("debugsyncedroom");

/**
 * [SyncedRoom](https://engine.needle.tools/docs/api/SyncedRoom) is a behaviour that will attempt to join a networked room based on the URL parameters or a random room.
 * It will also create a button in the menu to join or leave the room.
 * You can also join a networked room by calling the core methods like `this.context.connection.joinRoom("roomName")`.
 * 
 * @example Join a networked room
 * ```typescript
 * const myObject = new Object3D();
 * myObject.addComponent(SyncedRoom, { roomName: "myRoom" });
 * ```
 * 
 * @example Join a random networked room
 * ```typescript
 * const myObject = new Object3D();
 * myObject.addComponent(SyncedRoom, { joinRandomRoom: true });
 * ```
 * 
 * @example Join a random networked room with prefix - this ensures that no room name collisions happen when running multiple applications on the same server instance
 * ```typescript
 * const myObject = new Object3D();
 * myObject.addComponent(SyncedRoom, { joinRandomRoom: true, roomPrefix: "myApp_" });
 * ```
 * 
 * **Debug:** Use `?debugsyncedroom` URL parameter for logging.
 *
 * @summary Joins a networked room based on URL parameters or a random room
 * @category Networking
 * @group Components
 * @see {@link SyncedTransform} for synchronizing object transforms
 * @see {@link Voip} for voice communication in rooms
 * @see {@link ScreenCapture} for screen/video sharing
 * @link https://engine.needle.tools/docs/networking.html
 */
export class SyncedRoom extends Behaviour {

    /**
     * The name of the room to join.
     * @default ""
     */
    @serializable()
    public roomName: string = "";
    /**
     * The URL parameter name to use for the room name. E.g. if set to "room" the URL will look like `?room=roomName`.
     * @default "room"
     */
    @serializable()
    public urlParameterName: string = "room";
    /**
     * If true, the room will be joined automatically when this component becomes active.
     * @default undefined which means it will join a random room if no roomName is set.
     */
    @serializable()
    public joinRandomRoom?: boolean;
    /**
     * If true and no room parameter is found in the URL then no room will be joined.
     * @default false
     */
    @serializable()
    public requireRoomParameter: boolean = false;
    /**
     * If true, the room will be rejoined automatically when disconnected.
     * @default true
     */
    @serializable()
    public autoRejoin: boolean = true;

    /**
     * If true, a join/leave room button will be created in the menu.
     * @default true
     */
    @serializable()
    public createJoinButton: boolean = true;

    /**
     * If true, a join/leave room button for the view only URL will be created in the menu.
     * @default false
     */
    @serializable()
    public createViewOnlyButton: boolean = false;

    /**
     * Get current room name from the URL parameter or the view parameter.
     */
    get currentRoomName(): string | null {
        const view = utils.getParam(viewParamName);
        if (view) return view as string;
        return utils.getParam(this.urlParameterName) as string;
    }

    private _lastJoinedRoom?: string;

    /** The room prefix to use for the room name. E.g. if set to "room_" and the room name is "name" the final room name will be "room_name". */
    @serializable()
    set roomPrefix(val: string) {
        this._roomPrefix = val;
    }
    get roomPrefix(): string {
        return this._roomPrefix;
    }
    private _roomPrefix: string = "";

    /** @internal */
    awake() {
        if (this.joinRandomRoom === undefined && this.roomName?.length <= 0) {
            this.joinRandomRoom = true;
        }
        if (debug) console.log(`SyncedRoom roomName:${this.roomName}, urlParamName:${this.urlParameterName}, joinRandomRoom:${this.joinRandomRoom}`);
    }

    /** @internal */
    onEnable() {
        // if the url contains a view parameter override room and join in view mode
        const viewId = utils.getParam(viewParamName);
        if (viewId && typeof viewId === "string" && viewId.length > 0) {
            console.log("Join as viewer");
            this.context.connection.joinRoom(viewId, true);
            return;
        }
        // If setup to join a random room
        this.tryJoinRoom();

        if (this.createJoinButton) {
            const button = this.createRoomButton();
            this.context.menu.appendChild(button);
        }
        if (this.createViewOnlyButton) {
            this.onEnableViewOnlyButton()
        }
    }

    /** @internal */
    onDisable(): void {
        this._roomButton?.remove();
        this.onDisableViewOnlyButton();
        if (this.roomName && this.roomName.length > 0)
            this.context.connection.leaveRoom(this.roomName);
    }

    /** @internal */
    onDestroy(): void {
        this.destroyRoomButton();
    }

    /** Will generate a random room name, set it as an URL parameter and attempt to join the room */
    tryJoinRandomRoom() {
        this.setRandomRoomUrlParameter();
        this.tryJoinRoom();
    }

    /** Try to join the currently set roomName */
    tryJoinRoom(call: number = 0): boolean {
        if (call === undefined) call = 0;
        let hasRoomParameter = false;

        if (this.urlParameterName?.length > 0) {
            const val = utils.getParam(this.urlParameterName);
            if (val && (typeof val === "string" || typeof val === "number")) {
                hasRoomParameter = true;
                const roomNameParam = utils.sanitizeString(val.toString());
                this.roomName = roomNameParam;
            }
            else if (this.joinRandomRoom) {
                console.debug("No room name found in url, generating random one");
                this.setRandomRoomUrlParameter();
                if (call < 1)
                    return this.tryJoinRoom(call + 1);
            }
        }
        else {
            if (this.joinRandomRoom && (this.roomName === null || this.roomName === undefined || this.roomName.length <= 0)) {
                this.roomName = this.generateRoomName();
            }
        }

        if (this.requireRoomParameter && !hasRoomParameter) {
            if (debug || isDevEnvironment())
                console.warn("[SyncedRoom] Missing required room parameter \"" + this.urlParameterName + "\" in url - will not connect.\nTo allow joining a room without a query parameter you can set \"requireRoomParameter\" to false.");
            return false;
        }

        if (!this.context.connection.isConnected) {
            this.context.connection.connect();
        }

        this._lastJoinedRoom = this.roomName;
        if (this._roomPrefix)
            this.roomName = this._roomPrefix + this.roomName;

        if (this.roomName.length <= 0) {
            console.warn("[SyncedRoom] Room name is not set so we can not join a networked room.\nPlease choose one of the following options to fix this:\nA) Set a room name in the SyncedRoom component\nB) Set a room name in the URL parameter \"?" + this.urlParameterName + "=my_room\"\nC) Set \"joinRandomRoom\" to true");
            return false;
        }

        if (debug) console.log("Join " + this.roomName)

        this._userWantsToBeInARoom = true;
        this.context.connection.joinRoom(this.roomName);
        return true;
    }

    private _lastPingTime: number = 0;
    private _lastRoomTime: number = -1;
    private _userWantsToBeInARoom = false;

    /** @internal */
    update(): void {
        if (this.context.connection.isConnected) {
            if (this.context.time.time - this._lastPingTime > 3) {
                this._lastPingTime = this.context.time.time;
                this.context.connection.sendPing();
            }

            if (this.context.connection.isInRoom) {
                this._lastRoomTime = this.context.time.time;
            }
        }

        if (this._lastRoomTime > 0 && this.context.time.time - this._lastRoomTime > .3) {
            this._lastRoomTime = -1;

            if (this.autoRejoin) {
                if (this._userWantsToBeInARoom) {
                    console.log("Disconnected from networking backend - attempt reconnecting now")
                    this.tryJoinRoom();
                }
            }
            else if (isDevEnvironment())
                console.warn("You are not connected to a room anymore (possibly because the tab was inactive for too long and the server kicked you?)");
        }
    }

    /**
     * Get the URL to view the current room in view only mode.
     */
    getViewOnlyUrl(): string | null {
        if (this.context.connection.isConnected && this.context.connection.currentRoomViewId) {
            const url = window.location.search;
            const urlParams = new URLSearchParams(url);
            if (urlParams.has(this.urlParameterName))
                urlParams.delete(this.urlParameterName);
            urlParams.set(viewParamName, this.context.connection.currentRoomViewId);
            return window.location.origin + window.location.pathname + "?" + urlParams.toString();
        }
        return null;
    }

    private setRandomRoomUrlParameter() {
        const params = utils.getUrlParams();
        const room = this.generateRoomName();
        // if we already have this parameter
        if (utils.getParam(this.urlParameterName)) {
            params.set(this.urlParameterName, room);
        }
        else
            params.append(this.urlParameterName, room);
        utils.setState(room, params);
    }

    private generateRoomName(): string {
        let roomName = "";
        for (let i = 0; i < 6; i++) {
            roomName += Math.floor(Math.random() * 10).toFixed(0);
        }
        return roomName;
    }


    private _roomButton?: HTMLButtonElement;
    private _roomButtonIconJoin?: HTMLElement;
    private _roomButtonIconLeave?: HTMLElement;
    private createRoomButton() {
        if (this._roomButton) {
            return this._roomButton;
        }
        const button = document.createElement("button");
        this._roomButton = button;
        button.classList.add("create-room-button");
        button.setAttribute("priority", "90");
        button.onclick = () => {
            if (this.context.connection.isInRoom) {
                if (this.urlParameterName) {
                    utils.setParamWithoutReload(this.urlParameterName, null);
                }
                this.context.connection.leaveRoom();
                this._userWantsToBeInARoom = false;
            }
            else {
                if (this.urlParameterName) {
                    const name = getParam(this.urlParameterName);
                    // true check for ?room= without an actual name
                    if (!name || name === true) {
                        if (this._lastJoinedRoom)
                            utils.setParamWithoutReload(this.urlParameterName, this._lastJoinedRoom);
                        else
                            this.setRandomRoomUrlParameter();
                    };
                }
                this.tryJoinRoom();
            }
        };
        this._roomButtonIconJoin = getIconElement("group");
        this._roomButtonIconLeave = getIconElement("group_off");
        this.updateRoomButtonState();
        this.context.connection.beginListen(RoomEvents.JoinedRoom, this.updateRoomButtonState);
        this.context.connection.beginListen(RoomEvents.LeftRoom, this.updateRoomButtonState);
        return button;
    }
    private updateRoomButtonState = () => {
        if (!this._roomButton) return;

        if (this.context.connection.isInRoom) {
            this._roomButton.title = "Leave the networked room";
            this._roomButton.textContent = "Leave Room";
            this._roomButtonIconJoin?.remove();
            this._roomButton.prepend(this._roomButtonIconLeave!);
        }
        else {
            this._roomButton.title = "Create or join a networked room";
            this._roomButton.textContent = "Join Room";
            this._roomButtonIconLeave?.remove();
            this._roomButton.prepend(this._roomButtonIconJoin!);
        }
    }
    private destroyRoomButton() {
        this.context.connection.stopListen(RoomEvents.JoinedRoom, this.updateRoomButtonState);
        this.context.connection.stopListen(RoomEvents.LeftRoom, this.updateRoomButtonState);
    }


    private _viewOnlyButton?: HTMLButtonElement;
    private onEnableViewOnlyButton() {
        if (this.context.connection.isConnected) {
            this.onCreateViewOnlyButton();
        }
        else {
            this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onCreateViewOnlyButton);
            this.context.connection.beginListen(RoomEvents.JoinedRoom, this.onCreateViewOnlyButton);
        }
    }
    private onDisableViewOnlyButton() {
        this.context.connection.stopListen(RoomEvents.JoinedRoom, this.onCreateViewOnlyButton);
        this._viewOnlyButton?.remove();
    }

    private onCreateViewOnlyButton = () => {
        if (!this._viewOnlyButton) {
            const button = document.createElement("button");
            this._viewOnlyButton = button;
            button.classList.add("view-only-button");
            button.setAttribute("priority", "90");
            button.onclick = () => {
                const viewUrl = this.getViewOnlyUrl();
                if (viewUrl?.length) {
                    // share
                    if (navigator.canShare({ url: viewUrl })) {
                        navigator.share({ url: viewUrl })?.catch(err => {
                            console.warn(err);
                        });
                    }
                    else {
                        navigator.clipboard.writeText(viewUrl);
                        showBalloonMessage("View only URL copied to clipboard");
                    }
                }
                else {
                    showBalloonWarning("Could not create view only URL");
                }
            };
            button.title = "Copy the view only URL: A page accessed by the view only URL can not be modified by visiting users.";
            button.textContent = "Share View URL";
            button.prepend(getIconElement("visibility"));
        }
        this.context.menu.appendChild(this._viewOnlyButton);
    }
}
