import * as React from 'react';
import {d3Types, Graph, GraphProps, Forces} from "./graph";

import {
  NodeCreation,
  NodeDeletion,
  EdgeCreation,
  EdgeUpdate,
  PrimitiveDelta,
} from "../onion/primitive_delta";

import {NodeState, ValueState, GraphDeltaExecutor} from "../onion/delta_executor";


import {PrimitiveValue, UUID} from "../onion/types";

import {
  Version,
  initialVersion,
  VersionRegistry,
} from "../onion/version";

export type NodeType = d3Types.d3Node<NodeState|ValueState>;
export type LinkType = d3Types.d3Link<null>;
export type GraphType = d3Types.d3Graph<NodeState|ValueState,null>;

export type UserEditCallback = (deltas: PrimitiveDelta[], description: string) => void;
export type SetNodePositionCallback = (x:number, y:number) => void;

export interface EditableGraphProps {
  graph: GraphType;
  graphDeltaExecutor: GraphDeltaExecutor;
  forces: Forces;
  generateUUID: () => UUID;
  setNextNodePosition: SetNodePositionCallback;
  onUserEdit?: UserEditCallback;
}

interface EditableGraphState {
}


export class EditableGraph extends React.Component<EditableGraphProps, EditableGraphState> {
  mouseDownNode: NodeType | null = null; // only while mouse button is down, does this record the d3Node that was under the cursor when the mouse went down.

  constructor(props) {
    super(props);
    this.state = {showCrossHair: false};
  }

  mouseDownHandler = (event, {x,y}, mouseDownNode: NodeType | undefined) => {
    event.stopPropagation();
    if (mouseDownNode) {
      this.mouseDownNode = mouseDownNode;
    }
  }

  mouseUpHandler = (event, {x,y}, mouseUpNode: NodeType | undefined) => {
    event.stopPropagation();

    // Construct the delta(s) that capture the user's change:
    const deltas: PrimitiveDelta[] = (() => {
      if (event.button === 2) { // right mouse button
        if (this.mouseDownNode !== null && this.mouseDownNode.obj instanceof NodeState) {
          // create outgoing edge...
          if (mouseUpNode !== undefined) {
            // right mouse button was dragged from one node to another -> create/update edge from one node to the other
            const label = prompt("Edge label (ESC to cancel):", "label");
            if (label !== null) {
              return this.mouseDownNode.obj.getDeltasForSetEdge(label, mouseUpNode.obj);
            }
          }
          else {
            // right mouse button was dragged from a node to empty space -> create/update edge from node to a value
            const label = prompt("Edge label (ESC to cancel):", "label");
            if (label !== null) {
              let enteredValue: string|null = "\"42\"";
              while (true) {
                enteredValue = prompt("Target value (enter a JSON-parsable(!) string (with quotes), number, boolean or null):\n(ESC to cancel)", enteredValue);
                if (enteredValue === null) {
                  break;
                }
                let parsedValue;
                try {
                  parsedValue = JSON.parse(enteredValue);
                } catch (err) {
                  alert("Invalid JSON!");
                  continue;
                }
                const typeofParsedValue = typeof parsedValue;
                if (parsedValue !== null && typeofParsedValue !== "string" && typeofParsedValue !== "number" && typeofParsedValue !== "boolean") {
                  alert("Expected string, number, boolean or null. Got: " + typeofParsedValue);
                  continue;
                }
                return this.mouseDownNode.obj.getDeltasForSetEdge(label, this.props.graphDeltaExecutor.getValueState(parsedValue));
              }
            }
          }
        }
        else {
          // right mouse button clicked -> create node
          const uuid = this.props.generateUUID();
          this.props.setNextNodePosition(x,y);
          return [new NodeCreation(uuid)];
        }
      }
      else if (event.button === 1) { // middle mouse button
        if (mouseUpNode !== undefined) {
          // middle mouse button click on node -> delete node (and incoming/outgoing edges)
          return mouseUpNode.obj.getDeltasForDelete();
        }
      }
      return [];
    })();

    if (deltas.length > 0) {
      // Let the world know that there is a new (composite) delta, and a new version:
      // const composite = this.props.compositeLvl.createComposite(deltas);
      // const version = this.props.versionRegistry.createVersionUnsafe(this.props.version, composite);

      this.props.onUserEdit?.(deltas, deltas.map(d => d.getDescription()).join(","));
    }

    this.mouseDownNode = null;
  }

  render() {
    return (
      <Graph
        graph={this.props.graph}
        forces={this.props.forces}

        mouseDownHandler={this.mouseDownHandler}
        mouseUpHandler={this.mouseUpHandler}
      />
    );
  }
}