import addTextTrack from "../../../compat/add_text_track";
import type {
  ICompatTextTrack,
  IMediaElement,
} from "../../../compat/browser_compatibility_types";
import removeCue from "../../../compat/remove_cue";
import log from "../../../log";
import type { ITextTrackSegmentData } from "../../../transports";
import type { IRange } from "../../../utils/ranges";
import { convertToRanges } from "../../../utils/ranges";
import ManualTimeRanges from "../manual_time_ranges";
import type { ITextDisplayer, ITextDisplayerData } from "../types";
import parseTextTrackToCues from "./native_parsers";

/**
 * Implementation of an `ITextDisplayer` for "native" text tracks.
 * "Native" text tracks rely on a `<track>` HTMLElement and its associated
 * expected behavior to display subtitles synchronized to the video.
 * @class NativeTextDisplayer
 */
export default class NativeTextDisplayer implements ITextDisplayer {
  private readonly _videoElement: IMediaElement;
  private readonly _track: ICompatTextTrack;
  private readonly _trackElement: HTMLTrackElement | undefined;

  private _buffered: ManualTimeRanges;

  /**
   * @param {HTMLMediaElement} videoElement
   */
  constructor(videoElement: IMediaElement) {
    log.debug("text", "Creating NativeTextDisplayer");
    const { track, trackElement } = addTextTrack(videoElement);
    this._buffered = new ManualTimeRanges();
    this._videoElement = videoElement;
    this._track = track;
    this._trackElement = trackElement;
  }

  /**
   * Push text segment to the NativeTextDisplayer.
   * @param {Object} infos
   * @returns {Object}
   */
  public pushTextData(infos: ITextDisplayerData): IRange[] {
    log.debug("text", "Appending new native text tracks");
    if (infos.chunk === null) {
      return convertToRanges(this._buffered);
    }
    const { timestampOffset, appendWindow, chunk } = infos;
    const {
      start: startTime,
      end: endTime,
      data: dataString,
      type,
      language,
      initTimescale,
    } = chunk;
    const appendWindowStart = appendWindow[0] ?? 0;
    const appendWindowEnd = appendWindow[1] ?? Infinity;
    const cues = parseTextTrackToCues(
      type,
      dataString,
      { initTimescale, language },
      timestampOffset,
    );

    if (appendWindowStart !== 0 && appendWindowEnd !== Infinity) {
      // Removing before window start
      let i = 0;
      while (i < cues.length && cues[i].endTime <= appendWindowStart) {
        i++;
      }
      cues.splice(0, i);

      i = 0;
      while (i < cues.length && cues[i].startTime < appendWindowStart) {
        cues[i].startTime = appendWindowStart;
        i++;
      }

      // Removing after window end
      i = cues.length - 1;

      while (i >= 0 && cues[i].startTime >= appendWindowEnd) {
        i--;
      }
      cues.splice(i, cues.length);

      i = cues.length - 1;
      while (i >= 0 && cues[i].endTime > appendWindowEnd) {
        cues[i].endTime = appendWindowEnd;
        i--;
      }
    }

    let start: number;
    if (startTime !== undefined) {
      start = Math.max(appendWindowStart, startTime);
    } else {
      if (cues.length <= 0) {
        log.warn("text", "Current text tracks have no cues nor start time. Aborting");
        return convertToRanges(this._buffered);
      }
      log.warn("text", "No start time given. Guessing from cues.");
      start = cues[0].startTime;
    }

    let end: number;
    if (endTime !== undefined) {
      end = Math.min(appendWindowEnd, endTime);
    } else {
      if (cues.length <= 0) {
        log.warn("text", "Current text tracks have no cues nor end time. Aborting");
        return convertToRanges(this._buffered);
      }
      log.warn("text", "No end time given. Guessing from cues.");
      end = cues[cues.length - 1].endTime;
    }

    if (end <= start) {
      log.warn(
        "text",
        "Invalid text track appended: ",
        "the start time is inferior or equal to the end time.",
      );
      return convertToRanges(this._buffered);
    }

    if (cues.length > 0) {
      const firstCue = cues[0];

      // NOTE(compat): cleanup all current cues if the newly added
      // ones are in the past. this is supposed to fix an issue on
      // IE/Edge.
      // TODO Move to compat
      const currentCues = this._track.cues;
      if (currentCues !== null && currentCues.length > 0) {
        if (firstCue.startTime < currentCues[currentCues.length - 1].startTime) {
          this._removeData(firstCue.startTime, +Infinity);
        }
      }

      for (const cue of cues) {
        this._track.addCue(cue);
      }
    }
    this._buffered.insert(start, end);
    return convertToRanges(this._buffered);
  }

  /**
   * Remove buffered data.
   * @param {number} start - start position, in seconds
   * @param {number} end - end position, in seconds
   * @returns {Object}
   */
  public removeBuffer(start: number, end: number): IRange[] {
    this._removeData(start, end);
    return convertToRanges(this._buffered);
  }

  /**
   * Returns the currently buffered data, in a TimeRanges object.
   * @returns {Array.<Object>}
   */
  public getBufferedRanges(): IRange[] {
    return convertToRanges(this._buffered);
  }

  public reset(): void {
    log.debug("text", "Aborting NativeTextDisplayer");
    this._removeData(0, Infinity);
    this._clearTrackElement();
  }

  public stop(): void {
    log.debug("text", "Aborting NativeTextDisplayer");
    this._removeData(0, Infinity);
    const { _trackElement, _videoElement } = this;

    if (_trackElement !== undefined && _videoElement.hasChildNodes()) {
      try {
        _videoElement.removeChild(_trackElement);
      } catch (_e) {
        log.warn("text", "Can't remove track element from the video");
      }
    }

    this._track.mode = "disabled";

    if (this._trackElement !== undefined) {
      this._trackElement.innerHTML = "";
    }
  }

  private _removeData(start: number, end: number): void {
    log.debug("text", "Removing native text track data", { start, end });
    const track = this._track;
    const cues = track.cues;
    if (cues !== null) {
      for (let i = cues.length - 1; i >= 0; i--) {
        const cue = cues[i];
        const { startTime, endTime } = cue;
        if (startTime >= start && startTime <= end && endTime <= end) {
          removeCue(track, cue);
        }
      }
    }
    this._buffered.remove(start, end);
  }

  private _clearTrackElement(): void {
    const { _trackElement, _videoElement } = this;

    if (_trackElement !== undefined && _videoElement.hasChildNodes()) {
      try {
        _videoElement.removeChild(_trackElement);
      } catch (_e) {
        log.warn("text", "Can't remove track element from the video");
      }
    }

    // Ugly trick to work-around browser bugs by refreshing its mode
    const oldMode = this._track.mode;
    this._track.mode = "disabled";
    this._track.mode = oldMode;

    if (this._trackElement !== undefined) {
      this._trackElement.innerHTML = "";
    }
  }
}

/** Data of chunks that should be pushed to the NativeTextDisplayer. */
export interface INativeTextTracksBufferSegmentData {
  /** The text track data, in the format indicated in `type`. */
  data: string | BufferSource;
  /** The format of `data` (examples: "ttml", "srt" or "vtt") */
  type: string;
  /**
   * Language in which the text track is, as a language code.
   * This is mostly needed for "sami" subtitles, to know which cues can / should
   * be parsed.
   */
  language?: string | undefined;
  /** start time from which the segment apply, in seconds. */
  start?: number | undefined;
  /** end time until which the segment apply, in seconds. */
  end?: number | undefined;
}

/*
 * The following ugly code is here to provide a compile-time check that an
 * `INativeTextTracksBufferSegmentData` (type of data pushed to a
 * `NativeTextDisplayer`) can be derived from a `ITextTrackSegmentData`
 * (text track data parsed from a segment).
 *
 * It doesn't correspond at all to real code that will be called. This is just
 * a hack to tell TypeScript to perform that check.
 */
// @ts-expect-error: uncalled function just for type checking
function _checkType(input: ITextTrackSegmentData): void {
  function checkEqual(_arg: INativeTextTracksBufferSegmentData): void {
    /* nothing */
  }
  checkEqual(input);
}
