
H5P.DragNBar = (function (EventDispatcher) {
  var nextInstanceIndex = 0;

  /**
   * Constructor. Initializes the drag and drop menu bar.
   *
   * @class
   * @param {Array} buttons
   * @param {H5P.jQuery} $container
   * @param {H5P.jQuery} $dialogContainer
   * @param {object} [options] Collection of options
   * @param {boolean} [options.disableEditor=false] Determines if DragNBar should be displayed in view or editor mode
   * @param {boolean} [options.enableCopyPaste=true] Determines if copy & paste is supported
   * @param {H5P.jQuery} [options.$blurHandlers] When clicking these element(s) dnb focus will be lost
   * @param {object} [options.libraries] Libraries to check against for paste notification
   */
  function DragNBar(buttons, $container, $dialogContainer, options) {
    var self = this;

    EventDispatcher.call(this);
    this.overflowThreshold = 13; // How many buttons to display before we add the more button.
    this.buttons = buttons;
    this.$container = $container;
    this.$dialogContainer = $dialogContainer;
    this.dnd = new H5P.DragNDrop(this, $container);
    this.dnd.snap = 10;
    this.newElement = false;
    this.enabled = true;
    var defaultOptions = {
      disableEditor: false,
      enableCopyPaste: true
    };
    options = H5P.jQuery.extend(defaultOptions, options);
    this.enableCopyPaste = options.enableCopyPaste;
    this.isEditor = !options.disableEditor;
    this.$blurHandlers = options.$blurHandlers ? options.$blurHandlers : undefined;
    this.libraries = options.libraries;
    this.instanceIndex = nextInstanceIndex++;

    /**
     * Keeps track of created DragNBar elements
     * @type {Array}
     */
    this.elements = [];

    // Create a popup dialog
    this.dialog = new H5P.DragNBarDialog($dialogContainer, $container);

    // Fix for forcing redraw on $container, to avoid "artifcats" on safari
    this.$container.addClass('hardware-accelerated');

    if (this.isEditor) {
      this.transformButtonActive = false;
      this.initEditor();
      this.initClickListeners();

      H5P.$window.resize(function () {
        self.resize();
      });
    }

    /**
     * Add button group.
     *
     * @private
     * @param {object[]} Buttons.
     * @param {H5P.jQuery} $button Button to add button group to.
     * @param {object} [options] Options.
     * @param {string} [options.title] Title for the group.
     */
    this.addButtonGroup = function (buttons, $button, options) {
      const $buttonGroup = H5P.jQuery('<li class="h5p-dragnbar-li h5p-dragnbar-button-group" data-label="Image"></li>');
      // Add optional title to the group
      if (options && options.title && options.title !=='') {
        H5P.jQuery('<div class="h5p-dragnbar-button-title">' + options.title + '</div>')
          .appendTo($buttonGroup);
      }

      // Container for buttons
      const $buttonGroupButtons = H5P.jQuery('<ul class="h5p-dragnbar-button-buttons h5p-dragnbar-ul"></ul>')
        .appendTo($buttonGroup);

      // Add buttons
      buttons.forEach(function (button) {
        self.addButton(button, $buttonGroupButtons);
      });

      $buttonGroup.insertAfter($button.parent());
      return $buttonGroup;
    };
  }

  // Inherit support for events
  DragNBar.prototype = Object.create(EventDispatcher.prototype);
  DragNBar.prototype.constructor = DragNBar;

  return DragNBar;
})(H5P.EventDispatcher);

/**
 * Initializes editor functionality of DragNBar
 */
H5P.DragNBar.prototype.initEditor = function () {
  var that = this;
  this.dnr = new H5P.DragNResize(this.$container);
  this.dnr.snap = 10;

  // Update coordinates when element is resized
  this.dnr.on('moveResizing', function () {
    var offset = that.$element.offset();
    var position = that.$element.position();
    that.updateCoordinates(offset.left, offset.top, position.left, position.top);
  });

  // Set pressed to not lose focus at the end of resize
  this.dnr.on('stoppedResizing', function () {
    that.pressed = true;

    // Delete pressed after dnbelement has been refocused so it will lose focus on single click.
    setTimeout(function () {
      delete that.pressed;
    }, 10);
  });

  /**
   * Show transform panel listeners
   */
  this.dnr.on('showTransformPanel', function () {
    TransformPanel(true);
  });
  this.dnd.on('showTransformPanel', function () {
    TransformPanel(true);
  });

  /**
   * Hide transform panel listeners
   */
  this.dnr.on('hideTransformPanel', function () {
    if (!that.transformButtonActive) {
      TransformPanel(false);
    }
  });
  this.dnd.on('hideTransformPanel', function () {
    if (!that.transformButtonActive) {
      TransformPanel(false);
    }
  });

  /**
   * Trigger a context menu transform to either show or hide
   * the transform panel.
   *
   * @param {boolean} show
   */
  function TransformPanel(show) {
    if (that.focusedElement) {
      that.focusedElement.contextMenu.trigger('contextMenuTransform', {showTransformPanel: show});
    }
  }

  this.dnd.startMovingCallback = function () {
    that.dnd.min = {x: 0, y: 0};
    that.dnd.max = {
      x: that.$container.width() - that.$element.outerWidth(),
      y: that.$container.height() - that.$element.outerHeight()
    };

    if (that.newElement) {
      that.dnd.adjust.x = 10;
      that.dnd.adjust.y = 10;
      that.dnd.min.y -= that.$list.height();
    }

    return true;
  };

  this.dnd.stopMovingCallback = function () {
    var pos = {};

    if (that.newElement) {
      that.$container.css('overflow', '');
      if (Math.round(parseFloat(that.$element.css('top'))) < 0) {
        // Try to center element, but avoid overlapping
        pos.x = (that.dnd.max.x / 2);
        pos.y = (that.dnd.max.y / 2);
        that.avoidOverlapping(pos, that.$element);
      }
    }

    if (pos.x === undefined || pos.y === undefined ) {
      pos.x = Math.round(parseFloat(that.$element.css('left')));
      pos.y = Math.round(parseFloat(that.$element.css('top')));
    }

    that.stopMoving(pos.x, pos.y);
    that.newElement = false;

    delete that.dnd.min;
    delete that.dnd.max;
  };
};

/**
 * Tries to position the given element close to the requested coordinates.
 * Element can be skipped to check if spot is available.
 *
 * @param {object} pos
 * @param {number} pos.x
 * @param {number} pos.y
 * @param {(H5P.jQuery|Object)} element object with width&height if ran before insertion.
 */
H5P.DragNBar.prototype.avoidOverlapping = function (pos, $element) {
  // Determine size of element
  var size = $element;
  if (size instanceof H5P.jQuery) {
    size = window.getComputedStyle(size[0]);
    size = {
      width: parseFloat(size.width),
      height: parseFloat(size.height)
    };
  }
  else {
    $element = undefined;
  }

  // Determine how much they can be manuvered
  var containerStyle = window.getComputedStyle(this.$container[0]);
  var manX = parseFloat(containerStyle.width) - size.width;
  var manY = parseFloat(containerStyle.height) - size.height;

  var limit = 16;
  var attempts = 0;

  while (attempts < limit && this.elementOverlaps(pos.x, pos.y, $element)) {
    // Try to place randomly inside container
    if (manX > 0) {
      pos.x = Math.floor(Math.random() * manX);
    }
    if (manY > 0) {
      pos.y = Math.floor(Math.random() * manY);
    }
    attempts++;
  }
};

/**
 * Determine if moving the given element to its new position will cause it to
 * cover another element. This can make new or pasted elements difficult to see.
 * Element can be skipped to check if spot is available.
 *
 * @param {number} x
 * @param {number} y
 * @param {H5P.jQuery} [$element]
 * @returns {boolean}
 */
H5P.DragNBar.prototype.elementOverlaps = function (x, y, $element) {
  var self = this;

  // Use snap grid
  x = Math.round(x / 10);
  y = Math.round(y / 10);

  for (var i = 0; i < self.elements.length; i++) {
    var element = self.elements[i];
    if ($element !== undefined && element.$element === $element) {
      continue;
    }

    if (x === Math.round(parseFloat(element.$element.css('left')) / 10) &&
        y === Math.round(parseFloat(element.$element.css('top')) / 10)) {
      return true; // Stop loop
    }
  }

  return false;
};

// Key coordinates
var SHIFT = 16;
var CTRL = 17;
var DELETE = 46;
var BACKSPACE = 8;
var C = 67;
var V = 86;
var LEFT = 37;
var UP = 38;
var RIGHT = 39;
var DOWN = 40;

// Keep track of key state
var ctrlDown = false;

// How many pixels to move
var snapAmount = 1;

/**
 * Handle keydown events for the entire frame
 */
H5P.DragNBar.keydownHandler = function (event) {
  var self = event.data.instance;
  var activeElement = document.activeElement;

  // Don't care about keys if parent editor is not in focus
  // This means all editors using drag-n-bar need to set a tabindex
  // (it's not done inside this library)
  if (self.$dialogContainer.find(activeElement).length === 0 && self.$dialogContainer.get(0) !== activeElement) {
    return;
  }

  if (event.which === CTRL) {
    ctrlDown = true;

    if (self.dnd.snap !== undefined) {
      // Disable snapping
      delete self.dnd.snap;
    }
  }

  if (event.which === SHIFT) {
    snapAmount = self.dnd.snap;
  }

  if (event.which === LEFT && self.focusedElement) {
    if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
      return;
    }
    event.preventDefault();
    self.moveWithKeys(-snapAmount, 0);
  }
  else if (event.which === UP && self.focusedElement) {
    if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
      return;
    }
    event.preventDefault();
    self.moveWithKeys(0, -snapAmount);
  }
  else if (event.which === RIGHT && self.focusedElement) {
    if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
      return;
    }
    event.preventDefault();
    self.moveWithKeys(snapAmount, 0);
  }
  else if (event.which === DOWN && self.focusedElement) {
    if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
      return;
    }
    event.preventDefault();
    self.moveWithKeys(0, snapAmount);
  }
  else if (event.which === C && ctrlDown && self.focusedElement && self.$container.is(':visible')) {
    self.copyHandler(event);
  }
  else if (event.which === V && ctrlDown && window.localStorage && self.$container.is(':visible')) {
    self.pasteHandler(event);
  }
  else if ((event.which === DELETE || event.which === BACKSPACE) && self.focusedElement && self.$container.is(':visible') && activeElement.tagName.toLowerCase() !== 'input') {
    if (self.pressed === undefined) {
      self.focusedElement.contextMenu.trigger('contextMenuRemove');
      event.preventDefault(); // Prevent browser navigating back
    }
  }
};

/**
 * Copy object.
 * @param {Event} event - Event to check for copyable content.
 */
H5P.DragNBar.prototype.copyHandler = function (event) {

  if (!this.enableCopyPaste) {
    return;
  }

  var self = event === undefined ? this : event.data.instance;
  // Copy element params to clipboard
  var elementSize = window.getComputedStyle(self.focusedElement.$element[0]);
  var width = parseFloat(elementSize.width);
  var height = parseFloat(elementSize.height) / width;
  width = width / (parseFloat(window.getComputedStyle(self.$container[0]).width) / 100);
  height *= width;

  self.focusedElement.toClipboard(width, height);
  H5P.externalDispatcher.trigger('datainclipboard', {reset: false});
};

/**
 * Paste object.
 * @param {Event} event - Event to check for pastable content.
 */
H5P.DragNBar.prototype.pasteHandler = function (event) {
  var self = event === undefined ? this : event.data.instance;
  var activeElement = document.activeElement;

  // Don't paste if parent editor is not in focus
  if (!this.enableCopyPaste || self.preventPaste || self.dialog.isOpen() ||
      activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
    return;
  }

  if (self.$pasteButton.hasClass('disabled')) {
    // Inform user why pasting is not possible
    const pasteCheck = H5PEditor.canPastePlus(H5P.getClipboard(), this.libraries);
    if (pasteCheck.canPaste !== true) {
      if (pasteCheck.reason === 'pasteTooOld' || pasteCheck.reason === 'pasteTooNew') {
        self.confirmPasteError(pasteCheck.description, 0, function () {});
      }
      else {
        H5PEditor.attachToastTo(
          self.$pasteButton.get(0),
          pasteCheck.description,
          {position: {horizontal: 'center', vertical: 'above', noOverflowX: true}}
        );
      }
      return;
    }
  }

  var clipboardData = localStorage.getItem('h5pClipboard');
  if (clipboardData) {
    // Parse
    try {
      clipboardData = JSON.parse(clipboardData);
    }
    catch (err) {
      console.error('Unable to parse JSON from clipboard.', err);
      return;
    }

    // Update file URLs
    H5P.DragNBar.updateFileUrls(clipboardData.specific, function (path) {
      var isTmpFile = (path.substr(-4,4) === '#tmp');
      if (!isTmpFile && clipboardData.contentId) {
        // Comes from existing content

        let prefix;
        if (H5PEditor.contentId) {
          // .. to existing content
          prefix = '../' + clipboardData.contentId + '/';
        }
        else {
          // .. to new content
          prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';
        }
        return path.substr(0, prefix.length) === prefix ? path : prefix + path;
      }

      return path; // Will automatically be looked for in tmp folder
    });

    if (clipboardData.generic) {
      // Use reference instead of key
      clipboardData.generic = clipboardData.specific[clipboardData.generic];

      // Avoid multiple content with same ID
      delete clipboardData.generic.subContentId;
    }

    self.trigger('paste', clipboardData);
  }
};

/**
 * Set state of paste button.
 * @param {boolean} canPaste - If true, button will be enabled
 */
H5P.DragNBar.prototype.setCanPaste = function (canPaste) {
  canPaste = canPaste || false;
  if (this.$pasteButton) {
    this.$pasteButton.toggleClass('disabled', !canPaste);
  }
};

/**
 * Confirm replace if there is content selected
 *
 * @param {number} top Offset
 * @param {function} next Next callback
 */
H5P.DragNBar.prototype.confirmPasteError = function (message, top, next) {
  // Confirm changing library
  var confirmReplace = new H5P.ConfirmationDialog({
    headerText: H5PEditor.t('core', 'pasteError'),
    dialogText: message,
    cancelText: ' ',
    confirmText: H5PEditor.t('core', 'ok')
  }).appendTo(document.body);
  confirmReplace.on('confirmed', next);
  confirmReplace.show(top);
};

/**
 * Handle keypress events for the entire frame
 */
H5P.DragNBar.keypressHandler = function (event) {
  var self = event.data.instance;
  if (event.which === BACKSPACE && self.focusedElement && self.$container.is(':visible') && document.activeElement.tagName.toLowerCase() !== 'input') {
    event.preventDefault(); // Prevent browser navigating back
  }
};

/**
 * Handle keyup events for the entire frame
 */
H5P.DragNBar.keyupHandler = function (event) {
  var self = event.data.instance;

  if (event.which === CTRL) {
    // Update key state
    ctrlDown = false;

    // Enable snapping
    self.dnd.snap = 10;
  }
  if (event.which === SHIFT) {
    snapAmount = 1;
  }

  if (self.focusedElement && (event.which === LEFT || event.which === UP || event.which === RIGHT || event.which === DOWN)) {
    // Store position of element after moving
    var position = self.getElementSizeNPosition();
    self.stopMoving(Math.round(position.left), Math.round(position.top));
  }
};

/**
 * Handle click events for the entire frame
 */
H5P.DragNBar.clickHandler = function (event) {
  var self = event.data.instance;

  // Remove pressed on click
  delete self.pressed;
};

/**
 * Initialize click listeners
 */
H5P.DragNBar.prototype.initClickListeners = function () {
  var self = this;
  var index = self.instanceIndex;

  // Register event listeners
  var eventData = {
    instance: self
  };
  H5P.$body.on('keydown.dnb' + index, eventData, H5P.DragNBar.keydownHandler)
    .on('keypress.dnb' + index, eventData, H5P.DragNBar.keypressHandler)
    .on('keyup.dnb' + index, eventData, H5P.DragNBar.keyupHandler)
    .on('click.dnb' + index, eventData, H5P.DragNBar.clickHandler);

  // Set blur handler element if option has been specified
  var $blurHandlers = this.$container;
  if (this.$blurHandlers) {
    $blurHandlers = this.$blurHandlers;
  }

  function handleBlur() {
    // Remove coordinates picker if we didn't press an element.
    if (self.pressed !== undefined) {
      delete self.pressed;
    }
    else {
      self.blurAll();
      if (self.focusedElement !== undefined) {
        delete self.focusedElement;
      }
    }
  }

  $blurHandlers
    .keydown(function (e) {
      if (e.which === 9) { // pressed tab
        handleBlur();
      }
    })
    .click(handleBlur);
};

/**
 * Update file URLs. Useful when copying between different contents.
 *
 * @param {object} params Reference
 * @param {function} handler Modifies the path to work when pasted
 */
H5P.DragNBar.updateFileUrls = function (params, handler) {
  for (var prop in params) {
    if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
      var obj = params[prop];
      if (obj.path !== undefined && obj.mime !== undefined) {
        obj.path = handler(obj.path);
      }
      else {
        H5P.DragNBar.updateFileUrls(obj, handler);
      }
    }
  }
};

/**
 * Attaches the menu bar to the given wrapper.
 *
 * @param {jQuery} $wrapper
 * @returns {undefined}
 */
H5P.DragNBar.prototype.attach = function ($wrapper) {
  var self = this;
  $wrapper.html('');
  $wrapper.addClass('h5peditor-dragnbar');

  var $list = H5P.jQuery('<ul class="h5p-dragnbar-ul"></ul>').appendTo($wrapper);
  this.$list = $list;

  for (var i = 0; i < this.buttons.length; i++) {
    var button = this.buttons[i];

    if (i === this.overflowThreshold) {
      const $buttonMore = H5P.jQuery('<li class="h5p-dragnbar-li"><a href="#" title="' + H5PEditor.t('H5P.DragNBar', 'moreElements') + '" class="h5p-dragnbar-a h5p-dragnbar-more-button"></a><ul class="h5p-dragnbar-li-ul"></ul></li>');
      $list = $buttonMore
        .appendTo($list)
        .click(function (e) {
          $list.stop().slideToggle(300);
          e.preventDefault();
        })
        .children(':first')
        .next();

      // Close "more" on click somewhere else
      H5P.jQuery(document).click(function (event) {
        if (!H5P.jQuery(event.target).is($buttonMore.find('.h5p-dragnbar-more-button')) && $list.css('display') !== 'none') {
          $list.stop().slideToggle(300);
        }
      });
    }

    this.addButton(button, $list);
  }

  if (this.enableCopyPaste) {
    // Paste button
    this.$pasteButton = H5P.jQuery(
      '<li class="h5p-dragnbar-li paste-button disabled">' +
        '<a href="#" class="h5p-dragnbar-a h5p-dragnbar-paste-button" />' +
      '</li>'
    );

    H5P.jQuery('<span>', {
      'class': 'h5p-dragnbar-tooltip',
      'text': H5PEditor.t('H5P.DragNBar', 'paste')
    }).appendTo(this.$pasteButton);

    this.$pasteButton.find('.h5p-dragnbar-paste-button').click(function (event) {
      event.preventDefault(); // Avoid anchor click making window scroll
      self.pasteHandler();
    });
    if (this.buttons.length > this.overflowThreshold) {
      this.$pasteButton.insertAfter($list.parent());
    }
    else {
      this.$pasteButton.appendTo($list);
    }
  }

  this.containTooltips();
};

/**
 * Add button.
 *
 * @param {type} button
 * @param {Function} button.createElement Function for creating element
 * @param {type} $list
 * @returns {undefined}
 */
H5P.DragNBar.prototype.addButton = function (button, $list) {
  var that = this;

  const hasTitle = (button.title && button.title !== '');
  const ariaLabel = hasTitle ? ' aria-label="' + button.title + '"' : '';
  var $button = H5P.jQuery(
    '<li class="h5p-dragnbar-li" data-label="Image">' +
      '<a href="#" class="h5p-dragnbar-a h5p-dragnbar-' + button.id + '-button"' + ariaLabel + '></a>' +
    '</li>'
  ).appendTo($list);

  // Prevent empty tooltips (would show on Firefox)
  if (hasTitle) {
    H5P.jQuery('<span/>', {
      'class': 'h5p-dragnbar-tooltip',
      'text': button.title
    }).appendTo($button);
  }

  let $buttonGroup;
  if (button.type === 'group') {
    // Create dropdown button group
    $buttonGroup = this.addButtonGroup(button.buttons, $button, {title: button.titleGroup});
    $buttonGroup.addClass('h5peditor-dragnbar-gone');

    // Close group on click somewhere else
    H5P.jQuery(document).click(function (event) {
      const hitButton = H5P.jQuery(event.target).is($button); // Closing handled by button itself
      const hitButtonGroup = H5P.jQuery(event.target).closest('.h5p-dragnbar-button-group').length === 1;
      if (!hitButton && !hitButtonGroup) {
        $buttonGroup.toggleClass('h5peditor-dragnbar-gone', true);
        $button.find('.h5p-dragnbar-tooltip').toggleClass('h5peditor-dragnbar-gone', false);
      }
    });
  }

  $button
    .hover(function () {
      that.containTooltips();
    })
    .children()
    .click(function () {
      return false;
    }).mousedown(function (event) {
      if (event.which !== 1 || !that.enabled) {
        return;
      }

      // Switch between normal button and dropdown button group
      if (button.type === 'group') {
        if ($buttonGroup !== undefined) {
          // Set position here, because content types might add buttons out of order
          const offset = parseFloat($button.closest('.h5p-dragnbar').css('padding-left'));
          const position = $button.position().left - $buttonGroup.position().left - offset;
          if (position > 0) {
            $buttonGroup.css('left', position);
          }

          // Show dropdown and hide buttons tooltip
          $buttonGroup.toggleClass('h5peditor-dragnbar-gone');
          $button.find('.h5p-dragnbar-tooltip').toggleClass('h5peditor-dragnbar-gone');
        }
      }
      else {
        that.newElement = true;
        that.pressed = true;
        var createdElement = button.createElement();
        that.$element = createdElement;
        that.$container.css('overflow', 'visible');
        // y = 0 will make sure this press is regarded as outside of canvas to place element correctly
        that.dnd.press(that.$element, event.pageX, 0);
        that.focus(that.$element);
      }
    });
};

/**
 * Contain tooltips.
 *
 * @returns {undefined}
 */
H5P.DragNBar.prototype.containTooltips = function () {
  var that = this;

  var containerWidth = that.$container.outerWidth();

  this.$list.find('.h5p-dragnbar-tooltip').each(function () {
    // Get correct offset even if element is a child
    var width = H5P.jQuery(this).outerWidth();
    var parentWidth = H5P.jQuery(this).parents('.h5p-dragnbar-li').last().outerWidth();

    // Center the tooltip
    H5P.jQuery(this).css('left', -(width / 2) + (parentWidth / 2) + 'px');

    var offsetLeft = H5P.jQuery(this).position().left += H5P.jQuery(this).parents('.h5p-dragnbar-li').last().position().left;

    // If outside left edge
    if (offsetLeft <= 0) {
      H5P.jQuery(this).css('left', 0);
    }

    // If outside right edge
    if (offsetLeft + width > containerWidth) {
      H5P.jQuery(this).css('left', -(width - parentWidth));
    }
  });
};

/**
 * Change container.
 *
 * @param {jQuery} $container
 * @returns {undefined}
 */
H5P.DragNBar.prototype.setContainer = function ($container) {
  this.$container = $container;
  if (this.dnd) {
    this.dnd.$container = $container;
  }
  if (this.dnr) {
    this.dnr.$container = $container;
  }
};

/**
 * Handler for when the dragging stops. Makes sure the element is inside its container.
 *
 * @param {Number} left
 * @param {Number} top
 * @returns {undefined}
 */
H5P.DragNBar.prototype.stopMoving = function (left, top) {
  // Calculate percentage
  top = top / (this.$container.height() / 100);
  left = left / (this.$container.width() / 100);
  this.$element.css({top: top + '%', left: left + '%'});

  // Give others the result
  if (this.stopMovingCallback !== undefined) {
    this.stopMovingCallback(left, top);
  }
};

/**
 * @typedef SizeNPosition
 * @type Object
 * @property {number} width Outer width of the element
 * @property {number} height Outer height of the element
 * @property {number} left The X Coordinate
 * @property {number} top The Y Coordinate
 * @property {number} containerWidth Inner width of the container
 * @property {number} containerHeight Inner height of the container
 */

/**
 *
 * Only works when element is inside this.$container. This is assumed and no
 * are done.
 *
 * @param {H5P.jQuery} [$element] Defaults to focused element.
 * @throws 'No element given' if $element is missing
 * @return {SizeNPosition}
 */
H5P.DragNBar.prototype.getElementSizeNPosition = function ($element) {
  $element = $element || this.focusedElement.$element;
  if (!$element || !$element.length) {
    throw 'No element given';
  }

  // Always use outer size for element
  var size = $element[0].getBoundingClientRect();

  // Always use position relative to container for element
  var position = window.getComputedStyle($element[0]);

  // We include container inner size as well
  var containerSize = window.getComputedStyle(this.$container[0]);

  // Start preparing return value
  var sizeNPosition = {
    width: parseFloat(size.width),
    height: parseFloat(size.height),
    left: parseFloat(position.left),
    top: parseFloat(position.top),
    containerWidth: parseFloat(containerSize.width),
    containerHeight: parseFloat(containerSize.height)
  };

  if (position.left.substr(-1, 1) === '%' || position.top.substr(-1, 1) === '%') {
    // Some browsers(Safari) gets percentage value instead of pixel value.
    // Container inner size must be used to calculate such values.
    sizeNPosition.left *= (sizeNPosition.containerWidth / 100);
    sizeNPosition.top *= (sizeNPosition.containerHeight / 100);
  }

  return sizeNPosition;
};

/**
 * Makes it possible to move dnb elements by adding to it's x and y
 *
 * @param {number} x Amount to move on x-axis.
 * @param {number} y Amount to move on y-axis.
 */
H5P.DragNBar.prototype.moveWithKeys = function (x, y) {

  /**
   * Ensure that the given value is within the given boundaries.
   *
   * @private
   * @param {number} value
   * @param {number} min
   * @param {number} max
   * @return {number}
   */
  var withinBoundaries = function (value, min, max) {
    if (value < min) {
      value = min;
    }
    if (value > max) {
      value = max;
    }

    return value;
  };

  // Get size and position of current elemet in focus
  var sizeNPosition = this.getElementSizeNPosition();

  // Change position
  sizeNPosition.left += x;
  sizeNPosition.top += y;

  // Check that values are within boundaries
  sizeNPosition.left = withinBoundaries(sizeNPosition.left, 0, sizeNPosition.containerWidth - sizeNPosition.width);
  sizeNPosition.top = withinBoundaries(sizeNPosition.top, 0, sizeNPosition.containerHeight - sizeNPosition.height);

  // Determine new position style
  this.$element.css({
    left: sizeNPosition.left + 'px',
    top: sizeNPosition.top + 'px',
  });

  this.dnd.trigger('showTransformPanel');

  // Update position of context menu
  this.updateCoordinates(sizeNPosition.left, sizeNPosition.top, sizeNPosition.left, sizeNPosition.top);
};

/**
 * Makes it possible to focus and move the element around.
 * Must be inside $container.
 *
 * @param {H5P.jQuery} $element
 * @param {Object} [options]
 * @param {H5P.DragNBarElement} [options.dnbElement] Register new element with dnbelement
 * @param {boolean} [options.disableResize] Resize disabled
 * @param {boolean} [options.lock] Lock ratio during resize
 * @param {string} [clipboardData]
 * @returns {H5P.DragNBarElement} Reference to added dnbelement
 */
H5P.DragNBar.prototype.add = function ($element, clipboardData, options) {
  var self = this;
  options = options || {};
  if (this.isEditor && !options.disableResize) {
    this.dnr.add($element, options);
  }
  var newElement = null;

  // Check if element already exist
  if (options.dnbElement) {
    // Set element as added element
    options.dnbElement.setElement($element);
    newElement = options.dnbElement;
  }
  else {
    options.element = $element;
    options.disableCopy = !this.enableCopyPaste;
    newElement = new H5P.DragNBarElement(this, clipboardData, options);
    this.elements.push(newElement);
  }

  $element.addClass('h5p-dragnbar-element');

  if (this.isEditor) {
    if (newElement.contextMenu) {
      newElement.contextMenu.on('contextMenuCopy', function () {
        self.copyHandler();
      });
    }

    if ($element.attr('tabindex') === undefined) {
      // Make it possible to tab between elements.
      $element.attr('tabindex', '0');
    }

    $element.mousedown(function (event) {
      if (event.which !== 1) {
        return;
      }

      self.pressed = true;
      self.focus($element);
      if (self.dnr.active !== true) { // Moving can be stopped if the mousedown is doing something else
        self.dnd.press($element, event.pageX, event.pageY);
      }
    });
  }

  $element.focus(function () {
    self.focus($element);
  });

  return newElement;
};

/**
 * Remove given element in the UI.
 *
 * @param {H5P.DragNBarElement} dnbElement
 */
H5P.DragNBar.prototype.removeElement = function (dnbElement) {
  dnbElement.removeElement();
};

/**
 * Select the given element in the UI.
 *
 * @param {jQuery} $element
 * @returns {undefined}
 */
H5P.DragNBar.prototype.focus = function ($element) {
  var self = this;

  // Blur last focused
  if (this.focusedElement && this.focusedElement.$element !== $element) {
    this.focusedElement.blur();
    this.focusedElement.hideContextMenu();
  }

  if (!$element.is(':visible')) {
    return; // Do not focus invisible items (fixes FF refocus issue)
  }

  // Keep track of the element we have in focus
  self.$element = $element;
  this.dnd.setElement($element);

  // Show and update coordinates picker
  this.focusedElement = this.getDragNBarElement($element);

  if (this.focusedElement) {
    this.focusedElement.showContextMenu();
    this.focusedElement.focus();
    self.updateCoordinates();
  }

  // Wait for potential recreation of element
  setTimeout(function () {
    self.updateCoordinates();
    if (self.focusedElement && self.focusedElement.contextMenu && self.focusedElement.contextMenu.canResize) {
      self.focusedElement.contextMenu.updateDimensions();
    }
  }, 0);
};

/**
 * Get dnbElement from $element
 * @param {jQuery} $element
 * @returns {H5P.DragNBarElement} dnbElement with matching $element
 */
H5P.DragNBar.prototype.getDragNBarElement = function ($element) {
  var foundElement;
  // Find object with matching element
  this.elements.forEach(function (element) {
    if (element.getElement().is($element)) {
      foundElement = element;
    }
  });
  return foundElement;
};

/**
 * Deselect all elements in the UI.
 *
 * @returns {undefined}
 */
H5P.DragNBar.prototype.blurAll = function () {
  this.elements.forEach(function (element) {
    element.blur();
  });
  delete this.focusedElement;
};

/**
 * Resize DnB, make sure context menu is positioned correctly.
 */
H5P.DragNBar.prototype.resize = function () {
  var self = this;
  this.updateCoordinates();

  if (self.focusedElement) {
    self.focusedElement.resizeContextMenu(self.$element.offset().left - self.$element.parent().offset().left);
  }
};

/**
 * Update the coordinates of context menu.
 *
 * @param {Number} [left]
 * @param {Number} [top]
 * @param {Number} [x]
 * @param {Number} [y]
 * @returns {undefined}
 */
H5P.DragNBar.prototype.updateCoordinates = function (left, top, x, y) {
  if (!this.focusedElement) {
    return;
  }

  var containerPosition = this.$container.position();

  if (left && top && x && y) {
    left = x + containerPosition.left;
    top = y + containerPosition.top;
    this.focusedElement.updateCoordinates(left, top, x, y);
  }
  else {
    var position = this.$element.position();
    this.focusedElement.updateCoordinates(position.left + containerPosition.left, position.top + containerPosition.top, position.left, position.top);
  }
};

/**
 * Creates element data to store in the clipboard.
 *
 * @param {string} from Source of the element
 * @param {object} params Element options
 * @param {string} [generic] Which part of the parameters can be used by other libraries
 * @returns {string} JSON
 */
H5P.DragNBar.clipboardify = function (from, params, generic) {
  var clipboardData = {
    from: from,
    specific: params
  };

  if (H5PEditor.contentId) {
    clipboardData.contentId = H5PEditor.contentId;
  }

  // Add the generic part
  if (params[generic]) {
    clipboardData.generic = generic;
  }

  return clipboardData;
};

/**
 * Make sure the given element is inside the container.
 *
 * @param {SizeNPosition} sizeNPosition For the element
 * @returns {SizeNPosition} Only the properties which require change
 */
H5P.DragNBar.fitElementInside = function (sizeNPosition) {
  var style = {};

  if (sizeNPosition.left < 0) {
    // Element sticks out of the left side
    style.left = sizeNPosition.left = 0;
  }

  if (sizeNPosition.width + sizeNPosition.left > sizeNPosition.containerWidth) {
    // Element sticks out of the right side
    style.left = sizeNPosition.containerWidth - sizeNPosition.width;
    if (style.left < 0) {
      // Element is wider than the container
      style.left = 0;
      style.width = sizeNPosition.containerWidth;
    }
  }

  if (sizeNPosition.top < 0) {
    // Element sticks out of the top side
    style.top = sizeNPosition.top = 0;
  }

  if (sizeNPosition.height + sizeNPosition.top > sizeNPosition.containerHeight) {
    // Element sticks out of the bottom side
    style.top = sizeNPosition.containerHeight - sizeNPosition.height;
    if (style.top < 0) {
      // Element is higher than the container
      style.top = 0;
      style.height = sizeNPosition.containerHeight;
    }
  }

  return style;
};

/**
 * Clean up any event listeners
 */
H5P.DragNBar.prototype.remove = function () {
  var index = this.instanceIndex;

  H5P.$body.off('keydown.dnb' + index, H5P.DragNBar.keydownHandler)
    .off('keypress.dnb' + index, H5P.DragNBar.keypressHandler)
    .off('keyup.dnb' + index, H5P.DragNBar.keyupHandler)
    .off('click.dnb' + index, H5P.DragNBar.clickHandler);
};

/**
 * Toggle dragging on/off.
 * When off can not start dragging in any new elements until turned on.
 */
H5P.DragNBar.prototype.toggleDrag = function (enabled = true) {
  if (enabled === undefined) {
    this.enabled = !this.enabled;
  }
  else {
    this.enabled = enabled;
  }
}