/**
 * Замена фона видео: список дефолтных сцен, память о выборе и ленивое создание
 * процессора сегментации.
 *
 * Тяжёлая часть (библиотека + wasm-рантайм MediaPipe + модель, единицы мегабайт)
 * НЕ попадает в основной бандл: `@livekit/track-processors` подключается
 * динамическим `import()` в `createBackgroundProcessor`, то есть только когда
 * пользователь реально включает фон. Вход в конференцию от наличия этой фичи
 * не становится медленнее — это было прямым требованием.
 *
 * 🔴 Ассеты берутся СО СВОЕГО домена (`assetPaths` ниже). По умолчанию
 * библиотека тянет wasm с jsdelivr, а модель — с storage.googleapis.com;
 * VidConf ставят в закрытых контурах без внешнего интернета, и там фича молча
 * не заработала бы. Откуда берутся файлы — см. плагин `mediapipeWasm`
 * в `vite.config.ts` и `public/mediapipe/NOTICE.txt`.
 */

import { Track } from 'livekit-client'
import type { BackgroundProcessorWrapper } from '@livekit/track-processors'

/** Готовая сцена, поставляемая с продуктом (собственные рисунки, см. `design/backgrounds/`). */
export interface DefaultBackground {
  /** Имя файла без расширения — оно же часть ключа выбора (`default:office`). */
  id: string
  label: string
  url: string
}

export const DEFAULT_BACKGROUNDS: DefaultBackground[] = [
  { id: 'office', label: 'Офис', url: '/backgrounds/office.webp' },
  { id: 'beach', label: 'Пляж', url: '/backgrounds/beach.webp' },
  { id: 'space-station', label: 'Космическая станция', url: '/backgrounds/space-station.webp' },
]

/**
 * Ключ выбранного фона: `none`, `default:<id>` либо `custom:<uuid записи>`.
 *
 * Хранится именно ключ, а не URL картинки: URL своей картинки перестаёт быть
 * валидным, как только пользователь её удалил, и по ключу это видно сразу —
 * записи с таким id в списке нет, значит выбор сбрасывается на «без фона»
 * (см. `resolveBackgroundUrl`).
 */
export type BackgroundKey = string

export const NO_BACKGROUND: BackgroundKey = 'none'

const STORAGE_KEY = 'vidconf.virtualBackground'

/**
 * Загрузить сохранённый выбор фона.
 *
 * Выбор персистится между заходами (как режим показа сцены,
 * `lib/stageLayoutMode.ts`) и, что важнее, переживает переход «превью на входе
 * → комната»: пользователь выбирает фон на `JoinPage`, а применяется он к
 * публикуемому треку уже внутри конференции — передавать его через
 * navigation state нельзя, тот теряется при F5.
 */
export function loadBackgroundKey(): BackgroundKey {
  try {
    return window.localStorage.getItem(STORAGE_KEY) || NO_BACKGROUND
  } catch {
    // Приватный режим/запрет хранилища — фича должна работать и без памяти.
    return NO_BACKGROUND
  }
}

export function saveBackgroundKey(key: BackgroundKey): void {
  try {
    window.localStorage.setItem(STORAGE_KEY, key)
  } catch {
    // См. `loadBackgroundKey` — молча живём без персиста.
  }
}

/** Своя картинка пользователя в том виде, в каком её отдаёт API. */
export interface CustomBackground {
  id: string
  url: string
}

/**
 * URL картинки по ключу выбора; `null` — фон не нужен («без фона» либо ключ
 * указывает на уже удалённую свою картинку).
 */
export function resolveBackgroundUrl(
  key: BackgroundKey,
  customBackgrounds: CustomBackground[],
): string | null {
  if (key.startsWith('default:')) {
    const id = key.slice('default:'.length)
    return DEFAULT_BACKGROUNDS.find((item) => item.id === id)?.url ?? null
  }
  if (key.startsWith('custom:')) {
    const id = key.slice('custom:'.length)
    return customBackgrounds.find((item) => item.id === id)?.url ?? null
  }
  return null
}

/**
 * Поддерживает ли браузер замену фона.
 *
 * Проверка СИНХРОННАЯ и намеренно не трогает саму библиотеку: решение нужно
 * ДО её загрузки, чтобы не показывать кнопку, которая не сработает, и не
 * тянуть мегабайты впустую. Поэтому здесь буквально повторено условие
 * `supportsBackgroundProcessors()` из `@livekit/track-processors` — при
 * обновлении пакета сверять с ним.
 *
 * ⚠️ Конвейеров у библиотеки ДВА, и требовать современный нельзя:
 * - современный (`MediaStreamTrackProcessor`/`Generator`, Insertable Streams)
 *   есть только в Chrome и производных;
 * - запасной рисует кадры в canvas и отдаёт `canvas.captureStream()` — он
 *   работает в Safari и Firefox.
 *
 * Первая версия (0.0.35) требовала именно современный конвейер — и фича
 * молча отсутствовала в Safari и Firefox, хотя запасной путь там доступен.
 * Условие ниже — «умеет считать сегментацию» И «есть хоть какой-то конвейер».
 */
export function isVirtualBackgroundSupported(): boolean {
  if (typeof window === 'undefined') return false
  return canRunSegmentation() && hasAnyFramePipeline()
}

/**
 * Условие `BackgroundTransformer.isSupported`: чем библиотека считает маску и
 * собирает кадр. WebGL2 проверяется созданием пробного контекста — иначе никак,
 * но результат кэшируется на всю жизнь страницы: браузеры держат ограниченное
 * число живых WebGL-контекстов, и создавать новый на каждый рендер нельзя.
 */
let segmentationSupport: boolean | null = null
function canRunSegmentation(): boolean {
  if (segmentationSupport !== null) return segmentationSupport
  const hasApis =
    typeof OffscreenCanvas !== 'undefined' &&
    typeof VideoFrame !== 'undefined' &&
    typeof createImageBitmap !== 'undefined'
  if (!hasApis) {
    segmentationSupport = false
    return false
  }
  const probe = document.createElement('canvas').getContext('webgl2')
  // Пробный контекст сразу отпускаем — он больше не нужен, а слот в лимите
  // браузера занимал бы до сборки мусора.
  probe?.getExtension('WEBGL_lose_context')?.loseContext()
  segmentationSupport = Boolean(probe)
  return segmentationSupport
}

/** Условие `ProcessorWrapper.isSupported`: современный конвейер ЛИБО запасной на canvas. */
function hasAnyFramePipeline(): boolean {
  const modern = 'MediaStreamTrackProcessor' in window && 'MediaStreamTrackGenerator' in window
  const fallback =
    typeof HTMLCanvasElement !== 'undefined' && 'captureStream' in HTMLCanvasElement.prototype
  return modern || fallback
}

/**
 * Ассеты MediaPipe со своего домена — см. заголовок файла и `vite.config.ts`.
 *
 * `tasksVisionFileSet` — КАТАЛОГ с wasm-рантаймом: библиотека сама выберет
 * simd- или nosimd-вариант по возможностям браузера, поэтому в каталоге лежат
 * оба. `modelAssetPath` — конкретный файл модели сегментации.
 */
const LOCAL_ASSET_PATHS = {
  tasksVisionFileSet: '/mediapipe/wasm',
  modelAssetPath: '/mediapipe/selfie_segmenter.tflite',
}

/**
 * Создать процессор замены фона на картинку `imagePath`.
 *
 * Библиотека грузится динамическим `import()` — первый вызов скачивает её
 * вместе с wasm и моделью, последующие берут из кэша модулей/браузера.
 */
export async function createBackgroundProcessor(
  imagePath: string,
): Promise<BackgroundProcessorWrapper> {
  const { BackgroundProcessor } = await import('@livekit/track-processors')
  return BackgroundProcessor({
    mode: 'virtual-background',
    imagePath,
    assetPaths: LOCAL_ASSET_PATHS,
  })
}

/**
 * Запустить процессор на «сыром» треке камеры — для превью входа, где
 * LiveKit-трека ещё нет (в комнате всё это делает сам
 * `LocalVideoTrack.setProcessor`).
 *
 * ⚠️ Процессору обязателен `element` — `<video>`, в который проигрывается
 * ИСХОДНЫЙ поток: библиотека читает из него кадры и падает
 * `TypeError: Currently only video transformers are supported`, если элемента
 * нет. Порядок ровно как у самого LiveKit (`LocalTrack.setProcessor`):
 * сначала `init`, только потом подключение потока и `play()`.
 *
 * Элемент в DOM не добавляется — он служебный, зритель видит уже обработанный
 * поток; возвращается вызывающему, чтобы тот освободил его вместе с процессором.
 */
export async function startProcessorOnTrack(
  processor: BackgroundProcessorWrapper,
  rawTrack: MediaStreamTrack,
): Promise<HTMLVideoElement> {
  const element = document.createElement('video')
  await processor.init({ kind: Track.Kind.Video, track: rawTrack, element })
  element.muted = true
  element.playsInline = true
  element.autoplay = true
  element.srcObject = new MediaStream([rawTrack])
  await element.play()
  return element
}
