
/*
 * Copyright 2017-present Open Networking Foundation

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

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

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


(function () {
  'use strict';

  angular.module('xos.mcord-slicing')
  .directive('slicingTopo', function(){
    return {
      restrict: 'E',
      scope: {},
      bindToController: true,
      controllerAs: 'vm',
      templateUrl: 'templates/slicing-topo.tpl.html',
      controller: function($element, SliceGraph, McordSlicingTopo, _, NodePositioner, FormHandler, mCordSlicingIcons){

        let svg;
        let nodes, links;
        let nodeGroup, linkGroup, formGroup;
        let dragLine, dragStartNode, dragEndNode, selectedLink;
        let selectedNode, nodeSiblings;

        var t = d3.transition()
          .duration(500);

        this.activeSlices = [];

        const resetDragInfo = () => {
          // reset drag nodes
          dragStartNode = null;
          dragEndNode = null;

          // hide dragLine
          dragLine
            .classed('hidden', true);
        };

        McordSlicingTopo.query().$promise
        .then((topology) => {
          NodePositioner.storeEl($element[0]);
          handleSvg($element[0]);
          SliceGraph.buildGraph(topology);
          _nodes = SliceGraph.positionGraph($element[0]);
          _links = SliceGraph.getGraphLinks(_nodes);
          drawGraph();
        })
        .catch((e) => {
          throw new Error(e);
        });

        const handleSvg = (el) => {
          this.el = el;
          d3.select(el).select('svg').remove();

          svg = d3.select(el)
          .append('svg')
          .style('width', `${el.clientWidth}px`)
          .style('height', `${el.clientHeight}px`);

          linkGroup = svg.append('g')
            .attr({
              class: 'link-group'
            });

          nodeGroup = svg.append('g')
            .attr({
              class: 'node-group'
            });

          formGroup = d3.select(el)
            .append('div')
            .attr({
              class: 'form-container'
            });

          // line displayed when dragging nodes
          dragLine = svg.append('svg:path')
            .attr('class', 'dragline hidden')
            .attr('d', 'M0,0L0,0');
        };

        const tick = () => {

          // svg.selectAll('.node')
          // .attr({
          //   y: (n) => {
          //     console.log(n.y);
          //     return n.y;
          //   }
          // });

          svg.selectAll('.link')
            .attr('x1', d => d.source.x)
            .attr('y1', d => d.source.y)
            .attr('x2', d => d.target.x)
            .attr('y2', d => d.target.y);
        };

        // prepare the data to show all slices
        let _nodes = [];
        let _links = [];

        // attach slice details
        const attachSliceDetails = n => {
          let [newNodes, newLinks] = SliceGraph.getSliceDetail(n);
          _nodes = _nodes.concat(newNodes);
          _links = _links.concat(newLinks);
          drawGraph();
        };

        // remove slice detail
        const removeSliceDetails = sliceId => {

          SliceGraph.removeActiveSlice(sliceId);

          // if the selected node is part of the slice I'm closing
          // deselect the node
          if(selectedNode && selectedNode.sliceId === sliceId){
            selectedNode = null;
            nodeSiblings = null;
          }

          // remove control plane nodes related to this slice
          _nodes = _.filter(_nodes, n => {
            if(n.sliceId === sliceId && (n.plane === 'control' || n.type === 'button')){
              // if I remove the node check that there is no form attached
              FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
              return false;
            }
            return true;
          });

          // remove sliceId from data plane element
          _nodes = _.map(_nodes, n => {
            if(n.sliceId === sliceId){
              delete n.sliceId;
            }
            return n;
          });

          // remove control plane links related to this slice
          _links = _.filter(_links, l => {
            if(_.findIndex(_nodes, {id: l.data.source}) === -1){
              return false;
            }
            if(_.findIndex(_nodes, {id: l.data.target}) === -1){
              return false;
            }
            return true;
          });
          drawGraph();
        };

        const deleteLink = linkId => {
          // TODO
          // [ ] delete from graphlib
          // [ ] delete from backend
          console.log(_links);
          _.remove(_links, (l) => {
            return l.data.id === linkId;
          });
          console.log(_links);
          drawGraph();
        };

        const expandNode = (n) => {
          console.log('exp', n);
          resetDragInfo();
          const sliceComponents = ['ran-ru', 'ran-cu', 'pgw', 'sgw'];
          if(sliceComponents.indexOf(n.type) > -1 && n.plane === 'data' && !n.sliceId){
            attachSliceDetails(n);
          }
          else if (n.type === 'button'){
            removeSliceDetails(n.sliceId);
          }
          else if (!n.formAttached && n.model){
            n.formAttached = true;
            FormHandler.drawForm(n, linkGroup, formGroup);
          }
          else if (n.formAttached){
            n.formAttached = false;
            FormHandler.removeFormByParentNode(n, linkGroup, formGroup);
          }
        };

        const selectNextNode = () => {
          if(!selectedNode){
            selectedNode = _nodes[0];
            selectedNode.selected = true;
          }
          else {
            // TODO if no sliceId check only data plane successors
            nodeSiblings = SliceGraph.getNodeSuccessors(selectedNode);

            if(nodeSiblings.length === 0){
              return;
            };
            // reset current selected node
            selectedNode.selected = false;
            // find next node
            let nextNodeId = _.findIndex(_nodes, {id: nodeSiblings[0].id});
            selectedNode = _nodes[nextNodeId];
            selectedNode.selected = true;

            // NOTE I need to update sibling with successor of the parent
            // to enable vertical navigation
            let parents = SliceGraph.getNodeSuccessors(selectedNode);
            if(parents.lenght > 0){
              nodeSiblings = SliceGraph.getNodePredecessors(parents[0]);
            }
            else {
              nodeSiblings = null;
            }
          }
          drawGraph();
        };

        const selectPrevNode = () => {
          if(!selectedNode){
            selectedNode = _nodes[0];
            selectedNode.selected = true;
          }
          else {
            nodeSiblings = SliceGraph.getNodePredecessors(selectedNode);

            if(nodeSiblings.length === 0){
              return;
            };
            // reset current selected node
            selectedNode.selected = false;
            // find next node
            let prev = _.findIndex(_nodes, {id: nodeSiblings[0].id});

            if(prev < 0){
              prev = _nodes.length - 1;
            }
            selectedNode = _nodes[prev];
            selectedNode.selected = true;
          }
          drawGraph();
        };

        const sortByY = (a, b) => {
          if (a.y < b.y)
            return 1;
          if (a.y > b.y)
            return -1;
          return 0;
        };

        const getSameTypeNodes = (selectedNode) => {
          return _.filter(_nodes, n => {
            if(selectedNode.type === 'pgw' && n.type === 'button'){
              return true;
            }
            if(selectedNode.type === 'button' && n.type === 'pgw'){
              return true;
            }
            if (selectedNode.type === 'sgw' && n.type === 'mme'){
              return true;
            }
            if (selectedNode.type === 'mme' && n.type === 'sgw'){
              return true;
            }
            return n.type === selectedNode.type;
          }).sort(sortByY);
        };

        const selectNextSibling = () => {
          if(!selectedNode){
            selectedNode = _nodes[0];
            selectedNode.selected = true;
          }
          else {
            // reset current selected node
            selectedNode.selected = false;

            // find next node
            let sameTypeNodes = getSameTypeNodes(selectedNode);

            let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) + 1;
            if(nextSiblingId === sameTypeNodes.length){
              nextSiblingId = 0;
            }
            let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
            selectedNode = _nodes[nextNodeId];
            selectedNode.selected = true;
          }
          drawGraph();
        };

        const selectPrevSibling = () => {
          if(!selectedNode){
            selectedNode = _nodes[0];
            selectedNode.selected = true;
          }
          else {
            // reset current selected node
            selectedNode.selected = false;

            // find next node
            let sameTypeNodes = getSameTypeNodes(selectedNode);

            let nextSiblingId = _.findIndex(sameTypeNodes, {id: selectedNode.id}) - 1;
            if(nextSiblingId < 0){
              nextSiblingId = sameTypeNodes.length - 1;
            }
            let nextNodeId = _.findIndex(_nodes, {id: sameTypeNodes[nextSiblingId].id});
            selectedNode = _nodes[nextNodeId];
            selectedNode.selected = true;
          }
          drawGraph();
        };

        const drawGraph = () => {

          // svg.selectAll('.node-container').remove();
          // svg.selectAll('.link-container').remove();

          var force = d3.layout.force()
            .nodes(_nodes)
            .links(_links)
            .charge(-1060)
            .gravity(0.1)
            .linkDistance(200)
            .size([this.el.clientWidth, this.el.clientHeight])
            .on('tick', tick)
            .start();

          links = linkGroup.selectAll('.link-container')
            .data(_links, d => d.data.id)
            .enter()
            .insert('g')
            .attr({
              class: 'link-container',
              opacity: 0
            });

          links
            .transition(t)
            .attr({
              opacity: 1
            });

          links.insert('line')
            .attr('class', d => `link ${d.data.plane}`)
            .on('click', function(link ){
              selectedLink = link;

              // deselect all other links
              d3.selectAll('.link').classed('selected', false);

              d3.select(this).classed('selected', true);
            });

          nodes = nodeGroup.selectAll('.node')
            .data(_nodes, d => d.id)
            .attr({
              class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
            });

          nodes
            .enter()
            .append('g')
            .attr({
              class: 'node-container',
              transform: d => d.transform,
              opacity: 0
            });

          nodes.transition(t)
            .attr({
              opacity: 1
            });

          nodes.append('rect')
            .attr({
              class: d => `node ${d.plane} ${d.type} ${d.selected ? 'selected': ''}`,
              width: 100,
              height: 50,
              x: -50,
              y: -25
            });

          nodes.append('text')
            .attr({
              'text-anchor': 'left',
              'alignment-baseline': 'middle',
              x: -20
            })
            .text(d => `${d.name}`);

          nodes.on('click', (n) => {
            expandNode(n);
          });

          // draw icons
          const ues = nodes.filter(n => n.type === 'ue');
          ues.append('path')
          .attr({
            d: mCordSlicingIcons.mobile,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          const profiles = nodes.filter(n => n.type === 'profile');
          profiles.append('path')
          .attr({
            d: mCordSlicingIcons.profile,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          const rru = nodes.filter(n => n.type === 'ran-ru');
          rru.append('path')
          .attr({
            d: mCordSlicingIcons.rru,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          const rcu = nodes.filter(n => n.type === 'ran-cu');
          rcu.append('path')
          .attr({
            d: mCordSlicingIcons.rcu,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          const sgw = nodes.filter(n => n.type === 'sgw');
          sgw.append('path')
          .attr({
            d: mCordSlicingIcons.sgw,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          const pgw = nodes.filter(n => n.type === 'pgw');
          pgw.append('path')
          .attr({
            d: mCordSlicingIcons.pgw,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          const mme = nodes.filter(n => n.type === 'mme');
          mme.append('path')
          .attr({
            d: mCordSlicingIcons.mme,
            class: 'icon',
            transform: `translate(-40, -12.5), scale(0.5)`
          });

          nodes
            .on('mousedown', (n) => {
              // save a reference to dragStart
              dragStartNode = n;

              dragLine
                .classed('hidden', false)
                .attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + dragStartNode.x + ',' + dragStartNode.y);
            })
            .on('mouseover', (n) => {
              if(dragStartNode){
                dragEndNode = n;
              }
            });

          svg
            .on('mousemove', function(){
              if(!dragStartNode){
                return;
              }
              dragLine.attr('d', 'M' + dragStartNode.x + ',' + dragStartNode.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]);
            })
            .on('mouseup', () => {
              if(!dragStartNode || !dragEndNode){
                resetDragInfo();
                return;
              }

              // TODO
              // [X] check that I can connect the two nodes
              // [X] check link direction
              // [ ] save the new link in the BE

              // check that I can connect the 2 nodes
              const successorType = SliceGraph.getNodeDataPlaneSuccessors(dragStartNode)[0].type;
              if(dragEndNode.type !== successorType){
                resetDragInfo();
                return;
              }

              // create the link
              _links.push({
                source: dragStartNode,
                target: dragEndNode,
                data: {
                  id: `${dragStartNode.id}.${dragEndNode.id}`,
                  source: dragStartNode.id,
                  target: dragEndNode.id
                }
              });

              // update the graph
              // TODO recalculate graph positions
              drawGraph();

              resetDragInfo();
            });

          // remove exiting nodes
          svg.selectAll('.node-container')
            .data(_nodes, d => d.id)
            .exit()
            .transition(t)
            .attr({
              opacity: 0
            })
            .remove();

          // remove exiting links
          svg.selectAll('.link-container')
            .data(_links, d => d.data.id)
            .exit()
            .transition(t)
            .attr({
              opacity: 0
            })
            .remove();
        };

        d3.select('body')
          .on('keydown', function(){
            // console.log(d3.event.code);
            if(d3.event.code === 'Backspace' && selectedLink){
              // delete link
              deleteLink(selectedLink.data.id);
            }
            if(d3.event.code === 'Enter' && selectedNode){
              d3.event.preventDefault();
              expandNode(selectedNode);
            }
            if(d3.event.code === 'Escape' && selectedNode){
              selectedNode.selected = false;
              selectedNode = null;
              nodeSiblings = null;
              drawGraph();
            }
            if(d3.event.code === 'ArrowRight'){
              d3.event.preventDefault();
              selectNextNode();
            }
            if(d3.event.code === 'ArrowLeft'){
              d3.event.preventDefault();
              selectPrevNode();
            }
            if(d3.event.code === 'ArrowUp'){
              d3.event.preventDefault();
              selectNextSibling();
            }
            if(d3.event.code === 'ArrowDown'){
              d3.event.preventDefault();
              selectPrevSibling();
            }

          });
      }
    }
  });
})();