import { get, writable } from 'svelte/store';
import { getAccountData, messageMediaUrl, setAccountData, uploadMedia } from './api';
import { effectiveRooms } from './mute';
import { arrivalIn, isRoomWatched } from './notification-model';
import {
  DEFAULT_MESSAGE_SOUND,
  SOUND_MAX_BYTES,
  parseMessageSound,
  shouldPlaySound,
  type MessageSoundSettings
} from './message-sound-model';
import { selectedRoomId } from './store';
import type { RoomSummary } from './types';

const EVENT_TYPE = 'dev.malice.message_sound';
const DEFAULT_URL = '/sounds/message.wav';
const CACHE_NAME = 'malice-sounds';

export const messageSound = writable<MessageSoundSettings>(DEFAULT_MESSAGE_SOUND);

const resolved = new Map<string, Promise<string>>();
let lastPlayedAt = 0;

export async function loadMessageSound(): Promise<void> {
  try {
    messageSound.set(parseMessageSound(await getAccountData(EVENT_TYPE)));
  } catch {
    messageSound.set(DEFAULT_MESSAGE_SOUND);
  }
}

export async function saveMessageSound(settings: MessageSoundSettings): Promise<void> {
  await setAccountData(EVENT_TYPE, { mode: settings.mode, sound: settings.sound });
  messageSound.set(settings);
}

export function resetMessageSound(): void {
  messageSound.set(DEFAULT_MESSAGE_SOUND);
  for (const url of resolved.values()) void url.then((u) => URL.revokeObjectURL(u), () => undefined);
  resolved.clear();
}

const cacheKey = (mxc: string) => `/__sounds/${encodeURIComponent(mxc)}`;

async function remember(mxc: string, blob: Blob): Promise<void> {
  try {
    await (await caches.open(CACHE_NAME)).put(cacheKey(mxc), new Response(blob, { headers: { 'content-type': blob.type } }));
  } catch {}
}

async function fetchSound(mxc: string): Promise<Blob> {
  try {
    const hit = await (await caches.open(CACHE_NAME)).match(cacheKey(mxc));
    if (hit) return await hit.blob();
  } catch {}
  const res = await fetch(messageMediaUrl(mxc, 'Full'), { credentials: 'same-origin' });
  if (!res.ok) throw new Error('Sound unavailable');
  const blob = await res.blob();
  await remember(mxc, blob);
  return blob;
}

function resolve(mxc: string | null): Promise<string> {
  if (!mxc) return Promise.resolve(DEFAULT_URL);
  let url = resolved.get(mxc);
  if (!url) {
    url = fetchSound(mxc).then(
      (blob) => URL.createObjectURL(blob),
      () => DEFAULT_URL
    );
    resolved.set(mxc, url);
  }
  return url;
}

export async function playMessageSound(): Promise<void> {
  try {
    const audio = new Audio(await resolve(get(messageSound).sound));
    await audio.play();
  } catch {}
}

function decodes(file: File): Promise<boolean> {
  return new Promise((done) => {
    const url = URL.createObjectURL(file);
    const audio = new Audio();
    const finish = (ok: boolean) => {
      clearTimeout(timer);
      URL.revokeObjectURL(url);
      done(ok);
    };
    const timer = setTimeout(() => finish(false), 5000);
    audio.oncanplaythrough = () => finish(true);
    audio.onerror = () => finish(false);
    audio.preload = 'auto';
    audio.src = url;
  });
}

export async function uploadMessageSound(file: File): Promise<void> {
  if (!file.type.startsWith('audio/')) throw new Error('Choose an audio file.');
  if (file.size > SOUND_MAX_BYTES) throw new Error('Sounds must be smaller than 1 MB.');
  if (!(await decodes(file))) throw new Error('That audio file could not be played.');
  const { mxc } = await uploadMedia(file);
  await remember(mxc, file);
  await saveMessageSound({ ...get(messageSound), sound: mxc });
}

export function startMessageSound(): () => void {
  let previous = new Map<string, RoomSummary>();
  const unsubscribe = effectiveRooms.subscribe((list) => {
    const prior = previous;
    previous = new Map(list.map((room) => [room.room_id, room]));
    const now = Date.now();
    const mode = get(messageSound).mode;
    for (const room of list) {
      const arrival = arrivalIn(prior.get(room.room_id), room, now);
      if (!arrival) continue;
      if (isRoomWatched(room.room_id, get(selectedRoomId), document.visibilityState === 'visible', document.hasFocus())) continue;
      if (!shouldPlaySound(mode, room, arrival, lastPlayedAt, now)) continue;
      lastPlayedAt = now;
      void playMessageSound();
    }
  });
  return unsubscribe;
}
