/* Copyright 2019 GRAP - Quentin DUPONT
 * Copyright 2020 Tecnativa - Alexandre Díaz
 * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */

odoo.define('web_widget_numeric_step.field', function (require) {
    "use strict";

    var field_utils = require('web.field_utils');
    var Registry = require('web.field_registry');
    var FieldFloat = require('web.basic_fields').FieldFloat;


    var NumericStep = FieldFloat.extend({
        template: 'web_widget_numeric_step',
        className: 'o_field_numeric_step o_field_number',
        events: _.extend({}, _.omit(FieldFloat.prototype.events, ['change', 'input']), {
            'mousedown .btn_numeric_step': '_onStepMouseDown',
            'touchstart .btn_numeric_step': '_onStepMouseDown',
            'click .btn_numeric_step': '_onStepClick',
            'wheel .input_numeric_step': '_onWheel',
            'keydown .input_numeric_step': '_onKeyDown',
            'change .input_numeric_step': '_onChange',
            'input .input_numeric_step': '_onInput',
        }),
        supportedFieldTypes: ['float', 'integer'],

        // Values in milliseconds used for mouse down smooth speed feature
        DEF_CLICK_DELAY: 400,
        MIN_DELAY: 50,
        SUBSTRACT_DELAY_STEP: 25,

        DELAY_THROTTLE_CHANGE: 200,


        init: function () {
            this._super.apply(this, arguments);

            // Widget config
            var max_val = this.nodeOptions.max;
            var min_val = this.nodeOptions.min;
            if (!_.isUndefined(min_val) && !_.isUndefined(max_val) && min_val > max_val) {
                min_val = this.nodeOptions.max;
                max_val = this.nodeOptions.min;
            }

            this._config = {
                'step': Number(this.nodeOptions.step) || 1,
                'min': Number(min_val),
                'max': Number(max_val),
            };

            var self = this;
            this._lazyOnChangeTrigger = _.debounce(function() {
                self.$input.trigger("change");
            }, this.DELAY_THROTTLE_CHANGE);
        },

        /**
         * Add global events listeners
         *
         * @override
         */
        start: function () {
            var self = this;
            this._click_delay = this.DEF_CLICK_DELAY;
            this._autoStep = false;
            return this._super.apply(this, arguments).then(function () {
                document.addEventListener(
                    'mouseup', $.proxy(self, "_onMouseUp"), false);
                document.addEventListener(
                    'touchend', $.proxy(self, "_onMouseUp"), false);
            });
        },

        /**
         * Transform database value to usable widget value
         *
         * @override
         */
        _formatValue: function (value) {
            if (this.mode === 'edit') {
                return this._sanitizeNumberValue(value);
            }
            return this._super.apply(this, arguments);
        },

        /**
         * Transform widget value to usable database value
         *
         * @override
         */
        _parseValue: function () {
            var parsedVal = this._super.apply(this, arguments);
            if (this.mode === 'edit') {
                return Number(parsedVal) || 0;
            }
            return parsedVal;
        },

        /**
         * Adds HTML attributes to the input
         *
         * @override
         */
        _prepareInput: function () {
            var result = this._super.apply(this, arguments);
            this.$input.attr(_.pick(this.nodeOptions, ['placeholder']));
            // InputField hard set the input type to 'text' or 'password',
            // we force it again to be 'tel'.
            // The widget uses 'tel' type because offers a good layout on
            // mobiles and can accept alphanumeric characters.
            // The bad news is that require implement all good 'number' type
            // features like the minus and plus buttons, steps, min and max...
            // Perhaps in a near future this can be improved to have the best of
            // two types without hacky developments.
            this.$input.attr('type', 'tel');
            return result;
        },

        /**
         * Select the proper widget input
         *
         * @override
         */
        _renderEdit: function () {
            this._prepareInput(this.$el.find('input.input_numeric_step'));
        },

        /**
         * Increase/Decrease widget input value
         *
         * @param {String} mode can be "plus" or "minus"
         */
        _doStep: function (mode) {
            var cval = 0;
            try {
                var field = this.record.fields[this.name];
                cval = field_utils.parse[field.type](this.$input.val())
            } catch {
                cval = this.value;
                mode = false; // Only set the value in this case
            }
            if (mode === 'plus') {
                cval += this._config.step;
            } else if (mode === 'minus') {
                cval -= this._config.step;
            }
            var nval = this._sanitizeNumberValue(cval);
            if (nval !== this.lastSetValue || !mode) {
                this.$input.val(nval);
                // Every time that user update the value we must trigger an
                // onchange method.
                this._lazyOnChangeTrigger();
            }
        },

        // Handle Events
        _onStepClick: function (ev) {
            if (!this._autoStep) {
                var mode = ev.target.dataset.mode;
                this._doStep(mode);
            }
            this._autoStep = false;
        },

        _onStepMouseDown: function (ev) {
            this._interval = setTimeout(
                $.proxy(this, "_whileMouseDown", ev), this._click_delay);
        },

        _onMouseUp: function () {
            clearTimeout(this._interval);
            this._interval = false;
            this._click_delay = this.DEF_CLICK_DELAY;
        },

        _whileMouseDown: function (ev) {
            this._autoStep = true;
            var mode = ev.target.dataset.mode;
            this._doStep(mode);
            if (this._click_delay > this.MIN_DELAY) {
                this._click_delay -= this.SUBSTRACT_DELAY_STEP;
            }

            this._onStepMouseDown(ev);
        },

        /**
         * Enable mouse wheel support
         *
         * @param {WheelEvent} ev
         */
        _onWheel: function (ev) {
            ev.preventDefault();
            if (ev.originalEvent.deltaY > 0) {
                this._doStep('minus');
            } else {
                this._doStep('plus');
            }
        },

        /**
         * Enable keyboard arrows support
         *
         * @param {KeyEvent} ev
         */
        _onKeyDown: function (ev) {
            if (ev.keyCode === $.ui.keyCode.UP) {
                this._doStep('plus');
            } else if (ev.keyCode === $.ui.keyCode.DOWN) {
                this._doStep('minus');
            }
        },

        /**
         * Sanitize user input value
         *
         * @override
         */
        _onChange: function (ev) {
            ev.target.value = this._sanitizeNumberValue(ev.target.value);
            return this._super.apply(this, arguments);
        },

        // Helper Functions
        /**
         * Check limits and precision of the value.
         * If the value 'is not a number', the function does nothing to
         * be good with other possible modules.
         *
         * @param {Number} value
         * @returns {Number}
         */
        _sanitizeNumberValue: function (value) {
            var cval = Number(value);
            if (_.isNaN(cval)) {
                return value;
            }
            if (!_.isNaN(this._config.min) && cval < this._config.min) {
                cval = this._config.min;
            } else if (!_.isNaN(this._config.max) && cval > this._config.max) {
                cval = this._config.max;
            }

            var field = this.record.fields[this.name];
            var formattedValue = field_utils.format[field.type](cval, field, {
                data: this.record.data,
                escape: true,
                isPassword: false,
                digits: field.digits,
            });
            return formattedValue;
        },
    });

    Registry.add('numeric_step', NumericStep);

    return NumericStep;

});
