odoo.define('survey.session_leaderboard', function (require) {
'use strict';

var publicWidget = require('web.public.widget');
var SESSION_CHART_COLORS = require('survey.session_colors');

publicWidget.registry.SurveySessionLeaderboard = publicWidget.Widget.extend({
    init: function (parent, options) {
        this._super.apply(this, arguments);

        this.surveyAccessToken = options.surveyAccessToken;
        this.$sessionResults = options.sessionResults;

        this.BAR_MIN_WIDTH = '3rem';
        this.BAR_WIDTH = '24rem';
        this.BAR_HEIGHT = '3.8rem';
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Shows the question leaderboard on screen.
     * It's based on the attendees score (descending).
     *
     * We fade out the $sessionResults to fade in our rendered template.
     *
     * The width of the progress bars is set after the rendering to enable a width css animation.
     */
    showLeaderboard: function (fadeOut, isScoredQuestion) {
        var self = this;

        var resolveFadeOut;
        var fadeOutPromise;
        if (fadeOut) {
            fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; });
            self.$sessionResults.fadeOut(400, function () {
                resolveFadeOut();
            });
        } else {
            fadeOutPromise = Promise.resolve();
            self.$sessionResults.hide();
            self.$('.o_survey_session_leaderboard_container').empty();
        }

        var leaderboardPromise = this._rpc({
            route: _.str.sprintf('/survey/session/leaderboard/%s', this.surveyAccessToken)
        });

        Promise.all([fadeOutPromise, leaderboardPromise]).then(function (results) {
            var leaderboardResults = results[1];
            var $renderedTemplate = $(leaderboardResults);
            self.$('.o_survey_session_leaderboard_container').append($renderedTemplate);

            self.$('.o_survey_session_leaderboard_item').each(function (index) {
                var rgb = SESSION_CHART_COLORS[index % 10];
                $(this)
                    .find('.o_survey_session_leaderboard_bar')
                    .css('background-color', `rgba(${rgb},1)`);
                $(this)
                    .find('.o_survey_session_leaderboard_bar_question')
                    .css('background-color', `rgba(${rgb},${0.4})`);
            });

            self.$el.fadeIn(400, async function () {
                if (isScoredQuestion) {
                    await self._prepareScores();
                    await self._showQuestionScores();
                    await self._sumScores();
                    await self._reorderScores();
                }
            });
        });
    },

    /**
     * Inverse the process, fading out our template to fade int the $sessionResults.
     */
    hideLeaderboard: function () {
        var self = this;
        this.$el.fadeOut(400, function () {
            self.$('.o_survey_session_leaderboard_container').empty();
            self.$sessionResults.fadeIn(400);
        });
    },

    /**
     * This method animates the passed jQuery element from 0 points to {totalScore} points.
     * It will create a nice "animated" effect of a counter increasing by {increment} until it
     * reaches the actual score.
     *
     * @param {$.Element} $scoreEl the element to animate
     * @param {Integer} currentScore the currently displayed score
     * @param {Integer} totalScore to total score to animate to
     * @param {Integer} increment the base increment of each animation iteration
     * @param {Boolean} plusSign wether or not we add a "+" before the score
     * @private
     */
    _animateScoreCounter: function ($scoreEl, currentScore, totalScore, increment, plusSign) {
        var self = this;
        setTimeout(function () {
            var nextScore = currentScore + increment;
            if (nextScore > totalScore) {
                nextScore = totalScore;
            }
            $scoreEl.text(`${plusSign ? '+ ' : ''}${Math.round(nextScore)} p`);

            if (nextScore < totalScore) {
                self._animateScoreCounter($scoreEl, nextScore, totalScore, increment, plusSign);
            }
        }, 25);
    },

    /**
     * Helper to move a score bar from its current position in the leaderboard
     * to a new position.
     *
     * @param {$.Element} $score the score bar to move
     * @param {Integer} position the new position in the leaderboard
     * @param {Integer} offset an offset in 'rem'
     * @param {Integer} timeout time to wait while moving before resolving the promise
     */
    _animateMoveTo: function ($score, position, offset, timeout) {
        var animationDone;
        var animationPromise = new Promise(function (resolve) {
            animationDone = resolve;
        });
        $score.css('top', `calc(calc(${this.BAR_HEIGHT} * ${position}) + ${offset}rem)`);
        setTimeout(animationDone, timeout);
        return animationPromise;
    },

    /**
     * Takes the leaderboard prior to the current question results
     * and reduce all scores bars to a small width (3rem).
     * We keep the small score bars on screen for 1s.
     *
     * This visually prepares the display of points for the current question.
     *
     * @private
     */
    _prepareScores: function () {
        var self = this;
        var animationDone;
        var animationPromise = new Promise(function (resolve) {
            animationDone = resolve;
        });
        setTimeout(function () {
            this.$('.o_survey_session_leaderboard_bar').each(function () {
                var currentScore = parseInt($(this)
                    .closest('.o_survey_session_leaderboard_item')
                    .data('currentScore'))
                if (currentScore && currentScore !== 0) {
                    $(this).css('transition', `width 1s cubic-bezier(.4,0,.4,1)`);
                    $(this).css('width', self.BAR_MIN_WIDTH);
                }
            });
            setTimeout(animationDone, 1000);
        }, 300);

        return animationPromise;
    },

    /**
     * Now that we have summed the score for the current question to the total score
     * of the user and re-weighted the bars accordingly, we need to re-order everything
     * to match the new ranking.
     *
     * In addition to moving the bars to their new position, we create a "bounce" effect
     * by moving the bar a little bit more to the top or bottom (depending on if it's moving up
     * the ranking or down), the moving it the other way around, then moving it to its final
     * position.
     *
     * (Feels complicated when explained but it's fairly simple once you see what it does).
     *
     * @private
     */
    _reorderScores: function () {
        var self = this;
        var animationDone;
        var animationPromise = new Promise(function (resolve) {
            animationDone = resolve;
        });
        setTimeout(function () {
            self.$('.o_survey_session_leaderboard_item').each(async function () {
                var $score = $(this);
                var currentPosition = parseInt($(this).data('currentPosition'));
                var newPosition = parseInt($(this).data('newPosition'));
                if (currentPosition !== newPosition) {
                    var offset = newPosition > currentPosition ? 2 : -2;
                    await self._animateMoveTo($score, newPosition, offset, 300);
                    $score.css('transition', 'top ease-in-out .1s');
                    await self._animateMoveTo($score, newPosition, offset * -0.3, 100);
                    await self._animateMoveTo($score, newPosition, 0, 0);
                    animationDone();
                }
            });
        }, 1800);

        return animationPromise;
    },

    /**
     * Will display the score for the current question.
     * We simultaneously:
     * - increase the width of "question bar"
     *   (faded out bar right next to the global score one)
     * - animate the score for the question (ex: from + 0 p to + 40 p)
     *
     * (We keep a minimum width of 3rem to be able to display '+30 p' within the bar).
     *
     * @private
     */
    _showQuestionScores: function () {
        var self = this;
        var animationDone;
        var animationPromise = new Promise(function (resolve) {
            animationDone = resolve;
        });
        setTimeout(function () {
            this.$('.o_survey_session_leaderboard_bar_question').each(function () {
                var $barEl = $(this);
                var width = `calc(calc(100% - ${self.BAR_WIDTH}) * ${$barEl.data('widthRatio')} + ${self.BAR_MIN_WIDTH})`;
                $barEl.css('transition', 'width 1s ease-out');
                $barEl.css('width', width);

                var $scoreEl = $barEl
                    .find('.o_survey_session_leaderboard_bar_question_score')
                    .text('0 p');
                var questionScore = parseInt($barEl.data('questionScore'));
                if (questionScore && questionScore > 0) {
                    var increment = parseInt($barEl.data('maxQuestionScore') / 40);
                    if (!increment || increment === 0){
                        increment = 1;
                    }
                    $scoreEl.text('+ 0 p');
                    console.log($barEl.data('maxQuestionScore'));
                    setTimeout(function () {
                        self._animateScoreCounter(
                            $scoreEl,
                            0,
                            questionScore,
                            increment,
                            true);
                    }, 400);
                }
                setTimeout(animationDone, 1400);
            });
        }, 300);

        return animationPromise;
    },

    /**
     * After displaying the score for the current question, we sum the total score
     * of the user so far with the score of the current question.
     *
     * Ex:
     * We have ('#' for total score before question and '=' for current question score):
     * 210 p ####=================================== +30 p John
     * We want:
     * 240 p ###################################==== +30 p John
     *
     * Of course, we also have to weight the bars based on the maximum score.
     * So if John here has 50% of the points of the leader user, both the question score bar
     * and the total score bar need to have their width divided by 2:
     * 240 p ##################== +30 p John
     *
     * The width of both bars move at the same time to reach their new position,
     * with an animation on the width property.
     * The new width of the "question bar" should represent the ratio of won points
     * when compared to the total points.
     * (We keep a minimum width of 3rem to be able to display '+30 p' within the bar).
     *
     * The updated total score is animated towards the new value.
     * we keep this on screen for 500ms before reordering the bars.
     *
     * @private
     */
    _sumScores: function () {
        var self = this;
        var animationDone;
        var animationPromise = new Promise(function (resolve) {
            animationDone = resolve;
        });
        // values that felt the best after a lot of testing
        var growthAnimation = 'cubic-bezier(.5,0,.66,1.11)';
        setTimeout(function () {
            this.$('.o_survey_session_leaderboard_item').each(function () {
                var currentScore = parseInt($(this).data('currentScore'));
                var updatedScore = parseInt($(this).data('updatedScore'));
                var increment = parseInt($(this).data('maxQuestionScore') / 40);
                if (!increment || increment === 0){
                    increment = 1;
                }
                self._animateScoreCounter(
                    $(this).find('.o_survey_session_leaderboard_score'),
                    currentScore,
                    updatedScore,
                    increment,
                    false);

                var maxUpdatedScore = parseInt($(this).data('maxUpdatedScore'));
                var baseRatio = updatedScore / maxUpdatedScore;
                var questionScore = parseInt($(this).data('questionScore'));
                var questionRatio = questionScore /
                    (updatedScore && updatedScore !== 0 ? updatedScore : 1);
                // we keep a min fixed with of 3rem to be able to display "+ 5 p"
                // even if the user already has 1.000.000 points
                var questionWith = `calc(calc(calc(100% - ${self.BAR_WIDTH}) * ${questionRatio * baseRatio}) + ${self.BAR_MIN_WIDTH})`;
                $(this)
                    .find('.o_survey_session_leaderboard_bar_question')
                    .css('transition', `width ease .5s ${growthAnimation}`)
                    .css('width', questionWith);

                var updatedScoreRatio = 1 - questionRatio;
                var updatedScoreWidth = `calc(calc(100% - ${self.BAR_WIDTH}) * ${updatedScoreRatio * baseRatio})`;
                $(this)
                    .find('.o_survey_session_leaderboard_bar')
                    .css('min-width', '0px')
                    .css('transition', `width ease .5s ${growthAnimation}`)
                    .css('width', updatedScoreWidth);

                setTimeout(animationDone, 500);
            });
        }, 1400);

        return animationPromise;
    }
});

return publicWidget.registry.SurveySessionLeaderboard;

});
