
/**
 * @augments EicComponent
 * @category EICUI/AdvancedComponents
 */
class NodeMap extends EicComponent {

    entities = [];
    relations = [];

    constructor(el, options) {

        let defaultOptions = {
            resizable: true,
            allowDrag: true,
            orientation: 'linear',
            entity: {
                width: 200,
                height: 50,
                gap: 40
            },
            map: {
                position: { x: 0, y: 0 },
                size: { width: 0, height: 0 },
                dragging: false,
            },
            scale: {
                current: 1.0,
                factor: 0.085,
                min: 0.5,
                max: 4
            },
            guides: true
        }

        super(el, {...defaultOptions, ...(options || {})});
        
        if(this._constructed) return this;

		this.el.setAttribute('eicnodemap',''); 

        if(this.options.resizable)
            this.el.setAttribute('resizable','');
        else 
            this.el.removeAttribute('resizable');

        this.el.innerHTML = '';
        this.map = ui.create(`<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"></svg>`)
        this.el.append(this.map);
        
        this.viewbox = { x: 0, y: 0, w: this.el.clientWidth, h: this.el.clientHeight };

        if(this.options.allowDrag) {
            this.map.addEventListener('wheel', this.onMapScroll.bind(this));
            this.map.addEventListener('mousedown', this.onMapDragStart.bind(this));
            this.map.addEventListener('mousemove', this.onMapDragMove.bind(this));
            this.mouseUpHandler = this.onMapDragStop.bind(this);
            this.entityUpHandler = this.onEntityDragStop.bind(this);
        }
    }

    set loading(state) { 
        if(state == true) 
            this.el.setAttribute('loading','');
        else
            this.el.removeAttribute('loading');
    }
    get loading() { return this.el.hasAttribute('loading') }

    set data(value) {
        
        this.clear();

        if(value.entities) for(let entity of value.entities) this.addEntity(entity);
        if(value.relations) for(let relation of value.relations) this.addRelation(relation);
        
        this.reorder();
    }

    clear() { 
        this.map.innerHTML = ''; 
        this.entities = [];
    }

    reorder() {
        switch(this.options.orientation) {
            case 'linear': 
                this.reorderLinear();
                break;
            case 'radial':
            default:
                this.reorderRadial();
        }
    }

    reorderLinear() {
		
		// 1. prepare geometries and identify starters and enders
		for(let entity of this.entities) { 
			entity.geometry = {
				starter: false,
				ender: false,
				depth: null,
				siblings: null,
				index: null
			};

			if(!entity.relations.find(r => r.target.id == entity.id)) {
				entity.geometry.starter = true; 
				entity.geometry.depth = 0
			}

			entity.geometry.ender = !entity.relations.find(r => r.source.id == entity.id); 
		}

		// 2. dig paths using starters
		let starters = this.entities.filter(o => o.geometry.starter == true)
		for(let entity of starters) {
			this.digLinearPath(entity)
		}

		// 3. resolve siblings
		let maxSiblings = 0;
		for(let entity of this.entities) {
			entity.geometry.siblings = this.entities.filter(o => o.geometry.depth == entity.geometry.depth).length - 1
			maxSiblings = Math.max(maxSiblings, entity.geometry.siblings);
			let i = 0;
			
			for(let relation of entity.relations) {
				if(relation.target.id != entity.id && !relation.target.geometry.index) {
					relation.target.geometry.index = i;
					i++;
				}
			}
		}
		// 4. reposition
		let maxHeight = 3 * maxSiblings * (this.options.entity.height + (this.options.entity.gap))
		for(let entity of this.entities) {
			
			entity.x = this.options.entity.gap + (this.options.entity.width + this.options.entity.gap) * entity.geometry.depth;
			
			entity.y = maxHeight * (entity.geometry.index + 1) / (entity.geometry.siblings + 2);
			for(let relation of entity.relations) {
				relation.redraw();
			}
		}

		this.center();
    }

	digLinearPath(entity) {
		for(let relation of entity.relations) {
			if(relation.target.id != entity.id) {
				if(!relation.target.geometry.depth) {
					relation.target.geometry.depth = entity.geometry.depth + 1;
					this.digLinearPath(relation.target)
				}
			}
		}
	}

	center() {
		let box = this.map.getBBox();
        this.options.scale.current = Math.max(
            (box.height + this.options.entity.gap) / (this.viewbox.h ),
            (box.width + this.options.entity.gap) / (this.viewbox.w )
        )
        
        this.viewbox.y = box.y - (this.options.entity.gap / 2);
		this.viewbox.x = 20;
        this.setViewbox();
	}

    reorderRadial() {
        let a = (Math.PI * 2) / this.entities.length;
        let r = (this.options.entity.width) * Math.log(this.entities.length);
        let i = 0;
        let center = { x: this.viewbox.w / 2, y: this.viewbox.h / 2 }

        for(let entity of this.entities) {
            let pos = {
                x: center.x + (Math.cos(a * i - Math.PI * .5) * r) - (this.options.entity.width / 2),
                y: center.y + (Math.sin(a * i - Math.PI * .5) * r) - (this.options.entity.height / 2)
            } 
            entity.x = pos.x;
            entity.y = pos.y; 
            
            for(let relation of entity.relations) relation.redraw();
            
            i++;
        }
    }

    getEntityById(id) { return this.entities.find(entity => entity.id == id); }

    addEntity(entity) {
        let index = this.entities.length;
        let options = { ...entity, ...this.options.entity };
        let item = new NodeMapNode(null, options);
        item.x = this.options.entity.gap + (this.options.entity.width + this.options.entity.gap) * index;
        item.y = 30;

        item.el.addEventListener('selected', this.onEntitySelect.bind(this));
        
        this.entities.push(item);
        this.map.append(item.el);
    }

    removeEntity(entity) {
        let parents = entity.relations.filter(r => r.target.id == entity.id);
        let childs = entity.relations.filter(r => r.source.id == entity.id);

        for(let relation of parents) {
            let parent = relation.source;
            parent.relations = parent.relations.filter(r => r.target.id != entity.id);

            for(let relation of childs) {
                let child = relation.target;
                child.relations = child.relations.filter(r => r.source.id != entity.id);
                this.addRelation({source: parent.id, target: child.id});
            }
        }

        entity.relations.forEach(r => r.el.remove())
        entity.el.remove();
        this.entities = this.entities.filter(e => e.id != entity.id)

        this.reorder();
    }

    addRelation(relation) {
		let source = this.entities.find(entity => entity.id == relation.source);
		let target = this.entities.find(entity => entity.id == relation.target);
		let item =  new NodeMapRelation(source, target);
		source.relations.push(item);
		target.relations.push(item);
		this.map.prepend(item.el);
    }

    onEntityDragStart(event) {
        event.stopPropagation();
        event.preventDefault();

        this.current = event.currentTarget

        this.mouseStartPosition = { x: event.pageX, y: event.pageY }
        this.entityStartPosition = { x: Number.parseFloat(this.current.getAttribute("x")), y: Number.parseFloat(this.current.getAttribute("y")) }

        window.addEventListener("mouseup", this.entityUpHandler);

        this.entityDrag = true;
    }

    
    onEntityMove(event) {
        event.stopPropagation();
        event.preventDefault();

        this.mousePosition = { x: event.offsetX, y: event.offsetY };
        
        if(this.entityDrag) {
            this.current.setAttribute("x", this.entityStartPosition.x - (this.mouseStartPosition.x - event.pageX) * this.options.scale.current)
            this.current.setAttribute("y", this.entityStartPosition.y - (this.mouseStartPosition.y - event.pageY) * this.options.scale.current)
            
            let entity = this.entities.find(item => item.data.id == (this.options.entity.current.getAttribute("data-id")))
            let pos = { x: Number.parseFloat(this.options.entity.current.getAttribute("x")), y: Number.parseFloat(this.options.entity.current.getAttribute("y")) }
            
            /*
            for(let line of entity.lines) {
                if(line.getAttribute("data-source") == entity.data.id) {
                    line.setAttribute("x1", pos.x + (this.options.entity.width / 2));
                    line.setAttribute("y1", pos.y + (this.options.entity.height / 2));
                } else {
                    line.setAttribute("x2", pos.x + (this.options.entity.width / 2));
                    line.setAttribute("y2", pos.y + (this.options.entity.height / 2));
                }
            }
            */
        }
    }
    
    onEntityDragStop(event) {
        event.stopPropagation();
        event.preventDefault();

        window.removeEventListener("mouseup", this.entityUpHandler);
        this.options.entity.dragging = false;
    }
    
    onEntitySelect(event) {        
        let entity = event.detail;

        this.entities.forEach(item => {
            item.el.classList.remove('selected');
            item.el.classList.remove('related');
            //item.lines.forEach(line => line.classList.remove('related'))
        });
        entity.el.classList.add('selected');

        /*
        entity.lines.forEach(line => {
            line.classList.add('related');
            let other = this.entities.find(item => (item.data.id == line.getAttribute('data-target') && line.getAttribute('data-source') == selected.data.id) || (item.data.id == line.getAttribute('data-source') && line.getAttribute('data-target') == selected.data.id));
            other.el.classList.add('related');
        });
        */

        this.click(entity);
    }

    onMapScroll(event) {
        event.stopPropagation();
        event.preventDefault();
        
        let scale = 1. + (event.deltaY < 0 ? - this.options.scale.factor : this.options.scale.factor);
  
        if((this.options.scale.current * scale < this.options.scale.max) && (this.options.scale.current * scale > this.options.scale.min))
        {  
            let pos = {
                x: (this.mousePosition.x * this.options.scale.current) + this.viewbox.x, 
                y: (this.mousePosition.y * this.options.scale.current) + this.viewbox.y
            }

            this.viewbox.x = (this.viewbox.x - pos.x) * scale + pos.x;
            this.viewbox.y = (this.viewbox.y - pos.y) * scale + pos.y;
            this.options.scale.current *= scale;
        
            this.setViewbox();
        }
    }

    onMapDragStart(event) {
        this.mouseStartPosition = { x: event.pageX, y: event.pageY }
        this.viewboxStartPosition = { x: this.viewbox.x, y: this.viewbox.y }
        this.options.map.dragging = true;
        
        window.addEventListener("mouseup", this.mouseUpHandler);
    }

    onMapDragMove(event) {
        this.mousePosition = { x: event.offsetX, y: event.offsetY };
        
        if(this.options.map.dragging) {
            this.viewbox.x = this.viewboxStartPosition.x + (this.mouseStartPosition.x - event.pageX) * this.options.scale.current;
            this.viewbox.y = this.viewboxStartPosition.y + (this.mouseStartPosition.y - event.pageY) * this.options.scale.current;
            this.setViewbox();
        }
    }

    onMapDragStop(event) {
        window.removeEventListener("mouseup", this.mouseUpHandler);
        this.options.map.dragging = false;
    }

    onMapResize(event) {
        this.viewbox.w = this.find('.results').clientWidth;
        //this.viewbox.w = this.map.viewBox.baseVal.width;
        this.viewbox.h = this.find('.results').clientHeight;
        //this.viewbox.h = this.map.viewBox.baseVal.height;

        this.setViewbox();
    }

    setViewbox() { this.map.setAttribute("viewBox", this.viewbox.x + " " + this.viewbox.y + " " + (this.viewbox.w * this.options.scale.current) + " " + (this.viewbox.h * this.options.scale.current)); }

    click() {}
}

/**
 * @augments EicComponent
 * @category EICUI/AdvancedComponents
 */
class NodeMapNode extends EicComponent {

    /**
	 * 
	 */
	relations = [];
	
	/**
	 * 
	 */
	id = "";

    /**
     * 
     */
    options = {
        badge: {
            size: 'large',
            severity: 'danger'
        }
    }

    constructor(el, options) {
        super(el, options);

        this.setOptions(options);

        if(!el) {
            this.el = ui.create(
                `<svg class="entity" ${options.severity ? options.severity: '' } x="0" y="0"  text-anchor="middle">
                 <rect class="bg" width="${options.width}" height="${options.height}" x="2" y="5" rx="4"></rect>
                 <text  dominant-baseline="middle" text-anchor="middle" class="title" x="${options.width / 2}" y="${options.height / 3}">${options.title}</text>
                 <text  dominant-baseline="middle" text-anchor="middle" class="subtitle" x="${options.width / 2}" y="${(options.height / 3) * 2}">${options.subtitle}</text>
                </svg>`
             );

             if(options.badge) this.badge = options.badge;
        }

        if(options.data) {
            for(let property in options.data) {
                this.el.setAttribute('data-' + property, options.data[property]);
                if(property == "id") this.id = options.data[property];
            }    
        }

        this.el.addEventListener('click', this.onClick.bind(this));
    }
	/** 
	 * 
	*/
    set x(value) { this.el.setAttribute('x', value); }
    get x() { return parseInt(this.el.getAttribute('x')); }

	/** 
	 * 
	*/
    set y(value) { this.el.setAttribute('y', value); }
    get y() { return parseInt(this.el.getAttribute('y')); }

    /** 
	 * 
	*/
    set disabled(state) { state ? this.el.setAttribute('disabled',''): this.el.removeAttribute('disabled'); }
    get disabled() { return this.el.hasAttribute('disabled'); }

	/** 
	 * 
	*/
    get width() { return parseInt(this.el.querySelector('rect').getAttribute('width')); }
    get height() { return parseInt(this.el.querySelector('rect').getAttribute('height')); }

	/** 
	 * 
	*/
	set title(str) { this.el.querySelector('.title').innerHTML = str; }
	set subtitle(str) { this.el.querySelector('.subtitle').innerHTML = str; }

	/**
     * 
     */
    set badge(value) { 
        
        if(!this._badge && value != 0) {
            let r = 14;

            switch(this.options.badge.size) {
                case 'xxsmall': r = 8; break;
                case 'xsmall': r = 10; break;
                case 'small': r = 12; break;
                case 'large': r = 16; break;
                case 'xlarge': r = 18; break;
                case 'xxlarge': r = 20; break;
                case 'xxxlarge': r = 24; break;
            }

            this._badge = ui.create(
                `<svg class="badge" x="${this.options.width - r}" y="0" ${this.options.badge.size} ${this.options.badge.severity} text-anchor="middle">
                 <circle class="bg" cx="${r}" cy="${r}" r="${r}"></circle>
                 <text  dominant-baseline="middle" text-anchor="middle" x="${r}" y="${r}"></text>
                 </svg>`
            );
            this.el.append(this._badge)
            
        }

        this._badge.querySelector('text').textContent = value;
    }

	/** 
	 * 
	*/
    set data(value) { }

    onClick(event) {
        event.stopPropagation();
        event.preventDefault();

        this.el.dispatchEvent(new CustomEvent('selected', {detail: this}))
    }
}

/**
 * @augments EicComponent
 * @category EICUI/AdvancedComponents
 */
class NodeMapRelation extends EicComponent {

    source = null;
    target = null;

    constructor(source, target, options) {
        super(null, options);

        this.setOptions(options);

        this.source = source;
        this.target = target;

		//this.el = ui.create( `<line class="relation" x1="0" y1="0" x2="0" y2="0" data-source="${source.id}" data-target="${target.id}" />` );
		this.el = document.createElementNS('http://www.w3.org/2000/svg','path');
		this.el.setAttribute('class', 'relation');
		
		this.el.dataset.source = source.id;
		this.el.dataset.target = target.id;

        this.redraw();
    }

    redraw() {
		
		let o = {
			x: this.source.x + this.source.width / 2,
			y: this.source.y + this.source.height / 2
		}

		let d = {
			x: o.x + Math.abs((this.target.y + this.target.height / 2) - o.y),
			y: this.target.y + this.target.height / 2
		}
		
		let p1 = { x: o.x, y: d.y }

		let f = {
			x: this.target.x + this.target.width / 2,
			y: this.target.y + this.target.height / 2
		}

		let path = `M ${o.x} ${o.y} C ${p1.x} ${p1.y} ${p1.x} ${p1.y} ${d.x} ${d.y} L ${f.x} ${f.y}`

		this.el.setAttribute('d', path)
	}
}
