export const U16_MAX_SIZE = 0xffff;
export const U32_MAX_SIZE = 0xffffffff;

/**
 * A number of fields withing the data tracks packet specification assume wrap around behavior when
 * an unsigned type is incremented beyond its max size (ie, the packet `sequence` field). This
 * wrapper type manually reimplements this wrap around behavior given javascript's lack of fixed
 * size integer types.
 */
export class WrapAroundUnsignedInt<MaxSize extends number> {
  value: number;

  private maxSize: MaxSize;

  static u16(raw: number) {
    return new WrapAroundUnsignedInt(raw, U16_MAX_SIZE);
  }

  static u32(raw: number) {
    return new WrapAroundUnsignedInt(raw, U32_MAX_SIZE);
  }

  constructor(raw: number, maxSize: MaxSize) {
    this.value = raw;
    if (raw < 0) {
      throw new Error(
        'WrapAroundUnsignedInt: cannot faithfully represent an integer smaller than 0',
      );
    }
    if (maxSize > Number.MAX_SAFE_INTEGER) {
      throw new Error(
        'WrapAroundUnsignedInt: cannot faithfully represent an integer bigger than MAX_SAFE_INTEGER.',
      );
    }
    this.maxSize = maxSize;
    this.clamp();
  }

  /** Manually clamp the given containing value according to the wrap around max size bounds. Use
   * this after out of bounds modification to the contained value by external code. */
  clamp() {
    while (this.value > this.maxSize) {
      this.value -= this.maxSize + 1;
    }
    while (this.value < 0) {
      this.value += this.maxSize + 1;
    }
  }

  clone() {
    return new WrapAroundUnsignedInt(this.value, this.maxSize);
  }

  /** When called, maps the containing value to a new containing value. After mapping, the wrap
   * around external max size bounds are applied. Note that this is a mutative operation. */
  update(updateFn: (value: number) => number) {
    this.value = updateFn(this.value);
    this.clamp();
  }

  /** Increments the given `n` to the inner value. Note that this is a mutative operation. */
  increment(n = 1) {
    this.update((value) => value + n);
  }

  /** Decrements the given `n` from the inner value. Note that this is a mutative operation. */
  decrement(n = 1) {
    this.update((value) => value - n);
  }

  getThenIncrement() {
    const previousValue = this.value;
    this.increment();
    return new WrapAroundUnsignedInt(previousValue, this.maxSize);
  }

  /** Returns true if {@link this} is before the passed other {@link WrapAroundUnsignedInt}. */
  isBefore(other: WrapAroundUnsignedInt<MaxSize>) {
    const a = this.value >>> 0;
    const b = other.value >>> 0;
    const diff = (b - a) >>> 0;
    return diff !== 0 && diff < this.maxSize + 1;
  }
}

export class DataTrackTimestamp<RateInHz extends number> {
  rateInHz: RateInHz;

  timestamp: WrapAroundUnsignedInt<typeof U32_MAX_SIZE>;

  static fromRtpTicks(rtpTicks: number) {
    return new DataTrackTimestamp(rtpTicks, 90_000);
  }

  /** Generates a timestamp initialized to a non cryptographically secure random value, so that
   * different streams are more difficult to correlate in packet capture. */
  static rtpRandom() {
    const randomValue = Math.round(Math.random() * U32_MAX_SIZE);
    return DataTrackTimestamp.fromRtpTicks(randomValue);
  }

  private constructor(raw: number, rateInHz: RateInHz) {
    this.timestamp = WrapAroundUnsignedInt.u32(raw);
    this.rateInHz = rateInHz;
  }

  asTicks() {
    return this.timestamp.value;
  }

  clone() {
    return new DataTrackTimestamp(this.timestamp.value, this.rateInHz);
  }

  wrappingAdd(n: number) {
    this.timestamp.increment(n);
  }

  /** Returns true if {@link this} is before the passed other {@link DataTrackTimestamp}. */
  isBefore(other: DataTrackTimestamp<RateInHz>) {
    return this.timestamp.isBefore(other.timestamp);
  }
}

export class DataTrackClock<RateInHz extends number> {
  epoch: Date;

  base: DataTrackTimestamp<RateInHz>;

  previous: DataTrackTimestamp<RateInHz>;

  rateInHz: RateInHz;

  private constructor(rateInHz: RateInHz, epoch: Date, base: DataTrackTimestamp<RateInHz>) {
    this.epoch = epoch;
    this.base = base;
    this.previous = base.clone();
    this.rateInHz = rateInHz;
  }

  static startingNow<RateInHz extends number>(
    base: DataTrackTimestamp<RateInHz>,
    rateInHz: RateInHz,
  ) {
    return new DataTrackClock(rateInHz, new Date(), base);
  }

  static startingAtTime<RateInHz extends number>(
    epoch: Date,
    base: DataTrackTimestamp<RateInHz>,
    rateInHz: RateInHz,
  ) {
    return new DataTrackClock(rateInHz, epoch, base);
  }

  static rtpStartingNow(base: DataTrackTimestamp<90_000>) {
    return DataTrackClock.startingNow(base, 90_000);
  }

  static rtpStartingAtTime(epoch: Date, base: DataTrackTimestamp<90_000>) {
    return DataTrackClock.startingAtTime(epoch, base, 90_000);
  }

  now(): DataTrackTimestamp<RateInHz> {
    return this.at(new Date());
  }

  at(timestamp: Date) {
    let elapsedMs = timestamp.getTime() - this.epoch.getTime();
    let durationTicks = DataTrackClock.durationInMsToTicks(elapsedMs, this.rateInHz);

    let result = this.base.clone();
    result.wrappingAdd(durationTicks);

    // Enforce monotonicity in RTP wraparound space
    if (result.isBefore(this.previous)) {
      result = this.previous;
    }
    this.previous = result.clone();
    return result.clone();
  }

  /** Convert a duration since the epoch into clock ticks. */
  static durationInMsToTicks(durationMilliseconds: number, rateInHz: number) {
    // round(nanos * rate_hz / 1e9)
    let durationNanoseconds = durationMilliseconds * 1e6;
    let ticks = (durationNanoseconds * rateInHz + 500_000_000) / 1_000_000_000;
    return Math.round(ticks);
  }
}

export function coerceToDataView<Input extends DataView | ArrayBuffer | Uint8Array>(
  input: Input,
): DataView {
  if (input instanceof DataView) {
    return input;
  } else if (input instanceof ArrayBuffer) {
    return new DataView(input);
  } else if (input instanceof Uint8Array) {
    return new DataView(input.buffer, input.byteOffset, input.byteLength);
  } else {
    throw new Error(
      `Error coercing ${input} to DataView - input was not DataView, ArrayBuffer, or Uint8Array.`,
    );
  }
}
