define(['jquery', 'lodash', 'interact', 'ui/component', 'ui/component/stackable', 'ui/transformer', 'ui/interactUtils', 'util/position', 'lib/uuid', 'handlebars', 'lib/handlebars/helpers', 'css!ui/dynamicComponent/css/dynamicComponent.css'], function ($$1, _, interact, componentFactory, makeStackable, transformer, interactUtils, position, uuid, Handlebars, Helpers0, dynamicComponent_css) { 'use strict';

    $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
    _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
    interact = interact && Object.prototype.hasOwnProperty.call(interact, 'default') ? interact['default'] : interact;
    componentFactory = componentFactory && Object.prototype.hasOwnProperty.call(componentFactory, 'default') ? componentFactory['default'] : componentFactory;
    makeStackable = makeStackable && Object.prototype.hasOwnProperty.call(makeStackable, 'default') ? makeStackable['default'] : makeStackable;
    interactUtils = interactUtils && Object.prototype.hasOwnProperty.call(interactUtils, 'default') ? interactUtils['default'] : interactUtils;
    position = position && Object.prototype.hasOwnProperty.call(position, 'default') ? position['default'] : position;
    uuid = uuid && Object.prototype.hasOwnProperty.call(uuid, 'default') ? uuid['default'] : uuid;
    Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
    Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;

    if (!Helpers0.__initialized) {
      Helpers0(Handlebars);
      Helpers0.__initialized = true;
    }
    var Template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
      this.compilerInfo = [4,'>= 1.0.0'];
    helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
      var buffer = "", stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, self=this;

    function program1(depth0,data) {
      
      
      return "\n        <div class=\"dynamic-component-layer\"></div>\n        ";
      }

    function program3(depth0,data) {
      
      
      return "\n    <div class=\"dynamic-component-resize-container\">\n        <div class=\"dynamic-component-resize-wrapper\">\n            <div class=\"dynamic-component-resize\"></div>\n        </div>\n    </div>\n    ";
      }

      buffer += "<div class=\"dynamic-component-container\">\n    <div class=\"dynamic-component-title-bar\">\n        <i class=\"title-bar-icon\"></i>\n        <a title=\""
        + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Close", options) : helperMissing.call(depth0, "__", "Close", options)))
        + "\" class=\"closer\" href=\"#\"></a>\n    </div>\n    <div class=\"dynamic-component-content\">\n        ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.draggableContainer), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\n    </div>\n    ";
      stack1 = helpers['if'].call(depth0, (depth0 && depth0.resizable), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
      if(stack1 || stack1 === 0) { buffer += stack1; }
      buffer += "\n</div>";
      return buffer;
      });
    function layoutTpl(data, options, asString) {
      var html = Template(data, options);
      return (asString || true) ? html : $(html);
    }

    /**
     * This program is free software; you can redistribute it and/or
     * modify it under the terms of the GNU General Public License
     * as published by the Free Software Foundation; under version 2
     * of the License (non-upgradable).
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU General Public License for more details.
     *
     * You should have received a copy of the GNU General Public License
     * along with this program; if not, write to the Free Software
     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
     *
     * Copyright (c) 2016-2019 (original work) Open Assessment Technologies SA ;
     */
    var _defaults = {
      resizable: true,
      draggable: true,
      width: 240,
      height: 360,
      minWidth: 150,
      maxWidth: 600,
      minHeight: 100,
      maxHeight: 900,
      largeWidthThreshold: 380,
      smallWidthThreshold: 200,
      draggableContainer: 'parent',
      preserveAspectRatio: true,
      top: 0,
      left: 0,
      proportionalResize: false
    };

    /**
     * Defines a dynamicComponent
     * @typedef {Object} dynamicComponent
     */
    var dynamicComponent = {
      /**
       * Reset the position and the size
       * @returns {dynamicComponent} chains
       * @fires dynamicComponent#reset
       */
      reset: function reset() {
        if (this.is('rendered') && !this.is('disabled')) {
          this.resetPosition();
          this.resetSize();

          /**
           * @event dynamicComponent#reset
           */
          this.trigger('reset');
        }
        return this;
      },
      /**
       * Reset the component position to it's original value
       * @returns {dynamicComponent} chains
       * @fires dynamicComponent#move
       */
      resetPosition: function resetPosition() {
        var $element = this.getElement();
        if (this.is('rendered') && !this.is('disabled')) {
          interactUtils.restoreOriginalPosition($element);
          this.setCoords();
          $element.css({
            left: this.config.left,
            top: this.config.top
          });

          /**
           * @event dynamicComponent#move
           * @param {Object} position - the new positions
           */
          this.trigger('move', this.position);
        }
        return this;
      },
      /**
       * compute x/y coords of the component according to the start position and the dragged offset
       */
      setCoords: function setCoords() {
        var $element = this.getElement();

        // fixme: attributes data-x and data-y are added by interactUtils.
        // If the position is really needed, it should be computed differently
        this.position.x = parseFloat($element.attr('data-x')) + this.config.left;
        this.position.y = parseFloat($element.attr('data-y')) + this.config.top;
      },
      /**
       * Sets the size of the content, and adapts the component's size accordingly.
       * @param {Number} width - The width of the content, the full width of the component will be adjusted.
       * @param {Number} height - The height of the content, the full height of the component will be adjusted.
       * @returns {dynamicComponent} chains
       * @fires dynamicComponent#resize
       */
      setContentSize: function setContentSize(width, height) {
        var $element, $titleBar;
        if (this.is('rendered') && !this.is('disabled')) {
          $element = this.getElement();
          $titleBar = $$1('.dynamic-component-title-bar', $element);
          this.config.width = width + $element.outerWidth() - $element.width();
          this.config.height = height + $element.outerHeight() - $element.height() + $titleBar.outerHeight();
          this.resetSize();
        }
        return this;
      },
      /**
       * Reset the component size to it's original value
       * @returns {dynamicComponent} chains
       * @fires dynamicComponent#resize
       */
      resetSize: function resetSize() {
        var self = this;
        var $element;
        var $content;
        var $titleBar;
        if (this.is('rendered') && !this.is('disabled')) {
          $element = this.getElement();
          $content = $$1('.dynamic-component-content', $element);
          $titleBar = $$1('.dynamic-component-title-bar', $element);
          $element.css({
            width: this.config.width + 'px',
            height: this.config.height + 'px'
          });

          //defer to ensure the next reflow occurs before calculating the content size
          _.defer(function () {
            self.position.width = self.config.width;
            self.position.height = self.config.height;
            self.position.contentWidth = $titleBar.width();
            self.position.contentHeight = $element.height() - $titleBar.outerHeight();
            $content.css({
              width: self.position.contentWidth + 'px',
              height: self.position.contentHeight + 'px'
            });

            /**
             * @event dynamicComponent#resize
             * @param {Object} position - the new positions
             */
            self.trigger('resize', self.position);
          });
        }
        return this;
      }
    };

    /**
     * Builds an instance of the dynamic component
     * @param {Object} specs - extra functions to extend the component
     * @param {Object} defaults
     * @param {jQuery|HTMLElement|String} [defaults.renderTo] - An optional container in which renders the component
     * @param {Boolean} [defaults.replace] - When the component is appended to its container, clears the place before
     * @param {String} [defaults.title] - title to be displayed in the title bar
     * @param {Boolean} [defaults.resizable] - allow the component to be resizable
     * @param {Boolean} [defaults.draggable] - allow the component to be draggable
     * @param {Number} [defaults.width] - the initial width of the component content
     * @param {Number} [defaults.height] - the intial height of the component content
     * @param {Number} [defaults.minWidth] - the min width for resize
     * @param {Number} [defaults.minHeight] - the min height for resize
     * @param {Number} [defaults.maxWidth] - the max width for resize
     * @param {Number} [defaults.maxHeight] - the max height for resize
     * @param {Number} [defaults.largeWidthThreshold] - the width below which the container will get the class "small"
     * @param {Number} [defaults.smallWidthThreshold] - the width above which the container will get the class "large"
     * @param {Boolean} [defaults.preserveAspectRatio] - preserve ratio on resize
     * @param {jQuery|HTMLElement|String} [defaults.draggableContainer] - the DOMElement the draggable/resizable component will be constraint in
     * @param {Number} [defaults.top] - the initial position top absolute to the relative positioned container
     * @param {Number} [defaults.left] - the initial position left absolute to the relative positioned container
     * @param {Number} [defaults.stackingScope] - in which scope to stack the component
     * @param {Boolean} [defaults.proportionalResize] - resize proportionally in both dimensions
     * @returns {component}
     */
    var dynComponentFactory = function dynComponentFactory(specs, defaults) {
      var component;
      defaults = _.defaults(defaults || {}, _defaults);
      specs = _.defaults(specs || {}, dynamicComponent);
      component = componentFactory(specs, defaults).setTemplate(layoutTpl).on('init', function () {
        this.id = uuid();
      }).on('render', function () {
        var self = this;
        var $element = this.getElement();
        var config = this.config;
        var $content = $$1('.dynamic-component-content', $element);
        var $titleBar = $$1('.dynamic-component-title-bar', $element);
        var $contentOverlay = $$1('.dynamic-component-layer', $element);
        var $resizeControll = $$1('.dynamic-component-resize-wrapper', $element);
        var pixelRatio = window.devicePixelRatio;
        var interactElement;

        //prevent parent machine OS to handle its touch gestures on this particular element
        $element.css('touch-action', 'none');
        //keeps moving/resizing positions data
        self.position = {
          x: this.config.left,
          y: this.config.top,
          width: this.config.width,
          height: this.config.height
        };

        //set size + position
        this.resetPosition();
        this.resetSize();

        //init controls
        $titleBar.on('click touchstart', '.closer', function (e) {
          e.preventDefault();
          self.hide();
        }).on('click touchstart', '.reset', function (e) {
          e.preventDefault();
          self.resetSize();
        });

        /**
         * Init the component content
         * @event dynamicComponent#rendercontent
         * @param {jQueryElement} $content - the rendered content
         */
        this.trigger('rendercontent', $content);

        //make the dynamic-component draggable + resizable
        interactElement = interact($element[0]);
        if (config.draggable) {
          interactElement.draggable({
            inertia: false,
            autoScroll: true,
            manualStart: true,
            restrict: _.merge(getRestriction(), {
              elementRect: {
                left: 0,
                right: 1,
                top: 0,
                bottom: 1
              }
            }),
            onmove: event => moveComponent(event.dx, event.dy),
            onend: () => this.setCoords()
          });

          //manually start interactjs draggable on the handle
          interact($titleBar[0]).on('down', function (event) {
            var interaction = event.interaction;
            interaction.start({
              name: 'drag'
            }, interactElement, $element[0]);
          });
          $$1(window).on('resize.dynamic-component-' + self.id, function () {
            var container;

            //on browser zoom, reset the position to prevent having
            //the component pushed outside it's container
            if (window.devicePixelRatio !== pixelRatio) {
              pixelRatio = window.devicePixelRatio;
              container = getDraggableContainer();
              if (position.isInside(container, $element[0]) === false) {
                self.resetPosition();
              }
            }
          });
        }
        if (config.resizable) {
          interactElement.resizable({
            preserveAspectRatio: config.preserveAspectRatio,
            autoScroll: true,
            restrict: getRestriction(),
            edges: {
              left: false,
              right: '.dynamic-component-resize-wrapper',
              bottom: '.dynamic-component-resize-wrapper',
              top: false
            },
            onmove: e => resizeComponent(e.rect.width, e.rect.height, e.deltaRect.left, e.deltaRect.top)
          });
        }
        interactElement.on('dragstart resizeinertiastart', function () {
          $contentOverlay.addClass('dragging-active');
          $content.addClass('moving');
          $titleBar.addClass('moving');
        }).on('dragend', function () {
          $contentOverlay.removeClass('dragging-active');
          $content.removeClass('moving');
          $titleBar.removeClass('moving');
        }).on('resizestart', function () {
          $contentOverlay.addClass('dragging-active');
          $resizeControll.addClass('resizing');
          $content.addClass('sizing');
        }).on('resizeend', function () {
          $contentOverlay.removeClass('dragging-active');
          $resizeControll.removeClass('resizing');
          $content.removeClass('sizing');
        });

        //interact sometimes doesn't trigger the start event if the move is quick and ends over an iframe...
        $element.on('mousedown', function () {
          if (/-resize/.test($$1('html').css('cursor')) && !$contentOverlay.hasClass('dragging-active')) {
            $contentOverlay.addClass('dragging-active');
          }
        });

        // use after event because the component is hidden during regular event
        this.after('show', () => {
          const viewport = getParent()[0].getBoundingClientRect();
          let {
            width,
            height
          } = this.position;
          let x = 0;
          let y = 0;
          let resize = false;
          if (width > viewport.width) {
            // if proportional resize enabled calculate scale rate based on width
            // and apply it to height
            height = config.proportionalResize ? config.minHeight * (viewport.width / config.minWidth) : viewport.width * (this.position.height / this.position.width);
            width = viewport.width;
            resize = true;
            if (this.position.x) {
              x = -this.position.x;
            }
          } else if (this.position.x + width > viewport.width) {
            x = -this.position.x;
          }
          if (height > viewport.height) {
            height = viewport.height;
            // if proportional resize enabled calculate scale rate based on height
            // and apply it to width
            width = config.proportionalResize ? config.minWidth * (viewport.height / config.minHeight) : viewport.height * (this.position.width / this.position.height);
            resize = true;
            if (this.position.y) {
              y = -this.position.y;
            }
          } else if (this.position.y + height > viewport.height) {
            y = -this.position.y;
          }
          if (resize) {
            resizeComponent(width, height, x, y, true);
          } else if (x || y) {
            moveComponent(x, y);
          }
        });
        function getRestriction() {
          var draggableContainer = getDraggableContainer();
          if (!draggableContainer) {
            return {
              restriction: 'parent',
              endOnly: false
            };
          }
          return {
            restriction: draggableContainer,
            endOnly: false
          };
        }
        function getDraggableContainer() {
          var draggableContainer = config.draggableContainer;
          if (draggableContainer instanceof $$1 && draggableContainer.length) {
            draggableContainer = draggableContainer[0];
          }
          return draggableContainer;
        }
        function getParent() {
          const draggableContainer = getDraggableContainer();
          if (!draggableContainer || draggableContainer === 'parent') {
            return $element.parent();
          }
          return $$1(draggableContainer);
        }

        /**
         * Callback for on move event
         * @param {Number} x
         * @param {Number} y
         */
        function moveComponent(x, y) {
          interactUtils.moveElement($element, x, y);
          self.setCoords();
          self.trigger('move', self.position);
        }

        /**
         * Callback for on resize event
         * @param {Number} width
         * @param {Number} height
         * @param {Number} x
         * @param {Number} y
         * @param {Boolean} updateElementOffset - force element to be moved to provided coords
         */
        function resizeComponent(width, height) {
          let x = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
          let y = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
          let updateElementOffset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
          const $parent = getParent();
          let {
            left: elementOffsetLeft,
            top: elementOffsetTop
          } = $element.offset();
          const parentOffset = $parent.offset();
          if (updateElementOffset) {
            elementOffsetLeft += x;
            elementOffsetTop += y;
          }

          // if proportional resize enabled calculate scale rate
          // and apply it to width and height

          const dimensions = calculateSize(width, height);
          width = calculateOverlap(dimensions.width, elementOffsetLeft, parentOffset.left, $parent.width());
          height = calculateOverlap(dimensions.height, elementOffsetTop, parentOffset.top, $parent.height());
          if (height !== null && width !== null) {
            if (width <= config.smallWidthThreshold) {
              $element.addClass('small').removeClass('large');
            } else if (width >= config.largeWidthThreshold) {
              $element.addClass('large').removeClass('small');
            } else {
              $element.removeClass('small').removeClass('large');
            }
            interactUtils.moveElement($element, width > config.minWidth && width < config.maxWidth || updateElementOffset ? x : 0, height > config.minHeight && height < config.maxHeight || updateElementOffset ? y : 0);
            self.position.width = width;
            self.position.height = height;
            self.setCoords();
            $element.css({
              width: width + 'px',
              height: height + 'px'
            });
            _.defer(function () {
              self.position.contentWidth = $titleBar.width();
              self.position.contentHeight = $element.height() - $titleBar.outerHeight();
              $content.css({
                width: self.position.contentWidth + 'px',
                height: self.position.contentHeight + 'px'
              });
              self.trigger('resize', self.position);
            });
          }
        }

        /**
         * check if given side of dynamic component is overlapping the container and calculate size of that side
         * @param {Number} side - side value of the component to check and calculate, cold be height or width
         * @param {Number} elOffset - offset value towards child to parent container
         * @param {Number} parentOffset - offset value towards parent container to its ancestor
         * @returns {Number|null} - new width or height values for the side of the component or null if there is no overlap between it and container
         */
        function calculateOverlap(side, elOffset, parentOffset, parentValue) {
          var result = side;
          var fullSizeSide = elOffset + side;
          var fullSizeParent = parentOffset + parentValue;
          if (fullSizeSide > fullSizeParent) {
            if (config.proportionalResize) {
              result = null;
            } else {
              result -= fullSizeSide - fullSizeParent;
            }
          }
          return result;
        }

        /**
         * calculates size of the dynamic component compared to  configured max/min values and scale rate coefficient applied
         * @param {Number} width - width of the component at the moment of resizing
         * @param {Number} height -  height of the component at the moment of resizing
         * @returns {width,height} - object with adjusted weight and height
         */
        function calculateSize(width, height) {
          var scaleRate;
          if (config.proportionalResize) {
            scaleRate = Math.max(width / config.minWidth, height / config.minHeight);
            width = config.minWidth * scaleRate;
            height = config.minHeight * scaleRate;
          }
          if (width < config.minWidth) {
            width = config.minWidth;
          } else if (width > config.maxWidth) {
            width = config.maxWidth;
          }
          if (height < config.minHeight) {
            height = config.minHeight;
          } else if (height > config.maxHeight) {
            height = config.maxHeight;
          }
          return {
            width: width,
            height: height
          };
        }
      }).on('destroy', function () {
        $$1(window).off('resize.dynamic-component-' + this.id);
      });
      return makeStackable(component, {
        stackingScope: defaults.stackingScope
      });
    };

    return dynComponentFactory;

});
