import { app } from "scripts/app.js";
import type {
  INodeInputSlot,
  INodeOutputSlot,
  LGraphCanvas,
  LGraphNode,
  LLink,
  SerializedLGraphNode,
  Vector2,
} from "typings/litegraph.js";
import type { NodeMode } from "typings/comfy.js";
import {
  PassThroughFollowing,
  addConnectionLayoutSupport,
  getConnectedInputNodesAndFilterPassThroughs,
  getConnectedOutputNodesAndFilterPassThroughs,
} from "./utils.js";
import { wait } from "rgthree/common/shared_utils.js";
import { BaseCollectorNode } from "./base_node_collector.js";
import { NodeTypesString, stripRgthree } from "./constants.js";
import { fitString } from "./utils_canvas.js";
import { rgthree } from "./rgthree.js";

const MODE_ALWAYS = 0;
const MODE_MUTE = 2;
const MODE_BYPASS = 4;
const MODE_REPEATS = [MODE_MUTE, MODE_BYPASS];
const MODE_NOTHING = -99; // MADE THIS UP.

const MODE_TO_OPTION = new Map([
  [MODE_ALWAYS, "ACTIVE"],
  [MODE_MUTE, "MUTE"],
  [MODE_BYPASS, "BYPASS"],
  [MODE_NOTHING, "NOTHING"],
]);

const OPTION_TO_MODE = new Map([
  ["ACTIVE", MODE_ALWAYS],
  ["MUTE", MODE_MUTE],
  ["BYPASS", MODE_BYPASS],
  ["NOTHING", MODE_NOTHING],
]);

const MODE_TO_PROPERTY = new Map([
  [MODE_MUTE, "on_muted_inputs"],
  [MODE_BYPASS, "on_bypassed_inputs"],
  [MODE_ALWAYS, "on_any_active_inputs"],
]);

const logger = rgthree.newLogSession("[NodeModeRelay]");

/**
 * Like a BaseCollectorNode, this relay node connects to a Repeater node and _relays_ mode changes
 * changes to the repeater (so it can go on to modify its connections).
 */
class NodeModeRelay extends BaseCollectorNode {
  override readonly inputsPassThroughFollowing: PassThroughFollowing = PassThroughFollowing.ALL;

  static override type = NodeTypesString.NODE_MODE_RELAY;
  static override title = NodeTypesString.NODE_MODE_RELAY;
  override comfyClass = NodeTypesString.NODE_MODE_RELAY;

  static "@on_muted_inputs" = {
    type: "combo",
    values: ["MUTE", "ACTIVE", "BYPASS", "NOTHING"],
  };

  static "@on_bypassed_inputs" = {
    type: "combo",
    values: ["BYPASS", "ACTIVE", "MUTE", "NOTHING"],
  };

  static "@on_any_active_inputs" = {
    type: "combo",
    values: ["BYPASS", "ACTIVE", "MUTE", "NOTHING"],
  };

  constructor(title?: string) {
    super(title);
    this.properties["on_muted_inputs"] = "MUTE";
    this.properties["on_bypassed_inputs"] = "BYPASS";
    this.properties["on_any_active_inputs"] = "ACTIVE";

    this.onConstructed();
  }

  override onConstructed() {
    this.addOutput("REPEATER", "_NODE_REPEATER_", {
      color_on: "#Fc0",
      color_off: "#a80",
      shape: LiteGraph.ARROW_SHAPE,
    });

    setTimeout(() => {
      this.stabilize();
    }, 500);
    return super.onConstructed();
  }

  override onModeChange(from: NodeMode, to: NodeMode) {
    super.onModeChange(from, to);
    // If we aren't connected to anything, then we'll use our mode to relay when it changes.
    if (this.inputs.length <= 1 && !this.isInputConnected(0) && this.isAnyOutputConnected()) {
      const [n, v] = logger.infoParts(`Mode change without any inputs; relaying our mode.`);
      console[n]?.(...v);
      // Pass "to" since there may be other getters in the way to access this.mode directly.
      this.dispatchModeToRepeater(to);
    }
  }

  override configure(info: SerializedLGraphNode<LGraphNode>): void {
    // Patch a small issue (~14h) where multiple OPT_CONNECTIONS may have been created.
    // https://github.com/rgthree/rgthree-comfy/issues/206
    // TODO: This can probably be removed within a few weeks.
    if (info.outputs?.length) {
      info.outputs.length = 1;
    }
    super.configure(info);
  }

  override onDrawForeground(ctx: CanvasRenderingContext2D, canvas: LGraphCanvas): void {
    if (this.flags?.collapsed) {
      return;
    }
    if (
      this.properties["on_muted_inputs"] !== "MUTE" ||
      this.properties["on_bypassed_inputs"] !== "BYPASS" ||
      this.properties["on_any_active_inputs"] != "ACTIVE"
    ) {
      let margin = 15;
      ctx.textAlign = "left";
      let label = `*(MUTE > ${this.properties["on_muted_inputs"]},  `;
      label += `BYPASS > ${this.properties["on_bypassed_inputs"]},  `;
      label += `ACTIVE > ${this.properties["on_any_active_inputs"]})`;
      ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
      const oldFont = ctx.font;
      ctx.font = "italic " + (LiteGraph.NODE_SUBTEXT_SIZE - 2) + "px Arial";
      ctx.fillText(fitString(ctx, label, this.size[0] - 20), 15, this.size[1] - 6);
      ctx.font = oldFont;
    }
  }

  override computeSize(out: Vector2) {
    let size = super.computeSize(out);
    if (
      this.properties["on_muted_inputs"] !== "MUTE" ||
      this.properties["on_bypassed_inputs"] !== "BYPASS" ||
      this.properties["on_any_active_inputs"] != "ACTIVE"
    ) {
      size[1] += 17;
    }
    return size;
  }
  override onConnectOutput(
    outputIndex: number,
    inputType: string | -1,
    inputSlot: INodeInputSlot,
    inputNode: LGraphNode,
    inputIndex: number,
  ): boolean {
    let canConnect = super.onConnectOutput?.(
      outputIndex,
      inputType,
      inputSlot,
      inputNode,
      inputIndex,
    );
    let nextNode = getConnectedOutputNodesAndFilterPassThroughs(this, inputNode)[0] ?? inputNode;
    return canConnect && nextNode.type === NodeTypesString.NODE_MODE_REPEATER;
  }

  override onConnectionsChange(
    type: number,
    slotIndex: number,
    isConnected: boolean,
    link_info: LLink,
    ioSlot: INodeOutputSlot | INodeInputSlot,
  ): void {
    super.onConnectionsChange(type, slotIndex, isConnected, link_info, ioSlot);
    setTimeout(() => {
      this.stabilize();
    }, 500);
  }

  stabilize() {
    // If we aren't connected to a repeater, then theres no sense in checking. And if we are, but
    // have no inputs, then we're also not ready.
    if (!this.graph || !this.isAnyOutputConnected() || !this.isInputConnected(0)) {
      return;
    }
    const inputNodes = getConnectedInputNodesAndFilterPassThroughs(
      this,
      this,
      -1,
      this.inputsPassThroughFollowing,
    );
    let mode: NodeMode | -99 | null = undefined;
    for (const inputNode of inputNodes) {
      // If we haven't set our mode to be, then let's set it. Otherwise, mode will stick if it
      // remains constant, otherwise, if we hit an ALWAYS, then we'll unmute all repeaters and
      // if not then we won't do anything.
      if (mode === undefined) {
        mode = inputNode.mode;
      } else if (mode === inputNode.mode && MODE_REPEATS.includes(mode)) {
        continue;
      } else if (inputNode.mode === MODE_ALWAYS || mode === MODE_ALWAYS) {
        mode = MODE_ALWAYS;
      } else {
        mode = null;
      }
    }

    this.dispatchModeToRepeater(mode);
    setTimeout(() => {
      this.stabilize();
    }, 500);
  }

  /**
   * Sends the mode to the repeater, checking to see if we're modifying our mode.
   */
  private dispatchModeToRepeater(mode?: NodeMode | -99 | null) {
    if (mode != null) {
      const propertyVal = this.properties?.[MODE_TO_PROPERTY.get(mode) || ""];
      const newMode = OPTION_TO_MODE.get(propertyVal);
      mode = (newMode !== null ? newMode : mode) as NodeMode | -99;
      if (mode !== null && mode !== MODE_NOTHING) {
        if (this.outputs?.length) {
          const outputNodes = getConnectedOutputNodesAndFilterPassThroughs(this);
          for (const outputNode of outputNodes) {
            outputNode.mode = mode;
            wait(16).then(() => {
              outputNode.setDirtyCanvas(true, true);
            });
          }
        }
      }
    }
  }

  override getHelp() {
    return `
      <p>
        This node will relay its input nodes' modes (Mute, Bypass, or Active) to a connected
        ${stripRgthree(NodeTypesString.NODE_MODE_REPEATER)} (which would then repeat that mode
        change to all of its inputs).
      </p>
      <ul>
          <li><p>
            When all connected input nodes are muted, the relay will set a connected repeater to
            mute (by default).
          </p></li>
          <li><p>
            When all connected input nodes are bypassed, the relay will set a connected repeater to
            bypass (by default).
          </p></li>
          <li><p>
            When any connected input nodes are active, the relay will set a connected repeater to
            active (by default).
          </p></li>
          <li><p>
            If no inputs are connected, the relay will set a connected repeater to its mode <i>when
            its own mode is changed</i>. <b>Note</b>, if any inputs are connected, then the above
            will occur and the Relay's mode does not matter.
          </p></li>
      </ul>
      <p>
        Note, you can change which signals get sent on the above in the <code>Properties</code>.
        For instance, you could configure an inverse relay which will send a MUTE when any of its
        inputs are active (instead of sending an ACTIVE signal), and send an ACTIVE signal when all
        of its inputs are muted (instead of sending a MUTE signal), etc.
      </p>
    `;
  }
}

app.registerExtension({
  name: "rgthree.NodeModeRepeaterHelper",
  registerCustomNodes() {
    addConnectionLayoutSupport(NodeModeRelay, app, [
      ["Left", "Right"],
      ["Right", "Left"],
    ]);

    LiteGraph.registerNodeType(NodeModeRelay.type, NodeModeRelay);
    NodeModeRelay.category = NodeModeRelay._category;
  },
});
