import * as VAPI from "../artefacts/vapi/index.js";
import {
  asyncFilterMap,
  asyncFind,
  asyncIter,
  Duration,
  enforce,
  enforce_nonnull,
  Logger,
  same,
} from "vscript";
import { await_genlock_calibration } from "./genlock.js";
import { RollbackStack } from "./rollback_stack.js";
import { streak } from "./utils.js";

export interface PTPPars {
  ptp_domain: number;
  mode: "SlaveOnly" | "FreerunMaster" | "GPSMaster";
  log: Logger;
  delay_resp_mode: "Unicast" | "Multicast";
  ttl: number;
  log2_delayreq_interval: number;
  log2_announce_interval: number;
  log2_sync_interval: number;
  await_calibration: boolean;
  locking_policy: "Locking" | "Dynamic";
}

export async function setup_ptp(vm: VAPI.VM.Any, parameters?: Partial<PTPPars>) {
  const pars: Required<PTPPars> = {
    log2_delayreq_interval: -3,
    log2_announce_interval: -2,
    log2_sync_interval: -3,
    mode: "SlaveOnly",
    ptp_domain: 127,
    delay_resp_mode: "Unicast",
    ttl: 1,
    log: (msg, level) => {
      console.log(`[${level}]: ` + msg);
    },
    await_calibration: parameters?.await_calibration ?? true,
    locking_policy: "Locking",
    ...parameters,
  };

  enforce(!!pars);
  pars?.log?.(
    `Setting Up PTP for ${vm.raw.identify()} with mode: ${pars.mode} on domain: ${pars.ptp_domain}`,
    "Debug",
  );
  await vm.p_t_p_clock.mode.write(
    pars.mode === "FreerunMaster" ? "UseInternalOscillator" : "LockToInput",
  );
  const domain = pars.ptp_domain;
  const mode = pars.mode;
  const agents = await vm.p_t_p_flows.agents.ensure_allocated(2, "atleast");
  pars?.log?.(
    `Setting combinator for ${vm.raw.identify()} with mode: ${pars.mode} on domain: ${
      pars.ptp_domain
    }`,
    "Debug",
  );
  const combinator = await vm.time_flows.combinators.create_row({
    index: 0,
    allow_reuse_row: true,
  });
  await combinator.quorum.command.write(1);
  pars?.log?.(
    `Setting Up PTP Agents for ${vm.raw.identify()} with mode: ${pars.mode} on domain: ${
      pars.ptp_domain
    }`,
    "Debug",
  );
  await asyncIter(await vm.p_t_p_flows.ports.rows(), async (p) => {
    await p.multicast_event_ttl.command.write(pars.ttl);
    await p.multicast_general_ttl.command.write(pars.ttl);
  });
  await asyncIter(agents, async (agent, index) => {
    await agent.domain.command.write(domain);
    await agent.master_settings.t_src.command.write(
      mode != "SlaveOnly" ? vm.p_t_p_clock.output : null,
    );
    await agent.mode.write(pars.mode === "SlaveOnly" ? "SlaveOnly" : "MasterOnly");
    await agent.hosting_port.command.write(vm.p_t_p_flows.ports.row(index));
    await agent.master_settings.delay_resp_routing.command.write(pars.delay_resp_mode);
    await agent.master_settings.log2_sync_interval.command.write(pars.log2_sync_interval);
    await agent.master_settings.log2_announce_interval.command.write(pars.log2_announce_interval);
    await agent.slave_settings.delay_req_routing.command.write(pars.delay_resp_mode);
    await agent.slave_settings.log2_delayreq_interval.command.write(pars.log2_delayreq_interval);
  });
  await combinator.t_src.command.write([
    agents[0].output,
    agents[1].output,
    null,
    null,
    null,
    null,
    null,
    null,
  ]);
  const ptp_target = () => {
    switch (pars.mode) {
      case "FreerunMaster":
      case "SlaveOnly":
        return combinator.output;
      case "GPSMaster":
        return vm.master_clock.gps_receivers.row(0).output;
    }
  };
  await vm.p_t_p_clock.t_src.command.write(ptp_target());
  await vm.p_t_p_clock.parameters.locking_policy.write(pars.locking_policy);
  pars?.log?.(
    `Waiting for PTPClock to calibrate on ${vm.raw.identify()} with mode: ${pars.mode} on domain: ${
      pars.ptp_domain
    }`,
    "Debug",
  );
  enforce(!!vm.genlock);
  if (vm instanceof VAPI.AT1130.Root) {
    await asyncIter([...vm.genlock.instances], async (instance) => {
      await instance.t_src.command.write(vm.p_t_p_clock.output);
      await Promise.all(
        [instance.settings.audio, instance.settings.video].map(async (settings) => {
          const oldsettings = await settings.servo.status.read();
          await settings.servo.command.write({
            ...oldsettings,
            locking_policy: pars.locking_policy,
          });
        }),
      );
    });
  }

  // Wait for calibration
  if (!pars.await_calibration) {
    pars.log(`${vm.raw.identify()} - Setup PTP: Exiting before awaiting calibration`, "Info");
    return;
  }
  const ptp_states: VAPI.PTPClock.State[] =
    pars.mode == "GPSMaster" || pars.mode == "SlaveOnly"
      ? ["CalibratedAndLocked", "Calibrated"]
      : ["FreeRun"];
  if (pars.await_calibration)
    await vm.p_t_p_clock.state.wait_until((state) => ptp_states.includes(state), {
      timeout: new Duration(5, "min"),
    });
  enforce(!!vm.genlock);
  pars?.log?.(
    `Waiting for Genlock to calibrate on ${vm.raw.identify()} with mode: ${pars.mode} on domain: ${
      pars.ptp_domain
    }`,
    "Debug",
  );
  const t0 = new Date();
  if (vm instanceof VAPI.AT1130.Root) {
    await asyncIter([...vm.genlock.instances], async (instance) => {
      await instance.t_src.command.write(vm.p_t_p_clock.output);
      if (pars.await_calibration) {
        await Promise.all(
          [
            instance.backend.lanes.audio,
            instance.backend.lanes.video_f50_ish,
            instance.backend.lanes.video_f59_ish,
          ].map((lane) =>
            lane.wait_until((x) => x?.state === "Calibrated", {
              timeout: new Duration(5, "min"),
            }),
          ),
        );
        await Promise.all(
          [instance.settings.audio, instance.settings.video].map(async (settings) => {
            const oldsettings = await settings.servo.status.read();
            await settings.servo.command.write({
              ...oldsettings,
              locking_policy: pars.locking_policy,
            });
          }),
        );
      }
    });
  } else {
    if (pars.await_calibration)
      await vm.genlock.state.wait_until((s) => s === "Calibrated", {
        timeout: new Duration(5, "min"),
      });
  }
  if (pars.await_calibration) {
    const t_calibration = new Date();
    const elapsed = new Duration(t_calibration.valueOf() - t0.valueOf(), "ms");
    pars?.log?.(
      `Genlock Calibrated after ${elapsed.toString("precise")} on ${vm.raw.identify()} with mode: ${
        pars.mode
      } on domain: ${pars.ptp_domain}`,
      "Debug",
    );
  }
}

export async function ensureAgent(
  vm: VAPI.VM.Any,
  pars: {
    domain: number;
    port?: VAPI.Any.PTPFlows.Port;
  } & (
    | {
        mode: "SlaveOnly";
      }
    | {
        mode: "MasterSlave" | "MasterOnly";
        time_source: VAPI.AT1101.Time.Source | VAPI.AT1130.Time.Source;
      }
  ),
) {
  // not efficient, but nothing you'd call in a tight loop anyway
  const preexistingAgents = await vm.p_t_p_flows.agents.rows();
  const tryWithPort = async (port: VAPI.Any.PTPFlows.Port) => {
    let agent =
      (await asyncFind(preexistingAgents, async (agent) => {
        return (
          same(await agent.hosting_port.status.read(), port) &&
          (await agent.domain.status.read()) === pars.domain &&
          (await agent.mode.read()) === pars.mode
        );
      })) ??
      (await asyncFind(preexistingAgents, async (agent) => {
        return same(await agent.hosting_port.status.read(), port);
      })) ??
      (await asyncFind(preexistingAgents, async (agent) => {
        return (
          (await agent.domain.status.read()) === pars.domain &&
          (await agent.hosting_port.status.read()) === null
        );
      }));
    if (!agent) {
      agent = await vm.p_t_p_flows.agents.create_row();
      await agent.domain.command.write(pars.domain);
      await agent.hosting_port.command.write(port);
    }
    await agent.domain.command.write(pars.domain);
    if (pars.port) {
      await agent.hosting_port.command.write(pars.port);
    }
    await agent.mode.write(pars.mode);
    if (pars.mode !== "SlaveOnly") {
      await agent.master_settings.t_src.command.write(pars.time_source);
    }
    try {
      await agent.state.wait_until(
        (state) => {
          switch (pars.mode) {
            case "MasterOnly":
              return state === "Master";
            case "SlaveOnly":
              return state === "Slave";
            case "MasterSlave":
              return state === "Master" || state === "Slave";
          }
        },
        // this call should typically resolve within wait_until's default timeout, and
        // typically does. However, some tests mess with VLAN settings and/or deactivate switch
        // ports, so we may have to wait for IGMP to recover
        { timeout: new Duration(3, "min") },
      );
      return agent;
    } catch (_) {
      return null;
    }
  };
  const agents = (
    pars.port
      ? [await tryWithPort(pars.port)]
      : await Promise.all(
          (await vm.p_t_p_flows.ports.rows()).map((port: VAPI.Any.PTPFlows.Port) =>
            tryWithPort(port),
          ),
        )
  ).filter((maybeAgent) => maybeAgent !== null);
  if (agents.length === 0) {
    throw new Error(
      `Unable to find or allocate agent @ ${vm.raw.identify()}; requested parameters were: { domain: ${
        pars.domain
      }, mode: ${pars.mode}, port: ${pars.port?.raw.kwl ?? "<any>"}}`,
    );
  }
  return enforce_nonnull(agents[0]);
}

export async function stretch_clock_input(pars: {
  drift: number;
  blade: VAPI.VM.Any;
  rollback_stack?: RollbackStack;
  timeout?: Duration;
}) {
  const timeout = pars.timeout ?? new Duration(1, "min");
  // we'll force-reset all agents to make sure their input filters don't delay shift
  // propagation into the ptp clock (which would lead to a loss of calibration after exiting
  // stretch_clock_input)
  const activeAgents = await asyncFilterMap(
    await pars.blade.p_t_p_flows.agents.rows(),
    async (agent) => {
      const maybePort = await agent.hosting_port.status.read();
      await agent.hosting_port.command.write(null);
      return maybePort === null ? undefined : ([agent, maybePort] as const);
    },
  );
  const oldInput = await pars.blade.p_t_p_clock.t_src.command.read();
  const oldStretcher = await (async () => {
    for (const stretcher of await pars.blade.time_flows.stretchers.rows()) {
      if (same(stretcher.output, oldInput)) {
        return stretcher;
      }
    }
    return null;
  })();
  if (oldStretcher) {
    const oldDrift = await oldStretcher.frequency_shift.read();
    oldStretcher.frequency_shift.write(pars.drift);
    pars.rollback_stack?.push(async () => {
      await oldStretcher.frequency_shift.write(oldDrift);
    });
  } else {
    pars.rollback_stack?.push(() => pars.blade.p_t_p_clock.t_src.command.write(oldInput));
    const existing_stretchers = await pars.blade.time_flows.stretchers.rows();
    const stretcher =
      existing_stretchers.length > 0
        ? enforce_nonnull(existing_stretchers[0])
        : await pars.blade.time_flows.stretchers.create_row();
    await stretcher.frequency_shift.write(pars.drift);
    await stretcher.t_src.command.write(oldInput);
    await pars.blade.p_t_p_clock.t_src.command.write(stretcher.output);
  }
  await pars.blade.p_t_p_clock.mode.write("Disconnect");
  await pars.blade.p_t_p_clock.state.wait_until((s) => s === "FreeRun");
  await Promise.all(
    activeAgents.map(async (x) => {
      await x[0].hosting_port.command.write(x[1]);
    }),
  );
  await pars.blade.p_t_p_clock.mode.write("LockToInput");
  // FIXME: reenable genlock setup on AT300
  await Promise.all([pars.blade.p_t_p_clock.state.wait_until((s) => s === "Uncalibrated")]);
  if (pars.blade instanceof VAPI.AT1130.Root && pars.blade.genlock) {
    await asyncIter(
      [...pars.blade.genlock.instances],
      async (instance) => await instance.t_src.command.write(pars.blade.p_t_p_clock.output),
    );
  }
  await Promise.all([
    pars.blade.p_t_p_clock.state.wait_until((s) => s === "Calibrated", {
      timeout,
    }),
    streak(pars.blade.p_t_p_clock.relative_clock_speed, {
      timeout,
      n: 4,
      test: (x: { prev: number; value: number }) => Math.abs(x.prev - x.value) < 2e-7,
      poll_interval: new Duration(500, "ms"),
    }),
    await_genlock_calibration(pars.blade, timeout),
  ]);
}

export async function shift_clock_input(pars: {
  offset: Duration;
  blade: VAPI.VM.Any;
  rollback_stack?: RollbackStack;
  timeout?: Duration;
}) {
  const timeout = pars.timeout ?? new Duration(1, "min");
  // we'll force-reset all agents to make sure their input filters don't delay shift
  // propagation into the ptp clock (which would lead to a loss of calibration after exiting
  // stretch_clock_input)
  const activeAgents = await asyncFilterMap(
    await pars.blade.p_t_p_flows.agents.rows(),
    async (agent) => {
      const maybePort = await agent.hosting_port.status.read();
      await agent.hosting_port.command.write(null);
      return maybePort === null ? undefined : ([agent, maybePort] as const);
    },
  );
  const oldInput = await pars.blade.p_t_p_clock.t_src.command.read();
  const oldShifter = await (async () => {
    for (const shifter of await pars.blade.time_flows.shifters.rows()) {
      if (same(shifter.output, oldInput)) {
        return shifter;
      }
    }
    return null;
  })();
  if (oldShifter) {
    const oldOffset = await oldShifter.shift.read();
    oldShifter.shift.write(oldOffset.plus(pars.offset));
    pars.rollback_stack?.push(async () => {
      await oldShifter.shift.write(oldOffset);
    });
  } else {
    pars.rollback_stack?.push(() => pars.blade.p_t_p_clock.t_src.command.write(oldInput));
    const existing_shifters = await pars.blade.time_flows.shifters.rows();
    const shifter =
      existing_shifters.length > 0
        ? enforce_nonnull(existing_shifters[0])
        : await pars.blade.time_flows.shifters.create_row();
    await shifter.shift.write(pars.offset);
    await shifter.t_src.command.write(oldInput);
    await pars.blade.p_t_p_clock.t_src.command.write(shifter.output);
  }
  await pars.blade.p_t_p_clock.mode.write("Disconnect");
  await pars.blade.p_t_p_clock.state.wait_until((s) => s === "FreeRun");
  await Promise.all(
    activeAgents.map(async (x) => {
      await x[0].hosting_port.command.write(x[1]);
    }),
  );
  await pars.blade.p_t_p_clock.mode.write("LockToInput");
  // FIXME: reenable genlock setup on AT300, extend VAPI to cover both models at once
  await Promise.all([pars.blade.p_t_p_clock.state.wait_until((s) => s === "Uncalibrated")]);
  if (pars.blade instanceof VAPI.AT1130.Root && pars.blade.genlock) {
    await asyncIter(
      [...pars.blade.genlock.instances],
      async (instance) => await instance.t_src.command.write(pars.blade.p_t_p_clock.output),
    );
  }
  await Promise.all([
    pars.blade.p_t_p_clock.state.wait_until((s) => s === "Calibrated", {
      timeout,
    }),
    streak(pars.blade.p_t_p_clock.relative_clock_speed, {
      timeout,
      n: 4,
      test: (x: { prev: number; value: number }) => Math.abs(x.prev - x.value) < 2e-7,
      poll_interval: new Duration(500, "ms"),
    }),
    await_genlock_calibration(pars.blade, timeout),
  ]);
}
