import * as React from "react";
import {RountangleComponent} from "./RountangleComponent";
import {PrimitiveValue, UUID} from "../../onion/types";
import {GraphType} from "../editable_graph";
import {RountangleAction} from "./RountangleActions";
import {INodeState, IValueState, GraphState} from "../../onion/graph_state";
import {PrimitiveDelta, PrimitiveRegistry} from "../../onion/primitive_delta";
import {assert, assertNever} from "../../util/assert";
import {d3Types} from "../graph";

export interface Rountangle {
    readonly name:   string;
    readonly posX:   number;
    readonly posY:   number;
    readonly posZ:   number;
    readonly width:  number;
    readonly height: number;
}

export function isRountangle(d3node: d3Types.d3Node<INodeState | IValueState>) {
    if (!(d3node.obj.type === "node")) return false;
    const nodeState = d3node.obj as unknown as INodeState;
    const outgoing = nodeState.getOutgoingEdges();
    if (!(outgoing.get('type')?.asTarget() === 'Rountangle')) return false;
    if (!(typeof outgoing.get('label')?.asTarget()   === "string")) return false;
    if (!(typeof outgoing.get('x')?.asTarget()       === "number")) return false;
    if (!(typeof outgoing.get('y')?.asTarget()       === "number")) return false;
    if (!(typeof outgoing.get('z-index')?.asTarget() === "number")) return false;
    if (!(typeof outgoing.get('width')?.asTarget()   === "number")) return false;
    if (!(typeof outgoing.get('height')?.asTarget()  === "number")) return false;

    return true;
}

// Precondition: isRountangle(d3Node)
export function graphStateToRountangle(d3node: d3Types.d3Node<INodeState | IValueState>): [PrimitiveValue, Rountangle]  {
    if (!isRountangle(d3node)) {
        throw new Error(`RountangleParser Cannot parse: ${d3node}`);
    }

    const nodeState = d3node.obj as INodeState;
    const outgoing = nodeState.getOutgoingEdges();
    return [nodeState.creation.id.value, {
        name: outgoing.get("label")!.asTarget() as string,
        posX: outgoing.get("x")!.asTarget() as number,
        posY: outgoing.get("y")!.asTarget() as number,
        posZ: outgoing.get("z-index")!.asTarget() as number,
        width: outgoing.get("width")!.asTarget() as number,
        height: outgoing.get("height")!.asTarget() as number,
    }]
}

// export function getValueForLabelAsString(node:INodeState, label:string): string {
//     const delta = node.getOutgoingEdges().get(label);
//     if (delta) {
//         const value = delta.target.getTarget();
//         if (typeof value === "string") {
//             return value;
//         }
//         else {
//             throw new Error(`Outgoing edge "${label}" is not of type string. Type is: ${typeof value}`);
//         }
//     }
//     else {
//         throw new Error(`Node ID "${node.creation.id}" has no outgoing edge "${label}"`);
//     }
// }

// export function getValueForLabelAsNumber(node:INodeState, label:string): number {
//     const target = node.getOutgoingEdges().get(label)!.asTarget();
//     if (typeof target === "number") {
//         return target;
//     }
//     throw new Error(`Outgoing edge "${label}" is not of type number. Type is: ${typeof target}`);
// }

export interface RountangleEditorProps {
  graph: GraphType;
  graphState: GraphState;
  generateUUID: () => UUID;
  primitiveRegistry: PrimitiveRegistry;
  onUserEdit?: (deltas: PrimitiveDelta[], description: string) => void;
}

interface RountangleEditorState {
    baseWidth:     number;
    currentWidth:  number;
    baseHeight:    number;
    currentHeight: number;
    translateX:    number;
    translateY:    number;
    zoom:          number;
    dragging:      boolean;
    moved:         boolean;
}

export class RountangleEditor extends React.Component<RountangleEditorProps, RountangleEditorState> {
    readonly canvasRef: React.RefObject<SVGSVGElement>
    private onDispatchListeners: Set<(action: RountangleAction) => void>;

    constructor(props) {
        super(props);
        this.canvasRef = React.createRef<SVGSVGElement>();
        this.onDispatchListeners = new Set();
        this.state = {
            baseWidth: 350,
            baseHeight: 350,
            currentHeight: 350,
            currentWidth: 350,
            translateX: 0,
            translateY: 0,
            zoom: 1.0,
            dragging: false,
            moved: false
        }
    }

    componentDidMount() {
        this.addOnDispatchListener(_ => this.forceUpdate());
        // IMPORTANT: notice the `passive: false` option
        this.canvasRef.current?.addEventListener('wheel', this.onWheel, { passive: false });

    }

    componentWillUnmount() {
        this.canvasRef.current?.removeEventListener('wheel', this.onWheel);
    }


    // this function gets a list of key-value-pairs and returns a compositeDelta that represents the creation of the
    // appropriate nodes and edges in the graph state.
    createValueDeltas(nodeState: INodeState, values: [string,PrimitiveValue][]): PrimitiveDelta[] {
        const deltas: PrimitiveDelta[] = [];
        for (const [edgeLabel, value] of values) {
            deltas.push(...nodeState.getDeltasForSetEdge(this.props.primitiveRegistry, edgeLabel, value));
        }
        return deltas;
    }

    dispatch = (action: RountangleAction) => {
        const deltas: PrimitiveDelta[] = [];
        let uuid: UUID;
        let nodeState: INodeState | undefined;

        switch (action.tag) {
            case "createRountangle":
                uuid = new UUID(action.id);
                const createRountangleNodeDelta = this.props.primitiveRegistry.newNodeCreation(uuid);
                deltas.push(createRountangleNodeDelta);
                const edgeSpec: [string,PrimitiveValue][] = [
                    ["type","Rountangle"],
                    ["label",action.name],
                    ["x",action.posX],
                    ["y",action.posY],
                    ["z-index",action.posZ],
                    ["width",action.width],
                    ["height",action.height],
                ];
                deltas.push(... edgeSpec.map(([edgeLabel, value]) =>
                    this.props.primitiveRegistry.newEdgeCreation(createRountangleNodeDelta, edgeLabel, value)));
                break;
            case 'moveRountangle':
                // get nodeState from clicked node
                nodeState = this.props.graphState.nodes.get(action.id);
                if (nodeState !== undefined) {
                    deltas.push(...this.createValueDeltas(nodeState, [["x", action.newPosX], ["y", action.newPosY]]));
                }
                break;
            case 'deleteRountangle':
                 // get nodeState from clicked node
                 nodeState = this.props.graphState.nodes.get(action.id);
                 if (nodeState !== undefined) {
                     deltas.push(...nodeState.getDeltasForDelete(this.props.primitiveRegistry));
                 }
                 break;
            case 'resizeRountangle':
                nodeState = this.props.graphState.nodes.get(action.id);
                if (nodeState !== undefined) {
                    deltas.push(...this.createValueDeltas(nodeState, [["width",action.width],["height",action.height]]));
                }
                break;
            case 'renameRountangle':
                nodeState = this.props.graphState.nodes.get(action.id);
                if (nodeState !== undefined) {
                    deltas.push(...this.createValueDeltas(nodeState, [["label",action.newName]]));
                }
                break;
            case 'changeRountangleZ':
                // this.setState( (state,_) => ({
                //     ...state,
                //     [action.id]: {...state[action.id], posZ: action.newPosZ}
                // }));
                // TODO
                break;
            default: assertNever(action);
        }
        if (deltas.length > 0) {
            this.props.onUserEdit?.(deltas, action.tag);
        }

        this.onDispatchListeners.forEach(listener => listener(action));
    }

    addOnDispatchListener(newListener: (action: RountangleAction) => void) {
        this.onDispatchListeners.add(newListener);
    }

    onPointerDown = (event: React.PointerEvent<SVGSVGElement>) => {
        // only left mouse button
        if (event.button !== 0) return;
        event.stopPropagation();
        event.preventDefault();
        event.currentTarget.setPointerCapture(event.pointerId);

        if (!event.altKey) {
            this.setState({dragging: true, moved: false});
        }
    }

    onPointerMove = (event: React.PointerEvent<SVGSVGElement>) => {
        if (!this.state.dragging) return;

        if (event.movementY !== 0 ||event.movementX !== 0) {
            this.setState({
                translateX: this.state.translateX - event.movementX * this.state.zoom,
                translateY: this.state.translateY - event.movementY * this.state.zoom,
                moved:      true
            });
        }

        event.stopPropagation();
        event.preventDefault();
    }

    private clickToSVGPos = (x:number, y: number): DOMPoint | undefined => {
        // point transformation adapted from https://stackoverflow.com/a/70595400
        if (!this.canvasRef.current) return undefined;

        const screenCTM = this.canvasRef.current.getScreenCTM();
        if (screenCTM === null) return undefined;

        const refPoint = new DOMPoint(x, y);
        return refPoint.matrixTransform(screenCTM.inverse());
    }

    onPointerUp = (event: React.PointerEvent<SVGSVGElement>) => {
        event.stopPropagation();
        event.preventDefault();
        event.currentTarget.releasePointerCapture(event.pointerId);

        this.setState({
            dragging: false,
            moved:    false
        });

        // add new state on left mouse button and ALT-Key pressed
        if (event.button === 0 && event.altKey) {
            const cursorPoint = this.clickToSVGPos(event.clientX, event.clientY);

            if (cursorPoint) {
                const newRountangleName = prompt('Name', 'New Rountangle');
                if (newRountangleName) {
                    this.dispatch({
                        tag: 'createRountangle',
                        id: this.props.generateUUID().value.toString(),
                        posX: cursorPoint.x,
                        posY: cursorPoint.y,
                        posZ: 10,
                        width: 100,
                        height: 66,
                        name: newRountangleName});
                }
            }
        }
    }

    onWheel = (event: WheelEvent) => {
        event.preventDefault();
        event.stopPropagation();
        if (event.deltaY === 0) return;
        if(!this.canvasRef.current) return;

        const newZoom = event.deltaY > 0 ? Math.min(this.state.zoom * 1.1, 3.0) : Math.max(this.state.zoom * 0.9, 0.1);

        this.setState({
            zoom: newZoom,
            translateX: this.state.translateX + (this.state.baseWidth * this.state.zoom - this.state.baseWidth * newZoom)/2,
            translateY: this.state.translateY + (this.state.baseHeight * this.state.zoom - this.state.baseHeight * newZoom)/2,
            currentWidth: this.state.baseWidth * newZoom,
            currentHeight: this.state.baseHeight * newZoom
        })
    }

    render() {
        return(
            <svg
                   className={`re-background canvas ${this.state.dragging ? 'dragging' : ''}`}
                   onPointerDown={this.onPointerDown}
                   onPointerMove={this.onPointerMove}
                   onPointerUp={this.onPointerUp}
                   width='350px'
                   height='100%'
                   viewBox={`${this.state.translateX} ${this.state.translateY} ${this.state.currentWidth} ${this.state.currentHeight}`}
                   ref={this.canvasRef}
               >
                   {
                       Array.from(this.props.graph.nodes)
                       .filter(isRountangle)
                           .map(graphStateToRountangle)
                           .map(node => {
                            const [id, rountangle] = node;
                            return <RountangleComponent
                                key={id.toString()}
                                id={id}
                                name={rountangle.name}
                                posX={rountangle.posX}
                                posY={rountangle.posY}
                                posZ={rountangle.posZ}
                                width={rountangle.width}
                                height={rountangle.height}
                                zoom={this.state.zoom}
                                dispatch={this.dispatch}
                            />
                       })
                   }
            </svg>
        )
    }
}

