import { isDeepStrictEqual } from "./node/polyfills.js";
import * as VAPI from "../artefacts/vapi/index.js";
import { Duration, enforce, enforce_nonnull, pause } from "vscript";
import { SDIConnection } from "./sdi_connections.js";
import { is_3G_or_higher, is_higher_than_3G, standard_info } from "./video.js";

export interface Attributes352 {
  pixel_format: VAPI.Video.PixelFormat;
  substream: VAPI.Video.SubStream;
}

export async function read_attributes(e: VAPI.Any.Video.Essence): Promise<Attributes352> {
  return {
    pixel_format: await e.pixel_format.read(),
    substream: (await e.substream.read()).stream,
  };
}

export async function patiently_read_attributes(pars: {
  essence: VAPI.Any.Video.Essence;
  expected_result: Attributes352;
  timeout: Duration;
}): Promise<{ result: Attributes352; ms_taken: number }> {
  const t0_ms = new Date().valueOf();
  const t_max = t0_ms + pars.timeout.ms();
  let result = await read_attributes(pars.essence);
  while (new Date().valueOf() < t_max) {
    if (isDeepStrictEqual(result, pars.expected_result)) {
      break;
    }
    await pause(new Duration(50, "ms"));
    result = await read_attributes(pars.essence);
  }
  return { result, ms_taken: new Date().valueOf() - t0_ms };
}

export function modify_352(
  std: VAPI.Video.Standard,
  attributes: Attributes352,
  incoming_352: number,
): number {
  let result = incoming_352;
  const set_bits = (bit_positions: number[], value: number) => {
    let rem = value;
    for (const bit_position of bit_positions) {
      result = (result & ~(1 << bit_position)) | ((rem & 1) << bit_position);
      rem >>= 1;
    }
    enforce(rem === 0);
    result = result >>> 0; // reinterpret as unsigned32
  };

  const lookup = <E extends string>(x: E, choices: Record<E, number>): number => {
    return choices[x];
  };

  const lookup_nullable = <E extends string>(
    x: E | null,
    choices: Partial<Record<E, number>>,
  ): number | null => {
    if (x === null) return null;
    return choices[x] ?? null;
  };
  set_bits(
    [5, 6, 7],
    lookup(attributes.substream, {
      QUAD_2SI_1: 0,
      QUAD_2SI_2: 1,
      QUAD_2SI_3: 2,
      QUAD_2SI_4: 3,
      UNSPECIFIED_CHANNEL_0: 0,
      UNSPECIFIED_CHANNEL_1: 1,
      UNSPECIFIED_CHANNEL_2: 2,
      UNSPECIFIED_CHANNEL_3: 3,
      UNSPECIFIED_CHANNEL_4: 4,
      UNSPECIFIED_CHANNEL_5: 5,
      UNSPECIFIED_CHANNEL_6: 6,
      UNSPECIFIED_CHANNEL_7: 7,
    }) ?? 0,
  );
  // TCS: identical for 2082-10:2018, 425-1:2017, 292-1:2018
  set_bits([20, 21], lookup_nullable(attributes.pixel_format.tc, { SDR: 0, HLG: 1, PQ: 2 }) ?? 3);
  // CS: mostly identical between 2082 & 425; ignoring Color VANC enum (@2082) for now
  const colorimetry_bits = (std: VAPI.Video.Standard) => {
    /* 2082-10:2018/425-1:2017 */
    if (is_3G_or_higher(std)) return [12, 13];
    const details = standard_info(std);
    switch (details.height) {
      case 1080:
        return [12, 15]; // 292-1:2018
      // case 720:
      //   break;
    }
    throw Error("please implement me");
  };
  set_bits(
    colorimetry_bits(std),
    lookup_nullable(attributes.pixel_format.colorspace, {
      BT2020: 2,
      BT2100: 2,
      BT709: 0,
    }) ?? 3,
  );
  // LCD/bit range: identical for 425-1:2017, 2082-10:2018
  set_bits([4], attributes.pixel_format.lcd == "YCbCr" ? 0 : 1);
  switch (standard_info(std).height) {
    case 720:
      // TODO -- ?
      break;
    case 1080:
      if (is_higher_than_3G(std)) {
        set_bits(
          [0, 1],
          lookup_nullable(attributes.pixel_format.bit_depth, {
            BitDepth8: 0,
            BitDepth10: 1,
            BitDepth10_FullRange: 1,
            BitDepth12: 2,
            BitDepth12_FullRange: 2,
          }) ?? 3,
        );
      } else {
        set_bits(
          [0, 1],
          lookup_nullable(attributes.pixel_format.bit_depth, {
            BitDepth10: 1,
            BitDepth10_FullRange: 1,
            BitDepth12: 2,
            BitDepth12_FullRange: 2,
          }) ?? 0,
        );
      }
      break;
    case 2160:
      // anything to do here?
      break;
  }
  return result;
}

export async function read_raw_352(sdi_in: VAPI.Any.IOModule.SDIInput): Promise<number> {
  const results: (number | null)[] = [];
  for (let i = 0; i < 2; ++i) {
    results.push(await sdi_in.hw_status.smpte_352_c.read());
    results.push(await sdi_in.hw_status.smpte_352_y.read());
    await pause(new Duration(200, "ms"));
  }
  const non_nulls = results.filter((r) => r !== null).map((x) => enforce_nonnull(x));
  enforce(
    new Set(non_nulls).size === 1,
    `Got ambiguous readings from ${sdi_in.raw.kwl}: [${results.join(", ")}]`,
  );
  return enforce_nonnull(non_nulls[0]);
}

export async function patiently_read_raw_352(pars: {
  sdi_input: VAPI.Any.IOModule.SDIInput;
  expected_result: number;
  timeout: Duration;
}): Promise<{ result: number; ms_taken: number }> {
  const t0_ms = new Date().valueOf();
  const t_max = t0_ms + pars.timeout.ms();
  let result = await read_raw_352(pars.sdi_input);
  while (new Date().valueOf() < t_max) {
    if (result === pars.expected_result) {
      break;
    }
    await pause(new Duration(50, "ms"));
    result = await read_raw_352(pars.sdi_input);
  }
  return { result, ms_taken: new Date().valueOf() - t0_ms };
}

export async function setup_352_source(
  desired_attributes: Attributes352,
  expected_attributes: Attributes352,
  sdi_connection: SDIConnection,
): Promise<VAPI.Any.Video.Essence> {
  const v_src = enforce_nonnull(
    await sdi_connection.src.sdi.v_src.status.read(),
    `Video input to ${
      sdi_connection.src.raw.kwl
    }@${sdi_connection.src.raw.backing_store.identify()}`,
  );

  await sdi_connection.src.sdi.vanc_control.override_smpte_352_payload.write(null);
  //  await sdi_connection.src.sdi.vanc_control.command.write(tt);
  await pause(new Duration(250, "ms")); // :(
  const native_352 = await read_raw_352(sdi_connection.dst.sdi);
  const src = enforce_nonnull(v_src.source);
  const modified_352 = modify_352(
    enforce_nonnull((await sdi_connection.src.sdi.standard.read()) ?? (await src.standard.read())),
    desired_attributes,
    native_352,
  );
  await sdi_connection.src.sdi.vanc_control.override_smpte_352_payload.write(0x123);
  await sdi_connection.dst.sdi.hw_status.smpte_352_c.wait_until((x) => x === 0x123);
  await sdi_connection.src.sdi.vanc_control.override_smpte_352_payload.write(modified_352);
  //await sdi_connection.src.sdi.vanc_control.payload.write(modified_352);
  await sdi_connection.dst.sdi.hw_status.smpte_352_c.wait_until((x) => x !== 0x123);
  const result = sdi_connection.dst.sdi.output.video;
  enforce(isDeepStrictEqual(await read_attributes(result), expected_attributes));
  return result;
}

export async function iter_352_attributes(
  f: (
    attrs_in: Attributes352,
    attrs_out: Attributes352,
    progress: { iteration: number; num_iterations: number },
  ) => Promise<void>,
) {
  const num_iterations =
    (1 + VAPI.Video.Enums.ColorSpace.length) *
    (1 + VAPI.Video.Enums.TransferCharacteristics.length) *
    VAPI.Video.Enums.LuminanceAndColorDifferenceSignal.length *
    2 * // FIXME: iterate over all available bitdepth choices (even if not supported in HW)?
    VAPI.Video.Enums.SubStream.length;
  let iteration = 0;
  for (const maybe_cs_in of [null, ...VAPI.Video.Enums.ColorSpace]) {
    let maybe_cs_out = maybe_cs_in;
    if (maybe_cs_in !== null) {
      // FIXME: this is fragile -- may break on any related schema change
      switch (maybe_cs_in) {
        case "BT601":
          maybe_cs_out = null;
          break;
        case "BT2100":
          maybe_cs_out = "BT2020";
          break;
      }
    }
    for (const maybe_tcs of [null, ...VAPI.Video.Enums.TransferCharacteristics]) {
      for (const lcd of VAPI.Video.Enums.LuminanceAndColorDifferenceSignal) {
        for (const substream_in of VAPI.Video.Enums.SubStream) {
          let substream_out = substream_in;
          switch (substream_in) {
            case "QUAD_2SI_1":
              // QUAD_2SI_(i+1) has the same wire representation as UNSPECIFIED_CHANNEL_i; this
              // routine will still have to be adapted to some tests that use the splitter flag
              substream_out = "UNSPECIFIED_CHANNEL_0";
              break;
            case "QUAD_2SI_2":
              substream_out = "UNSPECIFIED_CHANNEL_1";
              break;
            case "QUAD_2SI_3":
              substream_out = "UNSPECIFIED_CHANNEL_2";
              break;
            case "QUAD_2SI_4":
              substream_out = "UNSPECIFIED_CHANNEL_3";
              break;
          }
          const bit_depths: VAPI.Video.BitDepth[] = ["BitDepth10", "BitDepth10_FullRange"];
          for (const bit_depth of bit_depths) {
            await f(
              {
                pixel_format: { colorspace: maybe_cs_in, tc: maybe_tcs, lcd, bit_depth },
                substream: substream_in,
              },
              {
                pixel_format: { colorspace: maybe_cs_out, tc: maybe_tcs, lcd, bit_depth },
                substream: substream_out,
              },
              { iteration, num_iterations },
            );
            iteration++;
          }
        }
      }
    }
  }
}
