
var extensionPoints={};


var DefaultView=function(iconPath)
{
    var self=this;
    this.iconPath=iconPath;
    this.apply=function(data)
    {
        var view=null;    
        if(data.type=='eObject')
        {
            switch(data.value.eClass.get("name"))
            {
                case "EReference":
                    if(data.value.get("containment"))
                    {
                        view={title: data.value.get("name"), icon:iconPath+"EReference.gif", eObject: data.value, modifiers:data.modifiers, lazy: true}; 
                    }
                    break;
                case "EAttribute":
                    view=null;
                    break;
                default:
                    view={title: data.value.eClass.get("name"), icon:iconPath+"EObject.gif", eObject: data.value,modifiers:data.modifiers, lazy: true}; 
            }
        }
        
        if(data.type=='value')
        {
            view={title: "value("+data.value.dataType+")="+data.value.value }; 
        }

        return view;
    };
}

/*
var DefaultObserver=function(ecoreSync,treeDOM)
{
    var ecoreSync=ecoreSync;
    this.eventListeners=[];
    this.treeDOM=treeDOM;
    this.observerInstances=[];

    this.invalidateNode=function(view,eObject)
    {
        //Invalidates all nodes refering to the supplied eObject
        //for all invalidated nodes the tree representation will be updated using the supplied view

        var self=this;
        var tree=$.ui.fancytree.getTree("#"+self.treeDOM.getAttribute("id"));                     
        tree.visit(function(e){
            if(e.data.eObject==eObject)
            {
                e.fromDict(view.apply({type:'eObject',value: eObject}));
            }
        })        
    }

    this.invalidateChildNodes=function(loader=null,view=null,parentEObject=null,eObject=null)
    {     
        console.error('Invalidated child nodes for #'+parentEObject.get("_#EOQ")+'/'+eObject.get("_#EOQ"))   
        var self=this;
        var tree=$.ui.fancytree.getTree("#"+self.treeDOM.getAttribute("id"));   
        loader.load(parentEObject,eObject).then(function(loadedData){

            //Synchronizing model and tree         

            var node=tree.findFirst(function(e){
                if(e.data.eObject==eObject && e.parent.data.eObject==parentEObject)
                {
                    return true;
                }           
                return false;
            })
            if(node)
            {
                var currentChildren=node.getChildren();
                var addedChildren=loadedData.filter(function(e){
                    var match=currentChildren.find(function(c){        
                        return e.value==c.data.eObject
                    })
                    if(match)
                    {
                        return false
                    }
                    else
                    {
                        return true
                    }
                })
                var removedChildren=currentChildren.filter(function(e){
                    var match=loadedData.find(function(c){        
                        return e.data.eObject==c.value
                    })
                    if(match)
                    {
                        return false
                    }
                    else
                    {
                        return true
                    }
                })
                addedChildren.map(function(e){
                    self.observe(e.value,e,loader,view) 
                    node.addNode(view.apply(e),'child');
                });            
                removedChildren.map(function(e){
                    node.removeChild(e);
                })               
            }
        });

    }


    this.observe=function(parentEObject,data,loader,view)
    {
        this.parentEObject=parentEObject    
        var self=this;
        
        //Actual Observer object
        var Observer=function(item)
        {
            this.loader=loader;
            this.view=view;
            var observer=this;
            if(item.type=='eObject')
            {
                let eObject=item.value;
                switch(eObject.eClass.get("name"))
                {
                    case "EReference":
                        parentEObject.on('add:'+eObject.get("name"),function(){ 
                            self.invalidateChildNodes(observer.loader,observer.view,parentEObject,eObject);
                        })
                        parentEObject.on('remove:'+eObject.get("name"),function(){ 
                            self.invalidateChildNodes(observer.loader,observer.view,parentEObject,eObject);                           
                        })            
                        break;
                    case "EAttribute":
                        console.error('Observe EAttribute');
                        console.error(eObject);
                        break;
                    default:                
                        console.error('Observe EObject of Misc Type');               
                        eObject.on('change',function(){
                            self.invalidateNode(observer.view,eObject)
                        });

                        break;                            
                }  
            }
            else
            {
                console.error('Observe item of type != eObject (NOT IMPLEMENTED)')
            }
        }

        //Create Observers for the supplied data items
        if(Array.isArray(data))
        {
            data.map(function(item){        
                self.observerInstances.push(new Observer(item));
            });     
        }
        else
        {
            self.observerInstances.push(new Observer(data));
        }    
    }
}
*/

var DefaultController=function(ecoreSync,treeDOM,eventBroker)
{
    var ecoreSync=ecoreSync;
    this.treeDOM=treeDOM;
    this.observerInstances=[];
    this.eventBroker = eventBroker;

    this.invalidateNode=function(view,eObject)
    {
        //Invalidates all nodes refering to the supplied eObject
        //for all invalidated nodes the tree representation will be updated using the supplied view

        var self=this;
        var tree=$.ui.fancytree.getTree("#"+self.treeDOM.getAttribute("id"));                     
        tree.visit(function(e){
            if(e.data.eObject==eObject)
            {
                e.fromDict(view.apply({type:'eObject',value: eObject}));
            }
        })        
    }

    this.invalidateChildNodes=function(view=null,parentEObject=null,eObject=null)
    {                
        var self=this;
        var tree=$.ui.fancytree.getTree("#"+self.treeDOM.getAttribute("id"));   
        self.load(parentEObject,eObject).then(function(loadedData){

            //Synchronizing model and tree         

            var node=tree.findFirst(function(e){
                if(e.data.eObject==eObject && e.parent.data.eObject==parentEObject)
                {
                    return true;
                }           
                return false;
            })
            if(node)
            {
                var currentChildren=node.getChildren();
                var addedChildren=loadedData.filter(function(e){
                    var match=currentChildren.find(function(c){        
                        return e.value==c.data.eObject
                    })
                    if(match)
                    {
                        return false
                    }
                    else
                    {
                        return true
                    }
                })
                var removedChildren=currentChildren.filter(function(e){
                    var match=loadedData.find(function(c){        
                        return e.data.eObject==c.value
                    })
                    if(match)
                    {
                        return false
                    }
                    else
                    {
                        return true
                    }
                })
                addedChildren.map(function(e){
                    self.startObserving(e.value,e,view) 
                    node.addNode(view.apply(e),'child');
                });            
                removedChildren.map(function(e){
                    node.removeChild(e);
                })               
        }
        });

    }

    this.onUpdate=function()
    {
       /* todo */  
       if($DEBUG) console.debug("default controller update detected")
    }

    this.onChange=function()
    {
      /* todo */     
      if($DEBUG) console.debug("default controller change detected")
    }

    this.startObserving=function(parentEObject,data,view)
    {
        this.parentEObject=parentEObject    
        var self=this;
        
        //Actual Observer object
        var Observer=function(item)
        {           
            this.view=view;
            var observer=this;
            if(item.type=='eObject')
            {
                let eObject=item.value;
                switch(eObject.eClass.get("name"))
                {
                    case "EReference":
                        parentEObject.on('add:'+eObject.get("name"),function(){ 
                            self.invalidateChildNodes(observer.view,parentEObject,eObject);
                        })
                        parentEObject.on('remove:'+eObject.get("name"),function(){ 
                            self.invalidateChildNodes(observer.view,parentEObject,eObject);                           
                        })            
                        break;
                    case "EAttribute":
                        break;
                    default:                     
                        eObject.on('change',function(){
                            self.invalidateNode(observer.view,eObject)
                        });

                        break;                            
                }  
            }
            else
            {
                if($DEBUG) console.debug('Observer for item of type != eObject is not implemented')
            }
        }

        //Create Observers for the supplied data items
        if(Array.isArray(data))
        {
            data.map(function(item){        
                self.observerInstances.push(new Observer(item));
            });     
        }
        else
        {
            self.observerInstances.push(new Observer(data));
        }    
    }
    
    this.load=function(parent,eObject)
    {        
        $.notify("Default Contoller used");
        var load=[];
        switch(eObject.eClass.get("name"))
        {
            case "EReference":
                load=ecoreSync.get(parent,eObject.get("name")).then(function(results){
                    var value=[];
                    if(Array.isArray(results))
                    {                      
                        value=results;
                    }
                    else
                    {
                        if(results!=null)
                        {
                        value=[results];
                        }                        
                    }
                    return Promise.resolve(value.map(function(e){ return {type:'eObject', value: e, modifiers:{}}}));
                });
                break;
            case "EAttribute":
                load=ecoreSync.get(parent,eObject.get("name")).then(function(value){
                    return Promise.resolve([{type:'value', value: {dataType: eObject.get("eType").get("name"), value: value, modifiers:{}} }]);
                });
                break;
            default:                
                load=ecoreSync.utils.isEClassInitialized(eObject.eClass).then(function(){
                    var features=eObject.eClass.get("eStructuralFeatures").array();
                    return Promise.resolve(features.map(function(e){ return {type:'eObject', value: e,modifiers:{eOwner:eObject}}}));
                });
                
                
                
        }           
   
        return load;
    };


}







export async function init(pluginAPI) { 
    
    var contextMenu=pluginAPI.require('contextMenu');
    var clipboard=pluginAPI.require('clipboard');

    var modules=await pluginAPI.loadModules(['js/TreeViewContextMenuProvider.js']);

    extensionPoints.controllers=pluginAPI.provide("plugin.ecoreTreeView.controllers");
    extensionPoints.views=pluginAPI.provide("plugin.ecoreTreeView.views");
    pluginAPI.provide("plugin.ecoreTreeView.menus");


    var menuProviders=[];
    extensionPoints.menus=pluginAPI.provide('ecoreTreeView.menus',modules[0].TreeViewContextMenuProvider,function(event){
        menuProviders.push(event.extension);
    });


    extensionPoints.edit=pluginAPI.provide("plugin.ecoreTreeView.edit");


  


    

    var create=async function(ecoreSync,id,DOMelement,rootObjectId=0,rootObjectName=null,theme=null){
        
        var controllers=pluginAPI.evaluate('plugin.ecoreTreeView.controllers').map(function(Controller){
            return new Controller(ecoreSync,DOMelement);
        });

        var views=pluginAPI.evaluate('plugin.ecoreTreeView.views').map(function(View){
            return new View(ecoreSync);
        });

        /*
        var menus=pluginAPI.evaluate('plugin.ecoreTreeView.menus').map(function(Menu){
            return new Menu(ecoreSync);
        });
        */



        var rootObject=await ecoreSync.getObject(rootObjectId);
        if(!rootObject) throw 'root object could not be resolved #'+rootObjectId
        var treeViewId=id;
        DOMelement.innerHTML='<br>';
        var rootObjectName=(rootObjectName || rootObject.get("name")) || 'Root';

        var eventBroker=pluginAPI.require('eventBroker');
        var SelectionChangedEvent = eventBroker.SelectionChangedEvent;

        var defaultView=new DefaultView(pluginAPI.getPath()+"ecore-icons/");  
        var defaultController=new DefaultController(ecoreSync,DOMelement,eventBroker); 
        
        
        //var defaultObserver=new DefaultObserver(ecoreSync,DOMelement);
        //window["debug"]={source: view.apply({ eObject: rootObject, featureName:null})};


        var view=views.find(function(e){ return e.canDisplay({type:'eObject',value: rootObject, modifiers:{}}); });
        if(!view)
        {
            view=defaultView;
        }                  

        $(DOMelement).fancytree({clickFolderMode:1,selectMode: 2, source: [view.apply({type:'eObject',value: rootObject})], lazyLoad: function (event, data) { 
            
                //select controller
                var controller=controllers.find(function(e){ return e.canLoad(data); });
                if(!controller)
                {
                    controller=defaultController;
                }

                var parentEObject=data.node.parent.data.eObject;
                if(data.node.data.modifiers)
                {
                    if(data.node.data.modifiers["eOwner"])
                    {
                        parentEObject=data.node.data.modifiers["eOwner"];
                    }
                }
            
                data.result = controller.load(parentEObject,data.node.data.eObject).then(function(loadedData){
                    var treeData=loadedData.map(function(nodeData){                            
                            var view=views.find(function(e){ return e.canDisplay(nodeData); }) || defaultView;
        
                            controller.onChange(nodeData,function(eObject){
                                //Updating node                                
                                var tree=$.ui.fancytree.getTree("#"+DOMelement.getAttribute("id"));                     
                                tree.visit(function(e){
                                    if(e.data.eObject==eObject)
                                    {
                                        e.fromDict(view.apply({type:'eObject',value: eObject}));
                                    }
                                })        
                                if($DEBUG) console.debug('plugin.ecoreTreeView updated changed node')
                            });    
                            return view.apply(nodeData);
                    }).filter(function(e){ return e!==null;}); 

                    controller.onUpdate(parentEObject,data.node.data.eObject,function(loadedData){
                            //controllers should fire this update if children are added or removed
                            //it should/can not be used for update events of children attributes (e.g. if a name changes)
                            //use onChange if you need to react to attribute changes   

                            //Synchronizing model and tree      
                            var tree=$.ui.fancytree.getTree("#"+DOMelement.getAttribute("id"));        
                            var node=tree.findFirst(function(e){
                                if(e.data.eObject==data.node.data.eObject && e.parent.data.eObject==parentEObject)
                                {
                                    return true;
                                }           
                                return false;
                            })
                            if(node)
                            {
                                var currentChildren=node.getChildren();
                                var addedChildren=loadedData.filter(function(e){
                                    var match=currentChildren.find(function(c){        
                                        return e.value==c.data.eObject
                                    })
                                    if(match)
                                    {
                                        return false
                                    }
                                    else
                                    {
                                        return true
                                    }
                                })
                                var removedChildren=currentChildren.filter(function(e){
                                    var match=loadedData.find(function(c){        
                                        return e.data.eObject==c.value
                                    })
                                    if(match)
                                    {
                                        return false
                                    }
                                    else
                                    {
                                        return true
                                    }
                                })

                                
                                addedChildren.map(function(c){   

                                    var view=views.find(function(v){ return v.canDisplay(c); });  
                                    controller.onChange(c,function(eObject){
                                        //Updating node                                
                                        var tree=$.ui.fancytree.getTree("#"+DOMelement.getAttribute("id"));                     
                                        tree.visit(function(e){
                                            if(e.data.eObject==eObject)
                                            {
                                                e.fromDict(view.apply({type:'eObject',value: eObject}));
                                            }
                                        })        
                                        if($DEBUG) console.debug('plugin.ecoreTreeView updated changed node')
                                    });        
                                    
                                    var successor=null;
                                    let idx= loadedData.indexOf(c)
                                    if(loadedData[idx+1])
                                    {
                                        successor=tree.findFirst(function(e){
                                            if(e.data.eObject==loadedData[idx+1].value && e.parent.data.eObject==eObject)
                                            {
                                                return true;
                                            }           
                                            return false;
                                        })
                                    }                          
                                    if(successor)
                                    {
                                        node.addChildren([view.apply(c)],successor);
                                    }
                                    else
                                    {
                                        node.addChildren([view.apply(c)]);
                                    }
                                });     
                                
                                removedChildren.map(function(e){
                                    node.removeChild(e);
                                })               
                            }
                            else
                            {
                                console.error('Node not found!')
                            }                               
                });

                return Promise.resolve(treeData);
                });
            },
            activate: function(event,data){
                var node = data.node;
                var tree = data.tree;                
                if(!event.ctrlKey && !event.shiftKey)
                {
                    tree.visit(function(node){
                        node.setSelected(false);
                    });

                    node.setActive(true);                  
                    node.setSelected(true);                  
                    
                }
                if(event.ctrlKey)
                {
                    node.toggleSelected();
                    return false;
                }                 
            },
            select: function(event, data) {
                // Display list of selected nodes
                var nodeSelection = data.tree.getSelectedNodes().join(", ");  
                var selectedEObjects=data.tree.getSelectedNodes().map(function(e){
                    return e.data.eObject
                })
                var activeNode=data.tree.getActiveNode();
                if(activeNode)
                {                   
                    var labelDom= activeNode.span.children[2];      
                    
                    //Depricated
                    // $app.selectionManager.SetSelection(new jsa.Selection({
                    //     elements: selectedEObjects,
                    //     eventSource: this,
                    //     domElement: labelDom
                    // })); 

                    //try new event broker
                    let changeEvent = new SelectionChangedEvent(this,selectedEObjects,labelDom);
                    eventBroker.publish("SELECTION/CHANGE",changeEvent);
                }
                
            },  
            edit: {
                // Available options with their default:
                adjustWidthOfs: 4,   // null: don't adjust input size to content
                inputCss: { minWidth: "3em" },
                triggerStart: ["f2"],
                beforeEdit: $.noop,   // Return false to prevent edit mode
                edit: $.noop,         // Editor was opened (available as data.input)
                beforeClose: $.noop,  // Return false to prevent cancel/save (data.input is available)
                save: function(){ return true },         // Save data.input.val() or return false to keep editor open
                close: $.noop,        // Editor was removed
            }         
        });

       //make connectors visible (dotted lines)
       $(".fancytree-container").addClass("fancytree-connectors");
       if(theme)
       {
        $(".fancytree-container").addClass(theme);
       }

       var mergeItems=function(itemsA,itemsB)
       {
            var items=Object.assign({},itemsA);;
            for(let key in itemsB)
            {
                if(items[key])
                {
                    
                    if(items[key]['items'] && itemsB[key]['items'])
                    {
                        items[key]['items']=mergeItems(items[key]['items'],itemsB[key]['items']);
                    }
                    else
                    {
                        console.error('cannot merge items')
                    }
                }
                else
                {
                    items[key]=itemsB[key];
                }
            }
            return items;
       }

       var mergeMenus=function(menus,node){
        var items={};
        var itemCallbacks={};
        for(let i in menus)
        {
            var menuDisplay=menus[i].display(node)
            for(const key in menuDisplay.items)
            {
                if(!items[key])
                {
                    items[key]=menuDisplay.items[key]
                    itemCallbacks[key]=menuDisplay.callback
                }
                else
                {
                    
                    if(items[key]['items'] && menuDisplay.items[key]['items'])
                    {
                       items[key]['items']=mergeItems(items[key]['items'],menuDisplay.items[key]['items']);                        
                    }
                    else
                    {
                        console.error('Cannot reassign key='+key+' for context menu of ecoreTreeView')
                    }
                }
            }
        }

        return {
            items: items,
            callback: function(key,options){
                if(itemCallbacks[key])
                {
                    return itemCallbacks[key](key,options);
                }
                else
                {
                    if($DEBUG) console.error('No callback known for context menu key='+key)
                }
            }
        }



       }




   


       var menus=pluginAPI.evaluate('plugin.ecoreTreeView.menus').map(function(Menu){
        return new Menu(ecoreSync);
       });


       var cmenuProviders=pluginAPI.evaluate('ecoreTreeView.menus');

       
       //registering context menu with tree
       $.contextMenu({
            selector: '#'+DOMelement.id.replace("#", "\\#")+' .fancytree-title',  // replace masks '#' in IDs like #WORKSPACE
            build: function($triggerElement, e){

                e.preventDefault();
                var menu=null;
                var node = $.ui.fancytree.getNode(e);  
                var offset=$($triggerElement).offset();         

                var applicableMenuProviders=cmenuProviders.filter(function(e){ return e.isApplicableToNode(node); })    
                if(applicableMenuProviders.length > 0)
                {
                    contextMenu.showContextMenu({x:offset.left+50,y:offset.top+10},contextMenu.util.collectAndMerge(applicableMenuProviders,node));        
                }        

                return false;                          
            }
        });

        //registering copy, cut and paste-handlers with tree
        $(DOMelement).on('paste','.fancytree-node',async function(event){
            var node = $.ui.fancytree.getNode(event);  
            await clipboard.evalEvent(event.originalEvent);      
            try{
                if(node.data.eObject.eClass.get('name')!='EReference')
                {
                    await clipboard.pasteToEObject(node.parent.data.eObject);
                }
                else
                {
                    await clipboard.pasteToEObject(node.data.eObject);
                }
            }
            catch(e)
            {
                console.error(e)
            }
            event.stopPropagation();
        });

        $(DOMelement).on('copy','.fancytree-node',async function(event){
            var node = $.ui.fancytree.getNode(event);                      
            await clipboard.evalEvent(event.originalEvent,ecoreSync.utils.getObjectURL(node.data.eObject));
            event.stopPropagation();
        });

        $(DOMelement).on('cut','.fancytree-node',async function(event){
            var node = $.ui.fancytree.getNode(event);                      
            await clipboard.evalEvent(event.originalEvent,ecoreSync.utils.getObjectURL(node.data.eObject));
            event.stopPropagation();
        });

        //TODO: Move to plugin?
        $(DOMelement).on('dblclick','.fancytree-node',async function(event){
            var node = $.ui.fancytree.getNode(event);
            eventBroker.publish('PROPERTIESVIEW/OPEN',{eObject:node.data.eObject,DOM:node.span.children[2]}); 
            event.stopPropagation();    
        })
        
   


        
    }



    
    pluginAPI.expose({create:create});
    return true;
};

export var meta={
    "id":"plugin.ecoreTreeView",
    description:"Ecore TreeView",
    "author":"Matthias Brunner",
    "version":"0.0.1",
    "requires":[
        'ecoreSync',
        'plugin.fancyTree',
        'plugin.contextMenu',
        'eventBroker',
        'contextMenu',
        'clipboard'
    ]
};



