import { AudioPlayer, AudioPlayerState, AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice";
import { Guild } from "discord.js";
import { adminLog, log } from "./utils";
import { Readable } from "node:stream";

export type Hratelny = string | Readable | {
	src: string | Readable;
	volume: number;
	type?: StreamType;
};

type Vojska = {
	conn: VoiceConnection;
	players: [AudioPlayer, AudioPlayer, AudioPlayer];
	queues: [AudioResource[], AudioResource[], AudioResource[]];
};

export const enum Priority {
	Time,
	Etc,
	Music
}

const vojsy = new Map<string, Vojska>();
let lastId = 0;

const getId = () => `Id:${lastId++}`;

/**
 * @returns Předchozí stav bota, před tímto příkazem
 * `true` znamená, že byl bot do tohoto kanelu již připojen
 * `false` znamená, že nebyl připojen nikam
 * `channelId` znamená, že v tutý gildě byl předtím připojenej do kanelu s tímto ID
 */
export const novejJoin = (guild: Guild, channelId: string) => new Promise<boolean | string>(async (resolve, reject) => {
	const guildId = guild.id;
	let prev: false | string = false;
	const gildina = vojsy.get(guildId);

	if (gildina) {
		const con = gildina.conn;
		if (con.joinConfig.channelId == channelId) {
			if (con.state.status != VoiceConnectionStatus.Ready) {
				log(`pri pripojovani do vojsu tady uz existovalo stejny pripojeni, ale status byl ${gildina.conn.state.status}, tak jsem to zahodil`);
				novejLeave(guildId);
			}
			else return resolve(true);
		}
		else {
			prev = con.joinConfig.channelId!;
			if (!prev) {
				adminLog(guild.client, "fakt tam nic neni, jak je to mozny?");
				prev = false;
			}

			// Přesunout se
			const state = guild.voiceStates.cache.get(guild.client.user!.id);
			if (state) {
				try {
					await state.setChannel(channelId);
					resolve(prev);
					return;
				} catch { }
			}

			// Z nějakýho neznámýho důvodu se nepodařilo přepojit, prostě vytvoříme nový připojení
			log(new Error("toxic discord back at it, prej"));
			novejLeave(guildId);
		}
	}

	const conn = joinVoiceChannel({
		channelId: channelId,
		guildId,
		adapterCreator: guild.voiceAdapterCreator
	});

	await entersState(conn, VoiceConnectionStatus.Ready, 1e4)
		.catch(e => {
			novejLeave(guildId);
			reject(e);
		});

	vypocitatCas(guildId);

	conn.on(VoiceConnectionStatus.Disconnected, () => {
		// Je možný, že bota někdo "přetáhnul" do jinýho vojsu, v tom případě připojení recyklovat nechem
		entersState(conn, VoiceConnectionStatus.Ready, 5e3).catch(() => {
			// Taky je možný, že ho někdo fakt odpojil, ale hned znova připojil
			// V tom případě už by bylo o recyklaci postaráno
			if (conn.state.status != VoiceConnectionStatus.Disconnected) return;
			log("recykluju");
			novejLeave(guildId);
		});
	});

	vojsy.set(guildId, {
		conn,
		players: Array(3).fill(0).map(() => createAudioPlayer().on("error", e => log("chyba v nějakým hráčovi", e))) as [AudioPlayer, AudioPlayer, AudioPlayer],
		queues: [[], [], []]
	});

	resolve(prev);
});

export function novejLeave(guildId: string) {
	const vojs = vojsy.get(guildId);
	if (!vojs) return false;

	clearTimeout(timeouty.get(guildId));
	timeouty.delete(guildId);

	if (vojs.conn.state.status != VoiceConnectionStatus.Destroyed)
		vojs.conn.destroy();

	vojsy.delete(guildId);

	return true;
}

/**
 * Tahle funkce by se měla zavolat pokaždý, když dojde k nějaký změně v queue nebo něco dohraje.
 * Řeší hovna typu, přeregistrace jinýho přehrávače, pouštění dalšího resource, a to je asi všechno
 * @param vojska Vojska, o kterou se má postarat
 * @param priority Priorita, na kterou se má brát ičkon ohled, je to buďto priorita nově zařazenýho zvuku, nebo prostě toho, s nejvyšší prioritou
 */
function handelieren(vojska: Vojska, priority: Priority) {

	for (let i = 0; i < priority; i++) {
		if (vojska.players[i].state.status != AudioPlayerStatus.Idle) return;
	}

	const hrac = vojska.players[priority];
	vojska.conn.subscribe(hrac);
	const resource = vojska.queues[priority].shift()!;
	hrac.play(resource);

	function nasafunkca(pred: AudioPlayerState, po: AudioPlayerState) {
		if (po.status != AudioPlayerStatus.Idle || pred.status != AudioPlayerStatus.Playing || pred.resource.metadata != resource.metadata) return;

		vojska?.players[priority].removeListener("stateChange", nasafunkca);

		let nejprioritnejsi = -1;
		for (let i = 0; i < 3; i++) {
			const hrac = vojska.players[i];
			if (hrac.state.status == AudioPlayerStatus.AutoPaused) {
				vojska.conn.subscribe(hrac);
				return;
			}
			if (!vojska.queues[i].length) continue;

			nejprioritnejsi = i;
			break;
		}
		if (nejprioritnejsi != -1)
			handelieren(vojska, nejprioritnejsi);
	}

	vojska.players[priority].on("stateChange", nasafunkca);
}

function makeResource(co: Hratelny, id: string) {
	if (typeof co == "string" || co instanceof Readable) return createAudioResource(co, { metadata: id });
	const res = createAudioResource(co.src, { inlineVolume: true, inputType: co.type || StreamType.Arbitrary, metadata: id });
	res.volume?.setVolume(co.volume);
	return res;
}

function convert(resources: { id: string; res: AudioResource; }[]) {
	const odpoved: AudioResource[] = [];
	resources.forEach(res => {
		odpoved.push(res.res);
	});

	return odpoved;
}

export const novejPlay = (guildId: string, co: Hratelny | Hratelny[], priority: Priority) => new Promise<AudioPlayerState>(async (resolve, reject) => {

	const vojska = vojsy.get(guildId);
	if (!vojska) return reject("nejsi pripojenej do vojsu debile");

	const resources: { id: string; res: AudioResource; }[] = [];
	if (!Array.isArray(co)) {
		const id = getId();
		resources.push({ id, res: makeResource(co, id) });
	} else {
		co.forEach(c => {
			const id = getId();
			resources.push({ id, res: makeResource(c, id) });
		});
	}

	// Tuto se možná může někdy hodit, třeba nikdy, kdovi
	// if (neprepisovat) {
	// 	vojska.queues[priority].push(resource);
	// } else {
	// 	vojska.queues[priority] = [resource];
	// }

	vojska.queues[priority] = convert(resources);

	const posledniId = resources.at(-1)?.id;

	function nasafunkca(pred: AudioPlayerState, po: AudioPlayerState) {
		if (!(("resource" in pred && pred.resource.metadata == posledniId) && (("resource" in po && po.resource.metadata != posledniId) || !("resource" in po)))) return;

		vojska?.players[priority].removeListener("stateChange", nasafunkca);
		resolve(po);
	}

	vojska.players[priority].on("stateChange", nasafunkca);

	handelieren(vojska, priority);
});

export const handlePrevVoice = (prev: boolean | string, guild: Guild) => {
	if (prev === true) return; // Byl jsem v tomdle vojsu
	if (prev === false) return novejLeave(guild.id); // Nebyl jsem ve vojsu vůbec
	novejJoin(guild, prev); // Byl jsem jinde
};

export function stopPlayer(guildId: string, priorita: Priority) {
	const vojs = vojsy.get(guildId);
	if (!vojs) return false;

	vojs.players[priorita].stop();
	return true;
}

export const getConn = (guildId: string) => vojsy.get(guildId)?.conn;

// Ohlašování času
const timeouty = new Map<string, NodeJS.Timeout>();

const nula = (a: number) => a < 10 ? `0${a}` : a;

const vypocitatCas = (guildId: string) => {
	const d = new Date();
	d.setMinutes((d.getMinutes() >= 30 ? 60 : 30));
	d.setSeconds(0);

	timeouty.set(guildId, setTimeout(() => rekniCas(guildId, `${nula(d.getHours())}${nula(d.getMinutes())}`), Number(d) - Number(new Date())));
};

const rekniCas = (guildId: string, cas: string) => {
	novejPlay(guildId, [{ src: "zvuky/intro.mp3", volume: 0.8 }, { src: `zvuky/${cas}.mp3`, volume: 2 }, { src: "zvuky/grg.mp3", volume: 0.5 }], Priority.Time)
		.then(() => {
			vypocitatCas(guildId);
		})
		.catch(err => { log("cas error:", err); });
};
