text: import {
    isUndefined,
    defaults,
    isArray,
    contains,
    toArray,
    keys
  } from 'underscore';
  import { getModel } from 'utils/mixins';
  
  const deps = [
    require('utils'),
    require('keymaps'),
    require('undo_manager'),
    require('storage_manager'),
    require('device_manager'),
    require('parser'),
    require('style_manager'),
    require('selector_manager'),
    require('modal_dialog'),
    require('code_manager'),
    require('panels'),
    require('rich_text_editor'),
    require('asset_manager'),
    require('css_composer'),
    require('trait_manager'),
    require('dom_components'),
    require('navigator'),
    require('canvas'),
    require('commands'),
    require('block_manager')
  ];
  
  const Backbone = require('backbone');
  const { Collection } = Backbone;
  let timedInterval;
  
  require('utils/extender')({
    Backbone: Backbone,
    $: Backbone.$
  });
  
  const $ = Backbone.$;
  const logs = {
    debug: console.log,
    info: console.info,
    warning: console.warn,
    error: console.error
  };
  
  module.exports = Backbone.Model.extend({
    defaults() {
      return {
        editing: 0,
        selected: new Collection(),
        clipboard: null,
        designerMode: false,
        componentHovered: null,
        previousModel: null,
        changesCount: 0,
        storables: [],
        modules: [],
        toLoad: [],
        opened: {},
        device: ''
      };
    },
  
    initialize(c = {}) {
      this.config = c;
      this.set('Config', c);
      this.set('modules', []);
      this.set('toLoad', []);
      this.set('storables', []);
      const el = c.el;
      const log = c.log;
      const toLog = log === true ? keys(logs) : isArray(log) ? log : [];
  
      if (el && c.fromElement) this.config.components = el.innerHTML;
      this.attrsOrig = el
        ? toArray(el.attributes).reduce((res, next) => {
            res[next.nodeName] = next.nodeValue;
            return res;
          }, {})
        : '';
  
      // Load modules
      deps.forEach(name => this.loadModule(name));
      this.on('change:componentHovered', this.componentHovered, this);
      this.on('change:changesCount', this.updateChanges, this);
      toLog.forEach(e => this.listenLog(e));
  
      // Deprecations
      [{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(
        event => {
          const eventFrom = event.from;
          const eventTo = event.to;
          this.listenTo(this, eventFrom, (...args) => {
            this.trigger(eventTo, ...args);
            this.logWarning(
              `The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`
            );
          });
        }
      );
    },
  
    listenLog(event) {
      this.listenTo(this, `log:${event}`, logs[event]);
    },
  
    /**
     * Get configurations
     * @param  {string} [prop] Property name
     * @return {any} Returns the configuration object or
     *  the value of the specified property
     */
    getConfig(prop) {
      const config = this.config;
      return isUndefined(prop) ? config : config[prop];
    },
  
    /**
     * Should be called after all modules and plugins are loaded
     * @param {Function} clb
     * @private
     */
    loadOnStart(clb = null) {
      const sm = this.get('StorageManager');
  
      // Generally, with `onLoad`, the module will try to load the data from
      // its configurations
      this.get('toLoad').forEach(module => {
        module.onLoad();
      });
  
      // Stuff to do post load
      const postLoad = () => {
        const modules = this.get('modules');
        modules.forEach(module => module.postLoad && module.postLoad(this));
        clb && clb();
      };
  
      if (sm && sm.canAutoload()) {
        this.load(postLoad);
      } else {
        postLoad();
      }
    },
  
    /**
     * Set the alert before unload in case it's requested
     * and there are unsaved changes
     * @private
     */
    updateChanges() {
      const stm = this.get('StorageManager');
      const changes = this.get('changesCount');
  
      if (this.config.noticeOnUnload) {
        window.onbeforeunload = changes ? e => 1 : null;
      }
  
      if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) {
        this.store();
      }
    },
  
    /**
     * Load generic module
     * @param {String} moduleName Module name
     * @return {this}
     * @private
     */
    loadModule(moduleName) {
      var c = this.config;
      var Mod = new moduleName();
      var name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1);
      var cfg = c[name] || c[Mod.name] || {};
      cfg.pStylePrefix = c.pStylePrefix || '';
  
      // Check if module is storable
      var sm = this.get('StorageManager');
  
      if (Mod.storageKey && Mod.store && Mod.load && sm) {
        cfg.stm = sm;
        var storables = this.get('storables');
        storables.push(Mod);
        this.set('storables', storables);
      }
  
      cfg.em = this;
      Mod.init({ ...cfg });
  
      // Bind the module to the editor model if public
      !Mod.private && this.set(Mod.name, Mod);
      Mod.onLoad && this.get('toLoad').push(Mod);
      this.get('modules').push(Mod);
      return this;
    },
  
    /**
     * Initialize editor model and set editor instance
     * @param {Editor} editor Editor instance
     * @return {this}
     * @private
     */
    init(editor) {
      this.set('Editor', editor);
    },
  
    getEditor() {
      return this.get('Editor');
    },
  
    /**
     * This method handles updates on the editor and tries to store them
     * if requested and if the changesCount is exceeded
     * @param  {Object} model
     * @param  {any} val  Value
     * @param  {Object} opt  Options
     * @private
     * */
    handleUpdates(model, val, opt = {}) {
      // Component has been added temporarily - do not update storage or record changes
      if (opt.temporary) {
        return;
      }
  
      timedInterval && clearInterval(timedInterval);
      timedInterval = setTimeout(() => {
        if (!opt.avoidStore) {
          this.set('changesCount', this.get('changesCount') + 1, opt);
        }
      }, 0);
    },
  
    /**
     * Callback on component hover
     * @param   {Object}   Model
     * @param   {Mixed}   New value
     * @param   {Object}   Options
     * @private
     * */
    componentHovered(editor, component, options) {
      const prev = this.previous('componentHovered');
      prev && this.trigger('component:unhovered', prev, options);
      component && this.trigger('component:hovered', component, options);
    },
  
    /**
     * Returns model of the selected component
     * @return {Component|null}
     * @private
     */
    getSelected() {
      return this.get('selected').last();
    },
  
    /**
     * Returns an array of all selected components
     * @return {Array}
     * @private
     */
    getSelectedAll() {
      return this.get('selected').models;
    },
  
    /**
     * Select a component
     * @param  {Component|HTMLElement} el Component to select
     * @param  {Object} [opts={}] Options, optional
     * @private
     */
    setSelected(el, opts = {}) {
      const multiple = isArray(el);
      const els = multiple ? el : [el];
      const selected = this.get('selected');
  
      // If an array is passed remove all selected
      // expect those yet to be selected
      multiple && this.removeSelected(selected.filter(s => !contains(els, s)));
  
      els.forEach(el => {
        const model = getModel(el, $);
        if (model && !model.get('selectable')) return;
        !multiple && this.removeSelected(selected.filter(s => s !== model));
        this.addSelected(model, opts);
      });
    },
  
    /**
     * Add component to selection
     * @param  {Component|HTMLElement} el Component to select
     * @param  {Object} [opts={}] Options, optional
     * @private
     */
    addSelected(el, opts = {}) {
      const model = getModel(el, $);
      const models = isArray(model) ? model : [model];
  
      models.forEach(model => {
        if (model && !model.get('selectable')) return;
        const selected = this.get('selected');
        opts.forceChange && selected.remove(model, opts);
        selected.push(model, opts);
      });
    },
  
    /**
     * Remove component from selection
     * @param  {Component|HTMLElement} el Component to select
     * @param  {Object} [opts={}] Options, optional
     * @private
     */
    removeSelected(el, opts = {}) {
      this.get('selected').remove(getModel(el, $), opts);
    },
  
    /**
     * Toggle component selection
     * @param  {Component|HTMLElement} el Component to select
     * @param  {Object} [opts={}] Options, optional
     * @private
     */
    toggleSelected(el, opts = {}) {
      const model = getModel(el, $);
      const models = isArray(model) ? model : [model];
  
      models.forEach(model => {
        if (this.get('selected').contains(model)) {
          this.removeSelected(model, opts);
        } else {
          this.addSelected(model, opts);
        }
      });
    },
  
    /**
     * Hover a component
     * @param  {Component|HTMLElement} el Component to select
     * @param  {Object} [opts={}] Options, optional
     * @private
     */
    setHovered(el, opts = {}) {
      const model = getModel(el, $);
      if (model && !model.get('hoverable')) return;
      opts.forceChange && this.set('componentHovered', '');
      this.set('componentHovered', model, opts);
    },
  
    /**
     * Set components inside editor's canvas. This method overrides actual components
     * @param {Object|string} components HTML string or components model
     * @return {this}
     * @private
     */
    setComponents(components) {
      return this.get('DomComponents').setComponents(components);
    },
  
    /**
     * Returns components model from the editor's canvas
     * @return {Components}
     * @private
     */
    getComponents() {
      var cmp = this.get('DomComponents');
      var cm = this.get('CodeManager');
  
      if (!cmp || !cm) return;
  
      var wrp = cmp.getComponents();
      return cm.getCode(wrp, 'json');
    },
  
    /**
     * Set style inside editor's canvas. This method overrides actual style
     * @param {Object|string} style CSS string or style model
     * @return {this}
     * @private
     */
    setStyle(style) {
      var rules = this.get('CssComposer').getAll();
      for (var i = 0, len = rules.length; i < len; i++) rules.pop();
      rules.add(style);
      return this;
    },
  
    /**
     * Returns rules/style model from the editor's canvas
     * @return {Rules}
     * @private
     */
    getStyle() {
      return this.get('CssComposer').getAll();
    },
  
    /**
     * Returns HTML built inside canvas
     * @return {string} HTML string
     * @private
     */
    getHtml() {
      const config = this.config;
      const exportWrapper = config.exportWrapper;
      const wrappesIsBody = config.wrappesIsBody;
      const js = config.jsInHtml ? this.getJs() : '';
      var wrp = this.get('DomComponents').getComponent();
      var html = this.get('CodeManager').getCode(wrp, 'html', {
        exportWrapper,
        wrappesIsBody
      });
      html += js ? `<script>${js}</script>` : '';
      return html;
    },
  
    /**
     * Returns CSS built inside canvas
     * @param {Object} [opts={}] Options
     * @return {string} CSS string
     * @private
     */
    getCss(opts = {}) {
      const config = this.config;
      const wrappesIsBody = config.wrappesIsBody;
      const avoidProt = opts.avoidProtected;
      const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles)
        ? opts.keepUnusedStyles
        : config.keepUnusedStyles;
      const cssc = this.get('CssComposer');
      const wrp = this.get('DomComponents').getComponent();
      const protCss = !avoidProt ? config.protectedCss : '';
  
      return (
        protCss +
        this.get('CodeManager').getCode(wrp, 'css', {
          cssc,
          wrappesIsBody,
          keepUnusedStyles
        })
      );
    },
  
    /**
     * Returns JS of all components
     * @return {string} JS string
     * @private
     */
    getJs() {
      var wrp = this.get('DomComponents').getWrapper();
      return this.get('CodeManager')
        .getCode(wrp, 'js')
        .trim();
    },
  
    /**
     * Store data to the current storage
     * @param {Function} clb Callback function
     * @return {Object} Stored data
     * @private
     */
    store(clb) {
      var sm = this.get('StorageManager');
      var store = {};
      if (!sm) return;
  
      // Fetch what to store
      this.get('storables').forEach(m => {
        var obj = m.store(1);
        for (var el in obj) store[el] = obj[el];
      });
  
      sm.store(store, res => {
        clb && clb(res);
        this.set('changesCount', 0);
        this.trigger('storage:store', store);
      });
  
      return store;
    },
  
    /**
     * Load data from the current storage
     * @param {Function} clb Callback function
     * @private
     */
    load(clb = null) {
      this.getCacheLoad(1, res => {
        this.get('storables').forEach(module => module.load(res));
        clb && clb(res);
      });
    },
  
    /**
     * Returns cached load
     * @param {Boolean} force Force to reload
     * @param {Function} clb Callback function
     * @return {Object}
     * @private
     */
    getCacheLoad(force, clb) {
      var f = force ? 1 : 0;
      if (this.cacheLoad && !f) return this.cacheLoad;
      var sm = this.get('StorageManager');
      var load = [];
  
      if (!sm) return {};
  
      this.get('storables').forEach(m => {
        var key = m.storageKey;
        key = typeof key === 'function' ? key() : key;
        var keys = key instanceof Array ? key : [key];
        keys.forEach(k => {
          load.push(k);
        });
      });
  
      sm.load(load, res => {
        this.cacheLoad = res;
        clb && clb(res);
        setTimeout(() => this.trigger('storage:load', res), 0);
      });
    },
  
    /**
     * Returns device model by name
     * @return {Device|null}
     * @private
     */
    getDeviceModel() {
      var name = this.get('device');
      return this.get('DeviceManager').get(name);
    },
  
    /**
     * Run default command if setted
     * @param {Object} [opts={}] Options
     * @private
     */
    runDefault(opts = {}) {
      var command = this.get('Commands').get(this.config.defaultCommand);
      if (!command || this.defaultRunning) return;
      command.stop(this, this, opts);
      command.run(this, this, opts);
      this.defaultRunning = 1;
    },
  
    /**
     * Stop default command
     * @param {Object} [opts={}] Options
     * @private
     */
    stopDefault(opts = {}) {
      var command = this.get('Commands').get(this.config.defaultCommand);
      if (!command) return;
      command.stop(this, this, opts);
      this.defaultRunning = 0;
    },
  
    /**
     * Update canvas dimensions and refresh data useful for tools positioning
     * @private
     */
    refreshCanvas() {
      this.set('canvasOffset', this.get('Canvas').getOffset());
    },
  
    /**
     * Clear all selected stuf inside the window, sometimes is useful to call before
     * doing some dragging opearation
     * @param {Window} win If not passed the current one will be used
     * @private
     */
    clearSelection(win) {
      var w = win || window;
      w.getSelection().removeAllRanges();
    },
  
    /**
     * Get the current media text
     * @return {string}
     */
    getCurrentMedia() {
      const config = this.config;
      const device = this.getDeviceModel();
      const condition = config.mediaCondition;
      const preview = config.devicePreviewMode;
      const width = device && device.get('widthMedia');
      return device && width && !preview ? `(${condition}: ${width})` : '';
    },
  
    /**
     * Return the component wrapper
     * @return {Component}
     */
    getWrapper() {
      return this.get('DomComponents').getWrapper();
    },
  
    /**
     * Return the count of changes made to the content and not yet stored.
     * This count resets at any `store()`
     * @return {number}
     */
    getDirtyCount() {
      return this.get('changesCount');
    },
  
    /**
     * Destroy editor
     */
    destroyAll() {
      const {
        DomComponents,
        CssComposer,
        UndoManager,
        Panels,
        Canvas,
        Keymaps
      } = this.attributes;
      DomComponents.clear();
      CssComposer.clear();
      UndoManager.clear().removeAll();
      Panels.getPanels().reset();
      Canvas.getCanvasView().remove();
      Keymaps.removeAll();
      this.view.remove();
      this.stopListening();
      $(this.config.el)
        .empty()
        .attr(this.attrsOrig);
    },
  
    setEditing(value) {
      this.set('editing', value);
      return this;
    },
  
    isEditing() {
      return !!this.get('editing');
    },
  
    log(msg, opts = {}) {
      const { ns, level = 'debug' } = opts;
      this.trigger('log', msg, opts);
      level && this.trigger(`log:${level}`, msg, opts);
  
      if (ns) {
        const logNs = `log-${ns}`;
        this.trigger(logNs, msg, opts);
        level && this.trigger(`${logNs}:${level}`, msg, opts);
      }
    },
  
    logInfo(msg, opts) {
      this.log(msg, { ...opts, level: 'info' });
    },
  
    logWarning(msg, opts) {
      this.log(msg, { ...opts, level: 'warning' });
    },
  
    logError(msg, opts) {
      this.log(msg, { ...opts, level: 'error' });
    },
  
    /**
     * Set/get data from the HTMLElement
     * @param  {HTMLElement} el
     * @param  {string} name Data name
     * @param  {any} value Date value
     * @return {any}
     * @private
     */
    data(el, name, value) {
      const varName = '_gjs-data';
  
      if (!el[varName]) {
        el[varName] = {};
      }
  
      if (isUndefined(value)) {
        return el[varName][name];
      } else {
        el[varName][name] = value;
      }
    }
  });
  