define(['lodash', 'i18n', 'moment', 'core/format', 'core/logger'], function (_, __, moment, format, loggerFactory) { 'use strict';

    _ = _ && Object.prototype.hasOwnProperty.call(_, 'default') ? _['default'] : _;
    __ = __ && Object.prototype.hasOwnProperty.call(__, 'default') ? __['default'] : __;
    moment = moment && Object.prototype.hasOwnProperty.call(moment, 'default') ? moment['default'] : moment;
    format = format && Object.prototype.hasOwnProperty.call(format, 'default') ? format['default'] : format;
    loggerFactory = loggerFactory && Object.prototype.hasOwnProperty.call(loggerFactory, 'default') ? loggerFactory['default'] : loggerFactory;

    /**
     * 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) 2018 (original work) Open Assessment Technologies SA ;
     */
    var logger = loggerFactory('taoQtiTest/runner/plugins/controls/timer/timers');

    /**
     * We receive values in seconds, so we convert them to milliseconds
     */
    var precision = 1000;

    /**
     * The timer's scope
     */
    var scopes = ['item', 'section', 'testPart', 'test'];

    /**
     * Map qti class names to scopes
     */
    var scopeMapping = {
      assessmentTest: 'test',
      assessmentSection: 'section',
      assessmentItemRef: 'item'
    };

    /**
     * helps you get the scope from a scope or qti class name
     * @param {String} value - scope or qti class name
     * @returns {String?} the scope
     */
    var getScope = function getScope(value) {
      if (scopeMapping[value]) {
        return scopeMapping[value];
      }
      if (scopes.includes(value)) {
        return value;
      }
      return null;
    };

    /**
     * The text of warning messages
     * TODO add warning messages for other timers types
     */
    var warningMessages = {
      item: __('Warning – You have %s remaining to complete this item.'),
      section: __('Warning – You have %s remaining to complete this section.'),
      testPart: __('Warning – You have %s remaining to complete this test part.'),
      test: __('Warning – You have %s remaining to complete this test.')
    };

    /**
     * The text of warning messages for screenreader
     */
    const warningMessagesForScreenraeder = {
      item: __('You have %s remaining to complete the current item.'),
      section: __('You have %s left to answer remaining %s questions.'),
      testPart: __('You have %s left to answer remaining %s questions.'),
      test: __('You have %s left to answer remaining %s questions.')
    };

    /**
     * Get the timers objects from the time constraints andt the given config
     * @param {Object[]} timeConstraints - as defined in the testContext
     * @param {Boolean} isLinear - is the current navigation mode linear
     * @param {Object} [config] - timers config
     * @param {Object[]} [config.warnings] - the warnings to apply to the timers (max only for now)
     * @param {Object[]} [config.warnings] - the warnings to apply to the timers (max only for now)
     * @returns {timer[]} the timers
     */
    function getTimers(timeConstraints, isLinear, config) {
      var timers = {};

      /**
       * The warnings comes in a weird format (ie. {scope:{threshold:level}}) , so we reformat them
       */
      var constraintsWarnings = _.reduce(config.warnings, function (acc, warnings, qtiScope) {
        var scope = getScope(qtiScope);
        acc[scope] = _.map(warnings, function (value, key) {
          return {
            threshold: parseInt(key, 10) * precision,
            message: function applyMessage(remainingTime) {
              var displayRemaining = moment.duration(remainingTime / precision, 'seconds').humanize();
              return format(warningMessages[scope], displayRemaining);
            },
            level: value,
            shown: false
          };
        });
        return acc;
      }, {});

      /**
       * The warnings comes in a weird format (ie. {scope:[threshold, ...]}) , so we reformat them
       */
      const constraintsWarningsForScreenreader = _.reduce(config.warningsForScreenreader, (acc, warnings, qtiScope) => {
        const scope = getScope(qtiScope);
        acc[scope] = _.map(warnings, value => ({
          threshold: parseInt(value, 10) * precision,
          message: function applyMessage(remainingTime, unansweredQuestions) {
            const displayRemaining = moment.duration(remainingTime / precision, 'seconds').humanize();
            return format(warningMessagesForScreenraeder[scope], displayRemaining, unansweredQuestions);
          },
          scope,
          shown: false
        }));
        return acc;
      }, {});

      /**
       * Build a timer of a given type from a time constraints
       * @param {String} type - min, max, locked
       * @param {Object} constraintData
       * @returns {timer} timer
       */
      var buildTimer = function buildTimer(type, constraintData) {
        /**
         * @typedef {Object} timer
         * @property {String} id - identify the timer (for max, it's the source for backward compat)
         * @property {String} type - min, max or locked
         * @property {String} label - the title to display
         * @property {String} scope - the timer's scope (item, section, etc.)
         * @property {String} qtiClassName - the QTI class of the timers applies to
         * @property {String} source - the ID of the element the timers belongs to
         * @property {Number} extraTime - additional time data, object
         * @property {Number} originalTime - the starting value of the timer, never changes, in ms.
         * @property {Number} remainingTime - current value, in ms.
         * @property {Number} remainingWithoutExtraTime - remaining time without extra time, in ms.
         * @property {Number} total - total time (original time + extra time), in ms.
         */
        var timer = _.pick(constraintData, ['label', 'scope', 'source', 'extraTime', 'qtiClassName']);
        timer.type = type;
        timer.allowLateSubmission = constraintData.allowLateSubmission;
        if (type === 'min') {
          timer.id = `${type}-${constraintData.scope}-${constraintData.source}`;
          timer.originalTime = constraintData.minTime * precision;
          timer.remainingTime = constraintData.minTimeRemaining * precision;
        } else {
          timer.id = constraintData.source;
          timer.originalTime = constraintData.maxTime * precision;
          timer.remainingTime = constraintData.maxTimeRemaining * precision;
        }
        timer.remainingWithoutExtraTime = timer.remainingTime;
        if (timer.extraTime && timer.type !== 'min') {
          timer.extraTime.consumed = timer.extraTime.consumed * precision;
          timer.extraTime.remaining = timer.extraTime.remaining * precision;
          timer.extraTime.total = timer.extraTime.total * precision;
          timer.total = timer.originalTime + timer.extraTime.total;
          timer.remainingTime += timer.extraTime.remaining;
        }

        //TODO supports warnings for other types
        if (type === 'max' && _.isArray(constraintsWarnings[timer.scope])) {
          timer.warnings = constraintsWarnings[timer.scope];
        }
        if (_.isArray(constraintsWarningsForScreenreader[timer.scope])) {
          timer.warningsForScreenreader = constraintsWarningsForScreenreader[timer.scope];
        }
        const stats = config.questionsStats[timer.scope];
        timer.unansweredQuestions = stats && stats.questions - stats.answered;
        return timer;
      };
      _.forEach(timeConstraints, function (timeConstraint) {
        var constraintData = _.clone(timeConstraint);
        var newTimer;
        constraintData.scope = getScope(timeConstraint.scope || timeConstraint.qtiClassName);
        if (!constraintData.scope) {
          logger.warn('Wrong data, a time constraint should apply to a valid scope, skipping');
        } else if (constraintData.minTime === false && constraintData.maxTime === false) {
          logger.warn('Time constraint defined with no time, skipping');

          // minTime = maxTime -> one locked timer
        } else if (config.guidedNavigation && isLinear && constraintData.maxTime && constraintData.minTime && constraintData.minTime === constraintData.maxTime && constraintData.maxTime > 0) {
          newTimer = buildTimer('locked', constraintData);
          timers[newTimer.id] = newTimer;
        } else {
          //minTime -> min timer
          if (isLinear && constraintData.minTime && constraintData.minTime > 0) {
            newTimer = buildTimer('min', constraintData);
            timers[newTimer.id] = newTimer;
          }

          //maxTime -> max timer
          if (constraintData.maxTime && constraintData.maxTime > 0) {
            newTimer = buildTimer('max', constraintData);
            timers[newTimer.id] = newTimer;
          }
        }
      });
      logger.debug('Timers built from timeConstraints', timers);
      return timers;
    }

    return getTimers;

});
