import React from "react";
import { DynamicGraph } from "./graph";
import { loadGraphJson } from "../common/datasets";
import { NodeDetails } from "./components/nodedetails";
import { SpaceSelect } from "./components/spaceselect";
import "./editor.css";
import { ForceGraph2D } from "react-force-graph";
import { Node } from "../common/graph/node";
import { HistoryNavigator } from "./components/historynavigator";
import { GraphElement } from "../common/graph/graphelement";
import { Link } from "../common/graph/link";
import { NodeTypesEditor } from "./components/nodetypeseditor";
import { SpaceManager } from "./components/spacemanager";
import { SelectLayer } from "./components/selectlayer";
import { GraphData } from "../common/graph/graph";
import { NodeType } from "../common/graph/nodetype";

type propTypes = {
    spaceId: string;
};
type stateTypes = {
    /**
     * Graph structure holding the basic information.
     */
    graph: DynamicGraph;

    /**
     * Should labels on nodes be rendered, or none at all.
     */
    visibleLabels: boolean;

    /**
     * Should feature be enabled, that nodes get connected with a link of dragged close enough to each other?
     */
    connectOnDrag: boolean;

    /**
     * Collection of all currently selected nodes. Can also be undefined or empty.
     */
    selectedNodes: Node[];

    /**
     * True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
     */
    keys: { [name: string]: boolean };

    /**
     * Current width of graph object. Used to specifically adjust and correct the graph size.
     */
    graphWidth: number;
};

/**
 * Coordinate structure used for the force-graph.
 */
type graphCoordinates = {
    x: number;
    y: number;
};
/**
 * Easy to access format for translated positions of a click event.
 */
type clickPosition = {
    graph: graphCoordinates;
    window: graphCoordinates;
};

/**
 * Knowledge space graph editor. Allows easy editing of the graph structure.
 */
export class Editor extends React.PureComponent<propTypes, stateTypes> {
    private maxDistanceToConnect = 15;
    private defaultWarmupTicks = 100;
    private warmupTicks = 100;
    private renderer: React.RefObject<any>;
    private graphContainer: React.RefObject<HTMLDivElement>;

    /**
     * True, if the graph was the target of the most recent click event.
     */
    private graphInFocus = false;

    constructor(props: propTypes) {
        super(props);

        // Making sure, all functions retain the proper this-bind
        this.loadGraph = this.loadGraph.bind(this);
        this.loadSpace = this.loadSpace.bind(this);
        this.extractPositions = this.extractPositions.bind(this);
        this.handleNodeClick = this.handleNodeClick.bind(this);
        this.onGraphDataChange = this.onGraphDataChange.bind(this);
        this.handleEngineStop = this.handleEngineStop.bind(this);
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.handleKeyUp = this.handleKeyUp.bind(this);
        this.forceUpdate = this.forceUpdate.bind(this);
        this.handleNodeCanvasObject = this.handleNodeCanvasObject.bind(this);
        this.handleLinkCanvasObject = this.handleLinkCanvasObject.bind(this);
        this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
        this.handleNodeDrag = this.handleNodeDrag.bind(this);
        this.handleElementRightClick = this.handleElementRightClick.bind(this);
        this.selectNode = this.selectNode.bind(this);
        this.handleResize = this.handleResize.bind(this);
        this.handleBoxSelect = this.handleBoxSelect.bind(this);
        this.handleNodeTypeSelect = this.handleNodeTypeSelect.bind(this);

        this.renderer = React.createRef();
        this.graphContainer = React.createRef();

        // Set as new state
        this.state = {
            graph: undefined,
            visibleLabels: true,
            connectOnDrag: false,
            selectedNodes: [], // TODO: Why was undefined allowed here?
            keys: {},
            graphWidth: 1000,
        };
    }

    /**
     * Tries to load initial graph after webpage finished loading.
     */
    componentDidMount() {
        if (this.props.spaceId !== undefined) {
            // Load initial space
            this.loadSpace(this.props.spaceId);
        }
    }

    /**
     * Loads a space from the database to the editor.
     * @param spaceId Id of space to load.
     * @returns Promise with boolean value that is true, if successful.
     */
    public loadSpace(spaceId: string): any {
        return loadGraphJson(spaceId).then(this.loadGraph);
    }

    /**
     * Loads another graph based on the data supplied. Note: Naming currently suggests that this only loads a GRAPH, not a SPACE. Needs further work and implementation to see if that makes sense or not.
     * @param data Serialized graph data.
     * @returns True, if successful.
     */
    public loadGraph(data: GraphData): boolean {
        console.log("Starting to load new graph ...");
        console.log(data);

        // Create graph
        const graph = new DynamicGraph();
        graph.fromSerializedObject(data);

        this.warmupTicks = this.defaultWarmupTicks; // Should only run for each newly initialized graph, but then never again

        // Set as new state
        console.log(graph);
        this.setState({
            graph: graph,
        });

        graph.onChangeCallbacks.push(this.onGraphDataChange);

        // Subscribe to global events
        document.onkeydown = this.handleKeyDown;
        document.onkeyup = this.handleKeyUp;
        document.onmousedown = this.handleMouseDown;
        window.onresize = this.handleResize;

        this.handleResize();

        return true;
    }

    /**
     * Processes page wide key down events. Stores corresponding key as pressed in state.
     *
     * Also triggers actions corresponding to shortcuts.
     */
    private handleKeyDown(event: KeyboardEvent) {
        const key: string = event.key;

        const keys = this.state.keys;
        keys[key] = true;

        this.setState({
            keys: keys,
        });

        this.handleShortcutEvents(key);
    }

    /**
     * Triggers actions that correspond with certain shortcuts.
     *
     * @param key Newly pressed key.
     */
    private handleShortcutEvents(key: string) {
        if (key === "Escape") {
            this.selectNode(undefined);
        } else if (
            key === "Delete" &&
            this.graphInFocus // Only delete if 2d-graph is the focused element
        ) {
            this.deleteSelectedNodes();
        }
    }

    /**
     * Deletes all nodes currently selected. Handles store points accordingly of the number of deleted nodes.
     */
    private deleteSelectedNodes() {
        const selectedNodes = this.state.selectedNodes;

        if (selectedNodes.length == 1) {
            selectedNodes[0].delete();
            selectedNodes.pop();
            this.selectNodes(selectedNodes);
        } else {
            selectedNodes.forEach((node: Node) => node.delete());
            this.deselect();
        }
    }

    /**
     * Processes page wide mouse down events.
     */
    private handleMouseDown() {
        this.graphInFocus = false;
    }

    /**
     * Processes page wide key up events. Stores corresponding key as not-pressed in state.
     */
    private handleKeyUp(event: KeyboardEvent) {
        const key: string = event.key;

        const keys = this.state.keys;
        keys[key] = false;

        this.setState({
            keys: keys,
        });
    }

    /**
     * Processes resize window event. Focusses on resizing the graph accordingly.
     */
    private handleResize() {
        const newGraphWidth = this.graphContainer.current.clientWidth;
        this.setState({
            graphWidth: newGraphWidth,
        });
    }

    /**
     * Handler for background click event on force graph. Adds new node by default.
     * @param event Click event.
     */
    private handleBackgroundClick(event: MouseEvent, position: clickPosition) {
        this.graphInFocus = true;

        // Is there really no node there? Trying to prevent small error, where this event is triggered, even if there is a node.
        const nearestNode = this.state.graph.getClosestNode(
            position.graph.x,
            position.graph.y
        );
        if (nearestNode !== undefined && nearestNode.distance < 4) {
            this.handleNodeClick(nearestNode.node);
            return;
        }

        // Just deselect if control key is pressed
        if (this.state.keys["Control"]) {
            this.selectNode(undefined);
            return;
        }

        // Add new node
        const node = this.state.graph.createNode(
            undefined,
            position.graph.x,
            position.graph.y,
            0,
            0
        );
        this.forceUpdate(); // TODO: Remove?

        // Select newly created node
        if (this.state.keys["Shift"]) {
            // Simply add to current selection of shift is pressed
            this.toggleNodeSelection(node);
        } else {
            this.selectNode(node);
        }
    }

    /**
     * Propagates the changed state of the graph.
     */
    private onGraphDataChange() {
        const nodes: Node[] = this.state.selectedNodes.map((node: Node) =>
            this.state.graph.node(node.id)
        );
        this.selectNodes(nodes);
        this.forceUpdate(); // TODO
    }

    /**
     * Calculates the corresponding coordinates for a click event for easier further processing.
     * @param event The corresponding click event.
     * @returns Coordinates in graph and coordinates in browser window.
     */
    private extractPositions(event: any): clickPosition {
        return {
            graph: this.renderer.current.screen2GraphCoords(
                event.layerX, // TODO: Replace layerx/layery non standard properties and fix typing
                event.layerY
            ),
            window: { x: event.clientX, y: event.clientY },
        };
    }

    private deselect() {
        this.setState({ selectedNodes: [] });
    }

    /**
     * Selects a single node, or clears selection if given undefined.
     * @param node Single node to select, or undefined.
     */
    private selectNode(node: Node) {
        this.selectNodes([node]);
    }

    /**
     * Selects multiple nodes, or clears selection if given undefined or empty array.
     * @param nodes Multiple nodes to mark as selected.
     */
    private selectNodes(nodes: Node[]) {
        this.setState({
            selectedNodes: nodes,
        });
    }

    // /**
    //  * Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned.
    //  */
    // private get selectedNodes(): Node[] {
    //     // TODO: Here are a lot of things that should not be possible by design
    //
    //     // Remove undefines
    //     let selectedNodes = this.state.selectedNodes.filter(
    //         (n: Node) => n !== undefined
    //     );
    //
    //     // Remove duplicates
    //     selectedNodes = [...new Set(selectedNodes)];
    //
    //     if (selectedNodes.length > 0) {
    //         return selectedNodes;
    //     }
    //
    //     return undefined;
    // }

    private handleNodeClick(node: Node) {
        this.graphInFocus = true;

        if (this.state.keys["Control"]) {
            // Connect to clicked node as parent while control is pressed
            if (this.state.selectedNodes.length == 0) {
                // Have no node connected, so select
                this.selectNode(node);
            } else if (!this.state.selectedNodes.includes(node)) {
                // Already have *other* node/s selected, so connect
                this.connectSelectionToNode(node);
            }
        } else if (this.state.keys["Shift"]) {
            this.toggleNodeSelection(node);
        } else {
            // By default, simply select node
            this.selectNode(node);
        }
        this.forceUpdate(); // TODO: Remove?
    }

    private connectSelectionToNode(node: Node) {
        if (this.state.selectedNodes.length == 0) {
            return;
        }

        if (this.state.selectedNodes.length == 1) {
            node.connect(this.state.selectedNodes[0]);
        } else {
            this.state.selectedNodes.forEach((selectedNode: Node) =>
                node.connect(selectedNode)
            );
        }
    }

    private toggleNodeSelection(node: Node) {
        // Convert selection to array as basis
        let selection = this.state.selectedNodes;

        // Add/Remove node
        if (selection.includes(node)) {
            // Remove node from selection
            selection = selection.filter((n: Node) => !n.equals(node));
        } else {
            // Add node to selection
            selection.push(node);
        }
        this.selectNodes(selection);
    }

    private handleNodeCanvasObject(
        node: Node,
        ctx: CanvasRenderingContext2D,
        globalScale: number
    ) {
        // TODO: Refactor

        // add ring just for highlighted nodes
        if (this.state.selectedNodes.includes(node)) {
            // Outer circle
            ctx.beginPath();
            ctx.arc(node.x, node.y, 4 * 0.7, 0, 2 * Math.PI, false);
            ctx.fillStyle = "white";
            ctx.fill();

            // Inner circle
            ctx.beginPath();
            ctx.arc(node.x, node.y, 4 * 0.3, 0, 2 * Math.PI, false);
            ctx.fillStyle = node.type.color;
            ctx.fill();
        }

        // Draw image
        const imageSize = 12;
        if (node.icon !== undefined) {
            const img = new Image();
            img.src = node.icon;

            ctx.drawImage(
                img,
                node.x - imageSize / 2,
                node.y - imageSize / 2,
                imageSize,
                imageSize
            );
        }

        // Draw label
        /**
         * Nothing selected? => Draw all labels
         * If this nodes is considered highlighted => Draw label
         * If this node is a neighbor of a selected node => Draw label
         */
        // TODO: Reenable node label rendering
        // const isNodeRelatedToSelection: boolean =
        //     this.state.selectedNodes.length != 0 ||
        //     this.isHighlighted(node) ||
        //     this.selectedNodes.some((selectedNode: Node) =>
        //         selectedNode.neighbors.includes(node)
        //     );
        //
        // if (this.state.visibleLabels && isNodeRelatedToSelection) {
        //     const label = node.name;
        //     const fontSize = 11 / globalScale;
        //     ctx.font = `${fontSize}px Sans-Serif`;
        //     const textWidth = ctx.measureText(label).width;
        //     const bckgDimensions = [textWidth, fontSize].map(
        //         (n) => n + fontSize * 0.2
        //     ); // some padding
        //
        //     const nodeHeightOffset = imageSize / 3 + bckgDimensions[1];
        //     ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
        //     ctx.fillRect(
        //         (node as any).x - bckgDimensions[0] / 2,
        //         (node as any).y - bckgDimensions[1] / 2 + nodeHeightOffset,
        //         ...bckgDimensions
        //     );
        //
        //     ctx.textAlign = "center";
        //     ctx.textBaseline = "middle";
        //     ctx.fillStyle = "white";
        //     ctx.fillText(
        //         label,
        //         (node as any).x,
        //         (node as any).y + nodeHeightOffset
        //     );
        // }

        // TODO: Render label as always visible
    }

    private handleLinkCanvasObject(
        link: Link,
        ctx: CanvasRenderingContext2D,
        globalScale: number
    ) {
        // Links already initialized?
        if (link.source.x === undefined) {
            return;
        }

        // Draw gradient link
        const gradient = ctx.createLinearGradient(
            link.source.x,
            link.source.y,
            link.target.x,
            link.target.y
        );
        // Have reversed colors
        // Color at source node referencing the target node and vice versa
        gradient.addColorStop(0, link.target.type.color);
        gradient.addColorStop(1, link.source.type.color);

        let lineWidth = 0.5;
        if (
            this.state.selectedNodes.some((node: Node) =>
                node.links.find(link.equals)
            )
        ) {
            lineWidth = 2;
        }
        lineWidth /= globalScale; // Scale with zoom

        ctx.beginPath();
        ctx.moveTo(link.source.x, link.source.y);
        ctx.lineTo(link.target.x, link.target.y);
        ctx.strokeStyle = gradient;
        ctx.lineWidth = lineWidth;
        ctx.stroke();
    }

    private handleNodeTypeSelect(type: NodeType) {
        const nodesWithType = this.state.graph.nodes.filter((n: Node) =>
            n.type.equals(type)
        );
        this.selectNodes(nodesWithType);
    }

    private handleNodeDrag(node: Node) {
        this.graphInFocus = true;

        if (
            !this.state.selectedNodes ||
            !this.state.selectedNodes.includes(node)
        ) {
            this.selectNode(node);
        }

        // Should run connect logic?
        if (!this.state.connectOnDrag) {
            return;
        }

        const closest = this.state.graph.getClosestNode(node.x, node.y, node);

        // Is close enough for new link?
        if (closest.distance > this.maxDistanceToConnect) {
            return;
        }

        // Does link already exist?
        if (node.neighbors.includes(closest.node)) {
            return;
        }

        // Add link
        node.connect(closest.node);
        this.forceUpdate();
    }

    /**
     * Processes right-click event on graph elements by deleting them.
     */
    private handleElementRightClick(element: GraphElement<unknown, unknown>) {
        this.graphInFocus = true;

        element.delete();
        this.forceUpdate(); // TODO: Necessary?
    }

    private handleEngineStop() {
        // Only do something on first stop for each graph
        if (this.warmupTicks <= 0) {
            return;
        }

        this.warmupTicks = 0; // Only warm up once, so stop warming up after the first freeze

        this.forceUpdate();
    }

    private handleBoxSelect(selectedNodes: Node[]) {
        if (selectedNodes !== undefined && selectedNodes.length <= 0) {
            return;
        }

        this.selectNodes(selectedNodes.concat(this.state.selectedNodes));
    }

    render(): React.ReactNode {
        return (
            <div id="ks-editor">
                <h1>Interface</h1>
                <SpaceSelect onLoadSpace={this.loadSpace} />
                <SpaceManager />
                <div id="content">
                    <div id="force-graph-renderer" ref={this.graphContainer}>
                        <SelectLayer
                            allNodes={
                                this.state.graph ? this.state.graph.nodes : []
                            }
                            screen2GraphCoords={
                                this.renderer.current
                                    ? this.renderer.current.screen2GraphCoords
                                    : undefined
                            }
                            isEnable={() => this.state.keys["Shift"]}
                            onBoxSelect={this.handleBoxSelect}
                        >
                            {this.state.graph ? (
                                <ForceGraph2D
                                    ref={this.renderer}
                                    width={this.state.graphWidth}
                                    graphData={this.state.graph}
                                    onNodeClick={this.handleNodeClick}
                                    autoPauseRedraw={false}
                                    cooldownTicks={0}
                                    warmupTicks={this.warmupTicks}
                                    onEngineStop={this.handleEngineStop}
                                    nodeCanvasObject={
                                        this.handleNodeCanvasObject
                                    }
                                    nodeCanvasObjectMode={() => "after"}
                                    linkCanvasObject={
                                        this.handleLinkCanvasObject
                                    }
                                    linkCanvasObjectMode={() => "replace"}
                                    nodeColor={(node: Node) => node.type.color}
                                    onNodeDrag={this.handleNodeDrag}
                                    onLinkRightClick={
                                        this.handleElementRightClick
                                    }
                                    onNodeRightClick={
                                        this.handleElementRightClick
                                    }
                                    onBackgroundClick={(event: any) =>
                                        this.handleBackgroundClick(
                                            event,
                                            this.extractPositions(event)
                                        )
                                    }
                                />
                            ) : undefined}
                        </SelectLayer>
                    </div>
                    <div id="sidepanel">
                        <HistoryNavigator
                            spaceId="space"
                            history={this.state.graph}
                            onChange={this.onGraphDataChange}
                        />
                        <hr />
                        <NodeDetails
                            selectedNodes={this.state.selectedNodes}
                            allTypes={
                                this.state.graph
                                    ? this.state.graph.objectGroups
                                    : []
                            }
                            onChange={this.forceUpdate}
                        />
                        <hr />
                        <h3>Node types</h3>
                        <NodeTypesEditor
                            onChange={this.forceUpdate}
                            graph={this.state.graph}
                            onSelectAll={this.handleNodeTypeSelect}
                        />
                        <hr />
                        <h3>Settings</h3>
                        <input
                            id="node-labe-visibility"
                            type={"checkbox"}
                            checked={this.state.visibleLabels}
                            onChange={(event) => {
                                const newValue = event.target.checked;
                                if (newValue == this.state.visibleLabels) {
                                    return;
                                }

                                this.setState({
                                    visibleLabels: newValue,
                                });
                            }}
                        />
                        <label htmlFor="node-labe-visibility">
                            Node labels
                        </label>
                        <br />
                        <input
                            id="connect-on-drag"
                            type={"checkbox"}
                            checked={this.state.connectOnDrag}
                            onChange={(event) => {
                                const newValue = event.target.checked;
                                if (newValue == this.state.connectOnDrag) {
                                    return;
                                }

                                this.setState({
                                    connectOnDrag: newValue,
                                });
                            }}
                        />
                        <label htmlFor="connect-on-drag">
                            Connect nodes when dragged
                        </label>
                        <hr />
                        <ul className="instructions">
                            <li>Click background to create node</li>
                            <li>
                                SHIFT+Click and drag on background to add nodes
                                to selection
                            </li>
                            <li>CTRL+Click background to clear selection</li>
                            <li>Click node to select and edit</li>
                            <li>
                                SHIFT+Click node to add or remove from selection
                            </li>
                            <li>CTRL+Click another node to connect</li>
                            <li>Right-Click node to delete</li>
                            <li>Right-Click link to delete</li>
                            {this.state.connectOnDrag ? (
                                <li>
                                    Drag node close to other node to connect
                                </li>
                            ) : (
                                ""
                            )}
                            <li>DELETE to delete selected nodes</li>
                            <li>ESCAPE to clear selection</li>
                        </ul>
                    </div>
                </div>
            </div>
        );
    }
}