import $ from 'jquery';
import arches from 'arches';
import AbstractModel from 'models/abstract';
import NodeModel from 'models/node';
import ko from 'knockout';
import _ from 'underscore';

export default AbstractModel.extend({
    /**
    * A backbone model to manage graph data
    * @augments AbstractModel
    * @constructor
    * @name GraphModel
    */

    url: arches.urls.graph,

    constructor: function (attributes, options) {
        options || (options = {});
        options.parse = true;
        AbstractModel.prototype.constructor.call(this, attributes, options);
    },

    /**
    * Flags the passed in node as selected
    * @memberof GraphModel.prototype
    * @param {NodeModel} node - the node to be selected
    */
    selectNode: function (newly_selected_node) {
        this.trigger('select-node', newly_selected_node);
        var currentlySelectedNode = this.get('selectedNode')();
        if (currentlySelectedNode && currentlySelectedNode.dirty()) {
            return false;
        } else {
            this.get('nodes')().forEach(function (node) {
                if (node !== newly_selected_node) {
                    node.selected(false);
                }
            });
            newly_selected_node.selected(true);
            return true;
        }
    },


    /**
     * deleteNode - deletes the passed in node from the db and updates the graph
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node to be deleted
     * @param  {function} callback - (optional) a callback function
     * @param  {object} scope - (optional) the scope used for the callback
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    deleteNode: function (node, callback, scope) {
        return this._doRequest({
            type: "DELETE",
            url: this.url + this.get('graphid') + '/delete_node',
            data: JSON.stringify({ nodeid: node.nodeid })
        }, function (response, status) {
            if (status === 'success' && response.responseJSON) {
                var parentNode = this.getParentNode(node);
                var getEdges = function (node) {
                    var edges = this.get('edges')()
                        .filter(function (edge) {
                            return edge.domainnode_id === node.nodeid;
                        });
                    var nodes = edges.map(function (edge) {
                        return this.get('nodes')().find(function (node) {
                            return edge.rangenode_id === node.nodeid;
                        });
                    }, this);
                    nodes.forEach(function (node) {
                        edges = edges.concat(getEdges.call(this, node));
                    }, this);
                    return edges;
                };

                var edges = getEdges.call(this, node);
                var nodes = edges.map(function (edge) {
                    return this.get('nodes')().find(function (node) {
                        return edge.rangenode_id === node.nodeid;
                    });
                }, this);
                var edge = this.get('edges')()
                    .find(function (edge) {
                        return edge.rangenode_id === node.nodeid;
                    });
                nodes.push(node);
                edges.push(edge);
                if (node.isCollector()) {
                    this.get('cards').remove(function (card) {
                        return card.nodegroup_id === node.nodeid;
                    });
                }
                this.get('edges').remove(function (edge) {
                    return _.contains(edges, edge);
                });
                this.get('nodes').remove(function (node) {
                    return _.contains(nodes, node);
                });
                parentNode.childNodes.remove(node);
                parentNode.selected(true);

                // adds event to trigger dirty state in graph-designer
                document.dispatchEvent(
                    new Event('deleteNode')
                );
            } else {
                this.trigger('error', response, 'deleteNode');
            }

            if (typeof callback === 'function') {
                scope = scope || this;
                callback.call(scope, response, status);
            }
        }, this, 'changed');
    },

    /**
     * getParentNode - gets the parent node of the passed in node
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node whose parent should be retrieved
     * @return {object} the parent node of the passed in node
     * If parent node of the passed in node can't be found then reutrn passed in node.
     */
    getParentNode: function (node) {
        var edge = this.get('edges')()
            .find(function (edge) {
                return edge.rangenode_id === node.nodeid;
            });
        if (edge) {
            return this.get('nodes')()
                .find(function (node) {
                    return edge.domainnode_id === node.nodeid;
                });
        } else {
            return node;
        }
    },

    /**
     * appendBranch - appends a graph onto a specific node within this graph
     * @memberof GraphModel.prototype
     * @param  {string} node - the node within this graph that we're connecting the branch to
     * @param  {string} property - the ontology property to use to connect the branch, leave null to use the first available property
     * @param  {string} branch_graph - the {@link GraphModel} we're appending to this graph
     * @param  {function} callback - the function to call after the response returns from the server
     * @param  {object} scope - the value of "this" in the callback function
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    appendBranch: function (node, property, branch_graph, callback, scope) {
        property = property ? property : null;
        if (property === null) {
            if (this.get('selectedNode')().ontologyclass()) {
                var ontology_connection = _.find(branch_graph.get('domain_connections'), function (domain_connection) {
                    return _.find(domain_connection.ontology_classes, function (ontology_class) {
                        return ontology_class === this.get('selectedNode')().ontologyclass();
                    }, this);
                }, this);
                if (ontology_connection) {
                    property = ontology_connection.ontology_property;
                } else {
                    if (typeof callback === 'function') {
                        scope = scope || self;
                        callback.call(scope, null, 'failed');
                    }
                    return;
                }
            }
        }

        return this._doRequest({
            type: "POST",
            url: this.url + this.get('graphid') + '/append_branch',
            data: JSON.stringify({
                nodeid: node.nodeid,
                property: property,
                graphid: branch_graph.get('graphid'),
                return_appended_graph: true,
            })
        }, function (response, status) {
            if (status === 'success' && response.responseJSON) {
                var branchroot = response.responseJSON.root;
                response.responseJSON.nodes.forEach(function (node) {
                    this.get('nodes').push(new NodeModel({
                        source: node,
                        datatypelookup: this.get('datatypelookup'),
                        graph: this,
                        "ontology_namespaces": this.get('root').ontology_namespaces
                    }));
                }, this);
                response.responseJSON.edges.forEach(function (edge) {
                    this.get('edges').push(edge);
                }, this);
                this.set('nodegroups', response.responseJSON.nodegroups);
                response.responseJSON.cards_x_nodes_x_widgets.forEach(function(widget){
                    this.get('cardwidgets').push(widget);
                }, this);
                this.get('cards')(
                    this.get('cards')().concat(
                        response.responseJSON.cards
                    )
                );

                if (!this.get('isresource')) {
                    this.get('nodes')().forEach(function (node) {
                        node.selected(false);
                        if (node.nodeid === branchroot.nodeid) {
                            node.selected(true);
                        }
                    });
                }
                this.constructTree(null, null, response.responseJSON.edges, true);
            } else {
                this.trigger('error', response, 'appendBranch');
            }

            if (typeof callback === 'function') {
                scope = scope || this;
                callback.call(scope, response, status);
            }
        }, this, 'changed');
    },

    /**
     * appendNode - appends a graph onto a specific node within this graph
     * @memberof GraphModel.prototype
     * @param  {string} node - the node within this graph onto which we're appending a new node
     * @param  {function} callback - the function to call after the response returns from the server
     * @param  {object} scope - the value of "this" in the callback function
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    appendNode: function (node, callback, scope) {
        return this._doRequest({
            type: "POST",
            url: this.url + this.get('graphid') + '/append_node',
            data: JSON.stringify({ nodeid: node.nodeid })
        }, function (response, status) {
            if (status === 'success' && response.responseJSON) {
                var nodeSource = response.responseJSON.node;
                nodeSource.parentproperty = response.responseJSON.edge.ontologyproperty;
                var newNode = new NodeModel({
                    source: nodeSource,
                    datatypelookup: this.get('datatypelookup'),
                    graph: this,
                    "ontology_namespaces": this.get('root').ontology_namespaces
                });
                newNode.childNodes = ko.observableArray([]);

                this.get('nodes').push(newNode);
                this.get('edges').push(response.responseJSON.edge);
                node.childNodes.unshift(newNode);

                // we set these to empty strings so that a user can easily choose the 
                // onology class and property they want instead of having to unselect 
                // the values chosen by default
                newNode.ontologyclass('');
                newNode.parentproperty('');

                if (!this.get('isresource')) {
                    this.selectNode(newNode);
                }
            } else {
                this.trigger('error', response, 'appendNode');
            }

            if (typeof callback === 'function') {
                scope = scope || this;
                callback.call(scope, response, status);
            }
        }, this, 'changed');
    },

    /**
     * moveNode - moves a node from one part of the graph to another
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node within this graph that we're moving
     * @param  {string} property - the ontology property to use to connect the branch
     * @param  {NodeModel} newParentNode - the node to which we moved our branch to
     * @param  {function} callback - the function to call after the response returns from the server
     * @param  {object} scope - the value of "this" in the callback function
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    moveNode: function (node, property, newParentNode, callback, scope) {
        return this._doRequest({
            type: "POST",
            url: this.url + this.get('graphid') + '/move_node',
            data: JSON.stringify({ nodeid: node.nodeid, property: property, newparentnodeid: newParentNode.nodeid })
        }, function (response, status) {
            if (status === 'success' && response.responseJSON) {
                this.get('edges')().find(function (edge) {
                    if (edge.edgeid === response.responseJSON.edges[0].edgeid) {
                        edge.domainnode_id = response.responseJSON.edges[0].domainnode_id;
                        return true;
                    }
                    return false;
                });
                this.get('nodes')().forEach(function (node) {
                    const found_node = response.responseJSON.nodes.find(function (response_node) {
                        return response_node.nodeid === node.nodeid;
                    });
                    if (found_node) {
                        node.parse(found_node);
                    }
                });
            } else {
                this.trigger('error', response, 'moveNode');
            }

            if (typeof callback === 'function') {
                scope = scope || this;
                callback.call(scope, response, status);
            }
        }, this, 'changed');
    },

    /**
     * updateNode - updates the values of a node
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node with updated values
     * @param  {function} callback - the function to call after the response returns from the server
     * @param  {object} scope - the value of "this" in the callback function
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    updateNode: function (node, callback, scope) {
        return this._doRequest({
            type: "POST",
            url: this.url + this.get('graphid') + '/update_node',
            data: JSON.stringify(node.toJSON())
        }, function (response, status) {
            if (status === 'success' && response.responseJSON) {
                _.each(this.get('nodes')(), function (node) {
                    var nodeJSON = _.find(response.responseJSON.nodes, function (returned_node) {
                        return node.nodeid === returned_node.nodeid;
                    });
                    node.parse(nodeJSON);
                }, this);
            } else {
                this.trigger('error', response, 'updateNode');
            }

            if (typeof callback === 'function') {
                scope = scope || this;
                callback.call(scope, response, status);
            }
        }, this, 'changed');
    },

    /**
     * exportBranch - creates a new branch rooted at the supplied node
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node with updated values
     * @param  {function} callback - the function to call after the response returns from the server
     * @param  {object} scope - the value of "this" in the callback function
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    exportBranch: function (node, callback, scope) {
        return this._doRequest({
            type: "POST",
            async: false,
            url: this.url + this.get('graphid') + '/export_branch',
            data: JSON.stringify(node.toJSON())
        }, function (response, status) {
            if (status !== 'success' || !response.responseJSON) {
                this.trigger('error', response, 'exportBranch');
            }

            if (typeof callback === 'function') {
                scope = scope || this;
                callback.call(scope, response, status);
            }
        }, this, 'changed');
    },

    /**
     * getValidNodesEdges - gets a list of possible ontology properties and classes the node
     * referenced by it's id could be based on the location of the node in the graph
     * @memberof GraphModel.prototype
     * @param  {string} nodeid - the node id of the node of interest
     * @param  {function} callback - function to call when the request returns
     * @param  {object} scope - (optional) the scope used for the callback
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    getValidNodesEdges: function (nodeid, callback, scope) {
        return this._doRequest({
            type: "GET",
            url: this.url + this.get('graphid') + '/get_related_nodes/' + nodeid,
        }, function (response, status) {
            callback.call(scope, response.responseJSON);
        }, this);
    },

    /**
     * getValidDomainClasses - gets a list of possible ontology properties and classes the node
     * referenced by it's id could use to be appened to other nodes
     * @memberof GraphModel.prototype
     * @param  {string} nodeid - the node id of the node of interest
     * @param  {function} callback - function to call when the request returns
     * @param  {object} scope - (optional) the scope used for the callback
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    getValidDomainClasses: function (nodeid, callback, scope) {
        return this._doRequest({
            type: "GET",
            url: this.url + this.get('graphid') + '/get_valid_domain_nodes/' + nodeid,
        }, function (response, status) {
            callback.call(scope, response.responseJSON);
        }, this);
    },

    /**
     * canAppend - test to see whether or not a graph can be appened to this graph at a specific location
     * @memberof GraphModel.prototype
     * @param  {object} graphToAppend - the {@link GraphModel} to test appending on to this graph
     * @param  {NodeModel} nodeToAppendTo - the node from which to append the graph, defaults to the graphs selected node
     * @return  {boolean} - true if the graph can be appended, false otherwise
     */
    canAppend: function (graphToAppend, nodeToAppendTo) {
        nodeToAppendTo = nodeToAppendTo ? nodeToAppendTo : this.get('selectedNode')();

        if (!!this.get('ontology_id') && !!graphToAppend.get('ontology_id')) {
            var found = !!_.find(graphToAppend.get('domain_connections'), function (domain_connection) {
                return !!_.find(domain_connection.ontology_classes, function (ontology_class) {
                    return ontology_class === nodeToAppendTo.ontologyclass();
                }, this);
            }, this);
            if (!found) {
                return false;
            }
        }

        return true;
    },

    /**
     * parse - parses the passed in attributes into a {@link GraphModel}
     * @memberof GraphModel.prototype
     * @param  {object} attributes - the properties to seed a {@link GraphModel} with
     */
    parse: function (attributes) {
        var self = this;
        var datatypelookup = {};

        attributes = _.extend({ datatypes: [], domain_connections: [] }, attributes);
        _.defaults(attributes, { selectRoot: true });
        this.set('domain_connections_loaded', false);

        _.each(attributes.datatypes, function (datatype) {
            datatypelookup[datatype.datatype] = datatype;
        }, this);
        this.set('datatypelookup', datatypelookup);

        _.each(attributes.data, function (value, key) {
            switch (key) {
                case 'edges':
                case 'cards':
                    this.set(key, ko.observableArray(value));
                    break;
                case 'nodes':
                    var nodes = [];
                    attributes.data.nodes.forEach(function (node, i) {
                        var nodeModel = new NodeModel({
                            source: node,
                            datatypelookup: datatypelookup,
                            graph: self,
                            "ontology_namespaces": attributes.ontology_namespaces
                        });
                        nodeModel.childNodes = ko.observableArray([]);
                        if (node.istopnode) {
                            this.set('root', nodeModel);
                        }
                        nodes.push(nodeModel);
                    }, this);
                    this.set('nodes', ko.observableArray(nodes));
                    break;
                case 'root':
                    break;
                default:
                    this.set(key, value);
            }
        }, this);

        this.tree = this.constructTree();

        this.set('selectedNode', ko.computed(function () {
            var selectedNode = _.find(self.get('nodes')(), function (node) {
                return node.selected();
            }, this);
            return selectedNode;
        }));

        var root = this.get('root');
        if (!!root) {
            if (attributes.selectRoot) {
                root.selected(true);
            }
        }

        this.graphCards = ko.computed(function () {
            var parentCards = [];
            if (this.get('cards')) {
                var allCards = this.get('cards')();
                this.get('nodegroups').filter(function (nodegroup) {
                    return !!nodegroup.parentnodegroup_id === false;
                }, this).forEach(function (nodegroup) {
                    parentCards = parentCards.concat(allCards.filter(function (card) {
                        return card.nodegroup_id === nodegroup.nodegroupid;
                    }, this));
                }, this);
            }
            return parentCards;
        }, this);

        this.set('cardwidgets', ko.observableArray());
    },

    /**
     * constructTree - creates a hierarchical node listing from this graphs nodes and edges, or the passed in nodes and edges
     * @memberof GraphModel.prototype
     * @param  {NodeModel} root - a reference to the root node in the nodes parameter, or of this graph if not defined
     * @param  {[NodeModel]} nodes - the nodes to make a tree from, defaults to the nodes in this graph
     * @param  {array} edges - the edges to make a tree from, defaults to the edges in this graph
     * @param  {boolean} append - if true, won't remove the existing hierarchy
     * @return {object} a hierchical node listing
     */
    constructTree: function (root, nodes, edges, append) {
        var nodeMap = {};
        root = !!root ? root : this.get('root');
        nodes = !!nodes ? nodes : this.get('nodes')();
        edges = !!edges ? edges : this.get('edges')();
        nodes.forEach(function (node) {
            nodeMap[node.id] = node;
            if (!ko.isObservable(node.childNodes)) {
                node.childNodes = ko.observableArray([]);
            } else {
                if (!append) {
                    node.childNodes.removeAll();
                }
            }
        });

        edges.forEach(function (edge) {
            nodeMap[edge.domainnode_id].childNodes.unshift(nodeMap[edge.rangenode_id]);
        });

        edges.forEach(function (edge) {
            nodeMap[edge.domainnode_id].childNodes.sort(function (left, right) {
                return left.attributes.source.sortorder == right.attributes.source.sortorder ? 0 : (left.attributes.source.sortorder < right.attributes.source.sortorder ? -1 : 1);
            });
        });


        return root;
    },

    /**
     * loadDomainConnections - loads the domain connections for the graph asyncronously
     * @memberof GraphModel.prototype
     * @return {Promise} the Promise gets passes the responseJSON of the request
     */
    loadDomainConnections: function () {
        if (!this.get('domain_connections_loaded')) {
            return this.getValidDomainClasses('', function (responseJSON) {
                this.set('domain_connections', responseJSON);
                this.set('domain_connections_loaded', true);
            }, this);
        } else {
            return Promise.resolve();
        }

    },

    /**
     * isNodeInParentGroup - test to see if the node is in a group that is not a child to another group
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node to test
     * @return {Boolean} true if the node is in a parent group, false otherwise
     */
    isNodeInParentGroup: function (node) {
        var isInParentGroup = false;
        var nodeGroupId = node.nodeGroupId();
        if (nodeGroupId) {
            var collector = _.find(this.get('nodes')(), function (node) {
                return node.nodeid === nodeGroupId;
            });
            var childNodesAndEdges = this.getChildNodesAndEdges(collector);
            var childGroupNode = childNodesAndEdges.nodes.find(function (childNode) {
                return childNode.nodeGroupId() !== nodeGroupId;
            });
            if (childGroupNode) {
                isInParentGroup = true;
            }
        }
        return isInParentGroup;
    },

    /**
     * isNodeInChildGroup - test to see if the node is in a group that is a child to another group
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node to test
     * @return {Boolean} true if the node is in a child group, false otherwise
     */
    isNodeInChildGroup: function (node) {
        var nodeGroupId = node.nodeGroupId();
        if (!nodeGroupId) {
            return false;
        }
        var parentNodes = this.getParentNodesAndEdges(node).nodes;
        var hasParentGroup = !!parentNodes.find(function (parentNode) {
            var parentNodeGroupId = parentNode.nodeGroupId();
            return parentNodeGroupId && parentNodeGroupId !== nodeGroupId;
        });
        return hasParentGroup;
    },

    /**
     * isGroupSemantic - test to see if all the nodes in a group are semantic
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node to use as a basis of finding the group
     * @return  {boolean} - true if the group contains only semantic nodes, otherwise false
     */
    isGroupSemantic: function (node) {
        return _.every(this.getGroupedNodes(node), function (node) {
            return node.datatype() === 'semantic';
        }, this);
    },

    /**
     * getGroupedNodes - given a node, get any other nodes that share the same group
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node to use as a basis of finding the group
     * @return  {array} - a list of {@link NodeModel}
     */
    getGroupedNodes: function (node) {
        var nodeGroupId = node.nodeGroupId();
        if (!nodeGroupId) {
            return [node];
        }
        return _.filter(this.get('nodes')(), function (node) {
            return node.nodeGroupId() && node.nodeGroupId() === nodeGroupId;
        });
    },

    /**
     * getParentNodesAndEdges - given a node, get all the parent nodes edges
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node from which to get the node's parents
     * @return  {object} - an object with a list of {@link NodeModel} and edges
     */
    getParentNodesAndEdges: function (node) {
        var self = this;
        var nodes = [];
        var edges = [];
        var edge = self.get('edges')().find(function (edge) {
            return edge.rangenode_id === node.nodeid;
        });
        if (edge) {
            var domainnode = self.get('nodes')().find(function (node) {
                return node.nodeid === edge.domainnode_id;
            });
            nodes.push(domainnode);
            edges.push(edge);

            var nodesAndEdges = self.getParentNodesAndEdges(domainnode);
            nodes = nodes.concat(nodesAndEdges.nodes);
            edges = edges.concat(nodesAndEdges.edges);
        }
        return {
            nodes: nodes,
            edges: edges
        };
    },

    /**
     * getChildNodesAndEdges - given a node, get all the child nodes edges
     * @memberof GraphModel.prototype
     * @param  {NodeModel} node - the node from which to get the node's childNodes
     * @return  {object} - an object with a list of {@link NodeModel} and edges
     */
    getChildNodesAndEdges: function (node) {
        var self = this;
        var nodes = [];
        var edges = [];
        self.get('edges')().filter(function (edge) {
            return edge.domainnode_id === node.nodeid;
        }).forEach(function (edge) {
            var rangenode = self.get('nodes')().find(function (node) {
                return node.nodeid === edge.rangenode_id;
            });
            nodes.push(rangenode);
            edges.push(edge);

            var nodesAndEdges = self.getChildNodesAndEdges(rangenode);
            nodes = nodes.concat(nodesAndEdges.nodes);
            edges = edges.concat(nodesAndEdges.edges);
        }, self);
        return {
            nodes: nodes,
            edges: edges
        };
    },

    /**
     * _doRequest - a wrapper around a simple ajax call
     * @memberof GraphModel.prototype
     * @param  {object} config - a config object to pass to the ajax request
     * @param  {function} callback - function to call when the request returns
     * @param  {object} scope - (optional) the scope used for the callback
     * @param  {string} eventname - (optional) the event to trigger upon successfull return of the request
     * @return  {jqXHR} - a Proimise compatible asynchronous request
     */
    _doRequest: function (config, callback, scope, eventname) {
        var self = this;
        return $.ajax($.extend({
            complete: function (request, status) {
                if (typeof callback === 'function') {
                    callback.call(scope || self, request, status);
                }
                if (!!eventname) {
                    self.trigger(eventname, self, request);
                }
            }
        }, config));
    }
});

