text: /*
   * Copyright 2015-present Open Networking Laboratory
   *
   * 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.
   *
   */
  
  /*
   ONOS GUI -- Topology Overlay Module.
  
   Provides overlay capabilities, allowing ONOS apps to provide additional
   custom data/behavior for the topology view.
  
   */
  
  (function () {
      'use strict';
  
      // constants
      var tos = 'TopoOverlayService: ';
  
      // injected refs
      var $log, $timeout, fs, gs, wss, ns, tss, tps, api;
  
      // internal state
      var overlays = {},
          current = null,
          reset = true;
  
      function error(fn, msg) {
          $log.error(tos + fn + '(): ' + msg);
      }
  
      function warn(fn, msg) {
          $log.warn(tos + fn + '(): ' + msg);
      }
  
      function mkGlyphId(oid, gid) {
          return (gid[0] === '*') ? oid + '-' + gid.slice(1) : gid;
      }
  
      function handleGlyphs(o) {
          var gdata = fs.isO(o.glyphs),
              oid = o.overlayId,
              gid = o.glyphId || 'unknown',
              data = {},
              note = [];
  
          o._glyphId = mkGlyphId(oid, gid);
  
          o.mkGid = function (g) {
              return mkGlyphId(oid, g);
          };
          o.mkId = function (s) {
              return oid + '-' + s;
          };
  
          // process glyphs if defined
          if (gdata) {
              angular.forEach(gdata, function (value, key) {
                  var fullkey = oid + '-' + key;
                  data['_' + fullkey] = value.vb;
                  data[fullkey] = value.d;
                  note.push('*' + key);
              });
              gs.registerGlyphs(data);
              $log.debug('registered overlay glyphs:', oid, note);
          }
      }
  
      function register(overlay) {
          var r = 'register',
              over = fs.isO(overlay),
              kb = over ? fs.isO(overlay.keyBindings) : null,
              id = over ? over.overlayId : '';
  
          if (!id) {
              return error(r, 'not a recognized overlay');
          }
          if (overlays[id]) {
              return warn(r, 'already registered: "' + id + '"');
          }
          overlays[id] = overlay;
          handleGlyphs(overlay);
  
          if (kb) {
              if (!fs.isA(kb._keyOrder)) {
                  warn(r, 'no _keyOrder array defined on keyBindings');
              } else {
                  kb._keyOrder.forEach(function (k) {
                      if (k !== '-' && !kb[k]) {
                          warn(r, 'no "' + k + '" property defined on keyBindings');
                      }
                  });
              }
          }
  
          $log.debug(tos + 'registered overlay: ' + id, overlay);
      }
  
      // returns the list of overlay identifiers
      function list() {
          return d3.map(overlays).keys();
      }
  
      // add a radio button for each registered overlay
      // return an overlay id to index map
      function augmentRbset(rset, switchFn) {
          var map = {},
              idx = 1;
  
          angular.forEach(overlays, function (ov) {
              rset.push({
                  gid: ov._glyphId,
                  tooltip: (ov.tooltip || '(no tooltip)'),
                  cb: function () {
                      tbSelection(ov.overlayId, switchFn);
                  }
              });
              map[ov.overlayId] = idx++;
          });
          return map;
      }
  
      // an overlay was selected via toolbar radio button press from user
      function tbSelection(id, switchFn) {
          var same = current && current.overlayId === id,
              payload = {},
              actions;
  
          function doop(op) {
              var oid = current.overlayId;
              $log.debug('Overlay:', op, oid);
              current[op]();
              payload[op] = oid;
          }
  
          if (reset || !same) {
              reset = false;
              current && doop('deactivate');
              current = overlays[id];
              current && doop('activate');
              actions = current && fs.isO(current.keyBindings);
              switchFn(id, actions);
  
              wss.sendEvent('topoSelectOverlay', payload);
  
              // Ensure summary and details panels are updated immediately..
              wss.sendEvent('requestSummary');
              tss.updateDetail();
          }
      }
  
      var coreButtons = {
          showDeviceView: {
              gid: 'switch',
              tt: 'Show Device View',
              path: 'device'
          },
          showFlowView: {
              gid: 'flowTable',
              tt: 'Show Flow View for this Device',
              path: 'flow'
          },
          showPortView: {
              gid: 'portTable',
              tt: 'Show Port View for this Device',
              path: 'port'
          },
          showGroupView: {
              gid: 'groupTable',
              tt: 'Show Group View for this Device',
              path: 'group'
          },
          showMeterView: {
              gid: 'meterTable',
              tt: 'Show Meter View for this Device',
              path: 'meter'
          }
      };
  
      // retrieves a button definition from the current overlay and generates
      //  a button descriptor to be added to the panel, with the data baked in
      function _getButtonDef(id, data) {
          var btns = current && current.buttons,
              b = btns && btns[id],
              cb = fs.isF(b.cb),
              f = cb ? function () { cb(data); } : function () {};
  
          return b ? {
              id: current.mkId(id),
              gid: current.mkGid(b.gid),
              tt: b.tt,
              cb: f
          } : null;
      }
  
      // install core buttons, and include any additional from the current overlay
      function installButtons(buttons, data, devId) {
          buttons.forEach(function (id) {
              var btn = coreButtons[id],
                  gid = btn && btn.gid,
                  tt = btn && btn.tt,
                  path = btn && btn.path;
  
              if (btn) {
                  tps.addAction({
                      id: 'core-' + id,
                      gid: gid,
                      tt: tt,
                      cb: function () { ns.navTo(path, {devId: devId }); }
                  });
              } else if (btn = _getButtonDef(id, data)) {
                  tps.addAction(btn);
              }
          });
      }
  
      function addDetailButton(id) {
          var b = _getButtonDef(id);
          if (b) {
              tps.addAction({
                  id: current.mkId(id),
                  gid: current.mkGid(b.gid),
                  cb: b.cb,
                  tt: b.tt
              });
          }
      }
  
  
      // === -----------------------------------------------------
      //  Hooks for overlays
  
      function _hook(x) {
          var h = current && current.hooks;
          return h && fs.isF(h[x]);
      }
  
      function escapeHook() {
          var eh = _hook('escape');
          return eh ? eh() : false;
      }
  
      function emptySelectHook() {
          var cb = _hook('empty');
          cb && cb();
      }
  
      function singleSelectHook(data) {
          var cb = _hook('single');
          cb && cb(data);
      }
  
      function multiSelectHook(selectOrder) {
          var cb = _hook('multi');
          cb && cb(selectOrder);
      }
  
      function mouseOverHook(what) {
          var cb = _hook('mouseover');
          cb && cb(what);
      }
  
      function mouseOutHook() {
          var cb = _hook('mouseout');
          cb && cb();
      }
  
      // Temporary function to allow overlays to modify link detail data
      // in the client. (In the near future, this will be done on the server).
      function modifyLinkDataHook(data, extra) {
          var cb = _hook('modifylinkdata');
          return cb && extra ? cb(data, extra) : data;
      }
  
      // Request from Intent View to visualize an intent on the topo view
      function showIntentHook(intentData) {
          $log.debug('^^ topoOverlay.showIntentHook(...) ^^');
          var cb = _hook('showintent');
          return cb && cb(intentData);
      }
  
      // === -----------------------------------------------------
      //  Event (from server) Handlers
  
      function setApi(_api_, _tss_) {
          api = _api_;
          tss = _tss_;
      }
  
      //process highlight event with optional delay
      function showHighlights(data) {
          function doHighlight() {
              _showHighlights(data);
          }
  
          if (data.delay) {
              $timeout(doHighlight, data.delay);
          } else {
              doHighlight();
          }
      }
  
      function _showHighlights(data) {
          var less;
  
          /*
             API to topoForce
               clearLinkTrafficStyle()
               removeLinkLabels()
               findLinkById( id )
               findNodeById( id )
               updateLinks()
               updateNodes()
               supLayers( bool, [less] )
               unsupNode( id, [less] )
               unsupLink( key, [less] )
           */
  
          api.clearNodeDeco();
          api.removeNodeBadges();
          api.clearLinkTrafficStyle();
          api.removeLinkLabels();
  
          // handle element suppression
          if (data.subdue) {
              less = data.subdue === 'min';
              api.supLayers(true, less);
  
          } else {
              api.supLayers(false);
              api.supLayers(false, true);
          }
  
          data.hosts.forEach(function (host) {
              var hdata = api.findNodeById(host.id),
                  badgeData = host.badge || null;
  
              if (hdata && hdata.el && !hdata.el.empty()) {
                  hdata.badge = badgeData;
                  if (!host.subdue) {
                      api.unsupNode(hdata.id, less);
                  }
                  // TODO: further highlighting?
              } else {
                  $log.warn('HILITE: no host element:', host.id);
              }
          });
  
          data.devices.forEach(function (device) {
              var ddata = api.findNodeById(device.id),
                  badgeData = device.badge || null;
  
              if (ddata && ddata.el && !ddata.el.empty()) {
                  ddata.badge = badgeData;
                  if (!device.subdue) {
                      api.unsupNode(ddata.id, less);
                  }
                  // TODO: further highlighting?
              } else {
                  $log.warn('HILITE: no device element:', device.id);
              }
          });
  
          data.links.forEach(function (link) {
              var ldata = api.findLinkById(link.id),
                  lab = link.label,
                  units, portcls, magnitude;
  
              if (ldata && ldata.el && !ldata.el.empty()) {
                  if (!link.subdue) {
                      api.unsupLink(ldata.key, less);
                  }
                  ldata.el.classed(link.css, true);
                  ldata.label = lab;
  
                  // TODO: this needs to be pulled out into traffic overlay
                  // inject additional styling for port-based traffic
                  if (fs.endsWith(lab, 'bps')) {
                      units = lab.substring(lab.length-4);
                      portcls = 'port-traffic-' + units;
  
                      // for GBps
                      if (units.substring(0,1) === 'G') {
                          magnitude = fs.parseBitRate(lab);
                          if (magnitude >= 9) {
                              portcls += '-choked'
                          }
                      }
                      ldata.el.classed(portcls, true);
                  }
              } else {
                  $log.warn('HILITE: no link element:', link.id);
              }
          });
  
          api.updateNodes();
          api.updateLinks();
      }
  
      // ========================================================================
  
      angular.module('ovTopo')
      .factory('TopoOverlayService',
          ['$log', '$timeout', 'FnService', 'GlyphService', 'WebSocketService',
              'NavService', 'TopoPanelService',
  
          function (_$log_, _$timeout_, _fs_, _gs_, _wss_, _ns_, _tps_) {
              $log = _$log_;
              $timeout = _$timeout_;
              fs = _fs_;
              gs = _gs_;
              wss = _wss_;
              ns = _ns_;
              tps = _tps_;
  
              return {
                  register: register,
                  setApi: setApi,
                  list: list,
                  augmentRbset: augmentRbset,
                  mkGlyphId: mkGlyphId,
                  tbSelection: tbSelection,
                  installButtons: installButtons,
                  addDetailButton: addDetailButton,
                  resetOnToolbarDestroy: function () { reset = true; },
                  hooks: {
                      escape: escapeHook,
                      emptySelect: emptySelectHook,
                      singleSelect: singleSelectHook,
                      multiSelect: multiSelectHook,
                      mouseOver: mouseOverHook,
                      mouseOut: mouseOutHook,
                      modifyLinkData: modifyLinkDataHook,
                      showIntent: showIntentHook
                  },
  
                  showHighlights: showHighlights
              }
          }]);
  
  }());