import { pixelsPerRem } from "../../app";
import { componentsByElement, componentsById } from "../../componentManagement";
import { ComponentBase } from "../componentBase";
import { NodeInputComponent } from "../nodeInput";
import { NodeOutputComponent } from "../nodeOutput";
import { GraphEditorComponent } from "./graphEditor";
import { AugmentedConnectionState } from "./graphStore";

/// Temporary function to get a component by its key
///
/// TODO / FIXME / REMOVEME
export function devel_getComponentByKey(key: string): ComponentBase {
    for (let component of Object.values(componentsById)) {
        if (component === undefined) {
            continue;
        }

        // @ts-ignore
        if (component.state._key_ === key) {
            return component;
        }
    }

    throw new Error(`Could not find component with key ${key}`);
}

/// Given the circle HTML element of a port, walk up the DOM to find the port
/// component that contains it.
export function getPortFromCircle(
    circleElement: HTMLElement
): NodeInputComponent | NodeOutputComponent {
    let portElement = circleElement.parentElement as HTMLElement;
    console.assert(
        portElement.classList.contains("rio-graph-editor-port"),
        "Port element does not have the expected class"
    );

    let portComponent = componentsByElement.get(portElement) as ComponentBase;
    console.assert(
        portComponent !== undefined,
        "Port element does not have a corresponding component"
    );

    console.assert(
        portComponent instanceof NodeInputComponent ||
            portComponent instanceof NodeOutputComponent,
        "Port component is not of the expected type"
    );

    // @ts-ignore
    return portComponent;
}

/// Given a port component, walk up the DOM to find the node component that
/// contains it.
export function getNodeFromPort(
    port: NodeInputComponent | NodeOutputComponent
): ComponentBase {
    // Walk up to find the node's body element
    let bodyElement = port.element.closest(
        ".rio-graph-editor-node-body"
    ) as HTMLElement;

    // Then walk back down to find the Rio component. Take care - there may ba
    // alignments and margins inbetween
    let nodeElement = bodyElement.querySelector(
        ".rio-component"
    ) as HTMLElement;

    // Now use the Rio component to find the actual component
    return componentsByElement.get(nodeElement) as ComponentBase;
}

/// Given the HTML element of a node, return the node's component by walking
/// the DOM.
export function getNodeComponentFromElement(
    nodeElement: HTMLElement
): ComponentBase {
    console.assert(
        nodeElement.classList.contains("rio-graph-editor-node"),
        "Node element does not have the expected class"
    );

    // The node body contains a Rio component. That component's ID is also the
    // node's ID.
    let componentElement = nodeElement.querySelector(
        ".rio-component"
    ) as HTMLElement;

    return componentsByElement.get(componentElement) as ComponentBase;
}

/// Creates a SVG path element representing a connection. Does not add it to
/// the DOM.
export function makeConnectionElement(): SVGPathElement {
    const svgPath = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "path"
    ) as SVGPathElement;

    svgPath.setAttribute("stroke", "var(--rio-local-text-color)");
    svgPath.setAttribute("stroke-opacity", "0.5");
    svgPath.setAttribute("stroke-width", "0.2rem");
    svgPath.setAttribute("fill", "none");

    return svgPath;
}

/// Given a port component, return the coordinates of the port's socket relative
/// to the viewport.
export function getPortViewportPosition(
    portComponent: NodeInputComponent | NodeOutputComponent
): [number, number] {
    // Find the circle's HTML element
    let circleElement = portComponent.element.querySelector(
        ".rio-graph-editor-port-circle"
    ) as HTMLElement;

    // Get the circle's bounding box
    let circleBox = circleElement.getBoundingClientRect();

    // Return the center of the circle
    return [
        circleBox.left + circleBox.width * 0.5,
        circleBox.top + circleBox.height * 0.5,
    ];
}

/// Updates the SVG path of a connection based on the state of the
/// connection. This is a convenience function which determines the start
/// and end points and delegates to the more general function.
export function updateConnectionFromObject(
    ge: GraphEditorComponent,
    connectionState: AugmentedConnectionState
): void {
    // From Port
    let fromPortComponent = componentsById[
        connectionState.fromPort
    ] as NodeOutputComponent;

    let [x1, y1] = getPortViewportPosition(fromPortComponent);

    // To Port
    let toPortComponent = componentsById[
        connectionState.toPort
    ] as NodeInputComponent;

    let [x4, y4] = getPortViewportPosition(toPortComponent);

    // Convert the coordinates to the editor's coordinate system
    const editorRect = ge.element.getBoundingClientRect();
    x1 = x1 - editorRect.left;
    y1 = y1 - editorRect.top;
    x4 = x4 - editorRect.left;
    y4 = y4 - editorRect.top;

    // Update the SVG path
    updateConnectionFromCoordinates(connectionState.element, x1, y1, x4, y4);
}

/// Updates the SVG path of a connection based on the coordinates of the
/// start and end points.
///
/// All coordinates are relative to the editor.
export function updateConnectionFromCoordinates(
    connectionElement: SVGPathElement,
    x1: number,
    y1: number,
    x4: number,
    y4: number
): void {
    // Control the curve's bend
    let signedDistance = Math.abs(x4 - x1);

    let velocity = Math.pow(signedDistance * 10, 0.6);
    velocity = Math.max(velocity, 3 * pixelsPerRem);

    // Calculate the intermediate points
    const x2 = x1 + velocity;
    const y2 = y1;

    const x3 = x4 - velocity;
    const y3 = y4;

    // Update the SVG path
    connectionElement.setAttribute(
        "d",
        `M${x1} ${y1} C ${x2} ${y2}, ${x3} ${y3}, ${x4} ${y4}`
    );
}

/// Returns `true` if the two lines intersect, `false` otherwise.
///
/// ChatGPT generated black magic.
export function linesIntersect(
    l1x1: number,
    l1y1: number,
    l1x2: number,
    l1y2: number,
    l2x1: number,
    l2y1: number,
    l2x2: number,
    l2y2: number
): boolean {
    const denominator =
        (l1x1 - l1x2) * (l2y1 - l2y2) - (l1y1 - l1y2) * (l2x1 - l2x2);

    if (denominator === 0) {
        return false;
    }

    const t =
        ((l1x1 - l2x1) * (l2y1 - l2y2) - (l1y1 - l2y1) * (l2x1 - l2x2)) /
        denominator;

    const u =
        -((l1x1 - l1x2) * (l1y1 - l2y1) - (l1y1 - l1y2) * (l1x1 - l2x1)) /
        denominator;

    return t >= 0 && t <= 1 && u >= 0 && u <= 1;
}
