/*global window: false */
"use strict";

var moment = require_once("moment");
moment = typeof moment === "function" ? moment : window.moment;

module.exports = function (Chart) {
  var helpers = Chart.helpers;
  var time = {
    units: [
      {
        name: "millisecond",
        steps: [1, 2, 5, 10, 20, 50, 100, 250, 500],
      },
      {
        name: "second",
        steps: [1, 2, 5, 10, 30],
      },
      {
        name: "minute",
        steps: [1, 2, 5, 10, 30],
      },
      {
        name: "hour",
        steps: [1, 2, 3, 6, 12],
      },
      {
        name: "day",
        steps: [1, 2, 5],
      },
      {
        name: "week",
        maxStep: 4,
      },
      {
        name: "month",
        maxStep: 3,
      },
      {
        name: "quarter",
        maxStep: 4,
      },
      {
        name: "year",
        maxStep: false,
      },
    ],
  };

  var defaultConfig = {
    position: "bottom",

    time: {
      parser: false, // false == a pattern string from http://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment
      format: false, // DEPRECATED false == date objects, moment object, callback or a pattern string from http://momentjs.com/docs/#/parsing/string-format/
      unit: false, // false == automatic or override with week, month, year, etc.
      round: false, // none, or override with week, month, year, etc.
      displayFormat: false, // DEPRECATED
      isoWeekday: false, // override week start day - see http://momentjs.com/docs/#/get-set/iso-weekday/

      // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/
      displayFormats: {
        millisecond: "h:mm:ss.SSS a", // 11:20:01.123 AM,
        second: "h:mm:ss a", // 11:20:01 AM
        minute: "h:mm:ss a", // 11:20:01 AM
        hour: "MMM D, hA", // Sept 4, 5PM
        day: "ll", // Sep 4 2015
        week: "ll", // Week 46, or maybe "[W]WW - YYYY" ?
        month: "MMM YYYY", // Sept 2015
        quarter: "[Q]Q - YYYY", // Q3
        year: "YYYY", // 2015
      },
    },
    ticks: {
      autoSkip: false,
    },
  };

  var TimeScale = Chart.Scale.extend({
    initialize: function () {
      if (!moment) {
        throw new Error("Chart.js - Moment.js could not be found! You must include it before Chart.js to use the time scale. Download at https://momentjs.com");
      }

      Chart.Scale.prototype.initialize.call(this);
    },
    getLabelMoment: function (datasetIndex, index) {
      return this.labelMoments[datasetIndex][index];
    },
    getMomentStartOf: function (tick) {
      if (this.options.time.unit === "week" && this.options.time.isoWeekday !== false) {
        return tick.clone().startOf("isoWeek").isoWeekday(this.options.time.isoWeekday);
      } else {
        return tick.clone().startOf(this.tickUnit);
      }
    },
    determineDataLimits: function () {
      this.labelMoments = [];

      // Only parse these once. If the dataset does not have data as x,y pairs, we will use
      // these
      var scaleLabelMoments = [];
      if (this.chart.data.labels && this.chart.data.labels.length > 0) {
        helpers.each(
          this.chart.data.labels,
          function (label, index) {
            var labelMoment = this.parseTime(label);

            if (labelMoment.isValid()) {
              if (this.options.time.round) {
                labelMoment.startOf(this.options.time.round);
              }
              scaleLabelMoments.push(labelMoment);
            }
          },
          this
        );

        this.firstTick = moment.min.call(this, scaleLabelMoments);
        this.lastTick = moment.max.call(this, scaleLabelMoments);
      } else {
        this.firstTick = null;
        this.lastTick = null;
      }

      helpers.each(
        this.chart.data.datasets,
        function (dataset, datasetIndex) {
          var momentsForDataset = [];
          var datasetVisible = this.chart.isDatasetVisible(datasetIndex);

          if (typeof dataset.data[0] === "object" && dataset.data[0] !== null) {
            helpers.each(
              dataset.data,
              function (value, index) {
                var labelMoment = this.parseTime(this.getRightValue(value));

                if (labelMoment.isValid()) {
                  if (this.options.time.round) {
                    labelMoment.startOf(this.options.time.round);
                  }
                  momentsForDataset.push(labelMoment);

                  if (datasetVisible) {
                    // May have gone outside the scale ranges, make sure we keep the first and last ticks updated
                    this.firstTick = this.firstTick !== null ? moment.min(this.firstTick, labelMoment) : labelMoment;
                    this.lastTick = this.lastTick !== null ? moment.max(this.lastTick, labelMoment) : labelMoment;
                  }
                }
              },
              this
            );
          } else {
            // We have no labels. Use the ones from the scale
            momentsForDataset = scaleLabelMoments;
          }

          this.labelMoments.push(momentsForDataset);
        },
        this
      );

      // Set these after we've done all the data
      if (this.options.time.min) {
        this.firstTick = this.parseTime(this.options.time.min);
      }

      if (this.options.time.max) {
        this.lastTick = this.parseTime(this.options.time.max);
      }

      // We will modify these, so clone for later
      this.firstTick = (this.firstTick || moment()).clone();
      this.lastTick = (this.lastTick || moment()).clone();
    },
    buildTicks: function (index) {
      this.ctx.save();
      var tickFontSize = helpers.getValueOrDefault(this.options.ticks.fontSize, Chart.defaults.global.defaultFontSize);
      var tickFontStyle = helpers.getValueOrDefault(this.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle);
      var tickFontFamily = helpers.getValueOrDefault(this.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily);
      var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily);
      this.ctx.font = tickLabelFont;

      this.ticks = [];
      this.unitScale = 1; // How much we scale the unit by, ie 2 means 2x unit per step
      this.scaleSizeInUnits = 0; // How large the scale is in the base unit (seconds, minutes, etc)

      // Set unit override if applicable
      if (this.options.time.unit) {
        this.tickUnit = this.options.time.unit || "day";
        this.displayFormat = this.options.time.displayFormats[this.tickUnit];
        this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true);
        this.unitScale = helpers.getValueOrDefault(this.options.time.unitStepSize, 1);
      } else {
        // Determine the smallest needed unit of the time
        var innerWidth = this.isHorizontal() ? this.width - (this.paddingLeft + this.paddingRight) : this.height - (this.paddingTop + this.paddingBottom);

        // Crude approximation of what the label length might be
        var tempFirstLabel = this.tickFormatFunction(this.firstTick, 0, []);
        var tickLabelWidth = this.ctx.measureText(tempFirstLabel).width;
        var cosRotation = Math.cos(helpers.toRadians(this.options.ticks.maxRotation));
        var sinRotation = Math.sin(helpers.toRadians(this.options.ticks.maxRotation));
        tickLabelWidth = tickLabelWidth * cosRotation + tickFontSize * sinRotation;
        var labelCapacity = innerWidth / tickLabelWidth;

        // Start as small as possible
        this.tickUnit = "millisecond";
        this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true);
        this.displayFormat = this.options.time.displayFormats[this.tickUnit];

        var unitDefinitionIndex = 0;
        var unitDefinition = time.units[unitDefinitionIndex];

        // While we aren't ideal and we don't have units left
        while (unitDefinitionIndex < time.units.length) {
          // Can we scale this unit. If `false` we can scale infinitely
          this.unitScale = 1;

          if (helpers.isArray(unitDefinition.steps) && Math.ceil(this.scaleSizeInUnits / labelCapacity) < helpers.max(unitDefinition.steps)) {
            // Use one of the prefedined steps
            for (var idx = 0; idx < unitDefinition.steps.length; ++idx) {
              if (unitDefinition.steps[idx] >= Math.ceil(this.scaleSizeInUnits / labelCapacity)) {
                this.unitScale = helpers.getValueOrDefault(this.options.time.unitStepSize, unitDefinition.steps[idx]);
                break;
              }
            }

            break;
          } else if (unitDefinition.maxStep === false || Math.ceil(this.scaleSizeInUnits / labelCapacity) < unitDefinition.maxStep) {
            // We have a max step. Scale this unit
            this.unitScale = helpers.getValueOrDefault(this.options.time.unitStepSize, Math.ceil(this.scaleSizeInUnits / labelCapacity));
            break;
          } else {
            // Move to the next unit up
            ++unitDefinitionIndex;
            unitDefinition = time.units[unitDefinitionIndex];

            this.tickUnit = unitDefinition.name;
            var leadingUnitBuffer = this.firstTick.diff(this.getMomentStartOf(this.firstTick), this.tickUnit, true);
            var trailingUnitBuffer = this.getMomentStartOf(this.lastTick.clone().add(1, this.tickUnit)).diff(this.lastTick, this.tickUnit, true);
            this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true) + leadingUnitBuffer + trailingUnitBuffer;
            this.displayFormat = this.options.time.displayFormats[unitDefinition.name];
          }
        }
      }

      var roundedStart;

      // Only round the first tick if we have no hard minimum
      if (!this.options.time.min) {
        this.firstTick = this.getMomentStartOf(this.firstTick);
        roundedStart = this.firstTick;
      } else {
        roundedStart = this.getMomentStartOf(this.firstTick);
      }

      // Only round the last tick if we have no hard maximum
      if (!this.options.time.max) {
        var roundedEnd = this.getMomentStartOf(this.lastTick);
        if (roundedEnd.diff(this.lastTick, this.tickUnit, true) !== 0) {
          // Do not use end of because we need this to be in the next time unit
          this.lastTick = this.getMomentStartOf(this.lastTick.add(1, this.tickUnit));
        }
      }

      this.smallestLabelSeparation = this.width;

      helpers.each(
        this.chart.data.datasets,
        function (dataset, datasetIndex) {
          for (var i = 1; i < this.labelMoments[datasetIndex].length; i++) {
            this.smallestLabelSeparation = Math.min(this.smallestLabelSeparation, this.labelMoments[datasetIndex][i].diff(this.labelMoments[datasetIndex][i - 1], this.tickUnit, true));
          }
        },
        this
      );

      // Tick displayFormat override
      if (this.options.time.displayFormat) {
        this.displayFormat = this.options.time.displayFormat;
      }

      // first tick. will have been rounded correctly if options.time.min is not specified
      this.ticks.push(this.firstTick.clone());

      // For every unit in between the first and last moment, create a moment and add it to the ticks tick
      for (var i = 1; i <= this.scaleSizeInUnits; ++i) {
        var newTick = roundedStart.clone().add(i, this.tickUnit);

        // Are we greater than the max time
        if (this.options.time.max && newTick.diff(this.lastTick, this.tickUnit, true) >= 0) {
          break;
        }

        if (i % this.unitScale === 0) {
          this.ticks.push(newTick);
        }
      }

      // Always show the right tick
      var diff = this.ticks[this.ticks.length - 1].diff(this.lastTick, this.tickUnit);
      if (diff !== 0 || this.scaleSizeInUnits === 0) {
        // this is a weird case. If the <max> option is the same as the end option, we can't just diff the times because the tick was created from the roundedStart
        // but the last tick was not rounded.
        if (this.options.time.max) {
          this.ticks.push(this.lastTick.clone());
          this.scaleSizeInUnits = this.lastTick.diff(this.ticks[0], this.tickUnit, true);
        } else {
          this.ticks.push(this.lastTick.clone());
          this.scaleSizeInUnits = this.lastTick.diff(this.firstTick, this.tickUnit, true);
        }
      }

      this.ctx.restore();
    },
    // Get tooltip label
    getLabelForIndex: function (index, datasetIndex) {
      var label = this.chart.data.labels && index < this.chart.data.labels.length ? this.chart.data.labels[index] : "";

      if (typeof this.chart.data.datasets[datasetIndex].data[0] === "object") {
        label = this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]);
      }

      // Format nicely
      if (this.options.time.tooltipFormat) {
        label = this.parseTime(label).format(this.options.time.tooltipFormat);
      }

      return label;
    },
    // Function to format an individual tick mark
    tickFormatFunction: function tickFormatFunction(tick, index, ticks) {
      var formattedTick = tick.format(this.displayFormat);
      var tickOpts = this.options.ticks;
      var callback = helpers.getValueOrDefault(tickOpts.callback, tickOpts.userCallback);

      if (callback) {
        return callback(formattedTick, index, ticks);
      } else {
        return formattedTick;
      }
    },
    convertTicksToLabels: function () {
      this.tickMoments = this.ticks;
      this.ticks = this.ticks.map(this.tickFormatFunction, this);
    },
    getPixelForValue: function (value, index, datasetIndex, includeOffset) {
      var labelMoment = value && value.isValid && value.isValid() ? value : this.getLabelMoment(datasetIndex, index);

      if (labelMoment) {
        var offset = labelMoment.diff(this.firstTick, this.tickUnit, true);

        var decimal = offset / this.scaleSizeInUnits;

        if (this.isHorizontal()) {
          var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
          var valueWidth = innerWidth / Math.max(this.ticks.length - 1, 1);
          var valueOffset = innerWidth * decimal + this.paddingLeft;

          return this.left + Math.round(valueOffset);
        } else {
          var innerHeight = this.height - (this.paddingTop + this.paddingBottom);
          var valueHeight = innerHeight / Math.max(this.ticks.length - 1, 1);
          var heightOffset = innerHeight * decimal + this.paddingTop;

          return this.top + Math.round(heightOffset);
        }
      }
    },
    getPixelForTick: function (index, includeOffset) {
      return this.getPixelForValue(this.tickMoments[index], null, null, includeOffset);
    },
    getValueForPixel: function (pixel) {
      var innerDimension = this.isHorizontal() ? this.width - (this.paddingLeft + this.paddingRight) : this.height - (this.paddingTop + this.paddingBottom);
      var offset = (pixel - (this.isHorizontal() ? this.left + this.paddingLeft : this.top + this.paddingTop)) / innerDimension;
      offset *= this.scaleSizeInUnits;
      return this.firstTick.clone().add(moment.duration(offset, this.tickUnit).asSeconds(), "seconds");
    },
    parseTime: function (label) {
      if (typeof this.options.time.parser === "string") {
        return moment(label, this.options.time.parser);
      }
      if (typeof this.options.time.parser === "function") {
        return this.options.time.parser(label);
      }
      // Date objects
      if (typeof label.getMonth === "function" || typeof label === "number") {
        return moment(label);
      }
      // Moment support
      if (label.isValid && label.isValid()) {
        return label;
      }
      // Custom parsing (return an instance of moment)
      if (typeof this.options.time.format !== "string" && this.options.time.format.call) {
        console.warn("options.time.format is deprecated and replaced by options.time.parser. See http://nnnick.github.io/Chart.js/docs-v2/#scales-time-scale");
        return this.options.time.format(label);
      }
      // Moment format parsing
      return moment(label, this.options.time.format);
    },
  });
  Chart.scaleService.registerScaleType("time", TimeScale, defaultConfig);
};
