
HTMLWidgets.widget({

  name : 'linevis',

  type : 'output',

  factory : function(el, width, height) {

    var elementId = el.id;
    var container = document.getElementById(elementId);
    var graph2d = new vis.Graph2d(container, [], {});
    var initialized = false;
    var ctSel = null;
    var ctFil = null;
    var allItems;

    return {

      renderValue: function(opts) {
        // alias this
        var that = this;

        if (!initialized) {
          initialized = true;

          // attach the widget to the DOM
          container.widget = that;

          // Set up the zoom button click listeners
          //var zoomMenu = container.getElementsByClassName("zoom-menu")[0];
          //zoomMenu.getElementsByClassName("zoom-in")[0]
          //  .onclick = function(ev) { that.zoomInLinevis(opts.zoomFactor); };
          //zoomMenu.getElementsByClassName("zoom-out")[0]
          //  .onclick = function(ev) { that.zoomOutLinevis(opts.zoomFactor); };

          // set listeners to events and pass data back to Shiny
          if (HTMLWidgets.shinyMode) {

            // The range of the window has changes (by dragging or zooming)
            graph2d.on('rangechanged', function (properties) {
              Shiny.onInputChange(
                elementId + "_window",
                [graph2d.getWindow().start, graph2d.getWindow().end]
              );
            });
            Shiny.onInputChange(
              elementId + "_window",
              [graph2d.getWindow().start, graph2d.getWindow().end]
            );

            // The data in the graph2d has changed
            graph2d.itemsData.on('*', function (event, properties, senderId) {
              Shiny.onInputChange(
                elementId + "_data" + ":linevisDF",
                graph2d.itemsData.get()
              );
            });
            Shiny.onInputChange(
              elementId + "_data" + ":linevisDF",
              graph2d.itemsData.get()
            );

            // An item was added or removed, send back the list of IDs
            graph2d.itemsData.on('add', function (event, properties, senderId) {
              Shiny.onInputChange(
                elementId + "_ids",
                graph2d.itemsData.getIds()
              );
            });
            graph2d.itemsData.on('remove', function (event, properties, senderId) {
              Shiny.onInputChange(
                elementId + "_ids",
                graph2d.itemsData.getIds()
              );
            });
            Shiny.onInputChange(
              elementId + "_ids",
              graph2d.itemsData.getIds()
            );

            // Visible items have changed
            var sendShinyVisible = function() {
              Shiny.onInputChange(
                elementId + "_visible",
                graph2d.getVisibleItems()
              );
            };
            graph2d.on('rangechanged', sendShinyVisible);
            graph2d.itemsData.on('add', sendShinyVisible);
            graph2d.itemsData.on('remove', sendShinyVisible);
            setTimeout(sendShinyVisible, 0);
          }

          // if a crosstalk dataframe is used, initialize crosstalk
          if (typeof(crosstalk) !== "undefined" && opts.crosstalk) {
            ctSel = new crosstalk.SelectionHandle(opts.crosstalk.group);
            ctSel.on("change", function(e) {
              if (e.sender !== ctSel) {
                that.setSelection({ itemId : e.value });
              }
            });
            graph2d.on('select', function (properties) {
              ctSel.set(properties.items);
            });

            ctFil = new crosstalk.FilterHandle(opts.crosstalk.group);
            ctFil.on("change", function(e) {
              if (e.value === null) {
                that.setItems({ data : allItems });
              } else {
                let keys = e.value;
                keys = keys.map(String); // workaround for https://github.com/rstudio/crosstalk/issues/140
                that.setItems({ data : allItems.filter(function(item) { return keys.includes(item.id); } ) });
              }
              // after doing a filter, a new set of items is used so the selection needs to be re-done
              if (ctSel !== null) {
                that.setSelection({ itemId : ctSel.value });
              }
            });
          }
        }

        // set the custom configuration options
        if (Array === opts.options.constructor) {
          opts['options'] = {};
        }
        if (opts['height'] !== null &&
            typeof opts['options']['height'] === "undefined") {
          opts['options']['height'] = opts['height'];
        }
        if (opts['timezone'] !== null) {
          opts['options']['moment'] = function(date) {
            return vis.moment(date).utcOffset(opts['timezone']);
          };
        }

        // log scale
        if (opts['options']['log_scale']) {
          opts['options']['dataAxis'] = {};
          opts['options']['dataAxis']['left'] = {};
          opts['options']['dataAxis']['left']['format'] = function(value) {
            return ''+Math.round(Math.exp(value) - 1);
          };
        }

        // interpolation
        //opts['options']['interpolation'] = {};
        //opts['options']['interpolation']['enabled'] = false;

        // legend
        //opts['options']['legend'] = {};
        //opts['options']['legend']['left'] = {};
        //opts['options']['legend']['left']['position'] = 'bottom-left';

        graph2d.setOptions(opts.options);

        // set the data items and groups
        graph2d.itemsData.clear();
        graph2d.itemsData.add(opts.items);
        graph2d.setGroups(opts.groups);


        // fit the items on the graph2d
        if (opts.fit) {
          graph2d.fit({ animation : false });
        }

        // Show or hide the zoom button
        //var zoomMenu = container.getElementsByClassName("zoom-menu")[0];
        //if (opts.showZoom) {
        //  zoomMenu.setAttribute("data-show-zoom", true);
        //} else {
        //  zoomMenu.removeAttribute("data-show-zoom");
        //}

        // Now that the graph2d is initialized, call any outstanding API
        // functions that the user wantd to run on the graph2d before it was
        // ready
        var numApiCalls = opts['api'].length;
        for (var i = 0; i < numApiCalls; i++) {
          var call = opts['api'][i];
          var method = call.method;
          delete call['method'];
          try {
            that[method](call);
          } catch(err) {}
        }

        // If crosstalk is enabled, respect its selection
        allItems = opts.items;
        if (ctFil !== null && ctFil.filteredKeys !== null) {
          let keys = ctFil.filteredKeys;
          keys = keys.map(String);
          that.setItems({ data : allItems.filter(function(item) { return keys.includes(item.id); } ) });
        }
        if (ctSel !== null) {
          that.setSelection({ itemId : ctSel.value });
        }
      },

      resize : function(width, height) {
        // the graph2d widget knows how to resize itself automatically
      },

      // zoom the graph2d in/out
      // I had to work out the math on paper so that zooming in and then out
      // will exactly negate each other
      zoomInLinevis : function(percentage, animation) {
        if (typeof animation === "undefined") {
          animation = true;
        }
        var range = graph2d.getWindow();
        var start = range.start.valueOf();
        var end = range.end.valueOf();
        var interval = end - start;
        var newInterval = interval / (1 + percentage);
        var distance = (interval - newInterval) / 2;
        var newStart = start + distance;
        var newEnd = end - distance;

        graph2d.setWindow({
          start   : newStart,
          end     : newEnd,
          animation : animation
        });
      },
      zoomOutLinevis : function(percentage, animation) {
        if (typeof animation === "undefined") {
          animation = true;
        }
        var range = graph2d.getWindow();
        var start = range.start.valueOf();
        var end = range.end.valueOf();
        var interval = end - start;
        var newStart = start - interval * percentage / 2;
        var newEnd = end + interval * percentage / 2;

        graph2d.setWindow({
          start   : newStart,
          end     : newEnd,
          animation : animation
        });
      },

      // export the graph2d object for others to use if they want to
      graph2d : graph2d,

      /* API functions that manipulate a graph2d's data */
      addItem : function(params) {
        graph2d.itemsData.add(params.data);
      },
      addItems : function(params) {
        graph2d.itemsData.add(params.data);
      },
      removeItem : function(params) {
        graph2d.itemsData.remove(params.itemId);
      },
      addCustomTime : function(params) {
        graph2d.addCustomTime(params.time, params.itemId);
      },
      removeCustomTime : function(params) {
        graph2d.removeCustomTime(params.itemId);
      },
      setCustomTime : function(params) {
        graph2d.setCustomTime(params.time, params.itemId);
      },
      setCurrentTime : function(params) {
        graph2d.setCurrentTime(params.time);
      },
      fitWindow : function(params) {
        graph2d.fit(params.options);
      },
      centerTime : function(params) {
        graph2d.moveTo(params.time, params.options);
      },
      centerItem : function(params) {
         if (typeof params.options === 'undefined') {
          params.options = { 'zoom' : false };
        } else if (typeof params.options.zoom === 'undefined') {
          params.options.zoom = false;
        }
        graph2d.focus(params.itemId, params.options);
      },
      setItems : function(params) {
        graph2d.itemsData.clear();
        graph2d.itemsData.add(params.data);
      },
      setGroups : function(params) {
        graph2d.setGroups(params.data);
      },
      setOptions : function(params) {
        graph2d.setOptions(params.options);
      },
      setWindow : function(params) {
        graph2d.setWindow(params.start, params.end, params.options);
      },
      zoomIn : function(params) {
        graph2d.zoomIn(params.percent, { animation : params.animation });
      },
      zoomOut : function(params) {
        graph2d.zoomOut(params.percent, { animation : params.animation });
      },
    };
  }
});

// Attach message handlers if in shiny mode (these correspond to API)
if (HTMLWidgets.shinyMode) {
  var fxns =
    ['addItem', 'addItems', 'removeItem', 'addCustomTime', 'removeCustomTime',
     'fitWindow', 'centerTime', 'centerItem', 'setItems', 'setGroups',
     'setOptions', 'setSelection', 'setWindow', 'setCustomTime', 'setCurrentTime',
     'zoomIn', 'zoomOut'];

  var addShinyHandler = function(fxn) {
    return function() {
      Shiny.addCustomMessageHandler(
        "linevis:" + fxn, function(message) {
          var el = document.getElementById(message.id);
          if (el) {
            delete message['id'];
            el.widget[fxn](message);
          }
        }
      );
    }
  };

  for (var i = 0; i < fxns.length; i++) {
    addShinyHandler(fxns[i])();
  }
}
