import { Any, AT1130, IOModule, VM } from "../artefacts/vapi/index.js";
import {
  asyncFilter,
  asyncIter,
  asyncZip,
  Duration,
  enforce,
  enforce_nonnull,
  Logger,
  pause,
  Watcher,
} from "vscript";
import { random_int } from "./random.js";
import { video_ref } from "./utils.js";

export interface SDIConnection {
  src: Any.IOModule.Output;
  dst: Any.IOModule.Input;
}

export async function wait_sdi_calibrated(o: Any.IOModule.Output) {
  if (o instanceof AT1130.IOModule.Output) {
    const tx_pll = enforce_nonnull(
      (await o.tx_clock.wait_until((c) => c !== null && c.pll !== null))?.pll,
    );
    await tx_pll.servo.wait_until((s) => s.state == "Calibrated");
  }
}

export async function findSDIConnections(pars: {
  /* list of machines we're allowed to temporarily disturb */
  shakeables: VM.Any[];
  /* list of machines we're not allowed to touch, but we suspect might be connected to some of our shakeables */
  bystanders: VM.Any[];
  verbose: boolean;
  log?: Logger;
}): Promise<SDIConnection[]> {
  const vms = await asyncFilter(pars.shakeables, async (vm) => {
    if (!vm.i_o_module) return false;
    return (
      (await vm.i_o_module.input.allocated_indices()).length +
        (await vm.i_o_module.output.allocated_indices()).length >
      0
    );
  });
  if (vms.length === 0) return [];
  const result: SDIConnection[] = [];
  const watchers: Watcher[] = [];
  const rollback: Array<() => Promise<void>> = [];
  const do_rollback = async () => {
    for (const action of rollback.reverse()) {
      await action();
    }
  };
  // TODO: temporarily reconfigure reconfigurable i/o's to output to maximize # of shakeable signal sinks
  try {
    // collect currently seen 352 payloads so we don't accidentally choose something that's
    // already present
    const current_payloads: Set<number> = new Set();
    const payload_sources: Map<number, Any.IOModule.Output> = new Map();
    const received_payloads: Map<Any.IOModule.Input, number | null> = new Map();
    await asyncIter(vms, async (vm) => {
      if (!vm.i_o_module || !vm.video_signal_generator) {
        return;
      }
      const old_std = await vm.video_signal_generator.instances
        .row(0)
        .output.standard.read();
      if (old_std !== "HD1080p50") {
        if (old_std !== null) {
          rollback.push(async () => {
            await vm.video_signal_generator?.instances
              .row(0)
              .standard.command.write(old_std);
          });
        }
        await vm.video_signal_generator.instances
          .row(0)
          .standard.command.write("HD1080p50");
      }
    });
    await asyncIter([...vms, ...pars.bystanders], async (vm) => {
      if (!vm.i_o_module) return;
      await asyncIter(await vm.i_o_module.input.rows(), async (input) => {
        const maybe_payload = await input.sdi.hw_status.smpte_352_c.read();
        if (maybe_payload !== null) current_payloads.add(maybe_payload);
        watchers.push(
          await input.sdi.hw_status.smpte_352_c.watch((smpte_352_c) => {
            received_payloads.set(input, smpte_352_c);
          }),
        );
      });
    });
    await asyncIter(vms, async (shakeable) => {
      if (
        !shakeable.i_o_module ||
        !shakeable.genlock ||
        !shakeable.video_signal_generator
      )
        return;
      await asyncIter(
        await shakeable.i_o_module.output.rows(),
        async (output) => {
          enforce(!!shakeable.genlock);
          enforce(!!shakeable.video_signal_generator);
          const genlock =
            shakeable instanceof AT1130.Root
              ? shakeable.genlock.instances.row(0).backend.output
              : shakeable.genlock.output;
          if ((await output.sdi.t_src.status.read()) === null) {
            await output.sdi.t_src.command.write(genlock);
          }
          if ((await output.sdi.standard.read()) === null) {
            const old_src = await output.sdi.v_src.status.read();
            rollback.push(async () => {
              await output.sdi.v_src.command.write(old_src as any);
            });
            await output.sdi.v_src.command.write(
              video_ref(
                shakeable.video_signal_generator.instances.row(0).output,
              ),
            );
          }

          let random_attempt = 0xffffffff;
          while (current_payloads.has(random_attempt)) {
            random_attempt = random_int(0, 0xffffffff, {
              prefer_boundaries: false,
            });
          }
          enforce(!current_payloads.has(random_attempt));
          current_payloads.add(random_attempt);
          const old_payload =
            await output.sdi.vanc_control.override_smpte_352_payload.read();
          rollback.push(async () => {
            await output.sdi.vanc_control.override_smpte_352_payload.write(
              old_payload,
            );
          });
          await output.sdi.vanc_control.override_smpte_352_payload.write(
            random_attempt,
          );
          payload_sources.set(random_attempt, output);
        },
      );
    });
    // shouldn't be necessary to wait that long, but let's rather be safe than sorry
    await pause(new Duration(2, "s"));
    for (const [input, maybe_payload] of received_payloads) {
      // FIXME: should also monitor signal liveness
      if (maybe_payload === null) continue;
      const source = payload_sources.get(maybe_payload);
      if (pars.verbose) {
        pars.log?.(
          `${
            source === undefined
              ? "<external>"
              : `${source.raw.kwl}@${source.raw.backing_store.ip}`
          } -> ${input.raw.kwl}@${input.raw.backing_store.ip}\n`,
          "Info",
        );
      }
      if (source) {
        result.push({ src: source, dst: input });
      }
    }
  } catch (e) {
    pars.log?.(`Error: ${e}\n`, "Error");
  } finally {
    await do_rollback();
    for (const watcher of watchers) {
      watcher.unwatch();
    }
  }
  return result;
}

interface SDISetupPars {
  log?: Logger;
  directions?: IOModule.ConfigDirection[];
}

export async function setup_sdi_io(
  vm: VM.Any,
  params?: SDISetupPars,
): Promise<[readonly Any.IOModule.Input[], readonly Any.IOModule.Output[]]> {
  const pars: Required<SDISetupPars> = {
    log:
      params?.log ??
      ((msg, level) => {
        console.log(`[${level}]: ` + msg);
      }),
    directions: [
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
      "Input",
    ],
    ...params,
  };
  pars.log(`Blade @${vm.raw.ip} setting up SDI IO`, "Debug");
  const io_type = enforce_nonnull(
    await vm.system.io_board.info.type.read(),
    "no IO module has been found; could you please plug one in and try again?",
  );
  pars.log(`Blade @${vm.raw.ip} found IO Board ${io_type.toString()}`, "Debug");
  enforce(!!vm.genlock, "Genlock software seems to be offline, aborting.");
  enforce(!!vm.i_o_module, "IO Module software seems to be offline, aborting.");
  const genlock =
    vm instanceof AT1130.Root
      ? vm.genlock.instances.row(0).backend.output
      : vm.genlock.output;
  enforce(!!genlock);
  switch (io_type) {
    case "IO_BNC_18_2":
    case "IO_BNC_10_10":
    case "IO_BNC_11_11":
    case "IO_BNC_2_18":
    case "IO_BNC_16_16":
    case "IO_BNC_11_11_GD32":
    case "IO_BNC_16_16_GD32":
      break;
    case "IO_BNC_16bidi_GD32":
    case "IO_MSC_v1":
    case "IO_MSC_v2_GD32":
    case "IO_MSC_v2":
    case "IO_BNC_16bidi":
    case "IO_BNC_2_2_16bidi":
      {
        const conf = await vm.i_o_module.configuration.rows();
        await asyncZip(conf, pars.directions, async (slot, dir, i) => {
          await slot.direction.write(dir, {
            retry_until: {
              criterion: "custom",
              validator: async () => {
                switch (dir) {
                  case "Input":
                    return await enforce_nonnull(
                      vm.i_o_module,
                    ).input.is_allocated(i);
                  case "Output":
                    return await enforce_nonnull(
                      vm.i_o_module,
                    ).output.is_allocated(i);
                }
              },
            },
          });
        });
      }
      break;
  }
  const sdi_in = await vm.i_o_module.input.rows();
  const sdi_out = await vm.i_o_module.output.rows();
  await asyncIter(sdi_in, async (io) => {
    if (vm instanceof AT1130.Root)
      await io.audio_timing.command.write({
        variant: "SynchronousOrSyntonous",
        value: {
          frequency: "F48000",
          genlock: vm.genlock!.instances.row(0),
        },
      });
    await io.mode.command.write("SDI");
  });
  await asyncIter(sdi_out, async (io) => {
    await io.mode.command.write("SDI");
    await io.sdi.t_src.command.write(genlock);
    await io.sdi.vanc_control.passthrough_c_y_0.command.write({
      c_unknown: true,
      y_obs: true,
      y_334_vbi: true,
      y_2020_amd: true,
      y_334_data: true,
      y_334_cea_608: true,
      y_rdd_8_op_47: true,
      y_334_cea_708_cdp: true,
      y_2010_ansi_scte_104: true,
      y_2031_dvb_scte_vbi: true,
      y_334_program: true,
      y_unknown: true,
    });
    await io.sdi.vanc_control.timecode_inserter.command.write({
      variant: "Passthrough",
      value: {},
    });
  });
  return [sdi_in, sdi_out];
}
