/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

/**
 * @api
 */
define([
    'ko',
    'underscore',
    'mageUtils',
    'uiElement'
], function (ko, _, utils, Element) {
    'use strict';

    return Element.extend({
        defaults: {
            template: 'ui/grid/paging/sizes',
            minSize: 1,
            maxSize: 999,
            statefull: {
                options: true,
                value: true
            },
            listens: {
                value: 'onValueChange',
                options: 'onSizesChange'
            }
        },

        /**
         * Initializes sizes component.
         *
         * @returns {Sizes} Chainable.
         */
        initialize: function () {
            this._super()
                .updateArray();

            return this;
        },

        /**
         * Initializes observable properties.
         *
         * @returns {Sizes} Chainable.
         */
        initObservable: function () {
            this._super()
                .track([
                    'value',
                    'editing',
                    'customVisible',
                    'customValue'
                ])
                .track({
                    optionsArray: []
                });

            this._value = ko.pureComputed({
                read: ko.getObservable(this, 'value'),

                /**
                 * Validates input field prior to updating 'value' property.
                 */
                write: function (value) {
                    value = this.normalize(value);

                    this.value = value;
                    this._value.notifySubscribers(value);
                },

                owner: this
            });

            return this;
        },

        /**
         * Starts editing of the specified size.
         *
         * @param {Number} value - Value of the size.
         * @returns {Sizes} Chainable.
         */
        edit: function (value) {
            this.editing = value;

            return this;
        },

        /**
         * Discards changes made to the currently editable size.
         *
         * @returns {Sizes} Chainable.
         */
        discardEditing: function () {
            var value = this.editing;

            if (value) {
                this.updateSize(value, value);
            }

            return this;
        },

        /**
         * Invokes 'discardEditing' and 'discardCustom' actions.
         *
         * @returns {Sizes} Chainable.
         */
        discardAll: function () {
            this.discardEditing()
                .discardCustom();

            return this;
        },

        /**
         * Returns value of the first size.
         *
         * @returns {Number}
         */
        getFirst: function () {
            return this.optionsArray[0].value;
        },

        /**
         * Returns size which matches specified value.
         *
         * @param {Number} value - Value of the item.
         * @returns {Object|Undefined}
         */
        getSize: function (value) {
            return this.options[value];
        },

        /**
         * Sets current size to the specified value.
         *
         * @param {Number} value - Value of the size.
         * @returns {Sizes} Chainable.
         */
        setSize: function (value) {
            this.value = value;

            return this;
        },

        /**
         * Adds a new value to sizes list.
         *
         * @param {Number} value - Value to be added.
         * @returns {Sizes} Chainable.
         */
        addSize: function (value) {
            var size;

            if (!this.hasSize(value)) {
                size = this.createSize(value);

                this.set('options.' + value, size);
            }

            return this;
        },

        /**
         * Removes provided value from the sizes list.
         *
         * @param {Number} value - Value to be removed.
         * @returns {Sizes} Chainable.
         */
        removeSize: function (value) {
            if (!this.hasSize(value)) {
                return this;
            }

            this.remove('options.' + value);

            if (this.isSelected(value)) {
                this.setSize(this.getFirst());
            }

            return this;
        },

        /**
         * Updates existing value to the provided one. If new value
         * is not specified, then sizes' '_value' property will be taken.
         *
         * @param {Number} value - Existing value that should be updated.
         * @param {(Number|String)} [newValue=size._value] - New size value.
         * @returns {Sizes} Chainable.
         */
        updateSize: function (value, newValue) {
            var size = this.getSize(value);

            if (!size) {
                return this;
            }

            newValue = newValue || size._value;

            if (isNaN(+newValue)) {
                this.discardEditing();

                return this;
            }

            newValue = this.normalize(newValue);

            this.remove('options.' + value)
                .addSize(newValue);

            if (this.isSelected(value)) {
                this.setSize(newValue);
            }

            return this;
        },

        /**
         * Creates new editable size instance with the provided value.
         *
         * @param {Number} value - Value of the size.
         * @returns {Object}
         */
        createSize: function (value) {
            return {
                value: value,
                label: value,
                _value: value,
                editable: true
            };
        },

        /**
         * Checks if provided value exists in the sizes list.
         *
         * @param {Number} value - Value to be checked.
         * @returns {Boolean}
         */
        hasSize: function (value) {
            return !!this.getSize(value);
        },

        /**
         * Hides and clears custom field.
         *
         * @returns {Sizes} Chainable.
         */
        discardCustom: function () {
            this.hideCustom()
                .clearCustom();

            return this;
        },

        /**
         * Shows custom field.
         *
         * @returns {Sizes} Chainable.
         */
        showCustom: function () {
            this.customVisible = true;

            return this;
        },

        /**
         * Hides custom field.
         *
         * @returns {Sizes} Chainable.
         */
        hideCustom: function () {
            this.customVisible = false;

            return this;
        },

        /**
         * Empties value of the custom field.
         *
         * @returns {Sizes} Chainable.
         */
        clearCustom: function () {
            this.customValue = '';

            return this;
        },

        /**
         * Adds a new size specified in the custom field.
         *
         * @returns {Sizes} Chainable.
         */
        applyCustom: function () {
            var value = this.customValue;

            value = this.normalize(value);

            this.addSize(value)
                .setSize(value)
                .discardCustom();

            return this;
        },

        /**
         * Checks if custom field is visible.
         *
         * @returns {Boolean}
         */
        isCustomVisible: function () {
            return this.customVisible;
        },

        /**
         * Converts provided value to a number and puts
         * it in range between 'minSize' and 'maxSize' properties.
         *
         * @param {(Number|String)} value - Value to be normalized.
         * @returns {Number}
         */
        normalize: function (value) {
            value = +value;

            if (isNaN(value)) {
                return this.getFirst();
            }

            return utils.inRange(Math.round(value), this.minSize, this.maxSize);
        },

        /**
         * Updates the array of options.
         *
         * @returns {Sizes} Chainable.
         */
        updateArray: function () {
            var array = _.values(this.options);

            this.optionsArray = _.sortBy(array, 'value');

            return this;
        },

        /**
         * Checks if provided value is in editing state.
         *
         * @param {Number} value - Value to be checked.
         * @returns {Boolean}
         */
        isEditing: function (value) {
            return this.editing === value;
        },

        /**
         * Checks if provided value is selected.
         *
         * @param {Number} value - Value to be checked.
         * @returns {Boolean}
         */
        isSelected: function (value) {
            return this.value === value;
        },

        /**
         * Listener of the 'value' property changes.
         */
        onValueChange: function () {
            this.discardAll()
                .trigger('close');
        },

        /**
         * Listener of the 'options' object changes.
         */
        onSizesChange: function () {
            this.editing = false;

            this.updateArray();
        }
    });
});
