import { Client, PermissionCalculator } from "revolt.js";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects";
import Collection from "revolt.js/dist/maps/Collection";

import { useCallback, useContext, useEffect, useState } from "preact/hooks";

import { AppContext } from "./RevoltClient";

export interface HookContext {
    client: Client;
    forceUpdate: () => void;
}

export function useForceUpdate(context?: HookContext): HookContext {
    const client = useContext(AppContext);
    if (context) return context;

    const H = useState(0);
    var updateState: (_: number) => void;
    if (Array.isArray(H)) {
        let [, u] = H;
        updateState = u;
    } else {
        console.warn("Failed to construct using useState.");
        updateState = () => {};
    }

    return { client, forceUpdate: () => updateState(Math.random()) };
}

// TODO: utils.d.ts maybe?
type PickProperties<T, U> = Pick<
    T,
    {
        [K in keyof T]: T[K] extends U ? K : never;
    }[keyof T]
>;

// The keys in Client that are an object
// for some reason undefined keeps appearing despite there being no reason to so it's filtered out
type ClientCollectionKey = Exclude<
    keyof PickProperties<Client, Collection<any>>,
    undefined
>;

function useObject(
    type: ClientCollectionKey,
    id?: string | string[],
    context?: HookContext,
) {
    const ctx = useForceUpdate(context);

    function update(target: any) {
        if (
            typeof id === "string"
                ? target === id
                : Array.isArray(id)
                ? id.includes(target)
                : true
        ) {
            ctx.forceUpdate();
        }
    }

    const map = ctx.client[type];
    useEffect(() => {
        map.addListener("update", update);
        return () => map.removeListener("update", update);
    }, [id]);

    return typeof id === "string"
        ? map.get(id)
        : Array.isArray(id)
        ? id.map((x) => map.get(x))
        : map.toArray();
}

export function useUser(id?: string, context?: HookContext) {
    if (typeof id === "undefined") return;
    return useObject("users", id, context) as Readonly<Users.User> | undefined;
}

export function useSelf(context?: HookContext) {
    const ctx = useForceUpdate(context);
    return useUser(ctx.client.user!._id, ctx);
}

export function useUsers(ids?: string[], context?: HookContext) {
    return useObject("users", ids, context) as (
        | Readonly<Users.User>
        | undefined
    )[];
}

export function useChannel(id?: string, context?: HookContext) {
    if (typeof id === "undefined") return;
    return useObject("channels", id, context) as
        | Readonly<Channels.Channel>
        | undefined;
}

export function useChannels(ids?: string[], context?: HookContext) {
    return useObject("channels", ids, context) as (
        | Readonly<Channels.Channel>
        | undefined
    )[];
}

export function useServer(id?: string, context?: HookContext) {
    if (typeof id === "undefined") return;
    return useObject("servers", id, context) as
        | Readonly<Servers.Server>
        | undefined;
}

export function useServers(ids?: string[], context?: HookContext) {
    return useObject("servers", ids, context) as (
        | Readonly<Servers.Server>
        | undefined
    )[];
}

export function useDMs(context?: HookContext) {
    const ctx = useForceUpdate(context);

    function mutation(target: string) {
        let channel = ctx.client.channels.get(target);
        if (channel) {
            if (
                channel.channel_type === "DirectMessage" ||
                channel.channel_type === "Group"
            ) {
                ctx.forceUpdate();
            }
        }
    }

    const map = ctx.client.channels;
    useEffect(() => {
        map.addListener("update", mutation);
        return () => map.removeListener("update", mutation);
    }, []);

    return map
        .toArray()
        .filter(
            (x) =>
                x.channel_type === "DirectMessage" ||
                x.channel_type === "Group" ||
                x.channel_type === "SavedMessages",
        ) as (
        | Channels.GroupChannel
        | Channels.DirectMessageChannel
        | Channels.SavedMessagesChannel
    )[];
}

export function useUserPermission(id: string, context?: HookContext) {
    const ctx = useForceUpdate(context);

    const mutation = (target: string) => target === id && ctx.forceUpdate();
    useEffect(() => {
        ctx.client.users.addListener("update", mutation);
        return () => ctx.client.users.removeListener("update", mutation);
    }, [id]);

    let calculator = new PermissionCalculator(ctx.client);
    return calculator.forUser(id);
}

export function useChannelPermission(id: string, context?: HookContext) {
    const ctx = useForceUpdate(context);

    const channel = ctx.client.channels.get(id);
    const server =
        channel &&
        (channel.channel_type === "TextChannel" ||
            channel.channel_type === "VoiceChannel")
            ? channel.server
            : undefined;

    const mutation = (target: string) => target === id && ctx.forceUpdate();
    const mutationServer = (target: string) =>
        target === server && ctx.forceUpdate();
    const mutationMember = (target: string) =>
        target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();

    useEffect(() => {
        ctx.client.channels.addListener("update", mutation);

        if (server) {
            ctx.client.servers.addListener("update", mutationServer);
            ctx.client.servers.members.addListener("update", mutationMember);
        }

        return () => {
            ctx.client.channels.removeListener("update", mutation);

            if (server) {
                ctx.client.servers.removeListener("update", mutationServer);
                ctx.client.servers.members.removeListener(
                    "update",
                    mutationMember,
                );
            }
        };
    }, [id]);

    let calculator = new PermissionCalculator(ctx.client);
    return calculator.forChannel(id);
}

export function useServerPermission(id: string, context?: HookContext) {
    const ctx = useForceUpdate(context);

    const mutation = (target: string) => target === id && ctx.forceUpdate();
    const mutationMember = (target: string) =>
        target.substr(26) === ctx.client.user!._id && ctx.forceUpdate();

    useEffect(() => {
        ctx.client.servers.addListener("update", mutation);
        ctx.client.servers.members.addListener("update", mutationMember);

        return () => {
            ctx.client.servers.removeListener("update", mutation);
            ctx.client.servers.members.removeListener("update", mutationMember);
        };
    }, [id]);

    let calculator = new PermissionCalculator(ctx.client);
    return calculator.forServer(id);
}