(function ($) {
  // plugin options, default values
  var defaultOptions = {
    tooltip: {
      show: false,
      cssClass: "flotTip",
      content: "%s | X: %x | Y: %y",
      // allowed templates are:
      // %s -> series label,
      // %c -> series color,
      // %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels),
      // %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels),
      // %x -> X value,
      // %y -> Y value,
      // %x.2 -> precision of X value,
      // %p -> percent
      // %n -> value (not percent) of pie chart
      xDateFormat: null,
      yDateFormat: null,
      monthNames: null,
      dayNames: null,
      shifts: {
        x: 10,
        y: 20,
      },
      defaultTheme: true,
      snap: true,
      lines: false,
      clickTips: false,

      // callbacks
      onHover: function (flotItem, $tooltipEl) {},

      $compat: false,
    },
  };

  // dummy default options object for legacy code (<0.8.5) - is deleted later
  defaultOptions.tooltipOpts = defaultOptions.tooltip;

  // object
  var FlotTooltip = function (plot) {
    // variables
    this.tipPosition = { x: 0, y: 0 };

    this.init(plot);
  };

  // main plugin function
  FlotTooltip.prototype.init = function (plot) {
    var that = this;

    // detect other flot plugins
    var plotPluginsLength = $.plot.plugins.length;
    this.plotPlugins = [];

    if (plotPluginsLength) {
      for (var p = 0; p < plotPluginsLength; p++) {
        this.plotPlugins.push($.plot.plugins[p].name);
      }
    }

    plot.hooks.bindEvents.push(function (plot, eventHolder) {
      // get plot options
      that.plotOptions = plot.getOptions();

      // for legacy (<0.8.5) implementations
      if (typeof that.plotOptions.tooltip === "boolean") {
        that.plotOptions.tooltipOpts.show = that.plotOptions.tooltip;
        that.plotOptions.tooltip = that.plotOptions.tooltipOpts;
        delete that.plotOptions.tooltipOpts;
      }

      // if not enabled return
      if (
        that.plotOptions.tooltip.show === false ||
        typeof that.plotOptions.tooltip.show === "undefined"
      )
        return;

      // shortcut to access tooltip options
      that.tooltipOptions = that.plotOptions.tooltip;

      if (that.tooltipOptions.$compat) {
        that.wfunc = "width";
        that.hfunc = "height";
      } else {
        that.wfunc = "innerWidth";
        that.hfunc = "innerHeight";
      }

      // create tooltip DOM element
      var $tip = that.getDomElement();

      // bind event
      $(plot.getPlaceholder()).bind("plothover", plothover);
      if (that.tooltipOptions.clickTips) {
        $(plot.getPlaceholder()).bind("plotclick", plotclick);
      }
      that.clickmode = false;

      $(eventHolder).bind("mousemove", mouseMove);
    });

    plot.hooks.shutdown.push(function (plot, eventHolder) {
      $(plot.getPlaceholder()).unbind("plothover", plothover);
      $(plot.getPlaceholder()).unbind("plotclick", plotclick);
      plot.removeTooltip();
      $(eventHolder).unbind("mousemove", mouseMove);
    });

    function mouseMove(e) {
      var pos = {};
      pos.x = e.pageX;
      pos.y = e.pageY;
      plot.setTooltipPosition(pos);
    }

    /**
     *  open the tooltip (if not already open) and freeze it on the current position till the next click
     */
    function plotclick(event, pos, item) {
      if (!that.clickmode) {
        // it is the click activating the clicktip
        plothover(event, pos, item);
        if (that.getDomElement().is(":visible")) {
          $(plot.getPlaceholder()).unbind("plothover", plothover);
          that.clickmode = true;
        }
      } else {
        // it is the click deactivating the clicktip
        $(plot.getPlaceholder()).bind("plothover", plothover);
        plot.hideTooltip();
        that.clickmode = false;
      }
    }

    function plothover(event, pos, item) {
      // Simple distance formula.
      var lineDistance = function (p1x, p1y, p2x, p2y) {
        return Math.sqrt((p2x - p1x) * (p2x - p1x) + (p2y - p1y) * (p2y - p1y));
      };

      // Here is some voodoo magic for determining the distance to a line form a given point {x, y}.
      var dotLineLength = function (x, y, x0, y0, x1, y1, o) {
        if (
          o &&
          !((o = (function (x, y, x0, y0, x1, y1) {
            if (typeof x0 !== "undefined") return { x: x0, y: y };
            else if (typeof y0 !== "undefined") return { x: x, y: y0 };

            var left,
              tg = -1 / ((y1 - y0) / (x1 - x0));

            return {
              x: (left =
                (x1 * (x * tg - y + y0) + x0 * (x * -tg + y - y1)) /
                (tg * (x1 - x0) + y0 - y1)),
              y: tg * left - tg * x + y,
            };
          })(x, y, x0, y0, x1, y1)),
          o.x >= Math.min(x0, x1) &&
            o.x <= Math.max(x0, x1) &&
            o.y >= Math.min(y0, y1) &&
            o.y <= Math.max(y0, y1))
        ) {
          var l1 = lineDistance(x, y, x0, y0),
            l2 = lineDistance(x, y, x1, y1);
          return l1 > l2 ? l2 : l1;
        } else {
          var a = y0 - y1,
            b = x1 - x0,
            c = x0 * y1 - y0 * x1;
          return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b);
        }
      };

      if (item) {
        plot.showTooltip(item, that.tooltipOptions.snap ? item : pos);
      } else if (
        that.plotOptions.series.lines.show &&
        that.tooltipOptions.lines === true
      ) {
        var maxDistance = that.plotOptions.grid.mouseActiveRadius;

        var closestTrace = {
          distance: maxDistance + 1,
        };

        var ttPos = pos;

        $.each(plot.getData(), function (i, series) {
          var xBeforeIndex = 0,
            xAfterIndex = -1;

          // Our search here assumes our data is sorted via the x-axis.
          // TODO: Improve efficiency somehow - search smaller sets of data.
          for (var j = 1; j < series.data.length; j++) {
            if (series.data[j - 1][0] <= pos.x && series.data[j][0] >= pos.x) {
              xBeforeIndex = j - 1;
              xAfterIndex = j;
            }
          }

          if (xAfterIndex === -1) {
            plot.hideTooltip();
            return;
          }

          var pointPrev = {
              x: series.data[xBeforeIndex][0],
              y: series.data[xBeforeIndex][1],
            },
            pointNext = {
              x: series.data[xAfterIndex][0],
              y: series.data[xAfterIndex][1],
            };

          var distToLine = dotLineLength(
            series.xaxis.p2c(pos.x),
            series.yaxis.p2c(pos.y),
            series.xaxis.p2c(pointPrev.x),
            series.yaxis.p2c(pointPrev.y),
            series.xaxis.p2c(pointNext.x),
            series.yaxis.p2c(pointNext.y),
            false
          );

          if (distToLine < closestTrace.distance) {
            var closestIndex =
              lineDistance(pointPrev.x, pointPrev.y, pos.x, pos.y) <
              lineDistance(pos.x, pos.y, pointNext.x, pointNext.y)
                ? xBeforeIndex
                : xAfterIndex;

            var pointSize = series.datapoints.pointsize;

            // Calculate the point on the line vertically closest to our cursor.
            var pointOnLine = [
              pos.x,
              pointPrev.y +
                (pointNext.y - pointPrev.y) *
                  ((pos.x - pointPrev.x) / (pointNext.x - pointPrev.x)),
            ];

            var item = {
              datapoint: pointOnLine,
              dataIndex: closestIndex,
              series: series,
              seriesIndex: i,
            };

            closestTrace = {
              distance: distToLine,
              item: item,
            };

            if (that.tooltipOptions.snap) {
              ttPos = {
                pageX: series.xaxis.p2c(pointOnLine[0]),
                pageY: series.yaxis.p2c(pointOnLine[1]),
              };
            }
          }
        });

        if (closestTrace.distance < maxDistance + 1)
          plot.showTooltip(closestTrace.item, ttPos);
        else plot.hideTooltip();
      } else {
        plot.hideTooltip();
      }
    }

    // Quick little function for setting the tooltip position.
    plot.setTooltipPosition = function (pos) {
      var $tip = that.getDomElement();

      var totalTipWidth = $tip.outerWidth() + that.tooltipOptions.shifts.x;
      var totalTipHeight = $tip.outerHeight() + that.tooltipOptions.shifts.y;
      if (
        pos.x - $(window).scrollLeft() >
        $(window)[that.wfunc]() - totalTipWidth
      ) {
        pos.x -= totalTipWidth;
        pos.x = Math.max(pos.x, 0);
      }
      if (
        pos.y - $(window).scrollTop() >
        $(window)[that.hfunc]() - totalTipHeight
      ) {
        pos.y -= totalTipHeight;
      }

      /*
	       The section applies the new positioning ONLY if pos.x and pos.y
	       are numbers. If they are undefined or not a number, use the last
	       known numerical position. This hack fixes a bug that kept pie
	       charts from keeping their tooltip positioning.
	     */

      if (isNaN(pos.x)) {
        that.tipPosition.x = that.tipPosition.xPrev;
      } else {
        that.tipPosition.x = pos.x;
        that.tipPosition.xPrev = pos.x;
      }
      if (isNaN(pos.y)) {
        that.tipPosition.y = that.tipPosition.yPrev;
      } else {
        that.tipPosition.y = pos.y;
        that.tipPosition.yPrev = pos.y;
      }
    };

    // Quick little function for showing the tooltip.
    plot.showTooltip = function (target, position, targetPosition) {
      var $tip = that.getDomElement();

      // convert tooltip content template to real tipText
      var tipText = that.stringFormat(that.tooltipOptions.content, target);
      if (tipText === "") return;

      $tip.html(tipText);
      plot.setTooltipPosition({ x: that.tipPosition.x, y: that.tipPosition.y });
      $tip
        .css({
          left: that.tipPosition.x + that.tooltipOptions.shifts.x,
          top: that.tipPosition.y + that.tooltipOptions.shifts.y,
        })
        .show();

      // run callback
      if (typeof that.tooltipOptions.onHover === "function") {
        that.tooltipOptions.onHover(target, $tip);
      }
    };

    // Quick little function for hiding the tooltip.
    plot.hideTooltip = function () {
      that.getDomElement().hide().html("");
    };

    plot.removeTooltip = function () {
      that.getDomElement().remove();
    };
  };

  /**
   * get or create tooltip DOM element
   * @return jQuery object
   */
  FlotTooltip.prototype.getDomElement = function () {
    var $tip = $("<div>");
    if (this.tooltipOptions && this.tooltipOptions.cssClass) {
      $tip = $("." + this.tooltipOptions.cssClass);

      if ($tip.length === 0) {
        $tip = $("<div />").addClass(this.tooltipOptions.cssClass);
        $tip.appendTo("body").hide().css({ position: "absolute" });

        if (this.tooltipOptions.defaultTheme) {
          $tip.css({
            background: "#fff",
            "z-index": "1040",
            padding: "0.4em 0.6em",
            "border-radius": "0.5em",
            "font-size": "0.8em",
            border: "1px solid #111",
            display: "none",
            "white-space": "nowrap",
          });
        }
      }
    }

    return $tip;
  };

  /**
   * core function, create tooltip content
   * @param  {string} content - template with tooltip content
   * @param  {object} item - Flot item
   * @return {string} real tooltip content for current item
   */
  FlotTooltip.prototype.stringFormat = function (content, item) {
    var percentPattern = /%p\.{0,1}(\d{0,})/;
    var seriesPattern = /%s/;
    var colorPattern = /%c/;
    var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded
    var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded
    var xPattern = /%x\.{0,1}(\d{0,})/;
    var yPattern = /%y\.{0,1}(\d{0,})/;
    var xPatternWithoutPrecision = "%x";
    var yPatternWithoutPrecision = "%y";
    var customTextPattern = "%ct";
    var nPiePattern = "%n";

    var x, y, customText, p, n;

    // for threshold plugin we need to read data from different place
    if (typeof item.series.threshold !== "undefined") {
      x = item.datapoint[0];
      y = item.datapoint[1];
      customText = item.datapoint[2];
    }

    // for CurvedLines plugin we need to read data from different place
    else if (typeof item.series.curvedLines !== "undefined") {
      x = item.datapoint[0];
      y = item.datapoint[1];
    } else if (
      typeof item.series.lines !== "undefined" &&
      item.series.lines.steps
    ) {
      x = item.series.datapoints.points[item.dataIndex * 2];
      y = item.series.datapoints.points[item.dataIndex * 2 + 1];
      // TODO: where to find custom text in this variant?
      customText = "";
    } else {
      x = item.series.data[item.dataIndex][0];
      y = item.series.data[item.dataIndex][1];
      customText = item.series.data[item.dataIndex][2];
    }

    // I think this is only in case of threshold plugin
    if (item.series.label === null && item.series.originSeries) {
      item.series.label = item.series.originSeries.label;
    }

    // if it is a function callback get the content string
    if (typeof content === "function") {
      content = content(item.series.label, x, y, item);
    }

    // the case where the passed content is equal to false
    if (typeof content === "boolean" && !content) {
      return "";
    }

    /* replacement of %ct and other multi-character templates must
           precede the replacement of single-character templates
           to avoid conflict between '%c' and '%ct'  and similar substrings
        */
    if (customText) {
      content = content.replace(customTextPattern, customText);
    }

    // percent match for pie charts and stacked percent
    if (typeof item.series.percent !== "undefined") {
      p = item.series.percent;
    } else if (typeof item.series.percents !== "undefined") {
      p = item.series.percents[item.dataIndex];
    }
    if (typeof p === "number") {
      content = this.adjustValPrecision(percentPattern, content, p);
    }

    // replace %n with number of items represented by slice in pie charts
    if (item.series.hasOwnProperty("pie")) {
      if (typeof item.series.data[0][1] !== "undefined") {
        n = item.series.data[0][1];
      }
    }
    if (typeof n === "number") {
      content = content.replace(nPiePattern, n);
    }

    // series match
    if (typeof item.series.label !== "undefined") {
      content = content.replace(seriesPattern, item.series.label);
    } else {
      //remove %s if label is undefined
      content = content.replace(seriesPattern, "");
    }

    // color match
    if (typeof item.series.color !== "undefined") {
      content = content.replace(colorPattern, item.series.color);
    } else {
      //remove %s if color is undefined
      content = content.replace(colorPattern, "");
    }

    // x axis label match
    if (this.hasAxisLabel("xaxis", item)) {
      content = content.replace(
        xLabelPattern,
        item.series.xaxis.options.axisLabel
      );
    } else {
      //remove %lx if axis label is undefined or axislabels plugin not present
      content = content.replace(xLabelPattern, "");
    }

    // y axis label match
    if (this.hasAxisLabel("yaxis", item)) {
      content = content.replace(
        yLabelPattern,
        item.series.yaxis.options.axisLabel
      );
    } else {
      //remove %ly if axis label is undefined or axislabels plugin not present
      content = content.replace(yLabelPattern, "");
    }

    // time mode axes with custom dateFormat
    if (this.isTimeMode("xaxis", item) && this.isXDateFormat(item)) {
      content = content.replace(
        xPattern,
        this.timestampToDate(
          x,
          this.tooltipOptions.xDateFormat,
          item.series.xaxis.options
        )
      );
    }
    if (this.isTimeMode("yaxis", item) && this.isYDateFormat(item)) {
      content = content.replace(
        yPattern,
        this.timestampToDate(
          y,
          this.tooltipOptions.yDateFormat,
          item.series.yaxis.options
        )
      );
    }

    // set precision if defined
    if (typeof x === "number") {
      content = this.adjustValPrecision(xPattern, content, x);
    }
    if (typeof y === "number") {
      content = this.adjustValPrecision(yPattern, content, y);
    }

    // change x from number to given label, if given
    if (typeof item.series.xaxis.ticks !== "undefined") {
      var ticks;
      if (this.hasRotatedXAxisTicks(item)) {
        // xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks
        ticks = "rotatedTicks";
      } else {
        ticks = "ticks";
      }

      // see https://github.com/krzysu/flot.tooltip/issues/65
      var tickIndex = item.dataIndex + item.seriesIndex;

      for (var xIndex in item.series.xaxis[ticks]) {
        if (
          item.series.xaxis[ticks].hasOwnProperty(tickIndex) &&
          !this.isTimeMode("xaxis", item)
        ) {
          var valueX = this.isCategoriesMode("xaxis", item)
            ? item.series.xaxis[ticks][tickIndex].label
            : item.series.xaxis[ticks][tickIndex].v;
          if (valueX === x) {
            content = content.replace(
              xPattern,
              item.series.xaxis[ticks][tickIndex].label.replace(/\$/g, "$$$$")
            );
          }
        }
      }
    }

    // change y from number to given label, if given
    if (typeof item.series.yaxis.ticks !== "undefined") {
      for (var yIndex in item.series.yaxis.ticks) {
        if (item.series.yaxis.ticks.hasOwnProperty(yIndex)) {
          var valueY = this.isCategoriesMode("yaxis", item)
            ? item.series.yaxis.ticks[yIndex].label
            : item.series.yaxis.ticks[yIndex].v;
          if (valueY === y) {
            content = content.replace(
              yPattern,
              item.series.yaxis.ticks[yIndex].label.replace(/\$/g, "$$$$")
            );
          }
        }
      }
    }

    // if no value customization, use tickFormatter by default
    if (typeof item.series.xaxis.tickFormatter !== "undefined") {
      //escape dollar
      content = content.replace(
        xPatternWithoutPrecision,
        item.series.xaxis
          .tickFormatter(x, item.series.xaxis)
          .replace(/\$/g, "$$")
      );
    }
    if (typeof item.series.yaxis.tickFormatter !== "undefined") {
      //escape dollar
      content = content.replace(
        yPatternWithoutPrecision,
        item.series.yaxis
          .tickFormatter(y, item.series.yaxis)
          .replace(/\$/g, "$$")
      );
    }

    return content;
  };

  // helpers just for readability
  FlotTooltip.prototype.isTimeMode = function (axisName, item) {
    return (
      typeof item.series[axisName].options.mode !== "undefined" &&
      item.series[axisName].options.mode === "time"
    );
  };

  FlotTooltip.prototype.isXDateFormat = function (item) {
    return (
      typeof this.tooltipOptions.xDateFormat !== "undefined" &&
      this.tooltipOptions.xDateFormat !== null
    );
  };

  FlotTooltip.prototype.isYDateFormat = function (item) {
    return (
      typeof this.tooltipOptions.yDateFormat !== "undefined" &&
      this.tooltipOptions.yDateFormat !== null
    );
  };

  FlotTooltip.prototype.isCategoriesMode = function (axisName, item) {
    return (
      typeof item.series[axisName].options.mode !== "undefined" &&
      item.series[axisName].options.mode === "categories"
    );
  };

  //
  FlotTooltip.prototype.timestampToDate = function (tmst, dateFormat, options) {
    var theDate = $.plot.dateGenerator(tmst, options);
    return $.plot.formatDate(
      theDate,
      dateFormat,
      this.tooltipOptions.monthNames,
      this.tooltipOptions.dayNames
    );
  };

  //
  FlotTooltip.prototype.adjustValPrecision = function (
    pattern,
    content,
    value
  ) {
    var precision;
    var matchResult = content.match(pattern);
    if (matchResult !== null) {
      if (RegExp.$1 !== "") {
        precision = RegExp.$1;
        value = value.toFixed(precision);

        // only replace content if precision exists, in other case use thickformater
        content = content.replace(pattern, value);
      }
    }
    return content;
  };

  // other plugins detection below

  // check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given
  FlotTooltip.prototype.hasAxisLabel = function (axisName, item) {
    return (
      $.inArray("axisLabels", this.plotPlugins) !== -1 &&
      typeof item.series[axisName].options.axisLabel !== "undefined" &&
      item.series[axisName].options.axisLabel.length > 0
    );
  };

  // check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used
  FlotTooltip.prototype.hasRotatedXAxisTicks = function (item) {
    return (
      $.inArray("tickRotor", this.plotPlugins) !== -1 &&
      typeof item.series.xaxis.rotatedTicks !== "undefined"
    );
  };

  //
  var init = function (plot) {
    new FlotTooltip(plot);
  };

  // define Flot plugin
  $.plot.plugins.push({
    init: init,
    options: defaultOptions,
    name: "tooltip",
    version: "0.8.5",
  });
})(jQuery);
