import autobind from "autobind-decorator";
import { action, makeObservable, observable } from "mobx";
import LayoutProperties, { ILayoutProperties } from "../modals/LayoutProperties";
import { IColorValueMap, IProfileMetaData } from "../ui/react-pathway-mapper";
import CBioPortalAccessor from "../utils/CBioPortalAccessor";
import SVGExporter from "../utils/SVGExporter";
import GenomicDataOverlayManager from "./GenomicDataOverlayManager";
import ShareDBManager from "./ShareDBManager";

const _ = require('lodash');

export default class EditorActionsManager {

    public static defaultLayoutProperties: ILayoutProperties =
    {
        name: 'fcose',
        nodeRepulsion: 4500,
        idealEdgeLength: 50,
        edgeElasticity: 0.45,
        nestingFactor: 0.1,
        gravity: 0.25,
        numIter: 2500,
        tile: true,
        animate: true,
        randomize: false,
        gravityRangeCompound: 1.5,
        // Gravity force (constant) for compounds
        gravityCompound: 1.0,
        // Gravity range (constant)
        gravityRange: 3.8,
        // Amount of vertical space to put between degree zero nodes during tiling (can also be a function)
        tilingPaddingVertical: 10,
        // Amount of horizontal space to put between degree zero nodes during tiling (can also be a function)
        tilingPaddingHorizontal: 10,
        // Initial cooling factor for incremental layout
        initialEnergyOnIncremental: 0.5,
        animationDuration: 2000,
        animationEasing: 'cubic-bezier(0.17,0.72,0.41,0.98)',
        nodeDimensionsIncludeLabels: true
    };

    public cy: any;
    public genomicDataOverlayManager: GenomicDataOverlayManager;
    public edgeEditing: any;
    public selectedNodeStack: any;
    public layoutProperties: ILayoutProperties;

    private FIT_CONSTANT: number;
    private observers: any[];
    private svgExporter: SVGExporter;
    private undoRedoManager: any;
    private isCbioPortal: boolean;
    private isCollaborative: boolean;
    private shareDBManager: ShareDBManager;
    private portalAccessor: CBioPortalAccessor;
    private viewUtilities: any;

    @observable
    private profiles: IProfileMetaData[];

    @observable
    private genomicDataOverlayColorScheme: IColorValueMap;

    private colorSchemeChangeCallback: (IColorValueMap) => void;

    constructor(isCollaborative: boolean, shareDBManager: any, cyInst: any, isCBioPortal: boolean,
                undoRedoManager: any, portalAccessor: CBioPortalAccessor, profiles: IProfileMetaData[],
                genomicDataOverlayColorScheme: IColorValueMap, colorSchemeChangeCallback: (IColorValueMap) => void)
    {
        // Set cy instance and set real time manager reference if collaborative mode
        makeObservable(this);
        this.cy = cyInst;
        this.isCollaborative = isCollaborative;
        this.isCbioPortal = isCBioPortal;
        this.profiles = profiles;
        this.genomicDataOverlayColorScheme = genomicDataOverlayColorScheme;
        this.colorSchemeChangeCallback = colorSchemeChangeCallback;

        const edgeEditingOptions = {
            bendPositionsFunction: function(ele) {
                return ele.data('bendPointPositions');
            },
            // A function parameter to get control point positions, should return positions of control points
            controlPositionsFunction: function(ele) {
                return ele.data('controlPointPositions');
            },
            // whether the anchor editing operations are undoable (requires cytoscape-undo-redo.js)
            undoable: true,
            // the size of bend shape is obtained by multipling width of edge with this parameter
            anchorShapeSizeFactor: 6,
            // whether to start the plugin in the enabled state
            enabled: !this.isCbioPortal,

            handleReconnectEdge: this.isCollaborative ?
                                this.reconnectEdge.bind(this) :
                                undefined,

            enableMultipleAnchorRemovalOption: true
        };

        if (!this.isCbioPortal) {
            this.edgeEditing = this.cy.edgeEditing(edgeEditingOptions);
        }
        this.portalAccessor = portalAccessor;
        if(this.isCollaborative) {
          this.shareDBManager = shareDBManager;
        }

        this.FIT_CONSTANT = 50;

        this.layoutProperties = _.clone(EditorActionsManager.defaultLayoutProperties);
        this.observers = [];
        this.genomicDataOverlayManager = new GenomicDataOverlayManager(this.cy);
        this.svgExporter = new SVGExporter(this.edgeEditing, this);

        this.selectedNodeStack = {};
        this.undoRedoManager = undoRedoManager;
        this.undoRedoManager.action("changePositions", this.doChangePosition, this.undoChangePosition);
        this.undoRedoManager.action("changeNodeSize", this.doChangeNodeSize, this.undoChangeNodeSize);
        this.undoRedoManager.action("changeCompoundSize", this.doChangeCompoundSize, this.undoChangeCompoundSize);
        this.undoRedoManager.action("changeName", this.doChangename, this.undoChangeName);
        this.undoRedoManager.action("hideNode", this.doHide, this.undoHide);
        this.undoRedoManager.action("showAllNodes", this.doShow, this.undoShow);
        // HighlightOthers is the type of highlight from the menu and by searching, while highlightInvalidGenes is for only invalid genes
        this.undoRedoManager.action("highlightInvalidGenes", this.doHighlightInvalidGenes, this.undoHighlightInvalidGenes);
        this.undoRedoManager.action("removeHighlightInvalidGenes", this.undoHighlightInvalidGenes, this.doHighlightInvalidGenes);
        this.undoRedoManager.action("highlightOthers", this.doHighlight, this.undoHighlight);
        this.undoRedoManager.action("removeOtherHighlight", this.undoHighlight, this.doHighlight);

    };

    @action.bound
    setProfile(index: number, profile: IProfileMetaData) {
        this.profiles[index] = profile;
    }

    @action.bound
    addProfile(profile: IProfileMetaData) {
        this.profiles.push(profile);
    }

    @action.bound
    removeProfiles() {
        this.profiles.length = 0;
    }

    @action.bound
    setGenomicDataOverlayColorScheme(scheme: IColorValueMap) {
        this.genomicDataOverlayColorScheme = scheme;
    }

    @autobind
    setViewUtilities(viewUtilitiesRef: any) {
        this.viewUtilities = viewUtilitiesRef;
    }

    getGenomicDataOverlayColorScheme() : IColorValueMap {
        return this.genomicDataOverlayColorScheme;
    }

    handleChangePositionByAlignment(movedNodeArr: any)
    {
        if (this.isCollaborative)
            this.shareDBManager.changeElementsPositionByAlignment(movedNodeArr);
        else
            this.undoRedoManager.do("changePositions", movedNodeArr)
    };

    /*
     * Undo redo for changing positions of nodes via programatically (node.position)
     * **/
    doChangePosition(movedNodes: any)
    {
        const newMovedNodes = [];

        for(let i = 0; i < movedNodes.length; i++)
        {
            const currentNodePosition =
                {
                    x: movedNodes[i].node.position().x,
                    y: movedNodes[i].node.position().y
                };
            newMovedNodes.push({node: movedNodes[i].node, oldPosition: currentNodePosition, nextPosition: movedNodes[i].nextPosition});
            movedNodes[i].node.position(movedNodes[i].nextPosition);
        }

        return newMovedNodes;
    };

    undoChangePosition(movedNodes: any)
    {
        var newMovedNodes = [];

        for(var i = 0; i < movedNodes.length; i++)
        {
            var currentNodePosition =
                {
                    x: movedNodes[i].node.position().x,
                    y: movedNodes[i].node.position().y
                };
            newMovedNodes.push({node: movedNodes[i].node, oldPosition: movedNodes[i].oldPosition, nextPosition: currentNodePosition});
            movedNodes[i].node.position(movedNodes[i].oldPosition);
        }

        return newMovedNodes;
    };

    changeNodePositionsByArrows(selectedNodes: any)
    {
        if (this.isCollaborative)
            this.shareDBManager.changeNodePositionsShareDB(selectedNodes);
        //node-editing extension already deals for the movement in local mode
    };

    /*
     * Undo redo for changing size of nodes
     * **/
    doChangeNodeSize (args: any)
    {
        args.ele.data('w', args.newWidth);
        args.ele.data('h', args.newHeight);

        return args;
    };

    undoChangeNodeSize(args: any)
    {
        args.ele.data('w', args.oldWidth);
        args.ele.data('h', args.oldHeight);
        return args;
    };

    /*
     * Undo redo for changing size of compounds
     * **/
    doChangeCompoundSize(args: any)
    {
        args.ele.style('min-width', args.newMinWidth);
        args.ele.style('min-width-bias-left', args.newMinWidthBiasLeft);
        args.ele.style('min-width-bias-right', args.newMinWidthBiasRight);
        args.ele.style('min-height', args.newMinHeight);
        args.ele.style('min-height-bias-top', args.newMinHeightBiasTop);
        args.ele.style('min-height-bias-bottom', args.newMinHeightBiasBottom);

        return args;
    };

    undoChangeCompoundSize(args: any)
    {
        if (args.oldMinWidth == undefined)
        {
            args.ele.style('min-width', 0);
            args.ele.style('min-width-bias-left', 0);
            args.ele.style('min-width-bias-right', 0);

        }
        else {
            args.ele.style('min-width', args.oldMinWidth);
            args.ele.style('min-width-bias-left', args.oldMinWidthBiasLeft);
            args.ele.style('min-width-bias-right', args.oldMinWidthBiasRight);
        }
        if (args.oldMinHeight == undefined)
        {
            args.ele.style('min-height', 0);
            args.ele.style('min-height-bias-top', 0);
            args.ele.style('min-height-bias-bottom', 0);
        }
        else {
            args.ele.style('min-height', args.oldMinHeight);
            args.ele.style('min-height-bias-top', args.oldMinHeightBiasTop);
            args.ele.style('min-height-bias-bottom', args.oldMinHeightBiasBottom);
        }

        return args;
    };

    changeName(ele: any, newName: any)
    {
        if (this.isCollaborative)
        {
            this.shareDBManager.changeName(ele, newName);
        }
        else
        {
            this.changeNameCy(ele, newName);
        }
    };

    changeNameCy(ele: any, newName: any)
    {
        var currentName = ele.data('name');
        var args = {ele: ele, oldName: currentName, newName: newName};
        this.undoRedoManager.do('changeName', args);
    };

    /*
     * Undo redo for changing name of nodes
     * **/
    doChangename(args: any)
    {

        var currentName = args.ele.data('name');
        var newArgs = {ele: args.ele, newName: args.newName, oldName: currentName};
        args.ele.data('name', args.newName);

        return newArgs;
    };

    undoChangeName(args: any)
    {

        var currentName = args.ele.data('name');
        var newArgs = {ele: args.ele, newName: args.newName, oldName: currentName};
        args.ele.data('name', args.oldName);

        return newArgs;
    };

    hideSelectedNodes()
    {
        //Hides the selected elements
        var sel = this.cy.nodes(":selected");
        var nodesToHide = sel;
        var b = true;
        //Hides the parents if they have no children
        sel.parent().each((parent: any) =>
        {
            b=true;
            parent.children().each(function(ch: any)
                {
                    if (!ch.selected())
                    {
                        if (ch.visible()) b=false;
                    }
                }
            );
            if (b) nodesToHide = nodesToHide.add(parent);
        });
        this.cy.elements(":selected").unselect();
        if (this.isCollaborative)
            this.shareDBManager.changeVisibility(nodesToHide, true);
        else
            this.undoRedoManager.do('hideNode', nodesToHide);
    };

    /*
     * Undo redo for hiding nodes
     * **/
    @autobind
    doHide(args: any)
    {
        this.viewUtilities.hide(args);
        return args;
    };

    @autobind
    undoHide(args: any)
    {
        this.viewUtilities.show(args);
        return args;
    };

    showAllNodes()
    {
        var hid = this.cy.nodes(":hidden");
        if (this.isCollaborative)
            this.shareDBManager.changeVisibility(hid, false);
        else
            this.undoRedoManager.do('showAllNodes', hid);
    };

    /*
     * Undo redo for showing all nodes
     * **/
    @autobind
    doShow(args: any)
    {
        this.viewUtilities.show(args);
        return args;
    };

    @autobind
    undoShow(args: any)
    {
        this.viewUtilities.hide(args);
        return args;
    };

    highlightElementsInitially(invalidHighlightedGenesIDs: string[], invalidGenesIDs: string[],
                               highlightedGenesIDs: string[], highlightedEdgesIDs: string[], hiddenGenesIDs: string[])
    {
        for (var i in invalidHighlightedGenesIDs)
        {
            this.cy.$('#'+invalidHighlightedGenesIDs[i]).addClass('invalidGeneHighlight');
        }
        for (var i in invalidGenesIDs)
        {
            this.cy.$('#'+invalidGenesIDs[i]).addClass('invalidGene');
        }
        for (var i in highlightedGenesIDs)
        {
            this.cy.$('#'+highlightedGenesIDs[i]).addClass('highlightedNode');
        }
        for (var i in highlightedEdgesIDs)
        {
            this.cy.$('#'+highlightedEdgesIDs[i]).addClass('highlightedEdge');
        }
        for (var i in hiddenGenesIDs)
        {
            this.viewUtilities.hide(this.cy.$('#'+hiddenGenesIDs[i]))
        }
    };

    validateGenes()
    {
        var geneSymbols = this.getGeneSymbols();
        this.portalAccessor.validateGenes(geneSymbols, this);
    }

    //Get all gene symbols
    getGeneSymbols()
    {
        var geneSymbols: any[] = [];
        this.cy.nodes().forEach( function (gene: any)
        {
            if(gene.data().type === "GENE")
                geneSymbols.push(gene.data().name);
        });
        return geneSymbols;
    }


    highlightInvalidGenes(validGeneSymbols: any)
    {
        if (this.isCollaborative)
        {
            var invalidGenes: any[] = [];
            this.cy.nodes().forEach( function (gene: any)
            {
                if(gene.data().type === "GENE")
                {
                    var geneName = gene.data().name;
                    if(validGeneSymbols.indexOf(geneName) < 0)
                        invalidGenes.push(gene.id());
                }
            });
            this.shareDBManager.changeHighlightInvalidGenes(invalidGenes, true);
            // TODO: Amendment by Ziya
            /*
            if (invalidGenes.length > 0)
                this.notificationManager.createNotification("Invalid genes are highlighted","fail");
            else
                this.notificationManager.createNotification("All gene symbols are valid","success");*/
        }
        else
        {
            var highlightedGenes = this.cy.collection();
            this.cy.nodes().forEach( function (gene: any)
            {
                if(gene.data().type === "GENE")
                {
                    var geneName = gene.data().name;
                    if(validGeneSymbols.indexOf(geneName) < 0)
                        highlightedGenes = highlightedGenes.add(gene);
                }
            });
            // TODO: Amendment by Ziya
            /*
                        if (highlightedGenes.size() > 0)
                            this.notificationManager.createNotification("Invalid genes are highlighted","fail");
                        else
                            this.notificationManager.createNotification("All gene symbols are valid","success");*/

            var nodesToAddInvalidHighlight = this.cy.collection();
            highlightedGenes.forEach(function(ele: any){
                if (!ele.hasClass('invalidGeneHighlight') &&  !ele.hasClass('invalidGene'))
                    nodesToAddInvalidHighlight = nodesToAddInvalidHighlight.union(ele);
            });
            this.undoRedoManager.do('highlightInvalidGenes', nodesToAddInvalidHighlight);
        }
    }

    removeInvalidGeneHighlights(actions: any[])
    {
        if (this.isCollaborative)
        {
            var geneIDs: any[] = [];
            this.cy.nodes().forEach( function (gene: any)
            {
                if(gene.data().type === "GENE")
                {
                    if(gene.hasClass('invalidGeneHighlight') || gene.hasClass('invalidGene'))
                        geneIDs.push(gene.id());
                }
            });
            this.shareDBManager.changeHighlightInvalidGenes(geneIDs, false);
        }
        else
        {
            var nodesToRemoveInvalidHighlight = this.cy.collection();
            this.cy.nodes().forEach(function(ele: any){
                if (ele.hasClass('invalidGeneHighlight') ||  ele.hasClass('invalidGene'))
                    nodesToRemoveInvalidHighlight = nodesToRemoveInvalidHighlight.union(ele);
            });
            actions.push({name: "removeHighlightInvalidGenes", param: nodesToRemoveInvalidHighlight});
            // this.undoRedoManager.do('removeHighlightInvalidGenes', nodesToRemoveInvalidHighlight);
        }
    }

    doHighlightInvalidGenes(args: any)
    {
        args.each(function(n: any)
        {
            if(n.hasClass('highlightedNode'))
            {
                n.removeClass('highlightedNode');
                n.addClass("invalidGeneHighlight");
            }
            else
                n.addClass("invalidGene");
        });
        return args;
    };

    undoHighlightInvalidGenes(args: any)
    {
        args.each(function(n: any)
        {
            if(n.hasClass('invalidGeneHighlight'))
            {
                n.removeClass('invalidGeneHighlight');
                n.addClass("highlightedNode");
            }
            else
                n.removeClass("invalidGene");
        });
        return args;
    }

    highlightSelected()
    {
        var sel = this.cy.elements(":selected");
        sel.unselect();
        var elementsToHighlight = this.cy.collection();
        sel.forEach(function(ele: any){
            if (!ele.hasClass('invalidGeneHighlight') &&  !ele.hasClass('highlightedNode') && !ele.hasClass('highlightedEdge'))
                elementsToHighlight = elementsToHighlight.union(ele);
        });
        if (this.isCollaborative)
            this.shareDBManager.changeHighlight(elementsToHighlight, true);
        else
            this.undoRedoManager.do('highlightOthers', elementsToHighlight);
    };

    highlightNeighbors()
    {
        var sel = this.cy.elements(":selected");
        var neighbors = sel.neighborhood();
        neighbors = neighbors.union(sel);
        neighbors.unselect();
        var elementsToHighlight = this.cy.collection();
        neighbors.forEach(function(ele: any){
            if (!ele.hasClass('invalidGeneHighlight') &&  !ele.hasClass('highlightedNode') && !ele.hasClass('highlightedEdge'))
                elementsToHighlight = elementsToHighlight.union(ele);
        });
        if (this.isCollaborative)
            this.shareDBManager.changeHighlight(elementsToHighlight, true);
        else
            this.undoRedoManager.do('highlightOthers', elementsToHighlight);
    };

    highlightBySearch(args: any[])
    {
        if (this.isCollaborative)
            this.shareDBManager.changeHighlight(args, true);
        else
            this.undoRedoManager.do('highlightOthers', args);
    };

    removeOtherHighlight(actions: any[])
    {
        var nodesToRemoveHighlight = this.cy.collection();
        //TODO cytoscape selectors may provide more handy functionality instead of iterating over !
        this.cy.elements().forEach(function(ele: any){
            if (ele.hasClass('highlightedNode') || ele.hasClass('highlightedEdge') || ele.hasClass('invalidGeneHighlight'))
                nodesToRemoveHighlight = nodesToRemoveHighlight.add(ele);
        });

        if (this.isCollaborative)
            this.shareDBManager.changeHighlight(nodesToRemoveHighlight, false);
        else
            actions.push({name: "removeOtherHighlight", param: nodesToRemoveHighlight});
        // this.undoRedoManager.do('removeOtherHighlight', nodesToRemoveHighlight);
    };

    /*
     * Undo redo for highlighting of nodes
     * **/
    doHighlight(args: any)
    {
        args.each(function(n: any)
        {
            if (n.isEdge())
                n.addClass("highlightedEdge");
            else
            {
                if(n.hasClass('invalidGene'))
                {
                    n.removeClass("invalidGene");
                    n.addClass("invalidGeneHighlight");
                }
                else
                    n.addClass("highlightedNode");
            }
        });
        return args;
    };

    undoHighlight(args: any)
    {
        args.each(function(n: any)
        {
            if (n.isEdge())
                n.removeClass("highlightedEdge");
            else
            {
                if(n.hasClass('invalidGeneHighlight'))
                {
                    n.removeClass("invalidGeneHighlight");
                    n.addClass("invalidGene");
                }
                else
                    n.removeClass("highlightedNode");
            }
        });
        return args;
    };

    removeAllHighlight()
    {
        let actions: any[] = [];
        this.removeInvalidGeneHighlights(actions);
        this.removeOtherHighlight(actions);
        this.cy.undoRedo().do("batch", actions);
    };

    postLayout()
    {
        if (this.isCollaborative)
        {
            //Previously this.cy.nodes() was sent as an argument in moveElements function but it caused a problem when
            // the compound node was moved before the child nodes
            var movedNodes = this.cy.collection();
            var parentNodes = this.cy.collection();
            this.cy.nodes().forEach(function(node: any)
            {
                if (!node.isParent())
                    movedNodes = movedNodes.add(node);
                else
                    parentNodes = parentNodes.add(node);
            });
            this.moveElements(movedNodes);
            this.moveElements(parentNodes);

            var newState = {
                zoomLevel: this.cy.zoom(),
                panLevel: this.cy.pan()
            };
            this.updateGlobalOptions(newState);
        }
    };

    /*
    * Gets the first empty index from the list in cloud model
    * **/
    getEmptyGroupID()
    {
        if(this.isCollaborative)
            return this.shareDBManager.getEmptyGroupID();
        else
            return this.genomicDataOverlayManager.getEmptyGroupID();
    };

    /*
     * Gets the first empty index from the list in cloud model
     * **/
    groupGenomicData(cancerNames: any[], groupID: string)
    {
        return this.shareDBManager.groupGenomicData(cancerNames, groupID);
    };

    addPubmedIDs(edge: any, pubmedIDs: number[])
    {
        if (this.isCollaborative)
        {
            this.shareDBManager.addPubmedIDs(edge.id(), pubmedIDs);
        }
        else
        {
            var pubmedArray = edge.data('pubmedIDs');
            var validPubmedIDs = _.filter(pubmedIDs, function(id: number){
                return !isNaN(id);
            });
            pubmedArray.push.apply(pubmedArray,validPubmedIDs);
            edge.data('pubmedIDs', _.uniq(pubmedArray));
        }
    }

    removePubmedID(edge: any, pubmedIDs: number[])
    {
        if (this.isCollaborative)
        {
            this.shareDBManager.removePubmedID(edge.id(), pubmedIDs);
        }
        else
        {
            var pubmedArray = edge.data('pubmedIDs');
            edge.data('pubmedIDs', _.difference(pubmedArray, pubmedIDs));
        }
    }

    updateEdgeAnchorPoints(edge: any)
    {
        if (this.isCollaborative)
        {
            var edgeCurveStyle = edge.css('curve-style')
            var numberOfAnchorPoints = 0;
            var anchors = this.edgeEditing?.getAnchorsAsArray(edge);
            if (anchors !== undefined)
                numberOfAnchorPoints = anchors.length / 2;
            var anchorPointsArray = [];
            for (var j = 0; j < numberOfAnchorPoints; j++)
            {
                anchorPointsArray.push(
                    {
                        x: anchors[2*j],
                        y: anchors[2*j+1]
                    }
                );
            }
            this.shareDBManager.updateEdgeAnchorPoints(edge.id(), anchorPointsArray, edgeCurveStyle);
        }
    }

    //Related to order the nodes according to the selection of user
    pushSelectedNodeStack(ele: any)
    {
        this.selectedNodeStack[ele.id()] = ele;
    }

    removeElementFromSelectedNodeStack(ele: any)
    {
        var nodeID = ele.id();
        if (nodeID in this.selectedNodeStack)
            delete this.selectedNodeStack[ele.id()];
    }

    clearSelectedNodeStack()
    {
        this.selectedNodeStack = {};
    }

    exportSVG()
    {
        return this.svgExporter.exportGraph(this.cy.nodes(), this.cy.edges());
    }

    //Simple observer-observable pattern for views!!!!!
    registerObserver(observer: any)
    {
        this.observers.push(observer);
    };

    notifyObservers()
    {
        for (var i in this.observers)
        {
            var observer = this.observers[i];
            observer.notify();
        }
    };

    registerGenomicDataObserver(observer: any)
    {
      this.genomicDataOverlayManager.registerObserver(observer);
    }

    updateGenomicDataVisibility(dataMap: any)
    {
        if(this.isCollaborative)
        {
            //TODO compound OP
            // this.shareDBManager.clearGenomicVisData();
            this.shareDBManager.addGenomicVisibilityData(dataMap);
        }
        else
        {
            for (var _key in dataMap)
            {
                this.genomicDataOverlayManager.updateGenomicDataVisibility(_key, dataMap[_key]);
            }
            this.genomicDataOverlayManager.showGenomicData();
        }
    }

    updateGenomicDataColorScheme(colorValueMap: IColorValueMap)
    {
        this.setGenomicDataOverlayColorScheme(colorValueMap);

        if(this.isCollaborative)
        {
            this.shareDBManager.updateGenomicDataOverlayColorScheme(colorValueMap);
        }
        else
        {
            this.genomicDataOverlayManager.updateColorScheme(colorValueMap);
            this.genomicDataOverlayManager.showGenomicData();
        }
    }

    //Global options related functions, zoom etc..
    getGlobalOptions()
    {
        return {
            zoomLevel: this.cy.zoom(),
            panLevel: this.cy.pan()
        };
    }

    changeGlobalOptions(op: any)
    {
        var globalOptions = op.li;
        this.cy.zoom(globalOptions.zoomLevel);
        this.cy.pan(globalOptions.panLevel);
    }

    updateGlobalOptions(newOptions: any)
    {
        if(this.isCollaborative)
            this.shareDBManager.updateGlobalOptions(newOptions);
    }

    //Layout properties related functions
    saveLayoutProperties(newLayoutProps: any)
    {
        if(this.isCollaborative)
        {
            // Call a real time function that updated real time object and
            // its callback (updateLayoutPropertiesCallback) will handle sync of this object
            // across collaborators
            this.shareDBManager.updateLayoutProperties(newLayoutProps);
        }
        else
        {
            this.layoutProperties = _.clone(newLayoutProps);
        }
    };

    updateLayoutPropertiesCallback(op: any)
    {
        var newLayoutProps = op.li;
        this.layoutProperties = _.clone(newLayoutProps);
        LayoutProperties.layoutProperties = _.clone(this.layoutProperties);
        //Notify observers to reflect changes on colalborative object to the views
        this.notifyObservers();
    };

    performLayout()
    {
        this.undoRedoManager.do("layout", {options: this.layoutProperties, eles: null, zoom: this.cy.zoom(), pan: this.cy.pan()});
    };

    //Node Related Functions
    addNode(nodeData: any, posData: any)
    {
        if (this.isCollaborative)
        {
            this.addNewNodeToShareDB(nodeData, posData);
        }
        else
        {
            this.addNodetoCy(nodeData,posData);
        }
    };

    addNodes(nodes: any[])
    {
        for (var i in nodes)
        {
            this.addNode(nodes[i].data, nodes[i].position);
        }
    };

    addNodesCy(nodes: any[])
    {
        var nodeArr: any[] = [];
        for (var i in nodes)
        {
            var nodeData = nodes[i].data;
            var posData = nodes[i].position;

            var newNode =
                {
                    group: "nodes",
                    data: nodeData,
                    position: {}
                };

            if (nodeData.parent === undefined )
            {
                delete newNode.data.parent;
            }

            if (posData)
            {
                newNode.position =
                    {
                        x: posData.x,
                        y: posData.y
                    }
            }
            nodeArr[i] = newNode;
        }

        this.cy.add(nodeArr);
        this.cy.nodes().updateCompoundBounds();

    };

    addNodetoCy(nodeData: any, posData: any)
    {
        var newNode =
            {
                group: "nodes",
                data: nodeData,
                position: {}
            };

        if (nodeData.parent === undefined )
        {
            delete newNode.data.parent;
        }

        if (posData)
        {
            newNode.position =
                {
                    x: posData.x,
                    y: posData.y
                }
        }

        //his.cy.add(newNode);
        this.cy.nodes().updateCompoundBounds();
        this.undoRedoManager.do("add", newNode);
    };

    shareDBNodeAddRemoveEventCallBack(op: any)
    {
        //Get real time node object and sync it to node addition or removal
        var isRemove = Object.keys(op)[1] === 'od';
        var node = op.oi || op.od;
        //Removal Operation
        if (isRemove)
        {
            var nodeID = op.p[1];
            //Remove element from existing graph
            var cyEle = this.cy.$("#" + nodeID);
            this.removeElementCy(cyEle);
            this.cy.nodes().updateCompoundBounds();
        }
        //Addition Operation
        else
        {
            this.addNewNodeLocally(node);
        }
    };

    addNewNodeLocally(realtimeNode: any)
    {
        var nodeData =
            {
                id: realtimeNode.id,
                type: realtimeNode.type,
                name: realtimeNode.name,
                parent: realtimeNode.parent,
                w: realtimeNode.w,
                h: realtimeNode.h
            };

        if (realtimeNode.x != "undefined" && realtimeNode.y != "undefined")
        {
            this.addNodetoCy(nodeData, {x: realtimeNode.x, y: realtimeNode.y});
        }
        else
        {
            // TODO: Amendment by Ziya
            this.addNodetoCy(nodeData, null);
        }

        this.cy.nodes().updateCompoundBounds();
    };

    addNewNodeToShareDB(nodeData: any, posData: any)
    {
        this.shareDBManager.addNewNode(nodeData,posData);
    };

    //Edge related functions
    addEdge(edgeData: any)
    {
        if (this.isCollaborative)
        {
            this.addNewEdgeShareDB(edgeData);
        }
        else
        {
            this.addNewEdgetoCy(edgeData);
        }
    };

    addEdges(edges: any[])
    {
        for (var i in edges)
        {
            this.addEdge(edges[i].data);
        }
    };

    addEdgesCy(edges: any[])
    {
        const newEdges = [];


        for (var i in edges)
        {
            const newEdge =
                {
                    group: "edges",
                    data: edges[i].data
                };
            newEdges.push(newEdge);
        }
        this.cy.add(newEdges);
    };

    addNewEdgeShareDB(edgeData: any)
    {
        this.shareDBManager.addNewEdge(edgeData);
    };

    addNewEdgetoCy(edgeData: any)
    {
        var newEdge =
            {
                group: "edges",
                data: edgeData
            };
        this.undoRedoManager.do("add", newEdge);
    };

    shareDBEdgeAddRemoveEventCallBack(op: any)
    {

        //Get real time node object and sync it to node addition or removal
        var isRemove = Object.keys(op)[1] === 'od';
        var edge = op.oi || op.od;

        //Removal Operation
        if (isRemove)
        {
            var edgeID = op.p[1];
            //Remove element from existing graph
            var cyEle = this.cy.$("#" + edgeID);
            this.removeElementCy(cyEle);
        }
        //Addition Operation
        else
        {
            this.addNewEdgeLocally(edge);
        }
    };

    addNewElementsLocally(realTimeNodeArray: any[], realTimeEdgeArray: any[])
    {
        const nodeList: any[] = [];
        const nodeMap: any = {};

        for (var i in realTimeNodeArray)
        {
            var realTimeNode = realTimeNodeArray[i];

            var nodeID = realTimeNode.id;

            //Added for backward compatibility when width was not defined
            var nodeWidth = (realTimeNode.w == undefined) ? 150 : realTimeNode.w ;
            var nodeHeight = (realTimeNode.h == undefined) ? 52 : realTimeNode.h ;
            var compoundMinWidth = (realTimeNode.minWidth == undefined) ? 0 : realTimeNode.minWidth ;
            var compoundMinWidthBiasLeft = (realTimeNode.minWidthBiasLeft == undefined) ? 0 : realTimeNode.minWidthBiasLeft ;
            var compoundMinWidthBiasRight = (realTimeNode.minWidthBiasRight == undefined) ? 0 : realTimeNode.minWidthBiasRight ;
            var compoundMinHeight = (realTimeNode.minHeight == undefined) ? 0 : realTimeNode.minHeight ;
            var compoundMinHeightBiasTop = (realTimeNode.minHeightBiasTop == undefined) ? 0 : realTimeNode.minHeightBiasTop ;
            var compoundMinHeightBiasBottom = (realTimeNode.minHeightBiasBottom == undefined) ? 0 : realTimeNode.minHeightBiasBottom;

            const nodeData =
                {
                    group: 'nodes',
                    // TODO: Amendment by Ziya
                    position: {},
                    data:
                        {
                            id: nodeID,
                            type: realTimeNode.type,
                            name: realTimeNode.name,
                            parent: realTimeNode.parent,
                            w: nodeWidth,
                            h: nodeHeight
                        },
                    style:
                        {
                            'min-width': compoundMinWidth,
                            'min-width-bias-left': compoundMinWidthBiasLeft,
                            'min-width-bias-right': compoundMinWidthBiasRight,
                            'min-height': compoundMinHeight,
                            'min-height-bias-top': compoundMinHeightBiasTop,
                            'min-height-bias-bottom': compoundMinHeightBiasBottom
                        }
                };

            if (nodeData.data.parent === undefined )
            {
                delete nodeData.data.parent;
            }

            if (realTimeNode.x && realTimeNode.y)
            {
                nodeData.position =
                    {
                        x: realTimeNode.x,
                        y: realTimeNode.y
                    }
            }

            nodeMap[nodeID] = nodeData;
            nodeList.push(nodeData);
        }

        var edgeList = [];
        for (var i in realTimeEdgeArray)
        {
            var edge = realTimeEdgeArray[i];
            var edgeID = edge.id;

            //If source and and target is somehow lost in remote model do not create this edge
            if(!(edge.source in nodeMap && edge.target in nodeMap))
                continue

            var tmpData = {
                id: edgeID,
                type: edge.type,
                source: edge.source,
                target: edge.target,
                pubmedIDs: edge.pubmedIDs,
                name: edge.name,
            }

            if (edge.edgeCurveStyle == "unbundled-bezier") {
                tmpData['controlPointPositions'] = edge.anchorPoints;
            }
            else {
                tmpData['bendPointPositions'] = edge.anchorPoints;
            }

            var edgeData =
                {
                    group: 'edges',
                    data: tmpData
                };

            edgeList.push(edgeData);
        }

        this.cy.add(nodeList);
        this.cy.add(edgeList);

        this.edgeEditing?.initAnchorPoints(this.cy.edges());

        this.cy.nodes().updateCompoundBounds();
    }

    addNewEdgeLocally(edge: any)
    {
        var edgeData ={
            id: edge.id,
            type: edge.type,
            source: edge.source,
            target: edge.target,
            pubmedIDs: edge.pubmedIDs,
            name: edge.name
        };
        if (edge.edgeCurveStyle == "unbundled-bezier") {
            edgeData['controlPointPositions'] = edge.anchorPoints;
        }
        else {
            edgeData['bendPointPositions'] = edge.anchorPoints;
        }
        this.addNewEdgetoCy(edgeData);
        this.edgeEditing?.initAnchorPoints(this.cy.getElementById( edge.id ));
    };

    reconnectEdge(sourceID: string, targetID: string, edgeData: any) {

        if(this.isCollaborative){
            var edge = this.cy.getElementById(edgeData.id);
            this.reconnectEdgeInShareDB(sourceID, targetID, edgeData);
        }
        else{
            var location = {
                source: sourceID,
                target: targetID
            };

            var edge = this.cy.getElementById(edgeData.id);
            edge.move(location);
        }

        return this.cy.getElementById(edgeData.id);
    };

    reconnectEdgeInShareDB(sourceID: string, targetID: string, edgeData: any) {
        this.shareDBManager.reconnectEdge(sourceID, targetID, edgeData);
    };

    //Removal functions
    removeElement(ele: any)
    {
        if (this.isCollaborative)
        {
            this.removeElementsFromShareDB(ele);
        }
        else
        {
            this.removeElementsCy(ele);
        }
    };

    removeElementCy(ele: any)
    {
        this.undoRedoManager.do("remove", ele);
    };

    removeElementsCy(ele: any)
    {
        this.undoRedoManager.do("remove", ele);
    };

    removeElementsFromShareDB(eles: any[])
    {
        var self = this;
        eles.forEach(function (ele)
        {
            self.shareDBManager.removeElement(ele.id());
        });
    };

    removeElementFromShareDB(ele: any)
    {
        this.shareDBManager.removeElement(ele.id());
    };

    changeParents(eles: any[], newParentId: string)
    {
        if(this.isCollaborative)
        {
            this.changeParentShareDB(eles, newParentId);
        }
        else
        {
            var parentData = newParentId ? newParentId : null;

            // Old manual way to change parents in local mode
            //this.changeParentCy(eles, newParentId);

            //Save element's previous width & height in dim array
            const dim : any[]= [];
            const id : any[]= [];
            eles.forEach(function (ele: any)
            {
                var parameters =
                    {
                        id: ele.id(),
                        width: ele.data("w"),
                        height: ele.data("h")
                    };
                dim.push(parameters);
                id.push(ele.id());
            });

            const parentElem = this.cy.getElementById(parentData);
            var param = {
                firstTime: true,
                parentData: parentData, // It keeps the newParentId (Just an id for each nodes for the first time)
                nodes: eles,
                posDiffX: (!parentData) ? 0 : parentElem.position('x') - eles[0].position('x'),
                posDiffY: (!parentData) ? 0 : parentElem.position('y') - eles[0].position('y')
            };
            this.undoRedoManager.do('changeParent', param);

            //The elements after change parent operation are different so we find them by using the saved ids
            // and add them to the collection
            var collection = this.cy.collection();
            for (var i in id)
            {
                var elementById = this.cy.getElementById(id[i]);
                collection = collection.add(elementById);
            }
            //Set their previous size to the new elements in the collection
            collection.forEach(function (ele: any, i: number)
            {
                if (ele.id() == dim[i].id)
                    ele.data("w", dim[i].width);
                ele.data("h", dim[i].height);
            });
        }
    };

    changeParentShareDB (eles: any, newParentId: string)
    {

        var classRef = this;
        function getTopLevelParents(eles: any[])
        {
            var tpMostNodes = classRef.cy.collection();
            const parentMap : any = {};

            //Get all parents
            eles.forEach(function (node)
            {
                if(node.isParent())
                    parentMap[node.id()] = node;
            });

            //Get all parents
            eles.forEach(function (node)
            {
                var nodeParent = node.parent();

                if(parentMap[nodeParent.id()] === undefined)
                    tpMostNodes = tpMostNodes.union(node);
            });

            return tpMostNodes;
        }


        class NodeObj{
          public nodeRef: any;
          public children: any;
          constructor(nodeObj: any){
            this.nodeRef = nodeObj;
            this.children = [];
          }
        }
        var connectedEdges = eles.connectedEdges();
        // Traverses given elements and constructs subgraph relations
        // creates a nested structure into rootnodeObj
        function traverseNodes(eles: any[], rootNodeObj: any)
        {
            eles.forEach(function (ele)
            {
                connectedEdges = connectedEdges.union(ele.connectedEdges());

                if(ele.isParent())
                {
                    rootNodeObj.children.push(new NodeObj(ele));
                    var lengthOfChildrenArray = rootNodeObj.children.length;
                    traverseNodes(ele.children(), rootNodeObj.children[lengthOfChildrenArray-1]);
                }
                else
                {
                    rootNodeObj.children.push(new NodeObj(ele));
                }
            });
        }

        //Create new collection
        var topMostNodes = getTopLevelParents(eles);

        var rootNodeR = new NodeObj(null);

        traverseNodes(topMostNodes, rootNodeR);
        this.shareDBManager.changeParent(rootNodeR, newParentId, connectedEdges);
    };

    changeParentCy(eles: any[], newParentId: string)
    {
        let lockedNodes: any = {};
        const self = this;

        function removeNodes(nodes: any)
        {
            //Get removed edges first
            let removedEles = nodes.connectedEdges().remove();
            const children = nodes.children();

            if (children != null && children.length > 0)
            {
                children.forEach(function(childNode: any)
                {
                    lockedNodes[childNode.id()] = true;
                });

                removedEles = removedEles.union(removeNodes(children));
            }

            removedEles = removedEles.union(nodes.remove());
            self.cy.nodes().updateCompoundBounds();
            return removedEles;
        }


        var removedEles = removeNodes(eles);
        this.undoRedoManager.do("remove", removedEles);

        for (var i = 0; i < removedEles.length; i++)
        {
            var removedNode = removedEles[i];

            //Just alter the parent id of corresponding nodes !
            if (removedNode.isEdge() || lockedNodes[removedNode.id()])
            {
                continue;
            }

            removedNode._private.data.parent = newParentId;
            if(removedNode._private.parent){
                delete removedNode._private.parent;
            }
        }

        self.cy.add(removedEles);
        this.undoRedoManager.do("add", removedEles);
        self.cy.nodes().updateCompoundBounds();
    };

    moveElements(eles: any[])
    {
        var classRef = this;
        //Sync movement to real time api
        if(this.isCollaborative)
        {
            eles.forEach(function (ele: any)
            {
                classRef.shareDBManager.moveElement(ele);
            });
        }
    };

    resizeElements(ele: any)
    {
        if(this.isCollaborative) {
            if (!ele.isParent()) {
                var previousWidth = ele.width();
                var previousHeight = ele.height();
                //Sync movement to real time api
                this.shareDBManager.resizeElement(ele, previousWidth, previousHeight);
            }
            else {
                var minWidth = ele.style('min-width');
                var minWidthBiasLeft = ele.style('min-width-bias-left');
                var minWidthBiasRight = ele.style('min-width-bias-right');
                var minHeight = ele.style('min-height');
                var minHeightBiasTop = ele.style('min-height-bias-top');
                var minHeightBiasBottom = ele.style('min-height-bias-bottom');

                //Sync movement to real time api
                this.shareDBManager.resizeCompound(ele, minWidth, minWidthBiasLeft, minWidthBiasRight, minHeight, minHeightBiasTop, minHeightBiasBottom);
            }
        }
    };

    mergeGraph(nodes: any[], edges: any[])
    {
        if (this.isCollaborative)
        {
            //Collaborative usage
            this.shareDBManager.mergeGraph(nodes,edges);
        }
        else
        {
            //Local usage file load
            this.mergeGraphCy(nodes,edges);
        }
        this.fitGraph();
    };

    mergeGraphCy(nodes: any[], edges: any[])
    {
        //Define arrays and maps
        const nodesToBeAdded = [];
        const edgesToBeAdded = [];
        const nodeMap : any= {};

        //Iterate over nodes and find nodes that does not exist in current graph by looking their name
        for (var index in nodes)
        {
            var ele = nodes[index];
            nodeMap[ele.data.id] = ele;

            if (this.cy.filter('node[name = "'+ele.data.name+'"]').length <= 0)
            {
                nodesToBeAdded.push(ele);
            }
        }

        this.cy.add(nodesToBeAdded);

        //Iterate over all edges
        for (var index in edges)
        {
            //Get corresponding source and target node in merge file
            var ele = edges[index];
            var sourceNode = nodeMap[ele.data.source];
            var targetNode = nodeMap[ele.data.target];

            //Check if there are nodes with same name in current graph
            var cySourceNode = this.cy.nodes('[name="'+sourceNode.data.name+'"]');
            var targetNode = this.cy.nodes('[name="'+targetNode.data.name+'"]');

            if (cySourceNode.length > 0)
            {
                ele.data.source = cySourceNode.id();
            }

            if (targetNode.length > 0)
            {
                ele.data.target = targetNode.id();
            }

            if (targetNode.length < 0 && cySourceNode.length < 0 ) {
                continue;
            }

            var edgesBtw = this.cy.filter('edge[source = "'+cySourceNode.id()+'"][target = "'+targetNode.id()+'"]');

            //We assume there could be one edge between source and target node with same type
            var isFound = false;
            edgesBtw.forEach(function(edge: any)
            {
                if (edge.data().type == ele.data.type)
                {
                    isFound = true;
                    return false;
                }
                //TODO: AMENDMENT by Ziya
                return true;
            });

            if (!isFound)
            {
                delete ele.data.id;
                edgesToBeAdded.push(ele);
            }
        }

        this.cy.add(edgesToBeAdded);
    };

    fitGraph()
    {
        if(this.isCollaborative)
        {
            this.cy.fit(this.FIT_CONSTANT);
            var newState =
                {
                    zoomLevel: this.cy.zoom(),
                    panLevel: this.cy.pan()
                };
            this.updateGlobalOptions(newState);
        }
        else
        {
            this.cy.fit(this.FIT_CONSTANT);
        }
    }

    loadFile(nodes: any[], edges: any[])
    {
        if (this.isCollaborative)
        {
            //Real time load graph
            this.loadfileShareDB(nodes, edges);
        }
        else
        {
            //Local usage file load
            this.loadFileCy(nodes,edges);
        }

        this.edgeEditing?.initAnchorPoints(this.cy.edges());

        this.fitGraph();
    };

    loadFileCy(nodes: any[], edges: any[])
    {
        //Remove all elements
        this.removeElementCy(this.cy.elements());

        this.addNodesCy(nodes);
        this.addEdgesCy(edges);
    };

    loadfileShareDB(nodes: any[], edges: any[])
    {
        this.shareDBManager.loadGraph(nodes,edges);
    };

    removeAllElements()
    {
        if (this.isCollaborative)
        {
            this.shareDBManager.removeAllElements();
        }
        else
        {
            this.cy.remove(this.cy.elements());
        }
    };

    updateHighlight(ele: any, isHighlighted: boolean)
    {
        if (isHighlighted)
            this.undoRedoManager.do('highlightOthers', ele);
        else
            this.undoRedoManager.do('removeOtherHighlight', ele);
    };


    @autobind
    updateVisibility(ele: any, isHidden: boolean)
    {
        if (isHidden)
            this.viewUtilities.hide(ele);
        else
            this.viewUtilities.show(ele);
    };

    updateElementCallback(op: any)
    {
        var ele = op.oi;
        var eleID = ele.id;
        var cyEle = this.cy.$("#" + eleID);
        this.changeNameCy(cyEle, ele.name);

        if (cyEle.isNode())
        {
            //Width and height of simple nodes and compounds is changed differently
            if (cyEle.isParent())
            {
                cyEle.style("min-width", ele.minWidth);
                cyEle.style("min-width-bias-left", ele.minWidthBiasLeft);
                cyEle.style("min-width-bias-right", ele.minWidthBiasRight);
                cyEle.style("min-height", ele.minHeight);
                cyEle.style("min-height-bias-top", ele.minHeightBiasTop);
                cyEle.style("min-height-bias-bottom", ele.minHeightBiasBottom);
            }
            else
            {
                cyEle.data('w', ele.w);
                cyEle.data('h', ele.h);
                //Position is changed only for simple nodes because the
                //position of compounds is defined by simple nodes' position
                cyEle.position({x: ele.x, y: ele.y});
            }

            this.updateVisibility(cyEle, ele.isHidden);
            this.updateHighlight(cyEle, ele.isHighlighted);

            if(ele.isInvalidGene)
            {
                this.doHighlightInvalidGenes(cyEle);
            }
            else
            {
                this.undoHighlightInvalidGenes(cyEle);
            }
        }
        else if(cyEle.isEdge())
        {
            var pubmedArray = ele.pubmedIDs;
            cyEle.data('pubmedIDs', pubmedArray);
            this.updateHighlight(cyEle, ele.isHighlighted);

            var anchorPoints = ele.anchorPoints;

            //If edge is reconnected
            if ( ele.source !== cyEle.source().id() || ele.target !== cyEle.target().id()){
                var location = {
                    source: ele.source,
                    target: ele.target
                };
                cyEle.move(location);
                //make sure that bend points are same
                this.edgeEditing?.initAnchorPoints(cyEle);
            }
            else {
                if (ele.edgeCurveStyle === "bezier") {
                    const anchors = this.edgeEditing?.getAnchorsAsArray(cyEle);
                    if (anchors && anchors.length > 0) {
                        for (let i = 0; i < anchors.length / 2; i++) {
                            this.edgeEditing?.deleteSelectedAnchor(cyEle, 0);
                        }
                    }
                }
                else if (ele.edgeCurveStyle === "unbundled-bezier") {
                    cyEle.data('controlPointPositions', anchorPoints);
                }
                else {
                    cyEle.data('bendPointPositions', anchorPoints);
                }

                this.edgeEditing?.initAnchorPoints(cyEle);
            }
        }
    };

    getGenomicDataSVG(node: any)
    {
      // @ts-ignore
        return this.genomicDataOverlayManager.generateSVGForNode(node);
    }

    getOncoprintDataSVG(node: any) 
    {
        return this.genomicDataOverlayManager.generateOncoprintForPatientNode(node);
    }

    removeGenomicData()
    {
        if(this.isCollaborative)
        {
            this.shareDBManager.clearGenomicData();
        }
        else
        {
            // TODO wrap this in afunction in genomic data overlay manager
            this.genomicDataOverlayManager.clearAllGenomicData();
            this.genomicDataOverlayManager.hideGenomicData();
            this.genomicDataOverlayManager.notifyObservers();
        }

    }

    addGenomicData(genomicData: any)
    {
        const groupID = this.getEmptyGroupID();

        if(this.isCollaborative)
        {
            var parsedGenomicData = this.genomicDataOverlayManager.prepareGenomicDataShareDB(genomicData);
            this.shareDBManager.addGenomicData(parsedGenomicData.genomicDataMap);
            this.shareDBManager.groupGenomicData(Object.keys(parsedGenomicData.visibilityMap),
                groupID);
            
            let currentVisibleGenomicDataCount = this.genomicDataOverlayManager.countVisibleGenomicDataByType();
            Object.keys(parsedGenomicData.visibilityMap).forEach(study => {
                if (parsedGenomicData.visibilityMap[study] && (currentVisibleGenomicDataCount >= 6)) {
                    parsedGenomicData.visibilityMap[study] = false;
                }
                else if(parsedGenomicData.visibilityMap[study]) {
                    currentVisibleGenomicDataCount++;
                }
            });

            this.shareDBManager.addGenomicVisibilityData(parsedGenomicData.visibilityMap);
        }
        else
        {
            this.genomicDataOverlayManager.addGenomicDataLocally(genomicData, groupID);
        }
    }

    adjustVisibilityShareDB(profileId: string, isEnabled: boolean){
        const targetProfileIndex = this.profiles.map(profile => profile.profileId).indexOf(profileId);
        this.setProfile(targetProfileIndex, {
            ...this.profiles[targetProfileIndex],
            enabled: isEnabled
        }); 
    }

    addToProfiles(profileId: string){
        // Check if this profile already exists
        if(this.profiles.map(profile => profile.profileId).includes(profileId)){
            return;
        }

        this.addProfile({
            profileId: profileId, 
            enabled: true
        });
    }

    addPortalGenomicData(genomicData: any, groupID: any, activeGroups?: any[])
    {
        
        if(this.isCollaborative)
        {
            var parsedGenomicData = this.genomicDataOverlayManager.preparePortalGenomicDataShareDB(genomicData);
            this.shareDBManager.addGenomicData(parsedGenomicData.genomicDataMap);
            this.shareDBManager.groupGenomicData(Object.keys(parsedGenomicData.visibilityMap),
                groupID);
            this.shareDBManager.addGenomicVisibilityData(parsedGenomicData.visibilityMap);
        }
        else
        {
            if( activeGroups !== undefined){
                this.genomicDataOverlayManager.addPortalGenomicData(genomicData, groupID, activeGroups);
            }
            else {
                this.genomicDataOverlayManager.addPortalGenomicData(genomicData, groupID);
            }
        }
    }

    shareDBGenomicDataHandler(op: any)
    {
        var isRemove = Object.keys(op)[1] === 'od';
        var newData = op.oi;
        var geneSymbol = op.p[1];


        if(!isRemove)
        {
            this.genomicDataOverlayManager.addGenomicDataWithGeneSymbol(geneSymbol, newData);

        }
        // Removal
        else
        {
            this.genomicDataOverlayManager.removeGenomicDataWithGeneSymbol(geneSymbol);
        }
    }


    shareDBGenomicDataGroupChangeHandler(op: any)
    {

        var isRemove = Object.keys(op)[1] === 'od';
        var data = op.oi;
        var key = op.p[1];

        // Addition
        if(!isRemove)
        {
            this.genomicDataOverlayManager.addGenomicGroupData(key, data);
            if(data.length !== 1){
                console.log("Grouped genomic data expected to be of length 1");
            }
            data.forEach((profileId: any) => {this.addToProfiles(profileId)});
        }
        else if (!data) {
            this.genomicDataOverlayManager.clearAllGenomicData();
        }
        this.genomicDataOverlayManager.showGenomicData((node: any) => {
            this.resizeElements(node);
        } );
        this.genomicDataOverlayManager.notifyObservers();
    }

    shareDBGenomicDataVisibilityHandler(op: any)
    {
        var data = op.oi;
        var key = op.p[1];
        var isRemove = Object.keys(op)[1] === 'od';
        // Addition
        if(!isRemove)
        {
            this.genomicDataOverlayManager.addGenomicVisData(key, data);

            this.adjustVisibilityShareDB(key, data);
        }
        // Removal
        else
        {
            this.genomicDataOverlayManager.removeGenomicVisData();
            this.removeProfiles();
        }

        this.genomicDataOverlayManager.showGenomicData((node: any) => {
            this.resizeElements(node);
        } );
        this.genomicDataOverlayManager.notifyObservers();
    }

    updateGenomicDataColorSchemeHandler(op: any)
    {
        let colorScheme = op.li;
        
        this.setGenomicDataOverlayColorScheme(colorScheme);

        this.colorSchemeChangeCallback(colorScheme);
        
        this.genomicDataOverlayManager.updateColorScheme(colorScheme);
        this.genomicDataOverlayManager.showGenomicData((node: any) => {
            this.resizeElements(node);
        } );
        this.genomicDataOverlayManager.notifyObservers();
    }

    resizeNodesToContent(nodes: any[])
    {
        if(this.isCollaborative)
        {
            const visibleNumberOfData = this.genomicDataOverlayManager.countVisibleGenomicDataByType();
            const labelWithData = 148 + (visibleNumberOfData-3) * 36;
            const rt = this.shareDBManager;
            nodes.forEach(function( ele ){
                if (!ele.isParent())
                {
                    var newWidth = 150;
                    var newHeight = 52;
                    if (ele.data('name') != "")
                    {
                        var labelLength = ele.style('label').length*10 + 6;
                        newWidth = labelLength;
                        newHeight = 24;
                    }
                    if (ele.data('type') !== "PROCESS" && visibleNumberOfData > 0)
                    {
                        newHeight = 52;
                        if (visibleNumberOfData < 4)
                        {
                            if (150 > newWidth)
                                newWidth = 150;
                        }
                        else
                        {
                            if (labelWithData > newWidth)
                                newWidth = labelWithData;
                        }
                    }
                    rt.setSizeOfElement(ele, newWidth, newHeight);
                }
                else
                {
                    //Set the minWidth, minHeight and other properties of compound to 0
                    rt.resizeCompound(ele, 0, 0, 0, 0, 0, 0);
                }
            });
        }
        else
        {
            const ur = this.cy.undoRedo();
            const actions: any[] = [];

            const visibleNumberOfData = this.genomicDataOverlayManager.countVisibleGenomicDataByType();
            const labelWithData = 150 + (visibleNumberOfData-3) * 36;
            nodes.forEach(function( ele: any){
                if (!ele.isParent())
                {
                    let newWidth = 150;
                    let newHeight = 52;
                    if (ele.data('name') != "") {
                        var labelLength = ele.style('label').length * 6 + 24;
                        newWidth = labelLength;
                        newHeight = 24;
                    }
                    if (ele.data('type') !== "PROCESS" && 
                        visibleNumberOfData > 0) {
                        
                        newHeight = 52;
                        if (visibleNumberOfData < 4) {
                            if (150 > newWidth)
                                newWidth = 150;
                        }
                        else {
                            if (labelWithData > newWidth)
                                newWidth = labelWithData;
                        }
                    }
                    const args = {
                        ele: ele,
                        oldWidth: ele.width(),
                        newWidth: newWidth,
                        oldHeight: ele.height(),
                        newHeight: newHeight
                    };
                    actions.push({name: "changeNodeSize", param: args});
                }
                else
                {
                    const args = {
                        ele: ele,
                        oldMinWidth: ele.style("min-width"),
                        newMinWidth: 0,
                        oldMinWidthBiasLeft: ele.style("min-width-bias-left"),
                        newMinWidthBiasLeft: 0,
                        oldMinWidthBiasRight: ele.style("min-width-bias-right"),
                        newMinWidthBiasRight: 0,
                        oldMinHeight: ele.style("min-height"),
                        newMinHeight: 0,
                        oldMinHeightBiasTop: ele.style("min-height-bias-top"),
                        newMinHeightBiasTop: 0,
                        oldMinHeightBiasBottom: ele.style("min-height-bias-bottom"),
                        newMinHeightBiasBottom: 0
                    };
                    actions.push({name: "changeCompoundSize", param: args});
                }
            });

            ur.do("batch", actions);
        }
        this.cy.nodeEditing('get').refreshGrapples();
    };



}
