import { AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, entersState, getVoiceConnection, joinVoiceChannel, PlayerSubscription, StreamType, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice";
import { ChannelType, Client, Guild, StageChannel, VoiceChannel } from "discord.js";
import { once } from "events";
import { FakePresence, JoinHovna, KomandNaExport, MuzikaFace, RunFunkce, SRecord, StatusyINaFounu } from "./types";
import { existsSync } from "fs";
import { Readable } from "node:stream";

const pripojeni: SRecord<PlayerSubscription> = {};
const timeAnouncability: Record<string, boolean> = {};

export const oddiakritikovat = (a: string) => {
  return a
    .replace(/[àáâãäåæāăą]/g, "a")
    .replace(/[çćĉċč]/g, "c")
    .replace(/[ďđ]/g, "d")
    .replace(/[èéêëðēĕėęě]/g, "e")
    .replace(/[ĝğġģǵ]/g, "g")
    .replace(/[ĥħ]/g, "h")
    .replace(/[ìíîïĩīĭįı]/g, "i")
    .replace(/[ĵ]/g, "j")
    .replace(/[ķĸ]/g, "k")
    .replace(/[ĺļľŀł]/g, "l")
    .replace(/[ñńņňŉŋ]/g, "n")
    .replace(/[òóôõöøōŏőœ]/g, "o")
    .replace(/[þ]/g, "p")
    .replace(/[ŕŗř]/g, "r")
    .replace(/[ßśŝşšſ]/g, "s")
    .replace(/[ţťŧ]/g, "t")
    .replace(/[ùúûüũūŭůűų]/g, "u")
    .replace(/[ŵ]/g, "w")
    .replace(/[ÿýŷ]/g, "y")
    .replace(/[źżž]/g, "z")
    .replace(/[ÀÁÂÃÄÅÆĀĂĄ]/g, "A")
    .replace(/[ÇĆĈĊČ]/g, "C")
    .replace(/[Ď]/g, "D")
    .replace(/[ÈÉÊËÐĒĔĖĘĚ]/g, "E")
    .replace(/[ĜĞĠĢ]/g, "G")
    .replace(/[Ĵ]/g, "J")
    .replace(/[Ķ]/g, "K")
    .replace(/[ĤĦ]/g, "H")
    .replace(/[ÌÍÎÏĨĪĬĮİ]/g, "I")
    .replace(/[ĹĻĽĿŁ]/g, "L")
    .replace(/[ÑŃŅŇŊ]/g, "N")
    .replace(/[ÒÓÔÕÖØŌŎŐŒ]/g, "O")
    .replace(/[Þ]/g, "P")
    .replace(/[ŔŖŘ]/g, "R")
    .replace(/[ŚŜŞŠ]/g, "S")
    .replace(/[ŢŤŦ]/g, "T")
    .replace(/[ÙÚÛÜŨŪŬŮŰŲ]/g, "U")
    .replace(/[Ŵ]/g, "W")
    .replace(/[ÝŶŸ]/g, "Y")
    .replace(/[ŹŻŽ]/g, "Z");
};

/**
 * returns relative time in format `x hodin x minut a x se kund`
 * @param cas Number of seconds to convert
 */
export const formatCas = (cas: number) => {
  const h = Math.floor(cas / 3600);
  const m = Math.floor(cas % 3600 / 60);
  const s = Math.floor(cas % 3600 % 60);

  return `${h} hodin ${m} mynut a ${s} se kund`;
};

export async function joinVoice(channel: VoiceChannel | StageChannel): Promise<JoinHovna>;
export async function joinVoice(channel: string, guild: Guild): Promise<JoinHovna>;
export async function joinVoice(channel: VoiceChannel | StageChannel | string, guild?: Guild): Promise<JoinHovna> {
  const channelId = typeof channel === "string" ? channel : channel.id;
  const guildId = typeof channel === "string" ? guild!.id : channel.guildId;
  const guilda = typeof channel === "string" ? guild! : channel.guild;

  let conn = getVoiceConnection(guildId);
  let prev: boolean | string = false;
  if (conn) {
    if (conn.joinConfig.channelId === channelId) {
      await entersState(conn, VoiceConnectionStatus.Ready, 3e4)
        .catch(err => {
          conn!.destroy();
          log("pri pripojovani do vojsu nastala chyba:");
          throw err;
        });
      return { conn, prev: true };
    }
    prev = conn.joinConfig.channelId!;
  }

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

  await entersState(conn, VoiceConnectionStatus.Ready, 3e4)
    .catch((err: Error) => {
      conn!.destroy();
      throw err;
    });

  if (!prev) {
    const player = createAudioPlayer();
    player.on("error", e => log("chyba pri hrani", e));
    pripojeni[guildId] = conn.subscribe(player)!;
    conn.on("error", e => {
      if (e.message.startsWith("Cannot perform IP discovery")) return;
      log("error nekde v conn", e);
    });
  }

  return { conn, prev };
}

const makeAudioPlayer = (co: string | MuzikaFace | Readable) => {
  if (typeof co == "string" || co instanceof Readable) return createAudioResource(co);
  const res = createAudioResource(co.name, { inlineVolume: true, inputType: co.type || StreamType.Arbitrary });
  res.volume?.setVolume(co.volume);
  return res;
};

export const play = (conn: VoiceConnection, co: string | string[] | MuzikaFace | MuzikaFace[] | Readable) => new Promise<void>(async (res, rej) => {
  if (conn.state.status !== VoiceConnectionStatus.Ready) return rej("conn není ready");

  const player = pripojeni[conn.joinConfig.guildId].player;
  if (!Array.isArray(co)) {
    const aud = makeAudioPlayer(co);
    player.play(aud);
    const funkce = () => {
      res();
      player.removeListener(AudioPlayerStatus.Idle, funkce);
    };
    player.on(AudioPlayerStatus.Idle, funkce);
    return;
  }

  const resources: AudioResource[] = [];
  co.forEach(c => {
    const res = makeAudioPlayer(c);
    resources.push(res);
  });
  for (const res of resources) {
    await once(res.playStream, "readable");
  }
  player.play(resources[0]);

  let i = 0;
  const funkce = () => {
    i++;
    if (i === resources.length) {
      res();
      player.removeListener(AudioPlayerStatus.Idle, funkce);
    }
    else player.play(resources[i]);
  };
  player.on(AudioPlayerStatus.Idle, funkce);
});

const leave = (guildId: string) => {
  const conn = getVoiceConnection(guildId);
  if (!conn) return;
  conn.destroy();
  delete pripojeni[guildId];
};

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

export function loadEnv() {
  if (!existsSync("config.json")) throw new Error("config.json neexistuje");
  process.env = { ...process.env, ...require("../../config.json") };
}

/**
 * Returns absolute date in format `d. m. h:mm:ss`
 * @param date Date object to convert
 */
export const formatter = new Intl.DateTimeFormat("cs", { day: "numeric", month: "short", hour: "numeric", minute: "numeric", second: "numeric" }).format;

export const ping = /^<@!?\d+>$/;

export function adminLog(client: Client, text: string) {
  if (process.env.adminChannel) {
    const adminChannel = client.channels.cache.get(process.env.adminChannel);
    if (adminChannel?.type == ChannelType.GuildText)
      adminChannel.send(text);
  }
  if (process.env.adminID) {
    client.users.cache.get(process.env.adminID)?.createDM().then(ch => ch.send(text));
  }
}

export function sortArr(arr: KomandNaExport[]) {
  return arr.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
}

export function getFirstArg(fn: RunFunkce | string) {
  if (typeof fn != "function") return;
  return fn.toString()
    .match(/(?:function\s.*?)?\(([^)]*)\)|\w+ =>/)![1]
    ?.split(",")[1]
    ?.trim();
}

export const getCurrentPlayer = (guildId: string) => pripojeni[guildId]?.player;

export const configureTimeAnouncment = (guild: string, state: boolean) => { timeAnouncability[guild] = state; };

export const canAnounceTime = (guild: string) => timeAnouncability[guild] ?? true;

export function log(...content: (string | Error)[]) {
  const jo = content.map(h => {
    if (typeof h == "string") return h;
    return `\x1b[31m${h.stack || h}\x1b[0m`;
  });

  const d = new Date();
  console.log(`[${d.getDate()}.${d.getMonth() + 1}. ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}]:`, ...jo);
}

export const statusOnFoun = (bef: FakePresence | null, aft: FakePresence) => {
  const role = { online: "Online", idle: "Idle", dnd: "DND", offline: "Offline" };

  if (!bef) bef = { status: 'offline', clientStatus: {} };
  const predAPo: StatusyINaFounu[] = [];

  [bef, aft].forEach((s, i) => {
    const mobile = s.clientStatus?.mobile;
    const deskotopy = s.clientStatus?.web
      ? s.clientStatus.desktop == "online" ? s.clientStatus.desktop : s.clientStatus.web
      : s.clientStatus?.desktop;

    if (mobile && mobile != deskotopy && (!deskotopy || deskotopy == "idle")) {
      predAPo[i] = `${role[mobile]}Phone` as StatusyINaFounu;
    } else {
      predAPo[i] = role[s.status] as StatusyINaFounu;
    }
  });

  return predAPo;
};
