/**
 * @file
 * Defines the behaviors needed for cropper integration.
 */

(function ($, Drupal) {
  'use strict';

  /**
   * @class Drupal.ImageWidgetCropType
   *
   * @param {Drupal.ImageWidgetCrop} instance
   *   The main ImageWidgetCrop instance that created this one.
   *
   * @param {HTMLElement|jQuery} element
   *   The wrapper element.
   */
  Drupal.ImageWidgetCropType = function (instance, element) {

    /**
     * The ImageWidgetCrop instance responsible for creating this type.
     *
     * @type {Drupal.ImageWidgetCrop}
     */
    this.instance = instance;

    /**
     * The Cropper plugin wrapper element.
     *
     * @type {jQuery}
     */
    this.$cropperWrapper = $();

    /**
     * The wrapper element.
     *
     * @type {jQuery}
     */
    this.$wrapper = $(element);

    /**
     * The table element, if any.
     *
     * @type {jQuery}
     */
    this.$table = this.$wrapper.find(this.selectors.table);

    /**
     * The image element.
     *
     * @type {jQuery}
     */
    this.$image = this.$wrapper.find(this.selectors.image);

    /**
     * The reset element.
     *
     * @type {jQuery}
     */
    this.$reset = this.$wrapper.find(this.selectors.reset);

    /**
     * @type {Cropper}
     */
    this.cropper = null;

    /**
     * Flag indicating whether this instance is enabled.
     *
     * @type {Boolean}
     */
    this.enabled = true;

    /**
     * The hard limit of the crop.
     *
     * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
     */
    this.hardLimit = {
      height: null,
      width: null,
      reached: {
        height: false,
        width: false
      }
    };

    /**
     * The unique identifier for this ImageWidgetCrop type.
     *
     * @type {String}
     */
    this.id = null;

    /**
     * Flag indicating whether the instance has been initialized.
     *
     * @type {Boolean}
     */
    this.initialized = false;

    /**
     * An object of recorded setInterval instances.
     *
     * @type {Object.<Number, jQuery>}
     */
    this.intervals = {};

    /**
     * The delta ratio of image based on its natural dimensions.
     *
     * @type {Number}
     */
    this.naturalDelta = null;

    /**
     * The natural height of the image.
     *
     * @type {Number}
     */
    this.naturalHeight = null;

    /**
     * The natural width of the image.
     *
     * @type {Number}
     */
    this.naturalWidth = null;

    /**
     * The original height of the image.
     *
     * @type {Number}
     */
    this.originalHeight = 0;

    /**
     * The original width of the image.
     *
     * @type {Number}
     */
    this.originalWidth = 0;

    /**
     * The current Cropper options.
     *
     * @type {Cropper.options}
     */
    this.options = {};

    /**
     * Flag indicating whether to show the default crop.
     *
     * @type {Boolean}
     */
    this.showDefaultCrop = true;

    /**
     * Flag indicating whether to show the default crop.
     *
     * @type {Boolean}
     */
    this.isRequired = false;

    /**
     * The soft limit of the crop.
     *
     * @type {{height: Number, width: Number, reached: {height: Boolean, width: Boolean}}}
     */
    this.softLimit = {
      height: null,
      width: null,
      reached: {
        height: false,
        width: false
      }
    };

    /**
     * The numeric representation of a ratio.
     *
     * @type {Number}
     */
    this.ratio = NaN;

    /**
     * The value elements.
     *
     * @type {Object.<String, jQuery>}
     */
    this.values = {
      applied: this.$wrapper.find(this.selectors.values.applied),
      height: this.$wrapper.find(this.selectors.values.height),
      width: this.$wrapper.find(this.selectors.values.width),
      x: this.$wrapper.find(this.selectors.values.x),
      y: this.$wrapper.find(this.selectors.values.y)
    };

    /**
     * Flag indicating whether the instance is currently visible.
     *
     * @type {Boolean}
     */
    this.visible = false;

    // Initialize the instance.
    this.init();
  };

  /**
   * The prefix used for all Image Widget Crop data attributes.
   *
   * @type {RegExp}
   */
  Drupal.ImageWidgetCropType.prototype.dataPrefix = /^drupalIwc/;

  /**
   * Default options to pass to the Cropper plugin.
   *
   * @type {Object}
   */
  Drupal.ImageWidgetCropType.prototype.defaultOptions = {
    autoCropArea: 1,
    background: false,
    responsive: false,
    viewMode: 1,
    zoomable: false
  };

  /**
   * The selectors used to identify elements for this module.
   *
   * @type {Object}
   */
  Drupal.ImageWidgetCropType.prototype.selectors = {
    image: '[data-drupal-iwc=image]',
    reset: '[data-drupal-iwc=reset]',
    table: '[data-drupal-iwc=table]',
    values: {
      applied: '[data-drupal-iwc-value=applied]',
      height: '[data-drupal-iwc-value=height]',
      width: '[data-drupal-iwc-value=width]',
      x: '[data-drupal-iwc-value=x]',
      y: '[data-drupal-iwc-value=y]'
    }
  };

  /**
   * The "ready" event handler for the Cropper plugin.
   */
  Drupal.ImageWidgetCropType.prototype.cropperReady = function () {
    // Set crop limits.
    this.built();

    // Restore saved crop box data.
    if (this.getValue('applied')) {
      // Convert data.
      var canvasData = this.cropper.getCanvasData();
      var cropBoxData = this.getValues(this.originalHeight / canvasData.height);
      cropBoxData.left = cropBoxData.x + canvasData.left;
      cropBoxData.top = cropBoxData.y + canvasData.top;

      // TEMP Bind height and width to max to avoid cropper.js bug.
      var containerData = this.cropper.getContainerData();
      var limited = this.options.viewMode === 1 || this.options.viewMode === 2;
      var maxCropBoxWidth = Math.min(containerData.width, limited ? canvasData.width : containerData.width);
      var maxCropBoxHeight = Math.min(containerData.height, limited ? canvasData.height : containerData.height);
      if (this.ratio) {
        if (maxCropBoxHeight * this.ratio > maxCropBoxWidth) {
          maxCropBoxHeight = maxCropBoxWidth / this.ratio;
        } else {
          maxCropBoxWidth = maxCropBoxHeight * this.ratio;
        }
      }
      cropBoxData.width = Math.min(cropBoxData.width, maxCropBoxWidth);
      cropBoxData.height = Math.min(cropBoxData.height, maxCropBoxHeight);

      // Restore data.
      this.cropper.setCropBoxData(cropBoxData);
    }
  }

  /**
   * The "built" event handler for the Cropper plugin.
   */
  Drupal.ImageWidgetCropType.prototype.built = function () {
    this.$cropperWrapper = this.$wrapper.find('.cropper-container');
    this.updateHardLimits();
    this.updateSoftLimits();
  };

  /**
   * The "cropend" event handler for the Cropper plugin.
   */
  Drupal.ImageWidgetCropType.prototype.cropEnd = function () {
    // Immediately return if there is no cropper instance (for whatever reason).
    if (!this.cropper) {
      return;
    }

    // Retrieve the cropper data.
    var data = this.cropper.getData();

    // Ensure the applied state is enabled.
    data.applied = 1;

    // Data returned by Cropper plugin should be multiplied with delta in order
    // to get the proper crop sizes for the original image.
    this.setValues(data, this.naturalDelta);

    // Trigger summary updates.
    this.$wrapper.trigger('summaryUpdated');
  };

  /**
   * The "cropmove" event handler for the Cropper plugin.
   */
  Drupal.ImageWidgetCropType.prototype.cropMove = function () {
    this.built();
  };

  /**
   * Destroys this instance.
   */
  Drupal.ImageWidgetCropType.prototype.destroy = function () {
    this.destroyCropper();

    this.$image.off('.iwc');
    this.$reset.off('.iwc');

    // Clear any intervals that were set.
    for (var interval in this.intervals) {
      if (this.intervals.hasOwnProperty(interval)) {
        clearInterval(interval);
        delete this.intervals[interval];
      }
    }
  };

  /**
   * Destroys the Cropper plugin instance.
   */
  Drupal.ImageWidgetCropType.prototype.destroyCropper = function () {
    this.$image.off('.iwc.cropper');
    if (this.cropper) {
      this.cropper.destroy();
      this.cropper = null;
    }
  };

  /**
   * Disables this instance.
   */
  Drupal.ImageWidgetCropType.prototype.disable = function () {
    if (this.cropper) {
      this.cropper.disable();
    }
    this.$table.removeClass('responsive-enabled--opened');
  };

  /**
   * Enables this instance.
   */
  Drupal.ImageWidgetCropType.prototype.enable = function () {
    if (this.cropper) {
      this.cropper.enable();
    }
    this.$table.addClass('responsive-enabled--opened');
  };

  /**
   * Retrieves a crop value.
   *
   * @param {'applied'|'height'|'width'|'x'|'y'} name
   *   The name of the crop value to retrieve.
   * @param {Number} [delta]
   *   The delta amount to divide value by, if any.
   *
   * @return {Number}
   *   The crop value.
   */
  Drupal.ImageWidgetCropType.prototype.getValue = function (name, delta) {
    var value = 0;
    if (this.values[name] && this.values[name][0]) {
      value = parseInt(this.values[name][0].value, 10) || 0;
    }
    return name !== 'applied' && value && delta ? Math.floor(value / delta) : value;
  };

  /**
   * Retrieves all crop values.
   *
   * @param {Number} [delta]
   *   The delta amount to divide value by, if any.
   *
   * @return {{applied: Number, height: Number, width: Number, x: Number, y: Number}}
   *   The crop value key/value pairs.
   */
  Drupal.ImageWidgetCropType.prototype.getValues = function (delta) {
    var values = {};
    for (var name in this.values) {
      if (this.values.hasOwnProperty(name)) {
        values[name] = this.getValue(name, delta);
      }
    }
    return values;
  };

  /**
   * Initializes the instance.
   */
  Drupal.ImageWidgetCropType.prototype.init = function () {
    // Immediately return if already initialized.
    if (this.initialized) {
      return;
    }

    // Set the default options.
    this.options = $.extend({}, this.defaultOptions);
    this.isRequired = this.$wrapper.data('drupalIwcRequired');

    // Extend this instance with data from the wrapper.
    var data = this.$wrapper.data();
    for (var i in data) {
      if (data.hasOwnProperty(i) && this.dataPrefix.test(i)) {
        // Remove Drupal + module prefix and lowercase the first letter.
        var prop = i.replace(this.dataPrefix, '');
        prop = prop.charAt(0).toLowerCase() + prop.slice(1);

        // Check if data attribute exists on this object.
        if (prop && this.hasOwnProperty(prop)) {
          var value = data[i];

          // Parse the ratio value.
          if (prop === 'ratio') {
            value = this.parseRatio(value);
          }
          this[prop] = typeof value === 'object' ? $.extend(true, {}, this[prop], value) : value;
        }
      }
    }

    // Bind necessary events.
    this.$image
      .on('visible.iwc', function () {
        this.visible = true;
        this.naturalHeight = parseInt(this.$image.prop('naturalHeight'), 10);
        this.naturalWidth = parseInt(this.$image.prop('naturalWidth'), 10);
        // Calculate delta between original and thumbnail images.
        this.naturalDelta = this.originalHeight && this.naturalHeight ? this.originalHeight / this.naturalHeight : null;
      }.bind(this))
      // Only initialize the cropper plugin once.
      .one('visible.iwc', this.initializeCropper.bind(this))
      .on('hidden.iwc', function () {
        this.visible = false;
      }.bind(this));

    this.$reset
      .on('click.iwc', this.reset.bind(this));

    // Star polling visibility of the image that should be able to be cropped.
    this.pollVisibility(this.$image);

    // Bind the drupalSetSummary callback.
    this.$wrapper.drupalSetSummary(this.updateSummary.bind(this));

    // Trigger the initial summaryUpdate event.
    this.$wrapper.trigger('summaryUpdated');
    var isIE = /*@cc_on!@*/false || !!document.documentMode;
    if (isIE) {
      var $image = this.$image;
      $('.image-data__crop-wrapper > summary').on('click', function () {
        setTimeout(function () {$image.trigger('visible.iwc')}, 100);
      });
    }
  };

  /**
   * Initializes the Cropper plugin.
   */
  Drupal.ImageWidgetCropType.prototype.initializeCropper = function () {
    // Calculate minimal height for cropper container (minimal width is 200).
    var minDelta = (this.originalWidth / 200);
    this.options.minContainerHeight = this.originalHeight / minDelta;

    // Only autoCrop if 'Show default crop' is checked or if there is a crop already set.
    this.options.autoCrop = this.showDefaultCrop || !!this.getValue('applied');

    // Set aspect ratio.
    this.options.aspectRatio = this.ratio;

    this.$image
      .on('ready.iwc.cropper', this.cropperReady.bind(this))
      .on('cropend.iwc.cropper', this.cropEnd.bind(this))
      .on('cropmove.iwc.cropper', this.cropMove.bind(this))
      .cropper(this.options);

    this.cropper = this.$image.data('cropper');
    this.options = this.cropper.options;

    // If "Show default crop" is checked apply default crop.
    if (this.showDefaultCrop) {
      // All data returned by cropper plugin multiple with delta in order to get
      // proper crop sizes for original image.
      this.setValue(this.$image.cropper('getData'), this.naturalDelta);
      this.$wrapper.trigger('summaryUpdated');
    }
  };

  /**
   * Creates a poll that checks visibility of an item.
   *
   * @param {HTMLElement|jQuery} element
   *   The element to poll.
   *
   * Replace once vertical tabs have proper events ?
   * When following issue are fixed @see https://www.drupal.org/node/2653570.
   */
  Drupal.ImageWidgetCropType.prototype.pollVisibility = function (element) {
    var $element = $(element);

    // Immediately return if there's no element.
    if (!$element[0]) {
      return;
    }

    var isElementVisible = function (el) {
      var rect = el.getBoundingClientRect();
      var vWidth = window.innerWidth || document.documentElement.clientWidth;
      var vHeight = window.innerHeight || document.documentElement.clientHeight;

      // Immediately Return false if it's not in the viewport.
      if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) {
        return false;
      }

      // Return true if any of its four corners are visible.
      var efp = function (x, y) {
        return document.elementFromPoint(x, y);
      };
      return (
        el.contains(efp(rect.left, rect.top))
        || el.contains(efp(rect.right, rect.top))
        || el.contains(efp(rect.right, rect.bottom))
        || el.contains(efp(rect.left, rect.bottom))
      );
    };

    var value = null;
    var interval = setInterval(function () {
      var visible = isElementVisible($element[0]);
      if (value !== visible) {
        $element.trigger((value = visible) ? 'visible.iwc' : 'hidden.iwc');
      }
    }, 250);
    this.intervals[interval] = $element;
  };

  /**
   * Parses a ration value into a numeric one.
   *
   * @param {String} ratio
   *   A string representation of the ratio.
   *
   * @return {Number.<float>|NaN}
   *   The numeric representation of the ratio.
   */
  Drupal.ImageWidgetCropType.prototype.parseRatio = function (ratio) {
    if (ratio && /:/.test(ratio)) {
      var parts = ratio.split(':');
      var num1 = parseInt(parts[0], 10);
      var num2 = parseInt(parts[1], 10);
      return num1 / num2;
    }
    return parseFloat(ratio);
  };

  /**
   * Reset cropping for an element.
   *
   * @param {Event} e
   *   The event object.
   */
  Drupal.ImageWidgetCropType.prototype.reset = function (e) {
    if (!this.cropper) {
      return;
    }

    if (e instanceof Event || e instanceof $.Event) {
      e.preventDefault();
      e.stopPropagation();
    }

    this.options = $.extend({}, this.cropper.options, this.defaultOptions);

    var delta = null;

    // Retrieve all current values and zero (0) them out.
    var values = this.getValues();
    for (var name in values) {
      if (values.hasOwnProperty(name)) {
        values[name] = 0;
      }
    }

    // If 'Show default crop' is not checked just re-initialize the cropper.
    if (!this.showDefaultCrop) {
      this.destroyCropper();
      this.initializeCropper();
    }
    // Reset cropper to the original values.
    else {
      this.cropper.reset();
      this.cropper.options = this.options;

      // Set the delta.
      delta = this.naturalDelta;

      // Merge in the original cropper values.
      values = $.extend(values, this.cropper.getData());
    }

    this.setValues(values, delta);
    this.$wrapper.trigger('summaryUpdated');
  };

  /**
   * The "resize" event handler proxied from the main instance.
   *
   * @see Drupal.ImageWidgetCrop.prototype.resize
   */
  Drupal.ImageWidgetCropType.prototype.resize = function () {
    // Immediately return if currently not visible.
    if (!this.visible) {
      return;
    }

    // Get previous data for cropper.
    var canvasDataOld = this.$image.cropper('getCanvasData');
    var cropBoxData = this.$image.cropper('getCropBoxData');

    // Re-render cropper.
    this.$image.cropper('render');

    // Get new data for cropper and calculate resize ratio.
    var canvasDataNew = this.$image.cropper('getCanvasData');
    var ratio = 1;
    if (canvasDataOld.width !== 0) {
      ratio = canvasDataNew.width / canvasDataOld.width;
    }

    // Set new data for crop box.
    $.each(cropBoxData, function (index, value) {
      cropBoxData[index] = value * ratio;
    });
    this.$image.cropper('setCropBoxData', cropBoxData);

    this.updateHardLimits();
    this.updateSoftLimits();
    this.$wrapper.trigger('summaryUpdated');
  };

  /**
   * Sets a single crop value.
   *
   * @param {'applied'|'height'|'width'|'x'|'y'} name
   *   The name of the crop value to set.
   * @param {Number} value
   *   The value to set.
   * @param {Number} [delta]
   *   A delta to modify the value with.
   */
  Drupal.ImageWidgetCropType.prototype.setValue = function (name, value, delta) {
    if (!this.values.hasOwnProperty(name) || !this.values[name][0]) {
      return;
    }
    value = value ? parseFloat(value) : 0;
    if (delta && name !== 'applied') {
      value = Math.floor(value * delta);

      // Bind height and width to image size when below hard limit. Solves floating-point bug.
      if (value < this.hardLimit[name]) {
        value = name === 'width' ? this.originalWidth : name === 'height' ? this.originalHeight : null;
      }
    }

    value = this.sanitizeSizes(name, value);

    this.values[name][0].value = value;
    this.values[name].trigger('change.iwc, input.iwc');
  };

    /**
     * Validate and sanitize with or height sizes to avoid overflow.
     *
     * @param {'applied'|'height'|'width'|'x'|'y'} name
     *   The name of the crop value to set.
     * @param {Number} value
     *   The value to set.
     */
    Drupal.ImageWidgetCropType.prototype.sanitizeSizes = function (name, value) {
        if (name === "width" || name === "height") {
            return this.recalculateOverflowSizes(name, value);
        }

        return value;
    };

    /**
     * Recalculate with or height sizes to avoid overflow for width or height.
     *
     * @param {'height'|'width'} name
     *   The name of the crop value to set.
     * @param {Number} value
     *   The value to set.
     */
    Drupal.ImageWidgetCropType.prototype.recalculateOverflowSizes = function (name, value) {
        var originalValue = 'original' + name.capitalize();
        if (value > this[originalValue]) {
            value--;
            return value;
        }

        return value;
    };

    String.prototype.capitalize = function() {
      return this.charAt(0).toUpperCase() + this.slice(1);
    };

  /**
   * Sets multiple crop values.
   *
   * @param {{applied: Number, height: Number, width: Number, x: Number, y: Number}} obj
   *   An object of key/value pairs of values to set.
   * @param {Number} [delta]
   *   A delta to modify the value with.
   */
  Drupal.ImageWidgetCropType.prototype.setValues = function (obj, delta) {
    for (var name in obj) {
      if (!obj.hasOwnProperty(name)) {
        continue;
      }
      this.setValue(name, obj[name], delta);
    }
  };

  /**
   * Converts horizontal and vertical dimensions to canvas dimensions.
   *
   * @param {Number} x - horizontal dimension in image space.
   * @param {Number} y - vertical dimension in image space.
   */
  Drupal.ImageWidgetCropType.prototype.toCanvasDimensions = function (x, y) {
    var imageData = this.cropper.getImageData();
    return {
      width: imageData.width * (x / this.originalWidth),
      height: imageData.height * (y / this.originalHeight)
    }
  };

  /**
   * Converts horizontal and vertical dimensions to image dimensions.
   *
   * @param {Number} x - horizontal dimension in canvas space.
   * @param {Number} y - vertical dimension in canvas space.
   */
  Drupal.ImageWidgetCropType.prototype.toImageDimensions = function (x, y) {
    var imageData = this.cropper.getImageData();
    return {
      width: x * (this.originalWidth / imageData.width),
      height: y * (this.originalHeight / imageData.height)
    }
  };

  /**
   * Update hard limits.
   */
  Drupal.ImageWidgetCropType.prototype.updateHardLimits = function () {
    // Immediately return if there is no cropper plugin instance or hard limits.
    if (!this.cropper || !this.hardLimit.width || !this.hardLimit.height) {
      return;
    }

    var options = this.cropper.options;

    // Limits works in canvas so we need to convert dimensions.
    var converted = this.toCanvasDimensions(this.hardLimit.width, this.hardLimit.height);
    options.minCropBoxWidth = converted.width;
    options.minCropBoxHeight = converted.height;

    // After updating the options we need to limit crop box.
    this.cropper.limitCropBox(true, false);
  };

  /**
   * Update soft limits.
   */
  Drupal.ImageWidgetCropType.prototype.updateSoftLimits = function () {
    // Immediately return if there is no cropper plugin instance or soft limits.
    if (!this.cropper || !this.softLimit.width || !this.softLimit.height) {
      return;
    }

    // We do comparison in image dimensions so lets convert first.
    var cropBoxData = this.cropper.getCropBoxData();
    var converted = this.toImageDimensions(cropBoxData.width, cropBoxData.height);

    var dimensions = ['width', 'height'];
    for (var i = 0, l = dimensions.length; i < l; i++) {
      var dimension = dimensions[i];
      if (converted[dimension] < this.softLimit[dimension]) {
        if (!this.softLimit.reached[dimension]) {
          this.softLimit.reached[dimension] = true;
        }
      }
      else if (this.softLimit.reached[dimension]) {
        this.softLimit.reached[dimension] = false;
      }
      this.$cropperWrapper.toggleClass('cropper--' + dimension + '-soft-limit-reached', this.softLimit.reached[dimension]);
    }
    this.$wrapper.trigger('summaryUpdated');
  };

  /**
   * Updates the summary of the wrapper.
   */
  Drupal.ImageWidgetCropType.prototype.updateSummary = function () {
    var summary = [];
    if (this.getValue('applied')) {
      summary.push(Drupal.t('Cropping applied.'));
    }
    if (this.softLimit.reached.height || this.softLimit.reached.width) {
      summary.push(Drupal.t('Soft limit reached.'));
    }
    return summary.join('<br>');
  };

    /**
     * Override Theme function for a vertical tabs.
     *
     * @param {object} settings
     *   An object with the following keys:
     * @param {string} settings.title
     *   The name of the tab.
     *
     * @return {object}
     *   This function has to return an object with at least these keys:
     *   - item: The root tab jQuery element
     *   - link: The anchor tag that acts as the clickable area of the tab
     *       (jQuery version)
     *   - summary: The jQuery element that contains the tab summary
     */
  Drupal.theme.verticalTab = function (settings) {
      var tab = {};
      this.isRequired = settings.details.data('drupalIwcRequired');
      tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>').append(tab.link = $('<a href="#"></a>').append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').html(settings.title)).append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>')));

      // If those Crop type is required add attributes.
      if (this.isRequired) {
        tab.title.addClass('js-form-required form-required');
      }

      return tab;
  };

}(jQuery, Drupal));
