/*
 * Vencord, a Discord client mod
 * Copyright (c) 2024 Vendicated and contributors
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { ApplicationAssetUtils, FluxDispatcher } from "@webpack/common";

import { Activity, ActivityType, BanchoStatusEnum, GameState, Modes, TosuApi, UserLoginStatus } from "./type";

const socketId = "tosu";
const OSU_APP_ID = "367827983903490050";
const OSU_LARGE_IMAGE = "373344233077211136";
const OSU_STARDARD_SMALL_IMAGE = "373370493127884800";
const OSU_MANIA_SMALL_IMAGE = "373370588703621136";
const OSU_TAIKO_SMALL_IMAGE = "373370519891738624";
const OSU_CATCH_SMALL_IMAGE = "373370543161999361";

const throttledOnMessage = throttle(onMessage, 3000, () => FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" }));

let ws: WebSocket;
let wsReconnect: NodeJS.Timeout;

export default definePlugin({
    name: "TosuRPC",
    description: "osu! RPC with data from tosu",
    authors: [Devs.AutumnVN],
    start() {
        (function connect() {
            ws = new WebSocket("ws://localhost:24050/websocket/v2");
            ws.addEventListener("error", () => ws.close());
            ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
            ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
        })();
    },
    stop() {
        ws.close();
        clearTimeout(wsReconnect);
        FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });
    }
});


async function onMessage(data: string) {
    const json: TosuApi = JSON.parse(data);
    // @ts-ignore
    if (json.error) return FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null, socketId: "tosu" });

    const { state, session, profile, beatmap, play, resultsScreen } = json;

    const activity: Activity = {
        application_id: OSU_APP_ID,
        name: "osu!",
        type: ActivityType.PLAYING,
        assets: {
            large_image: OSU_LARGE_IMAGE,
            large_text: profile.userStatus.number === UserLoginStatus.Connected ? `${profile.name} | #${profile.globalRank} | ${Math.round(profile.pp)}pp` : undefined,
        },
        timestamps: {
            start: Date.now() - session.playTime
        },
        flags: 1 << 0,
    };

    switch (profile.mode.number) {
        case Modes.Osu:
            activity.assets.small_image = OSU_STARDARD_SMALL_IMAGE;
            activity.assets.small_text = "osu!";
            break;
        case Modes.Mania:
            activity.assets.small_image = OSU_MANIA_SMALL_IMAGE;
            activity.assets.small_text = "osu!mania";
            break;
        case Modes.Taiko:
            activity.assets.small_image = OSU_TAIKO_SMALL_IMAGE;
            activity.assets.small_text = "osu!taiko";
            break;
        case Modes.Fruits:
            activity.assets.small_image = OSU_CATCH_SMALL_IMAGE;
            activity.assets.small_text = "osu!catch";
            break;
    }

    let player = "";
    let mods = "";
    let fc = "";
    let combo = "";
    let h100 = "";
    let h50 = "";
    let h0 = "";
    let sb = "";
    let pp = "";
    switch (state.number) {
        case GameState.Play:
            activity.type = profile.banchoStatus.number === BanchoStatusEnum.Playing ? ActivityType.PLAYING : ActivityType.WATCHING;

            player = profile.banchoStatus.number === BanchoStatusEnum.Playing ? "" : `${play.playerName} | `;
            mods = play.mods.name ? `+${play.mods.name} ` : "";
            activity.name = `${player}${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;

            combo = play.hits[0] === 0 && play.hits.sliderBreaks === 0
                ? `${play.combo.current}x`
                : `${play.combo.current}x/${play.combo.max}x`;
            pp = play.hits[0] === 0 && play.hits.sliderBreaks === 0
                ? `${Math.round(play.pp.current)}pp`
                : `${Math.round(play.pp.current)}pp/${Math.round(play.pp.fc)}pp`;
            activity.details = `${play.accuracy.toFixed(2)}% | ${combo} | ${pp}`;

            h100 = play.hits[100] > 0 ? `${play.hits[100]}x100` : "";
            h50 = play.hits[50] > 0 ? `${play.hits[50]}x50` : "";
            h0 = play.hits[0] > 0 ? `${play.hits[0]}xMiss` : "";
            sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
            activity.state = [h100, h50, h0, sb].filter(Boolean).join(" | ");

            const playRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${play.rank.current.toLowerCase().replace("x", "ss")}.png`);
            activity.assets.small_image = playRank;
            activity.assets.small_text = undefined;
            break;
        case GameState.ResultScreen:
            activity.type = ActivityType.WATCHING;

            mods = resultsScreen.mods.name ? `+${resultsScreen.mods.name} ` : "";
            activity.name = `${resultsScreen.playerName} | ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;

            fc = resultsScreen.maxCombo === beatmap.stats.maxCombo ? "FC" : `| ${resultsScreen.maxCombo}x/${beatmap.stats.maxCombo}x`;
            pp = !resultsScreen.pp.current ? ""
                : Math.round(resultsScreen.pp.current) === Math.round(resultsScreen.pp.fc)
                    ? `| ${Math.round(resultsScreen.pp.current)}pp`
                    : `| ${Math.round(resultsScreen.pp.current)}pp/${Math.round(resultsScreen.pp.fc)}pp`;
            activity.details = `${resultsScreen.accuracy.toFixed(2)}% ${fc} ${pp}`;

            h100 = resultsScreen.hits[100] > 0 ? `${resultsScreen.hits[100]}x100` : "";
            h50 = resultsScreen.hits[50] > 0 ? `${resultsScreen.hits[50]}x50` : "";
            h0 = resultsScreen.hits[0] > 0 ? `${resultsScreen.hits[0]}xMiss` : "";
            sb = play.hits.sliderBreaks > 0 ? `${play.hits.sliderBreaks}xSB` : "";
            activity.state = [h100, h50, h0].filter(Boolean).join(" | ");

            const resultRank = await getAsset(`https://raw.githubusercontent.com/AutumnVN/gosu-rich-presence/main/grade/${resultsScreen.rank.toLowerCase().replace("x", "ss")}.png`);
            activity.assets.small_image = resultRank;
            activity.assets.small_text = undefined;
            break;
        default:
            activity.type = ActivityType.LISTENING;
            mods = play.mods.name ? `+${play.mods.name} ` : "";
            activity.name = `${beatmap.artist} - ${beatmap.title} [${beatmap.version}] ${mods}(${beatmap.mapper}, ${beatmap.stats.stars.total.toFixed(2)}*)`;

            switch (state.number) {
                case GameState.Menu: activity.details = "Main Menu"; break;
                case GameState.Edit: activity.details = "Edit"; break;
                case GameState.SelectEdit: activity.details = "Song Select (Edit)"; break;
                case GameState.SelectPlay: activity.details = "Song Select (Play)"; break;
                case GameState.SelectDrawings: activity.details = "Select Drawings"; break;
                case GameState.Update: activity.details = "Update"; break;
                case GameState.Busy: activity.details = "Busy"; break;
                case GameState.Lobby: activity.details = "Lobby"; break;
                case GameState.MatchSetup: activity.details = "Match Setup"; break;
                case GameState.SelectMulti: activity.details = "Select Multi"; break;
                case GameState.RankingVs: activity.details = "Ranking Vs"; break;
                case GameState.OnlineSelection: activity.details = "Online Selection"; break;
                case GameState.OptionsOffsetWizard: activity.details = "Options Offset Wizard"; break;
                case GameState.RankingTagCoop: activity.details = "Ranking Tag Coop"; break;
                case GameState.RankingTeam: activity.details = "Ranking Team"; break;
                case GameState.BeatmapImport: activity.details = "Beatmap Import"; break;
                case GameState.PackageUpdater: activity.details = "Package Updater"; break;
                case GameState.Benchmark: activity.details = "Benchmark"; break;
                case GameState.Tourney: activity.details = "Tourney"; break;
                case GameState.Charts: activity.details = "Charts"; break;
            }

            switch (profile.banchoStatus.number) {
                case BanchoStatusEnum.Idle: activity.state = "Idle"; break;
                case BanchoStatusEnum.Afk: activity.state = "AFK"; break;
                case BanchoStatusEnum.Playing: activity.state = "Playing"; break;
                case BanchoStatusEnum.Editing: activity.state = "Editing"; break;
                case BanchoStatusEnum.Modding: activity.state = "Modding"; break;
                case BanchoStatusEnum.Multiplayer: activity.state = "Multiplayer"; break;
                case BanchoStatusEnum.Watching: activity.state = "Watching"; break;
                case BanchoStatusEnum.Testing: activity.state = "Testing"; break;
                case BanchoStatusEnum.Submitting: activity.state = "Submitting"; break;
                case BanchoStatusEnum.Paused: activity.state = "Paused"; break;
                case BanchoStatusEnum.Lobby: activity.state = "Lobby"; break;
                case BanchoStatusEnum.Multiplaying: activity.state = "Multiplaying"; break;
                case BanchoStatusEnum.OsuDirect: activity.state = "osu!direct"; break;
            }

            break;
    }

    if (beatmap.set > 0) {
        const mapBg = await getAsset(`https://assets.ppy.sh/beatmaps/${beatmap.set}/covers/list@2x.jpg`);
        const res = await fetch(mapBg.replace(/^mp:/, "https://media.discordapp.net/"), { method: "HEAD" });
        if (res.ok) activity.assets.large_image = mapBg;
    }

    FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity, socketId: "tosu" });
}

function throttle<T extends Function>(func: T, limit: number, timedOutCallback?: () => void): T {
    let inThrottle: boolean;
    let callbackTimeout: NodeJS.Timeout;
    return function (this: any, ...args: any[]) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
            if (timedOutCallback) {
                clearTimeout(callbackTimeout);
                callbackTimeout = setTimeout(timedOutCallback, limit * 2);
            }
        }
    } as any;
}

async function getAsset(key: string): Promise<string> {
    if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
    return (await ApplicationAssetUtils.fetchAssetIds(OSU_APP_ID, [key]))[0];
}
