import type {
  LGraphCanvas as TLGraphCanvas,
  LGraphGroup as TLGraphGroup,
  LGraph as TLGraph,
  AdjustedMouseEvent,
  Vector2,
} from "typings/litegraph.js";
import type {AdjustedMouseCustomEvent} from "typings/rgthree.js";

import {app} from "scripts/app.js";
import {rgthree} from "./rgthree.js";
import {getOutputNodes} from "./utils.js";
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";

const BTN_SIZE = 20;
const BTN_MARGIN: Vector2 = [6, 6];
const BTN_SPACING = 8;
const BTN_GRID = BTN_SIZE / 8;

const TOGGLE_TO_MODE = new Map([
  ["MUTE", LiteGraph.NEVER],
  ["BYPASS", 4],
]);

function getToggles() {
  return [...CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles", [])].reverse();
}

/**
 * Determines if the user clicked on an fast header icon.
 */
function clickedOnToggleButton(e: AdjustedMouseEvent, group: TLGraphGroup): string | null {
  const toggles = getToggles();
  const pos = group.pos;
  const size = group.size;
  for (let i = 0; i < toggles.length; i++) {
    const toggle = toggles[i];
    if (
      LiteGraph.isInsideRectangle(
        e.canvasX,
        e.canvasY,
        pos[0] + size[0] - (BTN_SIZE + BTN_MARGIN[0]) * (i + 1),
        pos[1] + BTN_MARGIN[1],
        BTN_SIZE,
        BTN_SIZE,
      )
    ) {
      return toggle;
    }
  }
  return null;
}

/**
 * Registers the GroupHeaderToggles which places a mute and/or bypass icons in groups headers for
 * quick, single-click ability to mute/bypass.
 */
app.registerExtension({
  name: "rgthree.GroupHeaderToggles",
  async setup() {

    /**
     * LiteGraph won't call `drawGroups` unless the canvas is dirty. Other nodes will do this, but
     * in small workflows, we'll want to trigger it dirty so we can be drawn if we're in hover mode.
     */
    setInterval(() => {
      if (
        CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") &&
        CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always"
      ) {
        app.canvas.setDirty(true, true);
      }
    }, 250);

    /**
     * Handles a click on the icon area if the user has the extension enable from settings.
     * Hooks into the already overriden mouse down processor from rgthree.
     */
    rgthree.addEventListener("on-process-mouse-down", ((e: AdjustedMouseCustomEvent) => {
      if (!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled")) return;

      const canvas = app.canvas as TLGraphCanvas;
      if (canvas.selected_group) {
        const originalEvent = e.detail.originalEvent;
        const group = canvas.selected_group;
        const clickedOnToggle = clickedOnToggleButton(originalEvent, group) || "";
        const toggleAction = clickedOnToggle?.toLocaleUpperCase();
        if (toggleAction) {
          if (toggleAction === "QUEUE") {
            const outputNodes = getOutputNodes(group._nodes);
            if (!outputNodes?.length) {
              rgthree.showMessage({
                id: "no-output-in-group",
                type: "warn",
                timeout: 4000,
                message: "No output nodes for group!",
              });
            } else {
              rgthree.queueOutputNodes(outputNodes.map((n) => n.id));
            }
          } else {
            const toggleMode = TOGGLE_TO_MODE.get(toggleAction);
            if (toggleMode) {
              group.recomputeInsideNodes();
              const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS);
              const isAllMuted =
                !hasAnyActiveNodes && group._nodes.every((n) => n.mode === LiteGraph.NEVER);
              const isAllBypassed =
                !hasAnyActiveNodes && !isAllMuted && group._nodes.every((n) => n.mode === 4);

              let newMode: 0 | 1 | 2 | 3 | 4 = LiteGraph.ALWAYS;
              if (toggleMode === LiteGraph.NEVER) {
                newMode = isAllMuted ? LiteGraph.ALWAYS : LiteGraph.NEVER;
              } else {
                newMode = isAllBypassed ? LiteGraph.ALWAYS : 4;
              }
              for (const node of group._nodes) {
                node.mode = newMode;
              }
            }
          }
          // Make it such that we're not then moving the group on drag.
          canvas.selected_group = null;
          canvas.dragging_canvas = false;
        }
      }
    }) as EventListener);

    /**
     * Overrides LiteGraph's Canvas method for drawingGroups and, after calling the original, checks
     * that the user has enabled fast toggles and draws them on the top-right of the app..
     */
    const drawGroups = LGraphCanvas.prototype.drawGroups;
    LGraphCanvas.prototype.drawGroups = function (
      canvasEl: HTMLCanvasElement,
      ctx: CanvasRenderingContext2D,
    ) {
      drawGroups.apply(this, [...arguments] as any);

      if (
        !CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") ||
        !rgthree.lastAdjustedMouseEvent
      ) {
        return;
      }

      const graph = app.graph as TLGraph;

      let groups: TLGraphGroup[];
      // Default to hover if not always.
      if (CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always") {
        const hoverGroup = graph.getGroupOnPos(
          rgthree.lastAdjustedMouseEvent.canvasX,
          rgthree.lastAdjustedMouseEvent.canvasY,
        );
        groups = hoverGroup ? [hoverGroup] : [];
      } else {
        groups = graph._groups || [];
      }

      if (!groups.length) {
        return;
      }

      const toggles = getToggles();

      ctx.save();
      for (const group of groups || []) {
        let anyActive = false;
        let allMuted = !!group._nodes.length;
        let allBypassed = allMuted;

        // Find the current state of the group's nodes.
        for (const node of group._nodes) {
          anyActive = anyActive || node.mode === LiteGraph.ALWAYS;
          allMuted = allMuted && node.mode === LiteGraph.NEVER;
          allBypassed = allBypassed && node.mode === 4;
          if (anyActive || (!allMuted && !allBypassed)) {
            break;
          }
        }

        // Display each toggle.
        for (let i = 0; i < toggles.length; i++) {
          const toggle = toggles[i];
          const pos = group._pos;
          const size = group._size;
          ctx.fillStyle = ctx.strokeStyle = group.color || "#335";
          const x = pos[0] + size[0] - BTN_MARGIN[0] - BTN_SIZE - (BTN_SPACING + BTN_SIZE) * i;
          const y = pos[1] + BTN_MARGIN[1];
          const midX = x + BTN_SIZE / 2;
          const midY = y + BTN_SIZE / 2;
          if (toggle === "queue") {
            const outputNodes = getOutputNodes(group._nodes);
            const oldGlobalAlpha = ctx.globalAlpha;
            if (!outputNodes?.length) {
              ctx.globalAlpha = 0.5;
            }
            ctx.lineJoin = "round";
            ctx.lineCap = "round";
            const arrowSizeX = BTN_SIZE * 0.6;
            const arrowSizeY = BTN_SIZE * 0.7;
            const arrow = new Path2D(
              `M ${x + arrowSizeX / 2} ${midY} l 0 -${arrowSizeY / 2} l ${arrowSizeX} ${arrowSizeY / 2} l -${arrowSizeX} ${arrowSizeY / 2} z`,
            );
            ctx.stroke(arrow);
            if (outputNodes?.length) {
              ctx.fill(arrow);
            }
            ctx.globalAlpha = oldGlobalAlpha;
          } else {
            const on = toggle === "bypass" ? allBypassed : allMuted;

            ctx.beginPath();
            ctx.lineJoin = "round";
            ctx.rect(x, y, BTN_SIZE, BTN_SIZE);

            ctx.lineWidth = 2;
            if (toggle === "mute") {
              ctx.lineJoin = "round";
              ctx.lineCap = "round";

              if (on) {
                ctx.stroke(
                  new Path2D(`
                    ${eyeFrame(midX, midY)}
                    ${eyeLashes(midX, midY)}
                `),
                );
              } else {
                const radius = BTN_GRID * 1.5;

                // Eyeball fill
                ctx.fill(
                  new Path2D(`
                    ${eyeFrame(midX, midY)}
                    ${eyeFrame(midX, midY, -1)}
                    ${circlePath(midX, midY, radius)}
                    ${circlePath(midX + BTN_GRID / 2, midY - BTN_GRID / 2, BTN_GRID * 0.375)}
                  `),
                  "evenodd",
                );

                // Eye Outline Stroke
                ctx.stroke(new Path2D(`${eyeFrame(midX, midY)} ${eyeFrame(midX, midY, -1)}`));

                // Eye lashes (faded)
                ctx.globalAlpha = this.editor_alpha * 0.5;
                ctx.stroke(new Path2D(`${eyeLashes(midX, midY)} ${eyeLashes(midX, midY, -1)}`));
                ctx.globalAlpha = this.editor_alpha;
              }
            } else {
              const lineChanges = on
                ? `a ${BTN_GRID * 3}, ${BTN_GRID * 3} 0 1, 1 ${BTN_GRID * 3 * 2},0
                  l ${BTN_GRID * 2.0} 0`
                : `l ${BTN_GRID * 8} 0`;

              ctx.stroke(
                new Path2D(`
                  M ${x} ${midY}
                  ${lineChanges}
                  M ${x + BTN_SIZE} ${midY} l -2  2
                  M ${x + BTN_SIZE} ${midY} l -2 -2
                `),
              );
              ctx.fill(new Path2D(`${circlePath(x + BTN_GRID * 3, midY, BTN_GRID * 1.8)}`));
            }
          }
        }
      }
      ctx.restore();
    };
  },
});

function eyeFrame(midX: number, midY: number, yFlip = 1) {
  return `
      M ${midX - BTN_SIZE / 2} ${midY}
      c ${BTN_GRID * 1.5} ${yFlip * BTN_GRID * 2.5}, ${BTN_GRID * (8 - 1.5)} ${
        yFlip * BTN_GRID * 2.5
      }, ${BTN_GRID * 8} 0
  `;
}

function eyeLashes(midX: number, midY: number, yFlip = 1) {
  return `
    M ${midX - BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l -1.15  ${1.25 * yFlip}
    M ${midX - BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l -0.90  ${1.5 * yFlip}
    M ${midX - BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l -0.50  ${1.75 * yFlip}
    M ${midX + BTN_GRID * 0.0} ${midY + yFlip * BTN_GRID * 2.0} l  0.00  ${2.0 * yFlip}
    M ${midX + BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l  0.50  ${1.75 * yFlip}
    M ${midX + BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l  0.90  ${1.5 * yFlip}
    M ${midX + BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l  1.15  ${1.25 * yFlip}
`;
}

function circlePath(cx: number, cy: number, radius: number) {
  return `
      M ${cx} ${cy}
      m ${radius}, 0
      a ${radius},${radius} 0 1, 1 -${radius * 2},0
      a ${radius},${radius} 0 1, 1  ${radius * 2},0
  `;
}
