text: odoo.define('web.AbstractWebClient', function (require) {
  "use strict";
  
  /**
   * AbstractWebClient
   *
   * This class defines a simple, basic web client.  It is mostly functional.
   * The WebClient is in some way the most important class for the web framework:
   * - this is the class that instantiate everything else,
   * - it is the top of the component tree,
   * - it coordinates many events bubbling up
   */
  
  var ActionManager = require('web.ActionManager');
  var concurrency = require('web.concurrency');
  var core = require('web.core');
  var config = require('web.config');
  var WarningDialog = require('web.CrashManager').WarningDialog;
  var data_manager = require('web.data_manager');
  var dom = require('web.dom');
  var KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
  var Loading = require('web.Loading');
  var RainbowMan = require('web.RainbowMan');
  var session = require('web.session');
  var utils = require('web.utils');
  var Widget = require('web.Widget');
  
  const env = require('web.env');
  
  var _t = core._t;
  
  var AbstractWebClient = Widget.extend(KeyboardNavigationMixin, {
      dependencies: ['notification'],
      events: _.extend({}, KeyboardNavigationMixin.events),
      custom_events: {
          call_service: '_onCallService',
          clear_uncommitted_changes: function (e) {
              this.clear_uncommitted_changes().then(e.data.callback);
          },
          toggle_fullscreen: function (event) {
              this.toggle_fullscreen(event.data.fullscreen);
          },
          current_action_updated: function (ev) {
              this.current_action_updated(ev.data.action, ev.data.controller);
          },
          // GENERIC SERVICES
          // the next events are dedicated to generic services required by
          // downstream widgets.  Mainly side effects, such as rpcs, notifications
          // or cache.
          warning: '_onDisplayWarning',
          load_action: '_onLoadAction',
          load_views: function (event) {
              var params = {
                  model: event.data.modelName,
                  context: event.data.context,
                  views_descr: event.data.views,
              };
              return data_manager
                  .load_views(params, event.data.options || {})
                  .then(event.data.on_success);
          },
          load_filters: function (event) {
              return data_manager
                  .load_filters(event.data)
                  .then(event.data.on_success);
          },
          create_filter: '_onCreateFilter',
          delete_filter: '_onDeleteFilter',
          push_state: '_onPushState',
          show_effect: '_onShowEffect',
          // session
          get_session: function (event) {
              if (event.data.callback) {
                  event.data.callback(session);
              }
          },
          do_action: function (event) {
              const actionProm = this.do_action(event.data.action, event.data.options || {});
              this.menu_dp.add(actionProm).then(function (result) {
                  if (event.data.on_success) {
                      event.data.on_success(result);
                  }
              }).guardedCatch(function (result) {
                  if (event.data.on_fail) {
                      event.data.on_fail(result);
                  }
              });
          },
          getScrollPosition: '_onGetScrollPosition',
          scrollTo: '_onScrollTo',
          set_title_part: '_onSetTitlePart',
          webclient_started: '_onWebClientStarted',
      },
      init: function (parent) {
          // a flag to determine that odoo is fully loaded
          odoo.isReady = false;
          this.client_options = {};
          this._super(parent);
          KeyboardNavigationMixin.init.call(this);
          this.origin = undefined;
          this._current_state = null;
          this.menu_dp = new concurrency.DropPrevious();
          this.action_mutex = new concurrency.Mutex();
          this.set('title_part', {"zopenerp": "Odoo"});
          this.env = env;
          this.env.bus.on('set_title_part', this, this._onSetTitlePart);
      },
      /**
       * @override
       */
      start: function () {
          KeyboardNavigationMixin.start.call(this);
          var self = this;
  
          // we add the o_touch_device css class to allow CSS to target touch
          // devices.  This is only for styling purpose, if you need javascript
          // specific behaviour for touch device, just use the config object
          // exported by web.config
          this.$el.toggleClass('o_touch_device', config.device.touch);
          this.on("change:title_part", this, this._title_changed);
          this._title_changed();
  
          var state = $.bbq.getState();
          // If not set on the url, retrieve cids from the local storage
          // of from the default company on the user
          var current_company_id = session.user_companies.current_company[0]
          if (!state.cids) {
              state.cids = utils.get_cookie('cids') !== null ? utils.get_cookie('cids') : String(current_company_id);
          }
          // If a key appears several times in the hash, it is available in the
          // bbq state as an array containing all occurrences of that key
          const cids = Array.isArray(state.cids) ? state.cids[0] : state.cids;
          let stateCompanyIDS = cids.split(',').map(cid => parseInt(cid, 10));
          var userCompanyIDS = _.map(session.user_companies.allowed_companies, function(company) {return company[0]});
          // Check that the user has access to all the companies
          if (!_.isEmpty(_.difference(stateCompanyIDS, userCompanyIDS))) {
              state.cids = String(current_company_id);
              stateCompanyIDS = [current_company_id]
          }
          // Update the user context with this configuration
          session.user_context.allowed_company_ids = stateCompanyIDS;
          $.bbq.pushState(state);
          // Update favicon
          $("link[type='image/x-icon']").attr('href', '/web/image/res.company/' + String(stateCompanyIDS[0]) + '/favicon/')
  
          return session.is_bound
              .then(function () {
                  self.$el.toggleClass('o_rtl', _t.database.parameters.direction === "rtl");
                  self.bind_events();
                  return Promise.all([
                      self.set_action_manager(),
                      self.set_loading()
                  ]);
              }).then(function () {
                  if (session.session_is_valid()) {
                      return self.show_application();
                  } else {
                      // database manager needs the webclient to keep going even
                      // though it has no valid session
                      return Promise.resolve();
                  }
              });
      },
      /**
       * @override
       */
      destroy: function () {
          KeyboardNavigationMixin.destroy.call(this);
          return this._super(...arguments);
      },
      bind_events: function () {
          var self = this;
          $('.oe_systray').show();
          this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function () {
              $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
          });
          // TODO: this handler seems useless since 11.0, should be removed
          this.$el.on('click', '.oe_dropdown_toggle', function (ev) {
              ev.preventDefault();
              var $toggle = $(this);
              var doc_width = $(document).width();
              var $menu = $toggle.siblings('.oe_dropdown_menu');
              $menu = $menu.length >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
              var state = $menu.is('.oe_opened');
              setTimeout(function () {
                  // Do not alter propagation
                  $toggle.add($menu).toggleClass('oe_opened', !state);
                  if (!state) {
                      // Move $menu if outside window's edge
                      var offset = $menu.offset();
                      var menu_width = $menu.width();
                      var x = doc_width - offset.left - menu_width - 2;
                      if (x < 0) {
                          $menu.offset({ left: offset.left + x }).width(menu_width);
                      }
                  }
              }, 0);
          });
          core.bus.on('click', this, function (ev) {
              $('.tooltip').remove();
              if (!$(ev.target).is('input[type=file]')) {
                  $(this.el.getElementsByClassName('oe_dropdown_menu oe_opened')).removeClass('oe_opened');
                  $(this.el.getElementsByClassName('oe_dropdown_toggle oe_opened')).removeClass('oe_opened');
              }
          });
          core.bus.on('connection_lost', this, this._onConnectionLost);
          core.bus.on('connection_restored', this, this._onConnectionRestored);
      },
      set_action_manager: function () {
          var self = this;
          this.action_manager = new ActionManager(this, session.user_context);
          this.env.bus.on('do-action', this, payload => {
              this.do_action(payload.action, payload.options || {})
                  .then(payload.on_success || (() => {}))
                  .guardedCatch(payload.on_fail || (() => {}));
          });
          var fragment = document.createDocumentFragment();
          return this.action_manager.appendTo(fragment).then(function () {
              dom.append(self.$el, fragment, {
                  in_DOM: true,
                  callbacks: [{widget: self.action_manager}],
              });
          });
      },
      set_loading: function () {
          this.loading = new Loading(this);
          return this.loading.appendTo(this.$el);
      },
      show_application: function () {
      },
      clear_uncommitted_changes: function () {
          return this.action_manager.clearUncommittedChanges();
      },
      destroy_content: function () {
          _.each(_.clone(this.getChildren()), function (el) {
              el.destroy();
          });
          this.$el.children().remove();
      },
      // --------------------------------------------------------------
      // Window title handling
      // --------------------------------------------------------------
      /**
       * Sets the first part of the title of the window, dedicated to the current action.
      */
      set_title: function (title) {
          this.set_title_part("action", title);
      },
      /**
       * Sets an arbitrary part of the title of the window. Title parts are
       * identified by strings. Each time a title part is changed, all parts
       * are gathered, ordered by alphabetical order and displayed in the title
       * of the window separated by ``-``.
       *
       * @private
       * @param {string} part
       * @param {string} title
       */
      set_title_part: function (part, title) {
          var tmp = _.clone(this.get("title_part"));
          tmp[part] = title;
          this.set("title_part", tmp);
      },
      _title_changed: function () {
          var parts = _.sortBy(_.keys(this.get("title_part")), function (x) { return x; });
          var tmp = "";
          _.each(parts, function (part) {
              var str = this.get("title_part")[part];
              if (str) {
                  tmp = tmp ? tmp + " - " + str : str;
              }
          }, this);
          document.title = tmp;
      },
      // --------------------------------------------------------------
      // do_*
      // --------------------------------------------------------------
      /**
       * When do_action is performed on the WebClient, forward it to the main ActionManager
       * This allows to widgets that are not inside the ActionManager to perform do_action
       */
      do_action: function () {
          return this.action_manager.doAction.apply(this.action_manager, arguments);
      },
      do_reload: function () {
          var self = this;
          return session.session_reload().then(function () {
              session.load_modules(true).then(
                  self.menu.proxy('do_reload'));
          });
      },
      do_push_state: function (state) {
          if (!state.menu_id && this.menu) { // this.menu doesn't exist in the POS
              state.menu_id = this.menu.getCurrentPrimaryMenu();
          }
          if ('title' in state) {
              this.set_title(state.title);
              delete state.title;
          }
          var url = '#' + $.param(state);
          this._current_state = $.deparam($.param(state), false); // stringify all values
          $.bbq.pushState(url);
          this.trigger('state_pushed', state);
      },
      // --------------------------------------------------------------
      // Connection notifications
      // --------------------------------------------------------------
      /**
       * Handler to be overridden, called each time the UI is updated by the
       * ActionManager.
       *
       * @param {Object} action the action of the currently displayed controller
       * @param {Object} controller the currently displayed controller
       */
      current_action_updated: function (action, controller) {
      },
      //--------------------------------------------------------------
      // Misc.
      //--------------------------------------------------------------
      toggle_fullscreen: function (fullscreen) {
          this.$el.toggleClass('o_fullscreen', fullscreen);
      },
  
      //--------------------------------------------------------------------------
      // Public
      //--------------------------------------------------------------------------
  
      /**
       * Returns the left and top scroll positions of the main scrolling area
       * (i.e. the '.o_content' div in desktop).
       *
       * @returns {Object} with keys left and top
       */
      getScrollPosition: function () {
          var scrollingEl = this.action_manager.el.getElementsByClassName('o_content')[0];
          return {
              left: scrollingEl ? scrollingEl.scrollLeft : 0,
              top: scrollingEl ? scrollingEl.scrollTop : 0,
          };
      },
  
      //--------------------------------------------------------------------------
      // Handlers
      //--------------------------------------------------------------------------
  
      /**
       * Calls the requested service from the env.
       *
       * For the ajax service, the arguments are extended with the target so that
       * it can call back the caller.
       *
       * @private
       * @param {OdooEvent} event
       */
      _onCallService: function (ev) {
          const payload = ev.data;
          let args = payload.args || [];
          if (payload.service === 'ajax' && payload.method === 'rpc') {
              // ajax service uses an extra 'target' argument for rpc
              args = args.concat(ev.target);
          }
          const service = this.env.services[payload.service];
          const result = service[payload.method].apply(service, args);
          payload.callback(result);
      },
      /**
       * Whenever the connection is lost, we need to notify the user.
       *
       * @private
       */
      _onConnectionLost: function () {
          this.connectionNotificationID = this.displayNotification({
              message: _t('Connection lost. Trying to reconnect...'),
              sticky: true
          });
      },
      /**
       * Whenever the connection is restored, we need to notify the user.
       *
       * @private
       */
      _onConnectionRestored: function () {
          if (this.connectionNotificationID) {
              this.call('notification', 'close', this.connectionNotificationID);
              this.displayNotification({
                  type: 'info',
                  message: _t('Connection restored. You are back online.'),
                  sticky: false
              });
              this.connectionNotificationID = false;
          }
      },
      /**
       * @private
       * @param {OdooEvent} e
       * @param {Object} e.data.filter the filter description
       * @param {function} e.data.on_success called when the RPC succeeds with its
       *   returned value as argument
       */
      _onCreateFilter: function (e) {
          data_manager
              .create_filter(e.data.filter)
              .then(e.data.on_success);
      },
      /**
       * @private
       * @param {OdooEvent} e
       * @param {Object} e.data.filter the filter description
       * @param {function} e.data.on_success called when the RPC succeeds with its
       *   returned value as argument
       */
      _onDeleteFilter: function (e) {
          data_manager
              .delete_filter(e.data.filterId)
              .then(e.data.on_success);
      },
      /**
       * Displays a warning in a dialog or with the notification service
       *
       * @private
       * @param {OdooEvent} e
       * @param {string} e.data.message the warning's message
       * @param {string} e.data.title the warning's title
       * @param {string} [e.data.type] 'dialog' to display in a dialog
       * @param {boolean} [e.data.sticky] whether or not the warning should be
       *   sticky (if displayed with the Notification)
       */
      _onDisplayWarning: function (e) {
          var data = e.data;
          if (data.type === 'dialog') {
              new WarningDialog(this, {
                  title: data.title,
              }, data).open();
          } else {
              data.type = 'warning';
              this.call('notification', 'notify', data);
          }
      },
      /**
       * Provides to the caller the current scroll position (left and top) of the
       * main scrolling area of the webclient.
       *
       * @private
       * @param {OdooEvent} ev
       * @param {function} ev.data.callback
       */
      _onGetScrollPosition: function (ev) {
          ev.data.callback(this.getScrollPosition());
      },
      /**
       * Loads an action from the database given its ID.
       *
       * @private
       * @param {OdooEvent} event
       * @param {integer} event.data.actionID
       * @param {Object} event.data.context
       * @param {function} event.data.on_success
       */
      _onLoadAction: function (event) {
          data_manager
              .load_action(event.data.actionID, event.data.context)
              .then(event.data.on_success);
      },
      /**
       * @private
       * @param {OdooEvent} e
       */
      _onPushState: function (e) {
          this.do_push_state(_.extend(e.data.state, {'cids': $.bbq.getState().cids}));
      },
      /**
       * Scrolls either to a given offset or to a target element (given a selector).
       * It must be called with: trigger_up('scrollTo', options).
       *
       * @private
       * @param {OdooEvent} ev
       * @param {integer} [ev.data.top] the number of pixels to scroll from top
       * @param {integer} [ev.data.left] the number of pixels to scroll from left
       * @param {string} [ev.data.selector] the selector of the target element to
       *   scroll to
       */
      _onScrollTo: function (ev) {
          var scrollingEl = this.action_manager.el.getElementsByClassName('o_content')[0];
          if (!scrollingEl) {
              return;
          }
          var offset = {top: ev.data.top, left: ev.data.left || 0};
          if (ev.data.selector) {
              offset = dom.getPosition(document.querySelector(ev.data.selector));
              // Substract the position of the scrolling element
              offset.top -= dom.getPosition(scrollingEl).top;
          }
  
          scrollingEl.scrollTop = offset.top;
          scrollingEl.scrollLeft = offset.left;
      },
      /**
       * @private
       * @param {Object} payload
       * @param {string} payload.part
       * @param {string} [payload.title]
       */
      _onSetTitlePart: function (payload) {
          var part = payload.part;
          var title = payload.title;
          this.set_title_part(part, title);
      },
      /**
       * Displays a visual effect (for example, a rainbowman0
       *
       * @private
       * @param {OdooEvent} e
       * @param {Object} [e.data] - key-value options to decide rainbowman
       *   behavior / appearance
       */
      _onShowEffect: function (e) {
          var data = e.data || {};
          var type = data.type || 'rainbow_man';
          if (type === 'rainbow_man') {
              if (session.show_effect) {
                  new RainbowMan(data).appendTo(this.$el);
              } else {
                  // For instance keep title blank, as we don't have title in data
                  this.call('notification', 'notify', {
                      title: "",
                      message: data.message,
                      sticky: false
                  });
              }
          } else {
              throw new Error('Unknown effect type: ' + type);
          }
      },
      /**
       * Reacts to the end of the loading of the WebClient as a whole
       * It allows for signalling to the rest of the ecosystem that the interface is usable
       *
       * @private
       */
      _onWebClientStarted: function() {
          if (!this.isStarted) {
              // Listen to 'scroll' event and propagate it on main bus
              this.action_manager.$el.on('scroll', core.bus.trigger.bind(core.bus, 'scroll'));
              odoo.isReady = true;
              core.bus.trigger('web_client_ready');
              if (session.uid === 1) {
                  this.$el.addClass('o_is_superuser');
              }
              this.isStarted = true;
          }
      }
  });
  
  return AbstractWebClient;
  
  });
  