import { isUndefined, isArray, contains, toArray, keys, bindAll } from 'underscore';
import Backbone from 'backbone';
import $ from '../../utils/cash-dom';
import Extender from '../../utils/extender';
import { getModel, hasWin, isEmptyObj } from '../../utils/mixins';
import { AddOptions, Model } from '../../common';
import Selected from './Selected';
import FrameView from '../../canvas/view/FrameView';
import Editor from '..';
import EditorView from '../view/EditorView';
import { ILoadableModule, IModule, IStorableModule } from '../../abstract/Module';
import CanvasModule from '../../canvas';
import ComponentManager from '../../dom_components';
import CssComposer from '../../css_composer';
import { EditorConfig, EditorConfigKeys } from '../config/config';
import Component from '../../dom_components/model/Component';
import BlockManager from '../../block_manager';
import SelectorManager from '../../selector_manager';
import ParserModule from '../../parser';
import StorageManager from '../../storage_manager';
import TraitManager from '../../trait_manager';
import LayerManager from '../../navigator';
import AssetManager from '../../asset_manager';
import DeviceManager from '../../device_manager';
import PageManager from '../../pages';
import I18nModule from '../../i18n';
import UtilsModule from '../../utils';
import KeymapsModule from '../../keymaps';
import ModalModule from '../../modal_dialog';
import PanelManager from '../../panels';
import CodeManagerModule from '../../code_manager';
import UndoManagerModule from '../../undo_manager';
import RichTextEditorModule from '../../rich_text_editor';
import CommandsModule from '../../commands';
import StyleManager from '../../style_manager';
import CssRule from '../../css_composer/model/CssRule';
import { HTMLGeneratorBuildOptions } from '../../code_manager/model/HtmlGenerator';
import { CssGeneratorBuildOptions } from '../../code_manager/model/CssGenerator';
import ComponentView from '../../dom_components/view/ComponentView';
import { ProjectData } from '../../storage_manager/model/IStorage';
import CssRules from '../../css_composer/model/CssRules';
import Frame from '../../canvas/model/Frame';
import { ComponentAdd, DragMode } from '../../dom_components/model/types';
import ComponentWrapper from '../../dom_components/model/ComponentWrapper';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';

Backbone.$ = $;

const deps: (new (em: EditorModel) => IModule)[] = [
  UtilsModule,
  I18nModule,
  KeymapsModule,
  UndoManagerModule,
  StorageManager,
  DeviceManager,
  ParserModule,
  StyleManager,
  SelectorManager,
  ModalModule,
  CodeManagerModule,
  PanelManager,
  RichTextEditorModule,
  TraitManager,
  LayerManager,
  CanvasModule,
  CommandsModule,
  BlockManager,
];
const storableDeps: (new (em: EditorModel) => IModule & IStorableModule)[] = [
  AssetManager,
  CssComposer,
  PageManager,
  ComponentManager,
];

Extender({ $ });

const logs = {
  debug: console.log,
  info: console.info,
  warning: console.warn,
  error: console.error,
};

export default class EditorModel extends Model {
  defaults() {
    return {
      editing: 0,
      selected: 0,
      clipboard: null,
      dmode: 0,
      componentHovered: null,
      previousModel: null,
      changesCount: 0,
      storables: [],
      modules: [],
      toLoad: [],
      opened: {},
      device: '',
    };
  }

  __skip = false;
  defaultRunning = false;
  destroyed = false;
  _config: EditorConfig;
  _storageTimeout?: ReturnType<typeof setTimeout>;
  attrsOrig: any;
  timedInterval?: ReturnType<typeof setTimeout>;
  updateItr?: ReturnType<typeof setTimeout>;
  view?: EditorView;

  get storables(): IStorableModule[] {
    return this.get('storables');
  }

  get modules(): IModule[] {
    return this.get('modules');
  }

  get toLoad(): ILoadableModule[] {
    return this.get('toLoad');
  }

  get selected(): Selected {
    return this.get('selected');
  }

  get shallow(): EditorModel {
    return this.get('shallow');
  }

  get I18n(): I18nModule {
    return this.get('I18n');
  }

  get Utils(): UtilsModule {
    return this.get('Utils');
  }

  get Commands(): CommandsModule {
    return this.get('Commands');
  }

  get Keymaps(): KeymapsModule {
    return this.get('Keymaps');
  }

  get Modal(): ModalModule {
    return this.get('Modal');
  }

  get Panels(): PanelManager {
    return this.get('Panels');
  }

  get CodeManager(): CodeManagerModule {
    return this.get('CodeManager');
  }

  get UndoManager(): UndoManagerModule {
    return this.get('UndoManager');
  }

  get RichTextEditor(): RichTextEditorModule {
    return this.get('RichTextEditor');
  }

  get Canvas(): CanvasModule {
    return this.get('Canvas');
  }

  get Editor(): Editor {
    return this.get('Editor');
  }

  get Components(): ComponentManager {
    return this.get('DomComponents');
  }

  get Css(): CssComposer {
    return this.get('CssComposer');
  }

  get Blocks(): BlockManager {
    return this.get('BlockManager');
  }

  get Selectors(): SelectorManager {
    return this.get('SelectorManager');
  }

  get Storage(): StorageManager {
    return this.get('StorageManager');
  }

  get Traits(): TraitManager {
    return this.get('TraitManager');
  }

  get Parser(): ParserModule {
    return this.get('Parser');
  }

  get Layers(): LayerManager {
    return this.get('LayerManager');
  }

  get Assets(): AssetManager {
    return this.get('AssetManager');
  }

  get Devices(): DeviceManager {
    return this.get('DeviceManager');
  }

  get Pages(): PageManager {
    return this.get('PageManager');
  }

  get Styles(): StyleManager {
    return this.get('StyleManager');
  }

  constructor(conf: EditorConfig = {}) {
    super();
    this._config = conf;
    const { config } = this;
    this.set('Config', conf);
    this.set('modules', []);
    this.set('toLoad', []);
    this.set('storables', []);
    this.set('selected', new Selected());
    this.set('dmode', config.dragMode);
    const { el, log } = config;
    const toLog = log === true ? keys(logs) : isArray(log) ? log : [];
    bindAll(this, 'initBaseColorPicker');

    if (el && config.fromElement) {
      config.components = el.innerHTML;
    }

    this.attrsOrig = el
      ? toArray(el.attributes).reduce((res, next) => {
          res[next.nodeName] = next.nodeValue;
          return res;
        }, {} as Record<string, any>)
      : '';

    // Move components to pages
    if (config.components && !config.pageManager) {
      config.pageManager = { pages: [{ component: config.components }] };
    }

    // Load modules
    deps.forEach(constr => this.loadModule(constr));
    storableDeps.forEach(constr => this.loadStorableModule(constr));
    this.on('change:componentHovered', this.componentHovered, this);
    this.on('change:changesCount', this.updateChanges, this);
    this.on('change:readyLoad change:readyCanvas', this._checkReady, 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}'`);
      });
    });
  }

  _checkReady() {
    if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) {
      this.set('ready', true);
    }
  }

  getContainer() {
    return this.config.el;
  }

  listenLog(event: string) {
    //@ts-ignore
    this.listenTo(this, `log:${event}`, logs[event]);
  }

  get config() {
    return this._config;
  }

  /**
   * Get configurations
   * @param  {string} [prop] Property name
   * @return {any} Returns the configuration object or
   *  the value of the specified property
   */
  getConfig<
    P extends EditorConfigKeys | undefined = undefined,
    R = P extends EditorConfigKeys ? EditorConfig[P] : EditorConfig
  >(prop?: P): R {
    const { config } = this;
    // @ts-ignore
    return isUndefined(prop) ? config : config[prop];
  }

  /**
   * Should be called once all modules and plugins are loaded
   * @private
   */
  loadOnStart() {
    const { projectData, headless } = this.config;
    const sm = this.get('StorageManager');

    // In `onLoad`, the module will try to load the data from its configurations.
    this.toLoad.reverse().forEach(mdl => mdl.onLoad());

    // Stuff to do post load
    const postLoad = () => {
      this.modules.forEach(mdl => mdl.postLoad && mdl.postLoad(this));
      this.set('readyLoad', 1);
    };

    if (headless) {
      projectData && this.loadData(projectData);
      postLoad();
    } else {
      // Defer for storage load events.
      this._storageTimeout = setTimeout(async () => {
        if (projectData) {
          this.loadData(projectData);
        } else if (sm?.canAutoload()) {
          try {
            await this.load();
          } catch (error) {
            this.logError(error as string);
          }
        }
        postLoad();
      });
    }

    // Create shallow editor.
    // Here we can create components/styles without altering/triggering the main EditorModel
    const shallow = new EditorModel({
      noticeOnUnload: false,
      storageManager: false,
      undoManager: false,
    });
    // We only need to load a few modules
    shallow.Pages.onLoad();
    shallow.Canvas.postLoad();
    this.set('shallow', shallow);
  }

  /**
   * 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.getDirtyCount();
    this.updateItr && clearTimeout(this.updateItr);
    this.updateItr = setTimeout(() => this.trigger('update'));

    if (this.config.noticeOnUnload) {
      window.onbeforeunload = changes ? () => true : null;
    }

    if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) {
      this.store().catch(err => this.logError(err));
    }
  }

  /**
   * Load generic module
   */
  private loadModule(InitModule: new (em: EditorModel) => IModule) {
    const Mod = new InitModule(this);
    this.set(Mod.name, Mod);
    Mod.onLoad && this.toLoad.push(Mod as ILoadableModule);
    this.modules.push(Mod);
    return Mod;
  }

  private loadStorableModule(InitModule: new (em: EditorModel) => IModule & IStorableModule) {
    const Mod = this.loadModule(InitModule) as IModule & IStorableModule;
    this.storables.push(Mod);
    return Mod;
  }

  /**
   * Initialize editor model and set editor instance
   * @param {Editor} editor Editor instance
   * @return {this}
   * @public
   */
  init(editor: Editor, opts = {}) {
    if (this.destroyed) {
      this.initialize(opts);
      this.destroyed = false;
    }
    this.set('Editor', editor);
  }

  getEditor(): Editor {
    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: any, val: any, opt: any = {}) {
    // Component has been added temporarily - do not update storage or record changes
    if (this.__skip || opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) {
      return;
    }

    this.timedInterval && clearTimeout(this.timedInterval);
    this.timedInterval = setTimeout(() => {
      const curr = this.getDirtyCount() || 0;
      const { unset, ...opts } = opt;
      this.set('changesCount', curr + 1, opts);
    }, 0);
  }

  changesUp(opts: any) {
    this.handleUpdates(0, 0, opts);
  }

  /**
   * Callback on component hover
   * @param   {Object}   Model
   * @param   {Mixed}   New value
   * @param   {Object}   Options
   * @private
   * */
  componentHovered(editor: any, component: any, options: any) {
    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}
   * @public
   */
  getSelected() {
    return this.selected.lastComponent();
  }

  /**
   * Returns an array of all selected components
   * @return {Array}
   * @public
   */
  getSelectedAll() {
    return this.selected.allComponents();
  }

  /**
   * Select a component
   * @param  {Component|HTMLElement} el Component to select
   * @param  {Object} [opts={}] Options, optional
   * @public
   */
  setSelected(el?: Component | Component[], opts: any = {}) {
    const { event } = opts;
    const ctrlKey = event && (event.ctrlKey || event.metaKey);
    const { shiftKey } = event || {};
    const els = (isArray(el) ? el : [el]).map(el => getModel(el, $));
    const selected = this.getSelectedAll();
    const mltSel = this.getConfig().multipleSelection;
    let added;

    // If an array is passed remove all selected
    // expect those yet to be selected
    const multiple = isArray(el);
    multiple && this.removeSelected(selected.filter(s => !contains(els, s)));

    els.forEach(el => {
      let model = getModel(el, undefined);

      if (model) {
        this.trigger('component:select:before', model, opts);

        // Check for valid selectable
        if (!model.get('selectable') || opts.abort) {
          if (opts.useValid) {
            let parent = model.parent();
            while (parent && !parent.get('selectable')) parent = parent.parent();
            model = parent;
          } else {
            return;
          }
        }
      }

      // Hanlde multiple selection
      if (ctrlKey && mltSel) {
        return this.toggleSelected(model);
      } else if (shiftKey && mltSel) {
        this.clearSelection(this.get('Canvas').getWindow());
        const coll = model.collection;
        const index = model.index();
        let min: number | undefined, max: number | undefined;

        // Fin min and max siblings
        this.getSelectedAll().forEach(sel => {
          const selColl = sel.collection;
          const selIndex = sel.index();
          if (selColl === coll) {
            if (selIndex < index) {
              // First model BEFORE the selected one
              min = isUndefined(min) ? selIndex : Math.max(min, selIndex);
            } else if (selIndex > index) {
              // First model AFTER the selected one
              max = isUndefined(max) ? selIndex : Math.min(max, selIndex);
            }
          }
        });

        if (!isUndefined(min)) {
          while (min !== index) {
            this.addSelected(coll.at(min));
            min++;
          }
        }

        if (!isUndefined(max)) {
          while (max !== index) {
            this.addSelected(coll.at(max));
            max--;
          }
        }

        return this.addSelected(model);
      }

      !multiple && this.removeSelected(selected.filter(s => s !== model));
      this.addSelected(model, opts);
      added = model;
    });
  }

  /**
   * Add component to selection
   * @param  {Component|HTMLElement} el Component to select
   * @param  {Object} [opts={}] Options, optional
   * @public
   */
  addSelected(el: Component | Component[], opts: any = {}) {
    const model = getModel(el, $);
    const models: Component[] = isArray(model) ? model : [model];

    models.forEach(model => {
      const { selected } = this;
      if (
        !model ||
        !model.get('selectable') ||
        // Avoid selecting children of selected components
        model.parents().some((parent: Component) => selected.hasComponent(parent))
      ) {
        return;
      }
      opts.forceChange && this.removeSelected(model, opts);
      // Remove from selection, children of the component to select
      const toDeselect = selected.allComponents().filter(cmp => contains(cmp.parents(), model));
      toDeselect.forEach(cmp => this.removeSelected(cmp, opts));

      selected.addComponent(model, opts);
      this.trigger('component:select', model, opts);
      this.Canvas.addSpot({
        type: CanvasSpotBuiltInTypes.Select,
        component: model,
      });
    });
  }

  /**
   * Remove component from selection
   * @param  {Component|HTMLElement} el Component to select
   * @param  {Object} [opts={}] Options, optional
   * @public
   */
  removeSelected(el: Component | Component[], opts = {}) {
    const component = getModel(el, $);
    this.selected.removeComponent(component, opts);
    const cmps: Component[] = isArray(component) ? component : [component];
    cmps.forEach(component =>
      this.Canvas.removeSpots({
        type: CanvasSpotBuiltInTypes.Select,
        component,
      })
    );
  }

  /**
   * Toggle component selection
   * @param  {Component|HTMLElement} el Component to select
   * @param  {Object} [opts={}] Options, optional
   * @public
   */
  toggleSelected(el: Component | Component[], opts: any = {}) {
    const model = getModel(el, $);
    const models = isArray(model) ? model : [model];

    models.forEach(model => {
      if (this.selected.hasComponent(model)) {
        this.removeSelected(model, opts);
      } else {
        this.addSelected(model, opts);
      }
    });
  }

  /**
   * Hover a component
   * @param  {Component|HTMLElement} cmp Component to select
   * @param  {Object} [opts={}] Options, optional
   * @private
   */
  setHovered(cmp?: Component | null, opts: any = {}) {
    const upHovered = (cmp?: Component, opts?: any) => {
      const { config, Canvas } = this;
      const current = this.getHovered();
      const selectedAll = this.getSelectedAll();
      const typeHover = CanvasSpotBuiltInTypes.Hover;
      const typeSpacing = CanvasSpotBuiltInTypes.Spacing;
      this.set('componentHovered', cmp || null, opts);

      if (current) {
        Canvas.removeSpots({ type: typeHover, component: current });
        Canvas.removeSpots({ type: typeSpacing, component: current });
      }

      if (cmp) {
        Canvas.addSpot({ type: typeHover, component: cmp });
        if (!selectedAll.includes(cmp) || config.showOffsetsSelected) {
          Canvas.addSpot({ type: typeSpacing, component: cmp });
        }
      }
    };

    if (!cmp) {
      return upHovered();
    }

    const ev = 'component:hover';
    let model = getModel(cmp, undefined) as Component | undefined;

    if (!model) return;

    opts.forceChange && upHovered();
    this.trigger(`${ev}:before`, model, opts);

    // Check for valid hoverable
    if (!model.get('hoverable')) {
      if (opts.useValid && !opts.abort) {
        let parent = model.parent();
        while (parent && !parent.get('hoverable')) parent = parent.parent();
        model = parent;
      } else {
        return;
      }
    }

    if (!opts.abort) {
      upHovered(model, opts);
      this.trigger(ev, model, opts);
    }
  }

  getHovered() {
    return this.get('componentHovered') as Component | undefined;
  }

  /**
   * Set components inside editor's canvas. This method overrides actual components
   * @param {Object|string} components HTML string or components model
   * @param {Object} opt the options object to be used by the [setComponents]{@link setComponents} method
   * @return {this}
   * @public
   */
  setComponents(components: ComponentAdd, opt: AddOptions = {}) {
    return this.Components.setComponents(components, opt);
  }

  /**
   * Returns components model from the editor's canvas
   * @return {Components}
   * @private
   */
  getComponents() {
    const cmp = this.Components;
    const cm = this.CodeManager;

    if (!cmp || !cm) return;

    const 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
   * @param {Object} opt the options object to be used by the `CssRules.add` method
   * @return {this}
   * @public
   */
  setStyle(style: any, opt = {}) {
    const cssc = this.Css;
    cssc.clear(opt);
    cssc.getAll().add(style, opt);
    return this;
  }

  /**
   * Add styles to the editor
   * @param {Array<Object>|Object|string} style CSS string or style model
   * @returns {Array<CssRule>}
   * @public
   */
  addStyle(style: any, opts = {}): CssRule[] {
    const res = this.getStyle().add(style, opts);
    return isArray(res) ? res : [res];
  }

  /**
   * Returns rules/style model from the editor's canvas
   * @return {Rules}
   * @private
   */
  getStyle(): CssRules {
    return this.Css.getAll();
  }

  /**
   * Change the selector state
   * @param {String} value State value
   * @returns {this}
   */
  setState(value: string) {
    this.set('state', value);
    return this;
  }

  /**
   * Get the current selector state
   * @returns {String}
   */
  getState(): string {
    return this.get('state') || '';
  }

  /**
   * Returns HTML built inside canvas
   * @param {Object} [opts={}] Options
   * @returns {string} HTML string
   * @public
   */
  getHtml(opts: { component?: Component } & HTMLGeneratorBuildOptions = {}): string {
    const { config } = this;
    const { optsHtml } = config;
    const js = config.jsInHtml ? this.getJs(opts) : '';
    const cmp = opts.component || this.Components.getComponent();
    let html = cmp
      ? this.CodeManager.getCode(cmp, 'html', {
          ...optsHtml,
          ...opts,
        })
      : '';
    html += js ? `<script>${js}</script>` : '';
    return html;
  }

  /**
   * Returns CSS built inside canvas
   * @param {Object} [opts={}] Options
   * @returns {string} CSS string
   * @public
   */
  getCss(opts: { component?: Component; avoidProtected?: boolean } & CssGeneratorBuildOptions = {}) {
    const { config } = this;
    const { optsCss } = config;
    const avoidProt = opts.avoidProtected;
    const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles;
    const cssc = this.Css;
    const wrp = opts.component || this.Components.getComponent();
    const protCss = !avoidProt ? config.protectedCss! : '';
    const css =
      wrp &&
      this.CodeManager.getCode(wrp, 'css', {
        cssc,
        keepUnusedStyles,
        ...optsCss,
        ...opts,
      });
    return wrp ? (opts.json ? css : protCss + css) : '';
  }

  /**
   * Returns JS of all components
   * @return {string} JS string
   * @public
   */
  getJs(opts: { component?: Component } = {}) {
    var wrp = opts.component || this.Components.getWrapper();
    return wrp ? this.CodeManager.getCode(wrp, 'js').trim() : '';
  }

  /**
   * Store data to the current storage.
   * @public
   */
  async store(options?: any) {
    const data = this.storeData();
    await this.Storage.store(data, options);
    this.clearDirtyCount();
    return data;
  }

  /**
   * Load data from the current storage.
   * @public
   */
  async load(options?: any) {
    const result = await this.Storage.load(options);
    this.loadData(result);
    return result;
  }

  storeData(): ProjectData {
    let result = {};
    // Sync content if there is an active RTE
    const editingCmp = this.getEditing();
    editingCmp && editingCmp.trigger('sync:content', { noCount: true });

    this.storables.forEach(m => {
      result = { ...result, ...m.store(1) };
    });
    return JSON.parse(JSON.stringify(result));
  }

  loadData(data: ProjectData = {}): ProjectData {
    if (!isEmptyObj(data)) {
      this.storables.forEach(module => module.clear());
      this.storables.forEach(module => module.load(data));
    }
    return data;
  }

  /**
   * Returns device model by name
   * @return {Device|null}
   * @private
   */
  getDeviceModel() {
    const name = this.get('device');
    return this.Devices.get(name);
  }

  /**
   * Run default command if setted
   * @param {Object} [opts={}] Options
   * @private
   */
  runDefault(opts = {}) {
    const 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 = true;
  }

  /**
   * Stop default command
   * @param {Object} [opts={}] Options
   * @private
   */
  stopDefault(opts = {}) {
    const commands = this.get('Commands');
    const command = commands.get(this.config.defaultCommand);
    if (!command || !this.defaultRunning) return;
    command.stop(this, this, opts);
    this.defaultRunning = false;
  }

  /**
   * Update canvas dimensions and refresh data useful for tools positioning
   * @public
   */
  refreshCanvas(opts: any = {}) {
    this.set('canvasOffset', null);
    this.set('canvasOffset', this.Canvas.getOffset());
    opts.tools && this.trigger('canvas:updateTools');
  }

  /**
   * 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?: Window) {
    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(): ComponentWrapper | undefined {
    return this.Components.getWrapper();
  }

  setCurrentFrame(frameView?: FrameView) {
    return this.set('currentFrame', frameView);
  }

  getCurrentFrame(): FrameView | undefined {
    return this.get('currentFrame');
  }

  getCurrentFrameModel() {
    return (this.getCurrentFrame() || {})?.model;
  }

  getIcon(icon: string) {
    const icons = this.config.icons || {};
    return icons[icon] || '';
  }

  /**
   * Return the count of changes made to the content and not yet stored.
   * This count resets at any `store()`
   * @return {number}
   */
  getDirtyCount(): number {
    return this.get('changesCount');
  }

  clearDirtyCount() {
    return this.set('changesCount', 0);
  }

  getZoomDecimal() {
    return this.Canvas.getZoomDecimal();
  }

  getZoomMultiplier() {
    return this.Canvas.getZoomMultiplier();
  }

  setDragMode(value: DragMode) {
    return this.set('dmode', value);
  }

  getDragMode(component?: Component): DragMode {
    const mode = component?.getDragMode() || this.get('dmode');
    return mode || '';
  }

  t(...args: any[]) {
    const i18n = this.get('I18n');
    return i18n?.t(...args);
  }

  /**
   * Returns true if the editor is in absolute mode
   * @returns {Boolean}
   */
  inAbsoluteMode(component?: Component) {
    return this.getDragMode(component) === 'absolute';
  }

  /**
   * Destroy editor
   */
  destroyAll() {
    const { config, view } = this;
    const editor = this.getEditor();
    // @ts-ignore
    const { editors = [] } = config.grapesjs || {};
    const shallow = this.get('shallow');
    this._storageTimeout && clearTimeout(this._storageTimeout);
    shallow?.destroyAll();
    this.stopListening();
    this.stopDefault();
    this.modules
      .slice()
      .reverse()
      .forEach(mod => mod.destroy());
    view && view.remove();
    this.clear({ silent: true });
    this.destroyed = true;
    ['_config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(
      //@ts-ignore
      i => (this[i] = {})
    );
    editors.splice(editors.indexOf(editor), 1);
    //@ts-ignore
    hasWin() && $(config.el).empty().attr(this.attrsOrig);
  }

  getEditing(): Component | undefined {
    const res = this.get('editing');
    return (res && res.model) || undefined;
  }

  setEditing(value: boolean | ComponentView) {
    this.set('editing', value);
    return this;
  }

  isEditing() {
    return !!this.get('editing');
  }

  log(msg: string, opts: any = {}) {
    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: string, opts?: any) {
    this.log(msg, { ...opts, level: 'info' });
  }

  logWarning(msg: string, opts?: any) {
    this.log(msg, { ...opts, level: 'warning' });
  }

  logError(msg: string, opts?: any) {
    this.log(msg, { ...opts, level: 'error' });
  }

  initBaseColorPicker(el: any, opts = {}) {
    const { config } = this;
    const { colorPicker = {} } = config;
    const elToAppend = config.el;
    const ppfx = config.stylePrefix;

    //@ts-ignore
    return $(el).spectrum({
      containerClassName: `${ppfx}one-bg ${ppfx}two-color`,
      appendTo: elToAppend || 'body',
      maxSelectionSize: 8,
      showPalette: true,
      palette: [],
      showAlpha: true,
      chooseText: 'Ok',
      cancelText: '⨯',
      ...opts,
      ...colorPicker,
    });
  }

  /**
   * Execute actions without triggering the storage and undo manager.
   * @param  {Function} clb
   * @private
   */
  skip(clb: Function) {
    this.__skip = true;
    const um = this.UndoManager;
    um ? um.skip(clb) : clb();
    this.__skip = false;
  }

  /**
   * Set/get data from the HTMLElement
   * @param  {HTMLElement} el
   * @param  {string} name Data name
   * @param  {any} value Date value
   * @return {any}
   * @private
   */
  data(el: any, name: string, value: any) {
    const varName = '_gjs-data';

    if (!el[varName]) {
      el[varName] = {};
    }

    if (isUndefined(value)) {
      return el[varName][name];
    } else {
      el[varName][name] = value;
    }
  }
}
