/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2026 metamuffin <metamuffin.org>
*/
import { OVar, show } from "../../jshelper/mod.ts";
import { test_media_capability, track_to_content_type } from "../mediacaps.ts";
import { BufferRange, Player } from "../player.ts";
import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts";
import { e } from "../../jshelper/src/element.ts";
import { FormatInfo, FragmentIndex, StreamContainer, TrackInfo } from "../types_stream.ts";
import { show_format } from "../mod.ts";

interface UsableFormat { format_index: number, usable_index: number, format: FormatInfo, container: StreamContainer }

export class MSEPlayerTrack extends PlayerTrack {
  public source_buffer!: SourceBuffer;
  private current_load?: AppendRange;
  private loading = new Set<number>();
  private append_queue: AppendRange[] = [];
  public index?: FragmentIndex
  public active_format = new OVar<UsableFormat | undefined>(undefined);
  public usable_formats: UsableFormat[] = []

  constructor(
    private player: Player,
    private base_url: string,
    track_index: number,
    public trackinfo: TrackInfo,
  ) {
    super(track_index);
    this.init()
  }

  async init() {
    this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }]
    try {
      const res = await fetch(`${this.base_url}?fragmentindex&t=${this.track_index}`, { headers: { "Accept": "application/json" } });
      if (!res.ok) return this.player.error.value = "Cannot download index.", undefined;
      let index!: FragmentIndex & { error: string; };
      try { index = await res.json(); }
      catch (_) { this.player.set_pers("Error: Failed to fetch node"); }
      if (index.error) return this.player.set_pers("server error: " + index.error), undefined;
      this.index = index
    } catch (e) {
      if (e instanceof TypeError) {
        this.player.set_pers("Cannot download index: Network Error");
      } else throw e;
    }
    this.buffered.value = []

    console.log(this.trackinfo);

    for (let i = 0; i < this.trackinfo.formats.length; i++) {
      const format = this.trackinfo.formats[i];
      for (const container of format.containers) {
        if (container != "webm" && container != "mpeg4") continue;
        if (await test_media_capability(format, container))
          this.usable_formats.push({ container, format, format_index: i, usable_index: this.usable_formats.length })
      }
    }

    // TODO prefer newer codecs
    const sort_key = (f: FormatInfo) => f.remux ? Infinity : f.bitrate
    this.usable_formats.sort((a, b) => sort_key(b.format) - sort_key(a.format))
    if (!this.usable_formats.length)
      return this.player.logger?.log("No availble format is supported by this device. The track can't be played back.")
    this.active_format.value = this.usable_formats[0]

    const ct = track_to_content_type(this.active_format.value!.format, this.active_format.value!.container);
    this.source_buffer = this.player.media_source.addSourceBuffer(ct);
    this.abort.signal.addEventListener("abort", () => {
      console.log(`destroy source buffer for track ${this.track_index}`);
      this.player.media_source.removeSourceBuffer(this.source_buffer);
    });
    this.source_buffer.mode = "segments";
    this.source_buffer.addEventListener("updateend", () => {
      if (this.abort.signal.aborted) return;
      if (this.current_load) {
        this.loading.delete(this.current_load.index);
        const cb = this.current_load.cb;
        this.current_load = undefined;
        cb()
      } else {
        console.warn("updateend but nothing is loading")
      }
      this.update_buf_ranges();
      this.tick_append();
    });
    this.source_buffer.addEventListener("error", e => {
      console.error("sourcebuffer error", e);
    });
    this.source_buffer.addEventListener("abort", e => {
      console.error("sourcebuffer abort", e);
    });

    this.update(this.player.video.currentTime)
  }

  choose_format() {
    if (!this.active_format.value) return
    const ai = this.active_format.value.usable_index
    const current_br = this.active_format.value.format.bitrate
    const avail_br = this.player.downloader.bandwidth_avail.value * 8
    const prev_format = this.active_format.value
    if (ai < this.usable_formats.length - 1 && current_br > avail_br) {
      this.active_format.value = this.usable_formats[ai + 1]
    }
    if (ai > 0 && avail_br > this.usable_formats[ai - 1].format.bitrate * 1.2) {
      this.active_format.value = this.usable_formats[ai - 1]
    }
    if (prev_format != this.active_format.value) {
      console.log(`abr ${show.metric(prev_format.format.bitrate)} -> ${show.metric(this.active_format.value.format.bitrate)}`);
      this.choose_format()
    }
  }

  update_buf_ranges() {
    if (!this.index) return;
    const ranges: BufferRange[] = [];
    for (let i = 0; i < this.source_buffer.buffered.length; i++) {
      ranges.push({
        start: this.source_buffer.buffered.start(i),
        end: this.source_buffer.buffered.end(i),
        status: "buffered"
      });
    }
    for (const r of this.loading) {
      ranges.push({ ...this.index[r], status: "loading" });
    }
    this.buffered.value = ranges;
  }

  override async update(target: number) {
    if (!this.index) return;
    this.update_buf_ranges(); // TODO required?

    const buffer_to = target + (target < 20 ? Math.max(1, target) : TARGET_BUFFER_DURATION)

    const blocking = [];
    for (let i = 0; i < this.index.length; i++) {
      const frag = this.index[i];
      if (frag.end < target) continue;
      if (frag.start >= buffer_to) break;
      if (!this.check_buf_collision(frag.start, frag.end)) continue;
      if (frag.start <= target + MIN_BUFFER_DURATION)
        blocking.push(this.load(i));
      else
        this.load(i);
    }
    await Promise.all(blocking);
  }
  check_buf_collision(start: number, end: number) {
    const EPSILON = 0.01;
    for (const r of this.buffered.value)
      if (r.end - EPSILON > start && r.start < end - EPSILON)
        return false;
    return true;
  }

  async load(index: number) {
    this.choose_format()
    this.loading.add(index);
    this.update_buf_ranges()
    // TODO update format selection
    const url = `${this.base_url}?fragment&t=${this.track_index}&f=${this.active_format.value!.format_index}&i=${index}&c=${this.active_format.value!.container}`;
    const buf = await this.player.downloader.download(url);
    await new Promise<void>(cb => {
      if (!this.index) return;
      if (this.abort.signal.aborted) return;
      this.append_queue.push({ buf, ...this.index[index], index, cb });
      this.tick_append();
    });
  }
  tick_append() {
    if (this.source_buffer.updating || this.current_load) return;
    if (this.append_queue.length) {
      const frag = this.append_queue[0];
      this.append_queue.splice(0, 1);
      this.current_load = frag;
      // TODO why is appending so unreliable?! sometimes it does not add it
      this.source_buffer.changeType(track_to_content_type(this.active_format.value!.format, this.active_format.value!.container));
      this.source_buffer.timestampOffset = this.active_format.value?.container == "mpeg4" ? frag.start : 0
      // this.source_buffer.timestampOffset = this.trackinfo.kind == "video" && !this.active_format.value!.format.remux ? frag.start : 0
      // this.source_buffer.timestampOffset = this.active_format.value?.format.remux ? 0 : frag.start
      // this.source_buffer.timestampOffset = 0
      this.source_buffer.appendBuffer(frag.buf);
    }
  }

  public debug(): OVar<HTMLElement> {
    const rtype = (t: string, b: BufferRange[]) => {
      const c = b.filter(r => r.status == t);
      // ${c.length} range${c.length != 1 ? "s" : ""}
      return `${c.reduce((a, v) => a + v.end - v.start, 0).toFixed(2)}s`
    }
    return this.active_format.liftA2(this.buffered, (p, b) =>
      e("pre",
        p ?
          `mse track ${this.track_index}: format ${p.format_index} (${p.format.remux ? "remux" : "transcode"})`
          + `\n\tformat: ${show_format(p.format)}`
          + `\n\tbuffer type: ${track_to_content_type(p.format, p.container)}`
          + `\n\tbuffered: ${rtype("buffered", b)} / queued: ${rtype("queued", b)} / loading: ${rtype("loading", b)}`
          : ""
      ) as HTMLElement
    )
  }
}
