import BinarySearch from '../utils/binary-search';
import type { Fragment, MediaFragment } from '../loader/fragment';
import type { LevelDetails } from '../loader/level-details';

/**
 * Returns first fragment whose endPdt value exceeds the given PDT, or null.
 * @param fragments - The array of candidate fragments
 * @param PDTValue - The PDT value which must be exceeded
 * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous
 */
export function findFragmentByPDT(
  fragments: MediaFragment[],
  PDTValue: number | null,
  maxFragLookUpTolerance: number,
): MediaFragment | null {
  if (
    PDTValue === null ||
    !Array.isArray(fragments) ||
    !fragments.length ||
    !Number.isFinite(PDTValue)
  ) {
    return null;
  }

  // if less than start
  const startPDT = fragments[0].programDateTime;
  if (PDTValue < (startPDT || 0)) {
    return null;
  }

  const endPDT = fragments[fragments.length - 1].endProgramDateTime;
  if (PDTValue >= (endPDT || 0)) {
    return null;
  }

  for (let seg = 0; seg < fragments.length; ++seg) {
    const frag = fragments[seg];
    if (pdtWithinToleranceTest(PDTValue, maxFragLookUpTolerance, frag)) {
      return frag;
    }
  }

  return null;
}

/**
 * Finds a fragment based on the SN of the previous fragment; or based on the needs of the current buffer.
 * This method compensates for small buffer gaps by applying a tolerance to the start of any candidate fragment, thus
 * breaking any traps which would cause the same fragment to be continuously selected within a small range.
 * @param fragPrevious - The last frag successfully appended
 * @param fragments - The array of candidate fragments
 * @param bufferEnd - The end of the contiguous buffered range the playhead is currently within
 * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous
 * @returns a matching fragment or null
 */
export function findFragmentByPTS(
  fragPrevious: MediaFragment | null,
  fragments: MediaFragment[],
  bufferEnd: number = 0,
  maxFragLookUpTolerance: number = 0,
  nextFragLookupTolerance: number = 0.005,
): MediaFragment | null {
  let fragNext: MediaFragment | null = null;
  if (fragPrevious) {
    fragNext = fragments[1 + fragPrevious.sn - fragments[0].sn] || null;
    // check for buffer-end rounding error
    const bufferEdgeError = (fragPrevious.endDTS as number) - bufferEnd;
    if (bufferEdgeError > 0 && bufferEdgeError < 0.0000015) {
      bufferEnd += 0.0000015;
    }
    if (
      fragNext &&
      fragPrevious.level !== fragNext.level &&
      fragNext.end <= fragPrevious.end
    ) {
      fragNext = fragments[2 + fragPrevious.sn - fragments[0].sn] || null;
    }
  } else if (bufferEnd === 0 && fragments[0].start === 0) {
    fragNext = fragments[0];
  }
  // Prefer the next fragment if it's within tolerance
  if (
    fragNext &&
    (((!fragPrevious || fragPrevious.level === fragNext.level) &&
      fragmentWithinToleranceTest(
        bufferEnd,
        maxFragLookUpTolerance,
        fragNext,
      ) === 0) ||
      fragmentWithinFastStartSwitch(
        fragNext,
        fragPrevious,
        Math.min(nextFragLookupTolerance, maxFragLookUpTolerance),
      ))
  ) {
    return fragNext;
  }
  // We might be seeking past the tolerance so find the best match
  const foundFragment = BinarySearch.search(
    fragments,
    fragmentWithinToleranceTest.bind(null, bufferEnd, maxFragLookUpTolerance),
  );
  if (foundFragment && (foundFragment !== fragPrevious || !fragNext)) {
    return foundFragment;
  }
  // If no match was found return the next fragment after fragPrevious, or null
  return fragNext;
}

function fragmentWithinFastStartSwitch(
  fragNext: Fragment,
  fragPrevious: Fragment | null,
  nextFragLookupTolerance: number,
): boolean {
  if (
    fragPrevious?.start === 0 &&
    fragPrevious.level < fragNext.level &&
    (fragPrevious.endPTS || 0) > 0
  ) {
    const firstDuration = fragPrevious.tagList.reduce((duration, tag) => {
      if (tag[0] === 'INF') {
        duration += parseFloat(tag[1]);
      }
      return duration;
    }, nextFragLookupTolerance);
    return fragNext.start <= firstDuration;
  }
  return false;
}

/**
 * The test function used by the findFragmentBySn's BinarySearch to look for the best match to the current buffer conditions.
 * @param candidate - The fragment to test
 * @param bufferEnd - The end of the current buffered range the playhead is currently within
 * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous
 * @returns 0 if it matches, 1 if too low, -1 if too high
 */
export function fragmentWithinToleranceTest(
  bufferEnd = 0,
  maxFragLookUpTolerance = 0,
  candidate: MediaFragment,
) {
  // eagerly accept an accurate match (no tolerance)
  if (
    candidate.start <= bufferEnd &&
    candidate.start + candidate.duration > bufferEnd
  ) {
    return 0;
  }
  // offset should be within fragment boundary - config.maxFragLookUpTolerance
  // this is to cope with situations like
  // bufferEnd = 9.991
  // frag[Ø] : [0,10]
  // frag[1] : [10,20]
  // bufferEnd is within frag[0] range ... although what we are expecting is to return frag[1] here
  //              frag start               frag start+duration
  //                  |-----------------------------|
  //              <--->                         <--->
  //  ...--------><-----------------------------><---------....
  // previous frag         matching fragment         next frag
  //  return -1             return 0                 return 1
  // logger.log(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`);
  // Set the lookup tolerance to be small enough to detect the current segment - ensures we don't skip over very small segments
  const candidateLookupTolerance = Math.min(
    maxFragLookUpTolerance,
    candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0),
  );
  if (
    candidate.start + candidate.duration - candidateLookupTolerance <=
    bufferEnd
  ) {
    return 1;
  } else if (
    candidate.start - candidateLookupTolerance > bufferEnd &&
    candidate.start
  ) {
    // if maxFragLookUpTolerance will have negative value then don't return -1 for first element
    return -1;
  }

  return 0;
}

/**
 * The test function used by the findFragmentByPdt's BinarySearch to look for the best match to the current buffer conditions.
 * This function tests the candidate's program date time values, as represented in Unix time
 * @param candidate - The fragment to test
 * @param pdtBufferEnd - The Unix time representing the end of the current buffered range
 * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous
 * @returns true if contiguous, false otherwise
 */
export function pdtWithinToleranceTest(
  pdtBufferEnd: number,
  maxFragLookUpTolerance: number,
  candidate: MediaFragment,
): boolean {
  const candidateLookupTolerance =
    Math.min(
      maxFragLookUpTolerance,
      candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0),
    ) * 1000;

  // endProgramDateTime can be null, default to zero
  const endProgramDateTime = candidate.endProgramDateTime || 0;
  return endProgramDateTime - candidateLookupTolerance > pdtBufferEnd;
}

export function findNearestWithCC(
  details: LevelDetails | undefined,
  cc: number,
  pos: number,
): MediaFragment | null {
  if (details) {
    if (details.startCC <= cc && details.endCC >= cc) {
      let fragments = details.fragments;
      const { fragmentHint } = details;
      if (fragmentHint) {
        fragments = fragments.concat(fragmentHint);
      }
      let closest: MediaFragment | undefined;
      BinarySearch.search(fragments, (candidate) => {
        if (candidate.cc < cc) {
          return 1;
        }
        if (candidate.cc > cc) {
          return -1;
        }
        closest = candidate;
        if (candidate.end <= pos) {
          return 1;
        }
        if (candidate.start > pos) {
          return -1;
        }
        return 0;
      });
      return closest || null;
    }
  }
  return null;
}

export function getNextFrag(
  details: LevelDetails,
  sn: number,
): MediaFragment | null {
  if (sn === details.endSN && details.fragmentHint) {
    return details.fragmentHint;
  }
  return details.fragments[1 + sn - details.startSN] || null;
}
