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

/**
 * The Controller class is the class coordinating the model and the renderer.
 * It is the C in MVC, and is what was formerly known in Odoo as a View.
 *
 * Its role is to listen to events bubbling up from the model/renderer, and call
 * the appropriate methods if necessary.  It also render control panel buttons,
 * and react to changes in the search view.  Basically, all interactions from
 * the renderer/model with the outside world (meaning server/reading in session/
 * reading localstorage, ...) has to go through the controller.
 */

var ActionMixin = require('web.ActionMixin');
var ajax = require('web.ajax');
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var mvc = require('web.mvc');

var QWeb = core.qweb;

var AbstractController = mvc.Controller.extend(ActionMixin, {
    custom_events: {
        get_controller_query_params: '_onGetControllerQueryParams',
        navigation_move: '_onNavigationMove',
        open_record: '_onOpenRecord',
        search: '_onSearch',
        switch_view: '_onSwitchView',
    },
    events: {
        'click a[type="action"]': '_onActionClicked',
    },

    /**
     * @override
     * @param {string} params.modelName
     * @param {string} [params.controllerID] an id to ease the communication
     *   with upstream components
     * @param {ControlPanelController} [params.controlPanel]
     * @param {any} [params.handle] a handle that will be given to the model (some id)
     * @param {boolean} params.isMultiRecord
     * @param {Object[]} params.actionViews
     * @param {string} params.viewType
     */
    init: function (parent, model, renderer, params) {
        this._super.apply(this, arguments);
        this._controlPanel = params.controlPanel;
        this._title = params.displayName;
        this.modelName = params.modelName;
        this.activeActions = params.activeActions;
        this.controllerID = params.controllerID;
        this.initialState = params.initialState;
        this.bannerRoute = params.bannerRoute;
        this.isMultiRecord = params.isMultiRecord;
        this.actionViews = params.actionViews;
        this.viewType = params.viewType;
        // use a DropPrevious to correctly handle concurrent updates
        this.dp = new concurrency.DropPrevious();
    },
    /**
     * Simply renders and updates the url.
     *
     * @returns {Deferred}
     */
    start: function () {
        var self = this;

        this.$el.addClass('o_view_controller');

        return this._super.apply(this, arguments).then(function () {
            if (self._controlPanel) {
                // render the ControlPanel elements (buttons, pager, sidebar...)
                self.controlPanelElements = self._renderControlPanelElements();
                self._controlPanel.$el.prependTo(self.$el);
            }

            return self._update(self.initialState);
        });
    },
    /**
     * @override
     */
    destroy: function () {
        if (this.$buttons) {
            this.$buttons.off();
        }
        if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
            this.controlPanelElements.$switch_buttons.off();
        }
        this._super.apply(this, arguments);
    },
    /**
     * Called each time the controller is attached into the DOM.
     */
    on_attach_callback: function () {
        if (this._controlPanel) {
            this._controlPanel.on_attach_callback();
        }
        this.renderer.on_attach_callback();
    },
    /**
     * Called each time the controller is detached from the DOM.
     */
    on_detach_callback: function () {
        if (this._controlPanel) {
            this._controlPanel.on_detach_callback();
        }
        this.renderer.on_detach_callback();
    },

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

    /**
     * @override
     */
    canBeRemoved: function () {
        // AAB: get rid of this option when on_hashchange mechanism is improved
        return this.discardChanges(undefined, {readonlyIfRealDiscard: true});
    },
    /**
     * Discards the changes made on the record associated to the given ID, or
     * all changes made by the current controller if no recordID is given. For
     * example, when the user opens the 'home' screen, the action manager calls
     * this method on the active view to make sure it is ok to open the home
     * screen (and lose all current state).
     *
     * Note that it returns a deferred, because the view could choose to ask the
     * user if he agrees to discard.
     *
     * @param {string} [recordID]
     *        if not given, we consider all the changes made by the controller
     * @returns {Deferred} resolved if properly discarded, rejected otherwise
     */
    discardChanges: function (recordID) {
        return $.when();
    },
    /**
     * Export the state of the controller containing information that is shared
     * between different controllers of a same action (like the current
     * searchQuery of the controlPanel).
     *
     * @returns {Object}
     */
    exportState: function () {
        var state = {};
        if (this._controlPanel) {
            state.cpState = this._controlPanel.exportState();
        }
        return state;
    },
    /**
     * Gives the focus to the renderer
     */
    giveFocus: function() {
        this.renderer.giveFocus();
    },
    /**
     * The use of this method is discouraged.  It is still snakecased, because
     * it currently is used in many templates, but we will move to a simpler
     * mechanism as soon as we can.
     *
     * @deprecated
     * @param {string} action type of action, such as 'create', 'read', ...
     * @returns {boolean}
     */
    is_action_enabled: function (action) {
        return this.activeActions[action];
    },
    /**
     * Short helper method to reload the view
     *
     * @param {Object} [params] This object will simply be given to the update
     * @returns {Deferred}
     */
    reload: function (params) {
        params = params || {};
        var def;
        var controllerState = params.controllerState || {};
        var cpState = controllerState.cpState;
        if (this._controlPanel && cpState) {
            def = this._controlPanel.importState(cpState).then(function (searchQuery) {
                params = _.extend({}, params, searchQuery);
            });
        }
        return $.when(def).then(this.update.bind(this, params, {}));
    },
    /**
     * For views that require a pager, this method will be called to allow the
     * controller to instantiate and render a pager. Note that in theory, the
     * controller can actually render whatever he wants in the pager zone.  If
     * your view does not want a pager, just let this method empty.
     *
     * @param {jQuery Node} $node
     */
    renderPager: function ($node) {
    },
    /**
     * Same as renderPager, but for the 'sidebar' zone (the zone with the menu
     * dropdown in the control panel next to the buttons)
     *
     * @param {jQuery Node} $node
     */
    renderSidebar: function ($node) {
    },
    /**
     * This is the main entry point for the controller.  Changes from the search
     * view arrive in this method, and internal changes can sometimes also call
     * this method.  It is basically the way everything notifies the controller
     * that something has changed.
     *
     * The update method is responsible for fetching necessary data, then
     * updating the renderer and wait for the rendering to complete.
     *
     * @param {Object} params will be given to the model and to the renderer
     * @param {Object} [options]
     * @param {boolean} [options.reload=true] if true, the model will reload data
     *
     * @returns {Deferred}
     */
    update: function (params, options) {
        var self = this;
        var shouldReload = (options && 'reload' in options) ? options.reload : true;
        var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
        // we check here that the updateIndex of the control panel hasn't changed
        // between the start of the update request and the moment the controller
        // asks the control panel to update itself ; indeed, it could happen that
        // another action/controller is executed during this one reloads itself,
        // and if that one finishes first, it replaces this controller in the DOM,
        // and this controller should no longer update the control panel.
        // note that this won't be necessary as soon as each controller will have
        // its own control panel
        var cpUpdateIndex = this._controlPanel && this._controlPanel.updateIndex;
        return this.dp.add(def).then(function (handle) {
            if (self._controlPanel && cpUpdateIndex !== self._controlPanel.updateIndex) {
                return;
            }
            self.handle = handle || self.handle; // update handle if we reloaded
            var state = self.model.get(self.handle);
            var localState = self.renderer.getLocalState();
            return self.dp.add(self.renderer.updateState(state, params)).then(function () {
                if (self._controlPanel && cpUpdateIndex !== self._controlPanel.updateIndex) {
                    return;
                }
                self.renderer.setLocalState(localState);
                return self._update(state);
            });
        });
    },

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

    /**
     * This method is the way a view can notifies the outside world that
     * something has changed.  The main use for this is to update the url, for
     * example with a new id.
     *
     * @private
     * @param {Object} [state] information that will be pushed to the outside
     *   world
     */
    _pushState: function (state) {
        this.trigger_up('push_state', {
            controllerID: this.controllerID,
            state: state || {},
        });
    },
    /**
     * Renders the html provided by the route specified by the
     * bannerRoute attribute on the controller (banner_route in the template).
     * Renders it before the view output and add a css class 'o_has_banner' to it.
     * There can be only one banner displayed at a time.
     *
     * If the banner contains stylesheet links or js files, they are moved to <head>
     * (and will only be fetched once).
     *
     * Route example:
     * @http.route('/module/hello', auth='user', type='json')
     * def hello(self):
     *     return {'html': '<h1>hello, world</h1>'}
     *
     * @private
     * @returns {Deferred}
     */
    _renderBanner: function () {
        if (this.bannerRoute !== undefined) {
            var self = this;
            return this.dp
                .add(this._rpc({route: this.bannerRoute}))
                .then(function (response) {
                    if (!response.html) {
                        self.$el.removeClass('o_has_banner');
                        return $.when();
                    }
                    self.$el.addClass('o_has_banner');
                    var $banner = $(response.html);
                    // we should only display one banner at a time
                    if (self._$banner && self._$banner.remove) {
                        self._$banner.remove();
                    }
                    // Css and js are moved to <head>
                    var defs = [];
                    $('link[rel="stylesheet"]', $banner).each(function (i, link) {
                        defs.push(ajax.loadCSS(link.href));
                        link.remove();
                    });
                    $('script[type="text/javascript"]', $banner).each(function (i, js) {
                        defs.push(ajax.loadJS(js.src));
                        js.remove();
                    });
                    return $.when.apply($, defs).then(function () {
                        $banner.prependTo(self.$('> .o_content'));
                        self._$banner = $banner;
                    });
                });
        }
        return $.when();
    },
    /**
     * Renders the control elements (buttons, pager and sidebar) of the current
     * view.
     *
     * @private
     * @returns {Object} an object containing the control panel jQuery elements
     */
    _renderControlPanelElements: function () {
        var elements = {
            $buttons: $('<div>'),
            $sidebar: $('<div>'),
            $pager: $('<div>'),
        };

        this.renderButtons(elements.$buttons);
        this.renderSidebar(elements.$sidebar);
        this.renderPager(elements.$pager);
        // remove the unnecessary outer div
        elements = _.mapObject(elements, function($node) {
            return $node && $node.contents();
        });
        elements.$switch_buttons = this._renderSwitchButtons();

        return elements;
    },
    /**
     * Renders the switch buttons and binds listeners on them.
     *
     * @private
     * @returns {jQuery}
     */
    _renderSwitchButtons: function () {
        var self = this;
        var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});

        if (views.length <= 1) {
            return $();
        }

        var template = config.device.isMobile ? 'ControlPanel.SwitchButtons.Mobile' : 'ControlPanel.SwitchButtons';
        var $switchButtons = $(QWeb.render(template, {
            viewType: this.viewType,
            views: views,
        }));
        // create bootstrap tooltips
        _.each(views, function (view) {
            $switchButtons.filter('.o_cp_switch_' + view.type).tooltip();
        });
        // add onclick event listener
        var $switchButtonsFiltered = config.device.isMobile ? $switchButtons.find('button') : $switchButtons.filter('button');
        $switchButtonsFiltered.click(_.debounce(function (event) {
            var viewType = $(event.target).data('view-type');
            self.trigger_up('switch_view', {view_type: viewType});
        }, 200, true));

        // set active view's icon as view switcher button's icon in mobile
        if (config.device.isMobile) {
            var activeView = _.findWhere(views, {type: this.viewType});
            $switchButtons.find('.o_switch_view_button_icon').addClass('fa fa-lg ' + activeView.icon);
        }

        return $switchButtons;
    },
    /**
     * @override
     * @private
     */
    _startRenderer: function () {
        return this.renderer.appendTo(this.$('.o_content'));
    },
    /**
     * This method is called after each update or when the start method is
     * completed.
     *
     * Its primary use is to be used as a hook to update all parts of the UI,
     * besides the renderer.  For example, it may be used to enable/disable
     * some buttons in the control panel, such as the current graph type for a
     * graph view.
     *
     * @private
     * @param {Object} state the state given by the model
     * @returns {Deferred}
     */
    _update: function () {
        // AAB: update the control panel -> this will be moved elsewhere at some point
        var cpContent = _.extend({}, this.controlPanelElements);
        this.updateControlPanel({
            cp_content: cpContent,
            title: this.getTitle(),
        });

        this._pushState();
        return this._renderBanner();
    },

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

    /**
     * When a user clicks on an <a> link with type="action", we need to actually
     * do the action. This kind of links is used a lot in no-content helpers.
     *
     * * if the link has both data-model and data-method attributes, the
     *   corresponding method is called, chained to any action it would
     *   return. An optional data-reload-on-close (set to a non-falsy value)
     *   also causes th underlying view to be reloaded after the dialog is
     *   closed.
     * * if the link has a name attribute, invoke the action with that
     *   identifier (see :class:`ActionManager.doAction` to not get the
     *   details)
     * * otherwise an *action descriptor* is built from the link's data-
     *   attributes (model, res-id, views, domain and context)
     *
     * @private
     * @param ev
     */
    _onActionClicked: function (ev) { // FIXME: maybe this should also work on <button> tags?
        var $target = $(ev.currentTarget);
        var self = this;
        var data = $target.data();

        if (data.method !== undefined && data.model !== undefined) {
            var options = {};
            if (data.reloadOnClose) {
                options.on_close = function () {
                    self.trigger_up('reload');
                };
            }
            this.dp.add(this._rpc({
                model: data.model,
                method: data.method,
            })).then(function (action) {
                if (action !== undefined) {
                    self.do_action(action, options);
                }
            });
        } else if ($target.attr('name')) {
            this.do_action(
                $target.attr('name'),
                data.context && {additional_context: data.context}
            );
        } else {
            this.do_action({
                name: $target.attr('title') || _.str.strip($target.text()),
                type: 'ir.actions.act_window',
                res_model: data.model || this.modelName,
                res_id: data.resId,
                target: 'current', // TODO: make customisable?
                views: data.views || (data.resId ? [[false, 'form']] : [[false, 'list'], [false, 'form']]),
                domain: data.domain || [],
            }, {
                additional_context: _.extend({}, data.context)
            });
        }
        return false;
    },
    /**
     * FIXME: this logic should be rethought
     *
     * Handles a context request: provides to the caller the state of the
     * current controller.
     *
     * @private
     * @param {OdooEvent} ev
     * @param {function} ev.data.callback used to send the requested state
     */
    _onGetControllerQueryParams: function (ev) {
        ev.stopPropagation();
        var state = this.getOwnedQueryParams();
        ev.data.callback(state || {});
    },
    /**
     * Called mainly from the control panel when the focus should be given to
     * the controller
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onNavigationMove : function (ev) {
        switch(ev.data.direction) {
            case 'down' :
                ev.stopPropagation();
                this.giveFocus();
                break;
        }
    },
    /**
     * When an Odoo event arrives requesting a record to be opened, this method
     * gets the res_id, and request a switch view in the appropriate mode
     *
     * Note: this method seems wrong, it relies on the model being a basic model,
     * to get the res_id.  It should receive the res_id in the event data
     * @todo move this to basic controller?
     *
     * @private
     * @param {OdooEvent} ev
     * @param {number} ev.data.id The local model ID for the record to be
     *   opened
     * @param {string} [ev.data.mode='readonly']
     */
    _onOpenRecord: function (ev) {
        ev.stopPropagation();
        var record = this.model.get(ev.data.id, {raw: true});
        this.trigger_up('switch_view', {
            view_type: 'form',
            res_id: record.res_id,
            mode: ev.data.mode || 'readonly',
            model: this.modelName,
        });
    },
    /**
     * Called when there is a change in the search view, so the current action's
     * environment needs to be updated with the new domain, context and groupby.
     *
     * @private
     * @param {OdooEvent} ev
     * @param {Array[]} ev.data.domain
     * @param {Object} ev.data.context
     * @param {string[]} ev.data.groupby
     */
    _onSearch: function (ev) {
        ev.stopPropagation();
        this.reload(_.extend({offset: 0}, ev.data));
    },
    /**
     * Intercepts the 'switch_view' event to add the controllerID into the data,
     * and lets the event bubble up.
     *
     * @param {OdooEvent} ev
     */
    _onSwitchView: function (ev) {
        ev.data.controllerID = this.controllerID;
    },

});

return AbstractController;

});
