define(['jquery', 'lodash', 'ui/component', 'handlebars', 'lib/handlebars/helpers', 'ui/dynamicComponent'], function ($$1, _, component, Handlebars, Helpers0, dynamicComponent) { 'use strict';

    $$1 = $$1 && Object.prototype.hasOwnProperty.call($$1, 'default') ? $$1['default'] : $$1;
    _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
    component = component && Object.prototype.hasOwnProperty.call(component, 'default') ? component['default'] : component;
    Handlebars = Handlebars && Object.prototype.hasOwnProperty.call(Handlebars, 'default') ? Handlebars['default'] : Handlebars;
    Helpers0 = Helpers0 && Object.prototype.hasOwnProperty.call(Helpers0, 'default') ? Helpers0['default'] : Helpers0;
    dynamicComponent = dynamicComponent && Object.prototype.hasOwnProperty.call(dynamicComponent, 'default') ? dynamicComponent['default'] : dynamicComponent;

    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, functionType="function", escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing;


      buffer += "<div class=\"magnifier\">\n    <div class=\"level\">";
      if (helper = helpers.level) { stack1 = helper.call(depth0, {hash:{},data:data}); }
      else { helper = (depth0 && depth0.level); stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper; }
      buffer += escapeExpression(stack1)
        + "</div>\n    <div class=\"overlay\"></div>\n    <div class=\"inner\"></div>\n    <div class=\"controls close\">\n        <a href=\"#\" class=\"control\" data-control=\"zoomIn\" title=\""
        + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Magnify more", options) : helperMissing.call(depth0, "__", "Magnify more", options)))
        + "\"><span class=\"icon-add\"></span></a>\n        <a href=\"#\" class=\"control\"  data-control=\"zoomOut\" title=\""
        + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Magnify less", options) : helperMissing.call(depth0, "__", "Magnify less", options)))
        + "\"><span class=\"icon-remove\"></span></a>\n        <a href=\"#\" class=\"closeMagnifier\" data-control=\"closeMagnifier\" title=\""
        + escapeExpression((helper = helpers.__ || (depth0 && depth0.__),options={hash:{},data:data},helper ? helper.call(depth0, "Close Magnifier", options) : helperMissing.call(depth0, "__", "Close Magnifier", options)))
        + "\"><span class=\"icon-result-nok\"></span></a>\n    </div>\n</div>\n";
      return buffer;
      });
    function magnifierPanelTpl(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 (original work) Open Assessment Technologies SA ;
     */

    /**
     * The screen pixel ratio
     * @type {Number}
     */
    var screenRatio = window.screen.width / window.screen.height;

    /**
     * Standard debounce delay for heavy process
     * @type {Number}
     */
    var debounceDelay = 50;

    /**
     * Standard scrolling throttling for the scrolling
     * It can be lower than the debounce delay as it is lighter in process and it improves the user experience
     * @type {Number}
     */
    var scrollingDelay = 20;

    /**
     * The default base size
     * @type {Number}
     */
    var defaultBaseSize = 116;

    /**
     * The minimum zoom level
     * @type {Number}
     */
    var defaultLevelMin = 2;

    /**
     * The maximum zoom level
     * @type {Number}
     */
    var defaultLevelMax = 8;

    /**
     * The default zoom level
     * @type {Number}
     */
    var defaultLevel = defaultLevelMin;

    /**
     * Some default values
     * @type {Object}
     */
    var defaultConfig = {
      level: defaultLevel,
      levelMin: defaultLevelMin,
      levelMax: defaultLevelMax,
      levelStep: 0.5,
      baseSize: defaultBaseSize,
      maxRatio: 0.5
    };
    var dynamicComponentDefaultConfig = {
      draggable: true,
      resizable: true,
      preserveAspectRatio: false,
      width: defaultBaseSize * defaultLevel,
      height: defaultBaseSize * defaultLevel / screenRatio,
      minWidth: defaultBaseSize * defaultLevelMin,
      minHeight: defaultBaseSize * defaultLevelMin / screenRatio,
      stackingScope: 'test-runner',
      top: 50,
      left: 10
    };

    /**
     * Creates a magnifier panel component
     * @param {Object} config
     * @param {Number} [config.level] - The default zoom level
     * @param {Number} [config.levelMin] - The minimum allowed zoom level
     * @param {Number} [config.levelMax] - The maximum allowed zoom level
     * @param {Number} [config.levelStep] - The level increment applied when using the controls + and -
     * @param {Number} [config.baseSize] - The base size used to assign the width and the height according to the zoom level
     * @param {Number} [config.maxRatio] - The ratio for the maximum size regarding the size of the window
     * @returns {magnifierPanel} the component (initialized)
     */
    function magnifierPanelFactory(config) {
      var initConfig = _.defaults(config || {}, defaultConfig);
      var zoomLevelMin = parseFloat(initConfig.levelMin);
      var zoomLevelMax = parseFloat(initConfig.levelMax);
      var zoomLevelStep = parseFloat(initConfig.levelStep);
      var zoomLevel = adjustZoomLevel(initConfig.level);
      var maxRatio = parseFloat(initConfig.maxRatio);
      var $initTarget = null;
      var controls = null;
      var observer = null;
      var targetWidth, targetHeight, dx, dy;
      var scrolling = [];
      var dynamicComponentInstance;
      var dynamicComponentConfig = _.defaults(config ? config.component || {} : {}, dynamicComponentDefaultConfig);

      /**
       * @typedef {Object} magnifierPanel
       */
      var magnifierPanel = component({
        /**
         * Gets the current zoom level
         * @returns {Number}
         */
        getZoomLevel: function getZoomLevel() {
          return zoomLevel;
        },
        /**
         * Gets the targeted content the magnifier will zoom
         * @returns {jQuery}
         */
        getTarget: function getTarget() {
          return controls && controls.$target;
        },
        /**
         * Sets the targeted content the magnifier will zoom
         * @param {jQuery} $newTarget
         * @returns {magnifierPanel}
         * @fires targetchange
         * @fires update
         */
        setTarget: function setTarget($newTarget) {
          if (controls) {
            controls.$target = $newTarget;
            controls.$viewTarget = null;
            setScrollingListener();

            /**
             * @event magnifierPanel#targetchange
             * @param {jQuery} $target
             */
            this.trigger('targetchange', controls.$target);
            this.update();
          } else {
            $initTarget = $newTarget;
          }
          return this;
        },
        /**
         * Sets the zoom level of the magnifier
         * @param {Number} level
         * @returns {magnifierPanel}
         * @fires zoom
         */
        zoomTo: function zoomTo(level) {
          if (level && _.isFinite(level)) {
            zoomLevel = adjustZoomLevel(level);
          }
          applyZoomLevel();
          showZoomLevel();
          updateMaxSize();
          updateZoom();

          /**
           * @event magnifierPanel#zoom
           * @param {Number} zoomLevel
           */
          this.trigger('zoom', zoomLevel);
          return this;
        },
        /**
         * Increments the zoom level of the magnifier
         * @param {Number} step
         * @returns {magnifierPanel}
         * @fires zoom
         */
        zoomBy: function zoomBy(step) {
          if (step && _.isFinite(step)) {
            this.zoomTo(zoomLevel + parseFloat(step));
          }
          return this;
        },
        /**
         * Zoom-in using the configured level step
         * @returns {magnifierPanel}
         * @fires zoom
         */
        zoomIn: function zoomIn() {
          return this.zoomBy(zoomLevelStep);
        },
        /**
         * Zoom-out using the configured level step
         * @returns {magnifierPanel}
         * @fires zoom
         */
        zoomOut: function zoomOut() {
          return this.zoomBy(-zoomLevelStep);
        },
        /**
         * Places the magnifier sight at a particular position on the target content
         * @param {Number} x
         * @param {Number} y
         * @returns {magnifierPanel}
         */
        zoomAt: function zoomAt(x, y) {
          var position;
          if (controls) {
            position = this.translate(x, y);
            controls.$inner.css({
              top: -position.top,
              left: -position.left
            });
          }
        },
        /**
         * Translates screen coordinates to zoom coordinates
         * @param {Number} x
         * @param {Number} y
         * @returns {Object}
         */
        translate: function translate(x, y) {
          return {
            top: translateMagnifier(y, targetHeight, dynamicComponentInstance.position.height),
            left: translateMagnifier(x, targetWidth, dynamicComponentInstance.position.width)
          };
        },
        /**
         * Updates the magnifier with the target content
         * @returns {magnifierPanel}
         * @fires update
         */
        update: function update() {
          if (controls && controls.$target) {
            controls.$clone = controls.$target.clone().removeAttr('id');
            controls.$clone.find('iframe').remove();
            controls.$clone.find('[name],[id],[data-serial]').removeAttr('name id data-serial');
            controls.$inner.empty().append(controls.$clone);
            controls.$clone.find('audio').prop('muted', true);
            applySize();
            applyZoomLevel();
            updateZoom();
            updateMaxSize();
            applyScrolling();

            /**
             * @event magnifierPanel#update
             */
            this.trigger('update');
          }
          return this;
        }
      }, defaultConfig);

      /**
       * Will update the magnifier content with the actual content
       * @type {Function}
       */
      var updateMagnifier = _.debounce(_.bind(magnifierPanel.update, magnifierPanel), debounceDelay);

      /**
       * Will update the magnifier content with the scrolling position
       * @type {Function}
       */
      var scrollingListenerCallback = _.throttle(function (event) {
        var $target = $$1(event.target);
        var scrollingTop = event.target.scrollTop;
        var scrollLeft = event.target.scrollLeft;
        var scrollId, scrollData;

        //check if the element is already known as a scrollable element
        if (controls && controls.$clone && $target.data('magnifier-scroll')) {
          scrollId = $target.data('magnifier-scroll');
          scrollData = _.find(scrolling, {
            id: scrollId
          });
          scrollData.scrollTop = scrollingTop;
          scrollData.scrollLeft = scrollLeft;

          //if in clone, scroll it
          scrollInClone(scrollData);
        } else {
          //if the element is not yet identified as a scrollable element, tag it and register its id
          scrollId = _.uniqueId('scrolling_');
          $target.attr('data-magnifier-scroll', scrollId);
          scrolling.push({
            id: scrollId,
            scrollTop: scrollingTop,
            scrollLeft: scrollLeft
          });

          //update all
          magnifierPanel.update();
        }
      }, scrollingDelay);

      /**
       * Scroll an element in the clone
       *
       * @param {Object} scrollData
       * @param {String} scrollData.id
       * @param {Number} [scrollData.scrollTop]
       * @param {Number} [scrollData.scrollLeft]
       */
      function scrollInClone(scrollData) {
        var $clonedTarget;
        if (controls && controls.$clone && scrollData && scrollData.id) {
          $clonedTarget = controls.$clone.find(`[data-magnifier-scroll=${scrollData.id}]`);
          if ($clonedTarget.length) {
            if (_.isNumber(scrollData.scrollTop)) {
              $clonedTarget[0].scrollTop = scrollData.scrollTop;
            }
            if (_.isNumber(scrollData.scrollLeft)) {
              $clonedTarget[0].scrollLeft = scrollData.scrollLeft;
            }
          }
        }
      }

      /**
       * Capture all scroll positions of elements inside current target
       */
      function updateScrollPositions() {
        if (!controls || !controls.$target) {
          return;
        }
        const elements = [controls.$target];
        let scrollOffsetsChanged = false;
        while (elements.length) {
          const $currentElement = $$1(elements.shift());
          const scrollLeft = $currentElement.scrollLeft();
          const scrollTop = $currentElement.scrollTop();
          let scrollId = $currentElement.data('magnifier-scroll');
          elements.push(...Array.from($currentElement.children()));
          if (scrollLeft > 0 || scrollTop > 0 || scrollId) {
            scrollOffsetsChanged = true;
            if (scrollId) {
              const scrollData = _.find(scrolling, {
                id: scrollId
              });
              scrollData.scrollTop = scrollTop;
              scrollData.scrollLeft = scrollLeft;
            } else {
              scrollId = _.uniqueId('scrolling_');
              $currentElement.attr('data-magnifier-scroll', scrollId);
              scrolling.push({
                id: scrollId,
                scrollTop,
                scrollLeft
              });
            }
          }
        }

        // If there is any changes to scroll offset inside the target the magnifier should be updated
        if (scrollOffsetsChanged) {
          magnifierPanel.update();
        }
      }

      /**
       * Initializes the listener for scrolling event and transfer the scrolling
       */
      function setScrollingListener() {
        updateScrollPositions();
        window.addEventListener('scroll', scrollingListenerCallback, true);
      }

      /**
       * Stops the listener for scrolling event
       */
      function removeScrollingListener() {
        window.removeEventListener('scroll', scrollingListenerCallback, true);
      }

      /**
       * Applies scrolling programmatically from the recorded list of elements to be scrolled
       */
      function applyScrolling() {
        _.forEach(scrolling, scrollInClone);
      }

      /**
       * Adjusts a provided zoom level to fit the constraints
       * @param {Number|String} level
       * @returns {Number}
       */
      function adjustZoomLevel(level) {
        return Math.max(zoomLevelMin, Math.min(parseFloat(level), zoomLevelMax));
      }

      /**
       * Applies the zoom level to the content
       */
      function applyZoomLevel() {
        if (controls) {
          controls.$inner.css({
            transform: `scale(${zoomLevel})`
          });
        }
      }

      /**
       * Shows the zoom level using a CSS animation
       */
      function showZoomLevel() {
        var $newZoomLevel;
        if (controls) {
          $newZoomLevel = controls.$zoomLevel.clone(true).html(zoomLevel);
          controls.$zoomLevel.before($newZoomLevel).remove();
          controls.$zoomLevel = $newZoomLevel;
        }
      }

      /**
       * Updates the max size according to the window's size
       */
      function updateMaxSize() {
        if (!dynamicComponentInstance) {
          return;
        }
        const $window = $$1(window);
        dynamicComponentInstance.config.maxWidth = $window.width() * maxRatio;
        dynamicComponentInstance.config.maxHeight = $window.height() * maxRatio;
      }

      /**
       * Forwards the size of the target to the cloned content
       */
      function applySize() {
        if (controls && controls.$clone) {
          targetWidth = controls.$target.width();
          targetHeight = controls.$target.height();
          controls.$clone.width(targetWidth).height(targetHeight);
        }
      }

      /**
       * Place the zoom sight at the right place inside the magnifier
       */
      function updateZoom() {
        var position;
        if (controls && controls.$target) {
          position = dynamicComponentInstance.position;
          position.x += dx + controls.$target.scrollLeft();
          position.y += dy + controls.$target.scrollTop();
          magnifierPanel.zoomAt(position.x, position.y);
        }
      }

      /**
       * Creates the observer that will react to DOM changes to update the magnifier
       */
      function createObserver() {
        observer = new window.MutationObserver(updateMagnifier);
      }

      /**
       * Starts to observe the DOM of the magnifier target
       */
      function startObserver() {
        if (controls && controls.$target) {
          observer.observe(controls.$target.get(0), {
            childList: true,
            // Set to true if additions and removals of the target node's child elements (including text nodes) are to be observed.
            attributes: true,
            // Set to true if mutations to target's attributes are to be observed.
            characterData: true,
            // Set to true if mutations to target's data are to be observed.
            subtree: true // Set to true if mutations to target and target's descendants are to be observed.
          });
        }

        setScrollingListener();
      }

      /**
       * Stops to observe the DOM of the magnifier target
       */
      function stopObserver() {
        observer.disconnect();
        removeScrollingListener();
      }

      /**
       * Translates a screen coordinate into the magnifier
       * @param {Number} coordinate
       * @param {Number} actualSize
       * @param {Number} magnifierSize
       * @returns {Number}
       */
      function translateMagnifier(coordinate, actualSize, magnifierSize) {
        var delta = 0;
        var ratio = zoomLevel;
        if (actualSize) {
          delta = actualSize * (zoomLevel - 1) / 2;
          ratio = (actualSize * zoomLevel - magnifierSize) / (actualSize - magnifierSize);
        }
        return coordinate * ratio - delta;
      }

      /**
       * Gets the top element from a particular absolute point.
       * @param {Number} x - the page X-coordinate of the point
       * @param {Number} y - the page Y-coordinate of the point
       * @returns {HTMLElement}
       */
      function getElementFromPoint(x, y) {
        var el;

        // this is done to prevent working with undefined coordinates
        x = x || 0;
        y = y || 0;
        if (controls) {
          controls.$overlay.addClass('hidden');
        }
        el = document.elementFromPoint(x, y);
        if (controls) {
          controls.$overlay.removeClass('hidden');
        }
        return el;
      }

      /**
       * Find the related node in the target. The both trees must have the same content.
       * @param {jQuery|HTMLElement} node - the node for which find a relation
       * @param {jQuery|HTMLElement} root - the root of the tree that contains the actual node
       * @param {jQuery|HTMLElement} target - the root of the tree that could contains the related node
       * @returns {jQuery}
       */
      function findSourceNode(node, root, target) {
        var $node = $$1(node);
        var $root = $$1(root);
        var $target = $$1(target);
        var indexes = [$node.index()];

        // compute map of node's parents indexes
        $node.parents().each(function () {
          var $this = $$1(this);
          if (!$this.is($root)) {
            indexes.push($this.index());
          } else {
            return false;
          }
        });

        // the last index is related to the root, so ignore it
        indexes.pop();

        // now try to find the same node using the path provided by the indexes map
        if (indexes.length) {
          $node = $target;
          _.forEachRight(indexes, function (index) {
            $node = $node.children().eq(index);
            if (!$node.length) {
              return false;
            }
          });
        } else {
          // nothing to search for...
          $node = $$1();
        }
        return $node;
      }
      dynamicComponentInstance = dynamicComponent({}).on('rendercontent', function ($content) {
        // eslint-disable-next-line consistent-this
        var dynamicComponentContext = this;
        var $element = this.getElement();
        $element.addClass('magnifier-container');
        magnifierPanel.setTemplate(magnifierPanelTpl).on('render', function () {
          var self = this;
          var $component = this.getElement();
          this.setState('hidden', true);

          // compute the padding of the magnifier content
          dx = ($component.outerWidth() - $component.width()) / 2;
          dy = ($component.outerHeight() - $component.height()) / 2;
          controls = {
            $target: $initTarget,
            $inner: $$1('.inner', $component),
            $zoomLevel: $$1('.level', $component),
            $overlay: $$1('.overlay', $component)
          };
          $initTarget = null;

          // click on zoom-out control
          $component.on('click touchstart', '.control[data-control="zoomOut"]', function (event) {
            event.preventDefault();
            self.zoomOut();
          });

          // click on zoom-in control
          $component.on('click touchstart', '.control[data-control="zoomIn"]', function (event) {
            event.preventDefault();
            self.zoomIn();
          });

          // click on close controls
          $component.on('click touchstart', '.closeMagnifier', function (event) {
            event.preventDefault();
            self.hide();
            self.trigger('close');
          });

          // interact through the magnifier glass with the zoomed content
          $component.on('click touchstart', '.overlay', function (event) {
            findSourceNode(getElementFromPoint(event.pageX, event.pageY), controls.$inner, controls.$target).click().focus();
          });
          createObserver();
          updateMaxSize();
          applyZoomLevel();
        }).on('show', function () {
          updateMagnifier();
          startObserver();
          dynamicComponentContext.show();
        }).on('hide', function () {
          stopObserver();
          dynamicComponentContext.hide();
        }).on('destroy', function () {
          stopObserver();
          $initTarget = null;
          controls = null;
          observer = null;
          dynamicComponentContext.destroy();
        }).init(initConfig).render($content);
      }).on('down move resize', function () {
        updateZoom();
      }).on('resize', function () {
        updateMaxSize();
      }).init(dynamicComponentConfig);
      return magnifierPanel;
    }

    return magnifierPanelFactory;

});
