import * as ipc from "../ipc/BrowserIPC";
import * as loader from "tc-loader";
import {Stage} from "tc-loader";

import {
    AbstractAvatarManager,
    AbstractAvatarManagerFactory, AvatarState, AvatarStateData, ClientAvatar,
    kIPCAvatarChannel,
    setGlobalAvatarManagerFactory, uniqueId2AvatarId
} from "../file/Avatars";
import {IPCChannel} from "../ipc/BrowserIPC";
import {AppParameters} from "../settings";
import {ChannelMessage} from "../ipc/BrowserIPC";
import {guid} from "../crypto/uid";
import { tr } from "tc-shared/i18n/localize";

function isEquivalent(a, b) {
    // Create arrays of property names
    const aProps = Object.getOwnPropertyNames(a);
    const bProps = Object.getOwnPropertyNames(b);

    // If number of properties is different,
    // objects are not equivalent
    if (aProps.length != bProps.length) {
        return false;
    }

    for (let i = 0; i < aProps.length; i++) {
        const propName = aProps[i];

        // If values of same property are not equal,
        // objects are not equivalent
        if (a[propName] !== b[propName]) {
            return false;
        }
    }

    // If we made it this far, objects
    // are considered equivalent
    return true;
}

class RemoteAvatar extends ClientAvatar {
    readonly avatarId: string;
    readonly type: "avatar" | "client-avatar";

    constructor(clientAvatarId: string, type: "avatar" | "client-avatar") {
        super(clientAvatarId);

        this.avatarId = guid();
        this.type = type;
    }

    protected destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]) {}

    public updateStateFromRemote(state: AvatarState, data: AvatarStateData[AvatarState]) {
        if(this.getState() === state && isEquivalent(this.getStateData(), data))
            return;

        this.setState(state, data, true);
    }
}

class RemoteAvatarManager extends AbstractAvatarManager {
    readonly handlerId: string;
    readonly ipcChannel: IPCChannel;
    private knownAvatars: RemoteAvatar[] = [];

    constructor(handlerId: string, ipcChannel: IPCChannel) {
        super();

        this.ipcChannel = ipcChannel;
        this.handlerId = handlerId;
    }

    destroy() {
        this.knownAvatars.forEach(e => e.destroy());
    }

    resolveAvatar(clientAvatarId: string, avatarHash?: string): ClientAvatar {
        const sendRequest = (avatar: RemoteAvatar) => this.ipcChannel.sendMessage("load-avatar", {
            avatarId: avatar.avatarId,
            handlerId: this.handlerId,

            keyType: "avatar",
            clientAvatarId: avatar.clientAvatarId,
            avatarVersion: avatarHash
        });

        const cachedAvatar = this.knownAvatars.find(e => e.type === "avatar" && e.avatarId === clientAvatarId);
        if(cachedAvatar) {
            if(cachedAvatar.getAvatarHash() !== avatarHash)
                sendRequest(cachedAvatar); /* update */
            return cachedAvatar;
        }

        let avatar = new RemoteAvatar(clientAvatarId, "avatar");
        avatar.setLoading();
        this.knownAvatars.push(avatar);
        sendRequest(avatar);
        return avatar;
    }

    resolveClientAvatar(client: { id?: number; database_id?: number; clientUniqueId: string }) {
        const sendRequest = (avatar: RemoteAvatar) => this.ipcChannel.sendMessage("load-avatar", {
            avatarId: avatar.avatarId,
            handlerId: this.handlerId,

            keyType: "client",
            clientId: client.id,
            clientUniqueId: client.clientUniqueId,
            clientDatabaseId: client.database_id
        });

        const clientAvatarId = uniqueId2AvatarId(client.clientUniqueId);
        const cachedAvatar = this.knownAvatars.find(e => e.type === "client-avatar" && e.clientAvatarId === clientAvatarId);
        if(cachedAvatar) {
            //sendRequest(cachedAvatar); /* just update in case */
            return cachedAvatar;
        }

        let avatar = new RemoteAvatar(clientAvatarId, "client-avatar");
        avatar.setLoading();
        this.knownAvatars.push(avatar);
        sendRequest(avatar);

        return avatar;
    }

    handleAvatarLoadCallback(data: any) {
        const avatar = this.knownAvatars.find(e => e.avatarId === data.avatarId);
        if(!avatar) return;

        if(!(data.success === true)) {
            avatar.setErrored({ message: data.message });
            return;
        }

        if(avatar.getAvatarHash() !== data.hash)
            avatar.events.fire("avatar_changed", { newAvatarHash: data.hash });

        avatar.updateStateFromRemote(data.state, data.stateData);
    }

    handleAvatarEvent(type: string, payload: any) {
        const avatar = this.knownAvatars.find(e => e.avatarId === payload.avatarId);
        if(!avatar) return;

        avatar.events.fire(type as any, payload, true);
    }
}

class RemoteAvatarManagerFactory extends AbstractAvatarManagerFactory {
    private readonly ipcChannel: IPCChannel;
    private manager: {[key: string]: RemoteAvatarManager} = {};

    private callbackHandlerQueried: () => void;

    constructor() {
        super();

        this.ipcChannel = ipc.getIpcInstance().createChannel(AppParameters.getValue(AppParameters.KEY_IPC_REMOTE_ADDRESS, "invalid"), kIPCAvatarChannel);
        this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this);
    }

    async initialize() {
        this.ipcChannel.sendMessage("query-handlers", {});

        await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.callbackHandlerQueried = undefined;
                reject(tr("handler query timeout"));
            }, 5000);

            this.callbackHandlerQueried = () => {
                clearTimeout(timeout);
                resolve();
            }
        });
    }

    getManager(handlerId: string): AbstractAvatarManager {
        return this.manager[handlerId];
    }

    hasManager(handlerId: string): boolean {
        return typeof this.manager[handlerId] !== "undefined";
    }

    private handleIpcMessage(_remoteId: string, broadcast: boolean, message: ChannelMessage) {
        if(broadcast) {
            if(message.type === "notify-handler-destroyed") {
                const manager = this.manager[message.data.handler];
                delete this.manager[message.data.handler];
                manager?.destroy();
            } else if(message.type === "notify-handler-created") {
                this.manager[message.data.handler] = new RemoteAvatarManager(message.data.handler, this.ipcChannel);
            }
        } else {
            if(message.type === "notify-handlers") {
                Object.values(this.manager).forEach(e => e.destroy());
                this.manager = {};

                for(const handlerId of message.data.handlers)
                    this.manager[handlerId] = new RemoteAvatarManager(handlerId, this.ipcChannel);

                if(this.callbackHandlerQueried)
                    this.callbackHandlerQueried();
            } else if(message.type === "load-avatar-result") {
                const manager = this.manager[message.data.handlerId];
                manager?.handleAvatarLoadCallback(message.data);
            } else if(message.type === "avatar-event") {
                const manager = this.manager[message.data.handlerId];
                manager?.handleAvatarEvent(message.data.type, message.data.payload);
            }
        }
    }
}

loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
    priority: 10,
    name: "IPC avatar init",
    function: async () => {
        let factory = new RemoteAvatarManagerFactory();
        await factory.initialize();
        setGlobalAvatarManagerFactory(factory);
    }
});