odoo.define('web.AbstractField', function (require) {
"use strict";

/**
 * This is the basic field widget used by all the views to render a field in a view.
 * These field widgets are mostly common to all views, in particular form and list
 * views.
 *
 * The responsabilities of a field widget are mainly:
 * - render a visual representation of the current value of a field
 * - that representation is either in 'readonly' or in 'edit' mode
 * - notify the rest of the system when the field has been changed by
 *   the user (in edit mode)
 *
 * Notes
 * - the widget is not supposed to be able to switch between modes.  If another
 *   mode is required, the view will take care of instantiating another widget.
 * - notify the system when its value has changed and its mode is changed to 'readonly'
 * - notify the system when some action has to be taken, such as opening a record
 * - the Field widget should not, ever, under any circumstance, be aware of
 *   its parent.  The way it communicates changes with the rest of the system is by
 *   triggering events (with trigger_up).  These events bubble up and are interpreted
 *   by the most appropriate parent.
 *
 * Also, in some cases, it may not be practical to have the same widget for all
 * views. In that situation, you can have a 'view specific widget'.  Just register
 * the widget in the registry prefixed by the view type and a dot.  So, for example,
 * a form specific many2one widget should be registered as 'form.many2one'.
 *
 * @module web.AbstractField
 */

var ajax = require('web.ajax');
var field_utils = require('web.field_utils');
var Widget = require('web.Widget');

var AbstractField = Widget.extend({
    events: {
        'keydown': '_onKeydown',
    },
    custom_events: {
        navigation_move: '_onNavigationMove',
    },

    /**
    * An object representing fields to be fetched by the model eventhough not present in the view
    * This object contains "field name" as key and an object as value.
    * That value object must contain the key "type"
    * see FieldBinaryImage for an example.
    */
    fieldDependencies: {},

    /**
     * If this flag is set to true, the field widget will be reset on every
     * change which is made in the view (if the view supports it). This is
     * currently a form view feature.
     */
    resetOnAnyFieldChange: false,
    /**
     * If this flag is given a string, the related BasicModel will be used to
     * initialize specialData the field might need. This data will be available
     * through this.record.specialData[this.name].
     *
     * @see BasicModel._fetchSpecialData
     */
    specialData: false,
    /**
     * to override to indicate which field types are supported by the widget
     *
     * @type Array<String>
     */
    supportedFieldTypes: [],

    /**
     * Abstract field class
     *
     * @constructor
     * @param {Widget} parent
     * @param {string} name The field name defined in the model
     * @param {Object} record A record object (result of the get method of
     *   a basic model)
     * @param {Object} [options]
     * @param {string} [options.mode=readonly] should be 'readonly' or 'edit'
     */
    init: function (parent, name, record, options) {
        this._super(parent);
        options = options || {};

        // 'name' is the field name displayed by this widget
        this.name = name;

        // the datapoint fetched from the model
        this.record = record;

        // the 'field' property is a description of all the various field properties,
        // such as the type, the comodel (relation), ...
        this.field = record.fields[name];

        // the 'viewType' is the type of the view in which the field widget is
        // instantiated. For standalone widgets, a 'default' viewType is set.
        this.viewType = options.viewType || 'default';

        // the 'attrs' property contains the attributes of the xml 'field' tag,
        // the inner views...
        var fieldsInfo = record.fieldsInfo[this.viewType];
        this.attrs = options.attrs || (fieldsInfo && fieldsInfo[name]) || {};

        // the 'additionalContext' property contains the attributes to pass through the context.
        this.additionalContext = options.additionalContext || {};

        // this property tracks the current (parsed if needed) value of the field.
        // Note that we don't use an event system anymore, using this.get('value')
        // is no longer valid.
        this.value = record.data[name];

        // recordData tracks the values for the other fields for the same record.
        // note that it is expected to be mostly a readonly property, you cannot
        // use this to try to change other fields value, this is not how it is
        // supposed to work. Also, do not use this.recordData[this.name] to get
        // the current value, this could be out of sync after a _setValue.
        this.recordData = record.data;

        // the 'string' property is a human readable (and translated) description
        // of the field. Mostly useful to be displayed in various places in the
        // UI, such as tooltips or create dialogs.
        this.string = this.attrs.string || this.field.string || this.name;

        // Widget can often be configured in the 'options' attribute in the
        // xml 'field' tag.  These options are saved (and evaled) in nodeOptions
        this.nodeOptions = this.attrs.options || {};

        // dataPointID is the id corresponding to the current record in the model.
        // Its intended use is to be able to tag any messages going upstream,
        // so the view knows which records was changed for example.
        this.dataPointID = record.id;

        // this is the res_id for the record in database.  Obviously, it is
        // readonly.  Also, when the user is creating a new record, there is
        // no res_id.  When the record will be created, the field widget will
        // be destroyed (when the form view switches to readonly mode) and a new
        // widget with a res_id in mode readonly will be created.
        this.res_id = record.res_id;

        // useful mostly to trigger rpcs on the correct model
        this.model = record.model;

        // a widget can be in two modes: 'edit' or 'readonly'.  This mode should
        // never be changed, if a view changes its mode, it will destroy and
        // recreate a new field widget.
        this.mode = options.mode || "readonly";

        // this flag tracks if the widget is in a valid state, meaning that the
        // current value represented in the DOM is a value that can be parsed
        // and saved.  For example, a float field can only use a number and not
        // a string.
        this._isValid = true;

        // this is the last value that was set by the user, unparsed.  This is
        // used to avoid setting the value twice in a row with the exact value.
        this.lastSetValue = undefined;

        // formatType is used to determine which format (and parse) functions
        // to call to format the field's value to insert into the DOM (typically
        // put into a span or an input), and to parse the value from the input
        // to send it to the server. These functions are chosen according to
        // the 'widget' attrs if is is given, and if it is a valid key, with a
        // fallback on the field type, ensuring that the value is formatted and
        // displayed according to the choosen widget, if any.
        this.formatType = this.attrs.widget in field_utils.format ?
                            this.attrs.widget :
                            this.field.type;
        // formatOptions (resp. parseOptions) is a dict of options passed to
        // calls to the format (resp. parse) function.
        this.formatOptions = {};
        this.parseOptions = {};

        // if we add decorations, we need to reevaluate the field whenever any
        // value from the record is changed
        if (this.attrs.decorations) {
            this.resetOnAnyFieldChange = true;
        }
    },
    /**
     * When a field widget is appended to the DOM, its start method is called,
     * and will automatically call render. Most widgets should not override this.
     *
     * @returns {Deferred}
     */
    start: function () {
        var self = this;
        return this._super.apply(this, arguments).then(function () {
            self.$el.attr('name', self.name);
            self.$el.addClass('o_field_widget');
            return self._render();
        });
    },

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

    /**
     * Activates the field widget. By default, activation means focusing and
     * selecting (if possible) the associated focusable element. The selecting
     * part can be disabled.  In that case, note that the focused input/textarea
     * will have the cursor at the very end.
     *
     * @param {Object} [options]
     * @param {boolean} [options.noselect=false] if false and the input
     *   is of type text or textarea, the content will also be selected
     * @param {Event} [options.event] the event which fired this activation
     * @returns {boolean} true if the widget was activated, false if the
     *                    focusable element was not found or invisible
     */
    activate: function (options) {
        if (this.isFocusable()) {
            var $focusable = this.getFocusableElement();
            $focusable.focus();
            if ($focusable.is('input[type="text"], textarea')) {
                $focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length;
                if (options && !options.noselect) {
                    $focusable.select();
                }
            }
            return true;
        }
        return false;
    },
    /**
     * This function should be implemented by widgets that are not able to
     * notify their environment when their value changes (maybe because their
     * are not aware of the changes) or that may have a value in a temporary
     * state (maybe because some action should be performed to validate it
     * before notifying it). This is typically called before trying to save the
     * widget's value, so it should call _setValue() to notify the environment
     * if the value changed but was not notified.
     *
     * @abstract
     * @returns {Deferred|undefined}
     */
    commitChanges: function () {},
    /**
     * Returns the main field's DOM element (jQuery form) which can be focused
     * by the browser.
     *
     * @returns {jQuery} main focusable element inside the widget
     */
    getFocusableElement: function () {
        return $();
    },
    /**
     * Returns true iff the widget has a visible element that can take the focus
     *
     * @returns {boolean}
     */
    isFocusable: function () {
        var $focusable = this.getFocusableElement();
        return $focusable.length && $focusable.is(':visible');
    },
    /**
     * this method is used to determine if the field value is set to a meaningful
     * value.  This is useful to determine if a field should be displayed as empty
     *
     * @returns {boolean}
     */
    isSet: function () {
        return !!this.value;
    },
    /**
     * A field widget is valid if it was checked as valid the last time its
     * value was changed by the user. This is checked before saving a record, by
     * the view.
     *
     * Note: this is the responsability of the view to check that required
     * fields have a set value.
     *
     * @returns {boolean} true/false if the widget is valid
     */
    isValid: function () {
        return this._isValid;
    },
    /**
     * this method is supposed to be called from the outside of field widgets.
     * The typical use case is when an onchange has changed the widget value.
     * It will reset the widget to the values that could have changed, then will
     * rerender the widget.
     *
     * @param {any} record
     * @param {OdooEvent} [event] an event that triggered the reset action. It
     *   is optional, and may be used by a widget to share information from the
     *   moment a field change event is triggered to the moment a reset
     *   operation is applied.
     * @returns {Deferred} A Deferred, which resolves when the widget rendering
     *   is complete
     */
    reset: function (record, event) {
        this._reset(record, event);
        return this._render() || $.when();
    },
    /**
     * Remove the invalid class on a field
     */
    removeInvalidClass: function () {
        this.$el.removeClass('o_field_invalid');
        this.$el.removeAttr('aria-invalid');
    },
    /**
     * Sets the given id on the focusable element of the field and as 'for'
     * attribute of potential internal labels.
     *
     * @param {string} id
     */
    setIDForLabel: function (id) {
        this.getFocusableElement().attr('id', id);
    },
    /**
     * add the invalid class on a field
     */
    setInvalidClass: function () {
        this.$el.addClass('o_field_invalid');
        this.$el.attr('aria-invalid', 'true');
    },
    /**
     * Update the modifiers with the newest value.
     * Now this.attrs.modifiersValue can be used consistantly even with
     * conditional modifiers inside field widgets, and without needing new
     * events or synchronization between the widgets, renderer and controller
     *
     * @param {Object | null} modifiers  the updated modifiers
     * @override
     */
    updateModifiersValue: function(modifiers) {
        this.attrs.modifiersValue = modifiers || {};
    },


    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Apply field decorations (only if field-specific decorations have been
     * defined in an attribute).
     *
     * @private
     */
    _applyDecorations: function () {
        var self = this;
        this.attrs.decorations.forEach(function (dec) {
            var isToggled = py.PY_isTrue(
                py.evaluate(dec.expression, self.record.evalContext)
            );
            self.$el.toggleClass(dec.className, isToggled);
        });
    },
    /**
     * Converts the value from the field to a string representation.
     *
     * @private
     * @param {any} value (from the field type)
     * @returns {string}
     */
    _formatValue: function (value) {
        var options = _.extend({}, this.nodeOptions, { data: this.recordData }, this.formatOptions);
        return field_utils.format[this.formatType](value, this.field, options);
    },
    /**
     * This method check if a value is the same as the current value of the
     * field.  For example, a fieldDate widget might want to use the moment
     * specific value isSame instead of ===.
     *
     * This method is used by the _setValue method.
     *
     * @private
     * @param {any} value
     * @returns {boolean}
     */
    _isSameValue: function (value) {
        return this.value === value;
    },
    /**
     * Converts a string representation to a valid value.
     *
     * @private
     * @param {string} value
     * @returns {any}
     */
    _parseValue: function (value) {
        return field_utils.parse[this.formatType](value, this.field, this.parseOptions);
    },
    /**
     * main rendering function.  Override this if your widget has the same render
     * for each mode.  Note that this function is supposed to be idempotent:
     * the result of calling 'render' twice is the same as calling it once.
     * Also, the user experience will be better if your rendering function is
     * synchronous.
     *
     * @private
     * @returns {Deferred|undefined}
     */
    _render: function () {
        if (this.attrs.decorations) {
            this._applyDecorations();
        }
        if (this.mode === 'edit') {
            return this._renderEdit();
        } else if (this.mode === 'readonly') {
            return this._renderReadonly();
        }
    },
    /**
     * Render the widget in edit mode.  The actual implementation is left to the
     * concrete widget.
     *
     * @private
     * @returns {Deferred|undefined}
     */
    _renderEdit: function () {
    },
    /**
     * Render the widget in readonly mode.  The actual implementation is left to
     * the concrete widget.
     *
     * @private
     * @returns {Deferred|undefined}
     */
    _renderReadonly: function () {
    },
    /**
     * pure version of reset, can be overridden, called before render()
     *
     * @private
     * @param {any} record
     * @param {OdooEvent} event the event that triggered the change
     */
    _reset: function (record, event) {
        this.lastSetValue = undefined;
        this.record = record;
        this.value = record.data[this.name];
        this.recordData = record.data;
    },
    /**
     * this method is called by the widget, to change its value and to notify
     * the outside world of its new state.  This method also validates the new
     * value.  Note that this method does not rerender the widget, it should be
     * handled by the widget itself, if necessary.
     *
     * @private
     * @param {any} value
     * @param {Object} [options]
     * @param {boolean} [options.doNotSetDirty=false] if true, the basic model
     *   will not consider that this field is dirty, even though it was changed.
     *   Please do not use this flag unless you really need it.  Our only use
     *   case is currently the pad widget, which does a _setValue in the
     *   renderEdit method.
     * @param {boolean} [options.notifyChange=true] if false, the basic model
     *   will not notify and not trigger the onchange, even though it was changed.
     * @param {boolean} [options.forceChange=false] if true, the change event will be
     *   triggered even if the new value is the same as the old one
     * @returns {Deferred}
     */
    _setValue: function (value, options) {
        // we try to avoid doing useless work, if the value given has not
        // changed.  Note that we compare the unparsed values.
        if (this.lastSetValue === value || (this.value === false && value === '')) {
            return $.when();
        }
        this.lastSetValue = value;
        try {
            value = this._parseValue(value);
            this._isValid = true;
        } catch (e) {
            this._isValid = false;
            this.trigger_up('set_dirty', {dataPointID: this.dataPointID});
            return $.Deferred().reject();
        }
        if (!(options && options.forceChange) && this._isSameValue(value)) {
            return $.when();
        }
        var def = $.Deferred();
        var changes = {};
        changes[this.name] = value;
        this.trigger_up('field_changed', {
            dataPointID: this.dataPointID,
            changes: changes,
            viewType: this.viewType,
            doNotSetDirty: options && options.doNotSetDirty,
            notifyChange: !options || options.notifyChange !== false,
            allowWarning: options && options.allowWarning,
            onSuccess: def.resolve.bind(def),
            onFailure: def.reject.bind(def),
        });
        return def;
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Intercepts navigation keyboard events to prevent their default behavior
     * and notifies the view so that it can handle it its own way.
     *
     * Note: the navigation keyboard events are stopped so that potential parent
     * abstract field does not trigger the navigation_move event a second time.
     * However, this might be controversial, we might wanna let the event
     * continue its propagation and flag it to say that navigation has already
     * been handled (TODO ?).
     *
     * @private
     * @param {KeyEvent} ev
     */
    _onKeydown: function (ev) {
        switch (ev.which) {
            case $.ui.keyCode.TAB:
                var event = this.trigger_up('navigation_move', {
                    direction: ev.shiftKey ? 'previous' : 'next',
                });
                if (event.is_stopped()) {
                    ev.preventDefault();
                    ev.stopPropagation();
                }
                break;
            case $.ui.keyCode.ENTER:
                ev.stopPropagation();
                this.trigger_up('navigation_move', {direction: 'next_line'});
                break;
            case $.ui.keyCode.ESCAPE:
                this.trigger_up('navigation_move', {direction: 'cancel', originalEvent: ev});
                break;
            case $.ui.keyCode.UP:
                ev.stopPropagation();
                this.trigger_up('navigation_move', {direction: 'up'});
                break;
            case $.ui.keyCode.RIGHT:
                ev.stopPropagation();
                this.trigger_up('navigation_move', {direction: 'right'});
                break;
            case $.ui.keyCode.DOWN:
                ev.stopPropagation();
                this.trigger_up('navigation_move', {direction: 'down'});
                break;
            case $.ui.keyCode.LEFT:
                ev.stopPropagation();
                this.trigger_up('navigation_move', {direction: 'left'});
                break;
        }
    },
    /**
     * Updates the target data value with the current AbstractField instance.
     * This allows to consider the parent field in case of nested fields. The
     * field which triggered the event is still accessible through ev.target.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onNavigationMove: function (ev) {
        ev.data.target = this;
    },
});

return AbstractField;

});
