// PyZX - Python library for quantum circuit rewriting 
//        and optimisation using the ZX-calculus
// Copyright (C) 2018 - Aleks Kissinger and John van de Wetering

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//    http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// When the next line is uncommented, the module is reloaded every time the javascript is imported
// This is useful for development.
require.undef('make_editor')

define('make_editor', ['d3'], function(d3) {
        
    // styling functions
    function nodeColor(t) {
        if (t == 0) return "black";
        else if (t == 1) return "#ccffcc";
        else if (t == 2) return "#ff8888";
        else if (t == 3) return "yellow";
    }

    function edgeColor(t,selected) {
        if (selected) return "#083bd4";
        if (t == 1) return "black";
        else if (t == 2) return "#08f";
    }

    function edgeWidth(selected) {
        if (selected) return "stroke-width: 2.5px";
        return "stroke-width: 1.5px";
    }

    function nodeStyle(selected) {
        return selected ? "stroke-width: 2px; stroke: #00f" : "stroke-width: 1.5px";
    }

    function prepareGraph(graph, selection) {
        var max_name = -1;
        
        var vtab = {};
        graph.nodes.forEach(function(d) {
            vtab[d.name] = d;
            if (d.name > max_name) max_name = d.name;
            d.selected = false;
            d.previouslySelected = false;
            d.nhd = [];
        });
        selection.nodes.forEach(function(d) {if (d.name in vtab) vtab[d.name].selected=true;});

        var ltab = {};
        graph.links.forEach(function(d) {
            ltab[d.source + "_" + d.target] = d;
            var s = vtab[d.source];
            var t = vtab[d.target];
            d.source = s;
            d.target = t;
            s.nhd.push(t);
            t.nhd.push(s);
            d.selected = false;
        });
        selection.links.forEach(function(d) {if (d.source +"_" + d.target in ltab) ltab[d.source +"_" + d.target].selected=true;});
        return max_name;
    }

    function showGraph(tag, model, show_labels) {
        var shiftKey, ctrlKey;

        // SETUP SVG ITEMS

        var svg = d3.select(tag)
            .attr("tabindex", 1)
            .on("keydown.brush", function() {shiftKey = d3.event.shiftKey;
            								 ctrlKey  = d3.event.ctrlKey || d3.event.metaKey;})
            .on("keyup.brush", function() {shiftKey = d3.event.shiftKey;
            							   ctrlKey  = d3.event.ctrlKey || d3.event.metaKey;})
            .each(function() { this.focus(); })
            .append("svg")
            .attr("style", "max-width: none; max-height: none")
            .attr("width", model.width)
            .attr("height", model.height);
        
        var brush = svg.append("g")
            .attr("class", "brush");
        
        var link = svg.append("g")
                .attr("class", "link")
                .selectAll("line")

        var node = svg.append("g")
                .attr("class", "node")
                .selectAll("g")
        
        const dragLine = svg.append('line')
                .attr('id', 'dragLine')
                .attr('class', 'link hidden')
                .attr("stroke", edgeColor(1))
                .attr("style", "stroke-width: 1.5px;pointer-events: none;")
                .attr("x1", 0)
                .attr("y1", 0)
                .attr("x2", 0)
                .attr("y2", 0);

        var mousedownNode = null;
        
        function resetMouseVars() {
            mousedownNode = null;
        }

        
        const vertexTypeBox = svg.append('g');
        vertexTypeBox.append('rect')
                .attr('x',0)
                .attr('y',model.height-24)
                .attr('width', 100).attr('height', 24)
                .attr("stroke", "black")
                .attr("fill", "white")
                .attr("style", "stroke-width: 2.5px");
        vertexTypeBox.append('text')
                .attr('x',5)
                .attr('y',model.height-7)
                .attr('style', 'pointer-events: none; user-select: none;')
                .text("Vertex type: Z");

        const edgeTypeBox = svg.append('g');
        edgeTypeBox.append('rect')
                .attr('x',100)
                .attr('y',model.height-24)
                .attr('width', 100).attr('height', 24)
                .attr("stroke", "black")
                .attr("fill", "white")
                .attr("style", "stroke-width: 2.5px");
        edgeTypeBox.append('text')
                .attr('x',105)
                .attr('y',model.height-7)
                .attr('style', 'pointer-events: none; user-select: none;')
                .text("Edge type: R");

        var addVertexType = 1;
        var addEdgeType = 1;

        function switchAddVertexType() {
            console.log("Switching vertex type");
            addVertexType = (addVertexType + 1) % 4;
            var thetext = vertexTypeBox.select("text");
            let options = ['B','Z','X','H'];
            thetext.text("Vertex type: " + options[addVertexType]);
            d3.event.stopImmediatePropagation();
            resetMouseVars();
        }

        function switchAddEdgeType() {
            console.log("Switching edge type");
            var thetext = edgeTypeBox.select("text");
            if (addEdgeType == 1) {
                thetext.text("Edge type: H");
                addEdgeType = 2;
            }
            else {
                thetext.text("Edge type: R");
                addEdgeType = 1;
            }
            d3.select("#dragLine").attr("stroke", edgeColor(addEdgeType));
            d3.event.stopImmediatePropagation();
            resetMouseVars();
        }

        edgeTypeBox.on("click", function(d) {switchAddEdgeType();});
        vertexTypeBox.on("click", function(d) {switchAddVertexType();});

        function deselectEdges() {
            link.attr("stroke", function(e) {return edgeColor(e.t,e.selected=false);})
                            .attr("style", edgeWidth(false));
        }

        
        function updateGraph() {
            console.log("Updating graph view")
            var node_size = model.node_size;
            var graph = model.graph;
            
            //First initialize all the nodes properly, before looking at edges.
            node = node.data(graph.nodes, function(d) {return d.name;});
            node.exit().remove();
            
            var newnodes = node.enter().append("g")

            newnodes.filter(function(d) { return d.t != 3; })
                .append("circle")
                .attr("stroke", "black");

            var hbox = newnodes.filter(function(d) { return d.t == 3; });

            hbox.append("rect")
                .attr("x", -0.75 * node_size).attr("y", -0.75 * node_size)
                .attr("width", node_size * 1.5).attr("height", node_size * 1.5)
                .attr("fill", nodeColor(3))
                .attr("stroke", "black");

            newnodes.append("text")
                .attr("y", 0.7 * node_size + 14)
                .attr("text-anchor", "middle")
                .attr("font-size", "12px")
                .attr("font-family", "monospace")
                .attr("fill", "#00d")
                .attr("style", 'pointer-events: none; user-select: none;')

            if (show_labels) {
                newnodes.append("text")
                    .attr("y", -0.7 * node_size - 5)
                    .text(function (d) { return String(d.name); })
                    .attr("text-anchor", "middle")
                    .attr("font-size", "8px")
                    .attr("font-family", "monospace")
                    .attr("fill", "#ccc")
                    .attr('style', 'pointer-events: none; user-select: none;');
            }

            //All the keyboard events of the nodes

            newnodes.on("mousedown", function(d) {
                if (ctrlKey) { // Start the adding of an edge
                    mousedownNode = d;
                    d3.event.stopImmediatePropagation();
                    dragLine.classed('hidden', false)
                        .attr("x1", mousedownNode.x)
                        .attr("y1", mousedownNode.y)
                        .attr("x2", mousedownNode.x)
                        .attr("y2", mousedownNode.y);
                }
                else if (shiftKey) { // Add the node to the selection
                    console.log("Adding to selection");
                    d3.select(this).select(":first-child").attr("style", nodeStyle(d.selected = !d.selected));
                    d3.event.stopImmediatePropagation();
                    resetMouseVars();
                    model.selection_changed();
                } else if (!d.selected) {
                    console.log("Deselecting all except this vertex");
                    node.select(":first-child").attr("style", function(p) { return nodeStyle(p.selected = d === p); });
                    deselectEdges();
                    resetMouseVars();
                    model.selection_changed();
                }
            })
            .on("mouseup", function(d) { //Check if we need to add an edge
                if (ctrlKey && mousedownNode) {
                    d3.event.stopImmediatePropagation();
                    dragLine.classed('hidden', true);
                    if (mousedownNode === d) {//released on self
                        resetMouseVars();
                        return; 
                    }
                    f = link.filter(function (e) {
                        return ((e.source.name == mousedownNode.name && e.target.name == d.name) ||
                                (e.target.name == mousedownNode.name && e.source.name == d.name));
                    })
                    if (!f.empty()) {
                        console.log("already edge present")
                        f.attr("stroke", function(d) {d.t = addEdgeType;return edgeColor(d.t)});
                    }
                    else {
                    const edge = {t:addEdgeType, source: mousedownNode, target: d, selected: false};
                    console.log("Adding edge")
                    mousedownNode.nhd.push(d);
                    d.nhd.push(mousedownNode);
                    model.graph.links.push(edge);
                    //updateGraph();
                    }
                    model.push_changes("Added edge");
                }
            })
            .on("dblclick", function(d) {
                const pi = '\u03c0';
                var phase = prompt("Input phase as fraction of pi (like 3/4 or 1):", d.phase);
                if (phase == null) {return;}
                if (phase.includes('.')) {alert("Invalid value " + phase) + phase; return;}
                if (phase != "") {
                    phase = phase.replace(pi, '').replace('pi', '');
                    if (phase.includes("/")) {
                        var terms = phase.split("/");
                        if (terms.length != 2) {alert("Invalid value " + phase); return;}
                        let a = terms[0]; let b = terms[1];
                        if (a!= "" && a != "-" && isNaN(a)) {alert("Invalid value " + phase); return;}
                        if (a=="1") {a = "";}
                        if (a=="-1") {a = "-";}
                        // if (a == "") {a=1;}
                        // else if (a == "-") {a=-1;}
                        // else if (isNaN(a)) {alert("Invalid value " + phase); return;}
                        // else {a = parseInt(a);}
                        if (isNaN(b)) {alert("Invalid value " + phase); return;}
                        b = parseInt(b);
                        phase = a + pi +"/" + b;
                    }
                    else {
                        if (isNaN(phase)) {alert("Invalid value " + phase); return;}
                        let a = parseInt(phase) % 2
                        if (d.t != 3) {
                            if (phase == "") {phase = pi;}
                            else if (a == 0) {phase = "";} 
                            else {phase = pi;}
                        }
                        else {
                            if (phase == "") {phase = "";}
                            else if (a == 1) {phase = "";}
                            else {phase = "0"}
                        }
                    }
                }
                d.phase = phase
                model.push_changes("Changed phase");

                d3.select(this).select("text").text(phase)
                                .attr("visibility", (phase == "") ? 'hidden' : 'visible')
            })
            .call(d3.drag().on("drag", function(d) {
                var dx = d3.event.dx;
                var dy = d3.event.dy;
                node.filter(function(d) { return d.selected; })
                    .attr("transform", function(d) {
                        d.x += dx;
                        d.y += dy;
                        return "translate(" + d.x + "," + d.y +")";
                    });


                link.filter(function(d) { return d.source.selected; })
                    .attr("x1", function(d) { return d.source.x; })
                    .attr("y1", function(d) { return d.source.y; });

                link.filter(function(d) { return d.target.selected; })
                    .attr("x2", function(d) { return d.target.x; })
                    .attr("y2", function(d) { return d.target.y; });

                // text.filter(function(d) { return d.selected; })
                //     .attr("x", function(d) { return d.x; })
                //     .attr("y", function(d) { return d.y + 0.7 * node_size + 14; });
            }).on("end", function(d) {model.push_changes("Moved selection");})
            );

            //Finally position all the nodes and update the texts and types
            
            node = newnodes.merge(node);
            node.attr("transform", function(d) {
                        return "translate(" + d.x + "," + d.y +")";
                    }).filter(function(d) {return d.t!=3;})
                .select(":first-child")
                    .attr("r", function(d) {
                           if (d.t == 0) return 0.5 * node_size;
                           else return node_size;
                        })
                    .attr("fill", function(d) { return nodeColor(d.t); })
                    .attr("style", function(d) { return nodeStyle(d.selected); });

            node.select("text").text(function (d) { return d.phase })
                    .attr("visibility", function(d) {return (d.phase == "") ? 'hidden' : 'visible';});

            //TODO: Right now, if a node changes from non-type 3 to type 3 or back, 
            //then the square wouldn't update to a circle and vice versa
            hbox = node.filter(function(d) { return d.t == 3; });
            hbox.select(":first-child").attr("style", function(d) { return nodeStyle(d.selected); });

            // Now let's construct and update all the edges
            
            link = link.data(graph.links, function(d) {return String(d.source.name) + "_" + String(d.target.name);});
            link.exit().remove();
            
            var newlinks = link.enter().append("line")
                .on("click", function(d) {
                    if (ctrlKey) {return;}
                    if (!shiftKey) {
                        deselectEdges();
                        node.select(":first-child").attr("style", function(n) {nodeStyle(n.selected=false)});
                    }
                    d.selected = !d.selected
                    d3.select(this).attr("stroke", edgeColor(d.t,d.selected))
                        .attr("style", edgeWidth(d.selected));
                    model.selection_changed();
                });
            
            link = newlinks.merge(link);
            link.attr("stroke", function(d) { return edgeColor(d.t,d.selected); })
                .attr("style", function(d) {return edgeWidth(d.selected); })
                .attr("x1", function(d) { return d.source.x; })
                .attr("y1", function(d) { return d.source.y; })
                .attr("x2", function(d) { return d.target.x; })
                .attr("y2", function(d) { return d.target.y; });
        } // End function updateGraph()
        
        updateGraph();
        
        // EVENTS FOR ADDING VERTICES AND EDGES
        svg.on("mousedown", function(d) {
            if (!ctrlKey) return;
            console.log("Adding vertex");
            const point = d3.mouse(this);
            model.max_name += 1
            const vert = { name: model.max_name, t: addVertexType, 
                           selected: false, previouslySelected: false,
                           nhd: [], x: point[0], y: point[1], phase:''};
            model.graph.nodes.push(vert);
            resetMouseVars();
            model.push_changes("Added vertex");
            //updateGraph();
            })
            .on("mousemove", function(d) {
                if (!mousedownNode) return;
                dragLine.attr("x2", d3.mouse(this)[0])
                    .attr("y2",d3.mouse(this)[1]);
            })
            .on("mouseup", function(d) {
                if (mousedownNode) {
                    dragLine.classed('hidden', true);
                    resetMouseVars();
                }
            });
        
        var lastKeyDown = -1;
        
        d3.select(tag).on("keydown", function() {
            if (lastKeyDown !== -1 && lastKeyDown != 16 && lastKeyDown != 17 // 16 == shiftKey, 17 == ctrlKey
                && lastKeyDown != 91 && lastKeyDown != 93 && lastKeyDown != 224) return; // 91,93,224 = metaKey in different browsers
            lastKeyDown = d3.event.keyCode;
            switch (d3.event.keyCode) {
                case 46: //delete
                case 8: //backspace
                    console.log("Deleting...")
                    d3.event.preventDefault();
                    node.each(function(d) {
                        if (!d.selected) return;
                        model.graph.nodes.splice(model.graph.nodes.indexOf(d),1);
                    });
                    link.each(function(d) {
                        if (!d.source.selected && !d.target.selected && !d.selected) return;
                        model.graph.links.splice(model.graph.links.indexOf(d),1);
                        var l = d.target.nhd;
                        l.splice(l.indexOf(d.source),1);
                        l = d.source.nhd;
                        l.splice(l.indexOf(d.target),1);
                        // if (d.source.selected) {
                        //     let l = d.target.nhd;
                            
                        // }
                        // if (d.target.selected) {
                            
                        // }
                    });
                    model.push_changes("Delete");
                    model.selection_changed();
                    break;
                case 88: // X
                    d3.event.preventDefault();
                    switchAddVertexType(); break;
                case 69: // E
                    d3.event.preventDefault();
                    switchAddEdgeType(); break
                case 90: // Z
                	console.log("Z press");
                	if (!ctrlKey) return;
                	d3.event.preventDefault();
                	if (!shiftKey) {model.perform_action("undo");}
                	else {model.perform_action("redo");}
                	break;
                case 86: // V
                    console.log("V press")
                    if (!ctrlKey) return;
                    if (shiftKey) return;
                    d3.event.preventDefault();
                    model.perform_action("paste")
                    break;
                case 67: // C
                    console.log("C press")
                    if (!ctrlKey) return;
                    if (shiftKey) return;
                    d3.event.preventDefault();
                    model.perform_action("copy")
                    break;
            }
            
        }).on("keyup", function() {
            lastKeyDown = -1;
        });

        // EVENTS FOR DRAGGING AND SELECTION
        
        brush.call(d3.brush().keyModifiers(false).filter(() => !ctrlKey)
            //.extent([[0, 0], [model.width, model.height]])
            .on("start", function() {
                if (d3.event.sourceEvent.type !== "end") {
                    node.select(":first-child").attr("style", function(d) {
                        return nodeStyle(
                            d.selected = d.previouslySelected = shiftKey &&
                            d.selected);
                    });
                    if (!shiftKey) deselectEdges();
                    model.selection_changed();
                }
            })
            .on("brush", function() {
                if (d3.event.sourceEvent.type !== "end") {
                    var selection = d3.event.selection;
                    node.select(":first-child").attr("style", function(d) {
                        return nodeStyle(d.selected = d.previouslySelected ^
                            (selection != null
                            && selection[0][0] <= d.x && d.x < selection[1][0]
                            && selection[0][1] <= d.y && d.y < selection[1][1]));
                    });
                }
            })
            .on("end", function() {
                if (d3.event.selection != null) {
                    d3.select(this).call(d3.event.target.move, null);
                    model.selection_changed();
                }
            }));

        return updateGraph;
    }

    return {
        prepareGraph: prepareGraph,
        showGraph: showGraph
    };
});
