/**
 * With Style Manager you build categories (called sectors) of CSS properties which could be used to customize the style of components.
 * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/GrapesJS/grapesjs/blob/master/src/style_manager/config/config.ts)
 * ```js
 * const editor = grapesjs.init({
 *  styleManager: {
 *    // options
 *  }
 * })
 * ```
 *
 * Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
 *
 * ```js
 * // Listen to events
 * editor.on('style:sector:add', (sector) => { ... });
 *
 * // Use the API
 * const styleManager = editor.StyleManager;
 * styleManager.addSector(...);
 * ```
 * ## Available Events
 * * `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
 * * `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
 * * `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
 * * `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
 * * `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
 * * `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
 * * `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
 * <!--
 * * `styleManager:update:target` - The target (Component or CSSRule) is changed
 * * `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
 * * `styleManager:change:{propertyName}` - As above but for a specific style property
 * -->
 *
 * ## Methods
 * * [getConfig](#getconfig)
 * * [addSector](#addsector)
 * * [getSector](#getsector)
 * * [getSectors](#getsectors)
 * * [removeSector](#removesector)
 * * [addProperty](#addproperty)
 * * [getProperty](#getproperty)
 * * [getProperties](#getproperties)
 * * [removeProperty](#removeproperty)
 * * [select](#select)
 * * [getSelected](#getselected)
 * * [getSelectedAll](#getselectedall)
 * * [getSelectedParents](#getselectedparents)
 * * [addStyleTargets](#addstyletargets)
 * * [getBuiltIn](#getbuiltin)
 * * [getBuiltInAll](#getbuiltinall)
 * * [addBuiltIn](#addbuiltin)
 * * [addType](#addtype)
 * * [getType](#gettype)
 * * [getTypes](#gettypes)
 *
 * [Sector]: sector.html
 * [CssRule]: css_rule.html
 * [Component]: component.html
 * [Property]: property.html
 *
 * @module docsjs.StyleManager
 */

import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
import { isComponent } from '../utils/mixins';
import { AddOptions, Debounced, Model } from '../common';
import defaults, { StyleManagerConfig } from './config/config';
import Sector, { SectorProperties } from './model/Sector';
import Sectors from './model/Sectors';
import Properties from './model/Properties';
import PropertyFactory from './model/PropertyFactory';
import SectorsView from './view/SectorsView';
import { ItemManagerModule } from '../abstract/Module';
import EditorModel from '../editor/model/Editor';
import Property, { PropertyProps } from './model/Property';
import Component from '../dom_components/model/Component';
import CssRule from '../css_composer/model/CssRule';
import StyleableModel, { StyleProps } from '../domain_abstract/model/StyleableModel';
import { CustomPropertyView } from './view/PropertyView';
import { PropertySelectProps } from './model/PropertySelect';
import { PropertyNumberProps } from './model/PropertyNumber';
import { PropertyStackProps } from './model/PropertyStack';

export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps;

export type StyleManagerEvent =
  | 'style:sector:add'
  | 'style:sector:remove'
  | 'style:sector:update'
  | 'style:property:add'
  | 'style:property:remove'
  | 'style:property:update'
  | 'style:target';

export type StyleTarget = StyleableModel;

export const evAll = 'style';
export const evPfx = `${evAll}:`;
export const evSector = `${evPfx}sector`;
export const evSectorAdd = `${evSector}:add`;
export const evSectorRemove = `${evSector}:remove`;
export const evSectorUpdate = `${evSector}:update`;
export const evProp = `${evPfx}property`;
export const evPropAdd = `${evProp}:add`;
export const evPropRemove = `${evProp}:remove`;
export const evPropUp = `${evProp}:update`;
export const evLayerSelect = `${evPfx}layer:select`;
export const evTarget = `${evPfx}target`;
export const evCustom = `${evPfx}custom`;

export type StyleModuleParam<T extends keyof StyleManager, N extends number> = Parameters<StyleManager[T]>[N];

const propDef = (value: any) => value || value === 0;

const stylesEvents = {
  all: evAll,
  sectorAdd: evSectorAdd,
  sectorRemove: evSectorRemove,
  sectorUpdate: evSectorUpdate,
  propertyAdd: evPropAdd,
  propertyRemove: evPropRemove,
  propertyUpdate: evPropUp,
  layerSelect: evLayerSelect,
  target: evTarget,
  custom: evCustom,
};

export default class StyleManager extends ItemManagerModule<
  StyleManagerConfig,
  /** @ts-ignore */
  Sectors
> {
  builtIn: PropertyFactory;
  upAll: Debounced;
  properties: typeof Properties;
  events!: typeof stylesEvents;
  sectors: Sectors;
  SectView!: SectorsView;
  Sector = Sector;
  storageKey = '';
  __ctn?: HTMLElement;

  /**
   * Get configuration object
   * @name getConfig
   * @function
   * @return {Object}
   */

  /**
   * Initialize module. Automatically called with a new instance of the editor
   * @param {Object} config Configurations
   * @private
   */
  constructor(em: EditorModel) {
    super(em, 'StyleManager', new Sectors([], { em }), stylesEvents, defaults);
    bindAll(this, '__clearStateTarget');
    const c = this.config;
    const ppfx = c.pStylePrefix;
    if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
    this.builtIn = new PropertyFactory();
    this.properties = new Properties([], { em, module: this });
    this.sectors = this.all; // TODO check if (module: this) is required
    const model = new Model({ targets: [] });
    this.model = model;

    // Triggers for the selection refresh and properties
    const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
    this.upAll = debounce(() => this.__upSel(), 0);
    model.listenTo(em, ev, this.upAll as any);
    // Clear state target on any component selection change, without debounce (#4208)
    model.listenTo(em, 'component:toggled', this.__clearStateTarget);

    // Triggers only for properties (avoid selection refresh)
    const upProps = debounce(() => {
      this.__upProps();
      this.__trgCustom();
    }, 0);
    model.listenTo(em, 'styleable:change undo redo', upProps);

    // Triggers only custom event
    const trgCustom = debounce(() => this.__trgCustom(), 0);
    model.listenTo(em, `${evLayerSelect} ${evTarget}`, trgCustom);

    // Other listeners
    model.on('change:lastTarget', () => em.trigger(evTarget, this.getSelected()));
  }

  __upSel() {
    this.select(this.em.getSelectedAll() as any);
  }

  __trgCustom(opts: { container?: HTMLElement } = {}) {
    this.__ctn = this.__ctn || opts.container;
    this.em.trigger(this.events.custom, { container: this.__ctn });
  }

  __trgEv(event: string, ...data: any[]) {
    this.em.trigger(event, ...data);
  }

  __clearStateTarget() {
    const { em } = this;
    const stateTarget = this.__getStateTarget();
    stateTarget &&
      em?.skip(() => {
        em.Css.remove(stateTarget);
        this.model.set({ stateTarget: null });
      });
  }

  onLoad() {
    // Use silent as sectors' view will be created and rendered on StyleManager.render
    this.sectors.add(this.config.sectors!, { silent: true });
  }

  postRender() {
    this.__appendTo();
  }

  /**
   * Add new sector. If the sector with the same id already exists, that one will be returned.
   * @param {String} id Sector id
   * @param {Object} sector Sector definition. Check the [available properties](sector.html#properties)
   * @param {Object} [options={}] Options
   * @param {Number} [options.at] Position index (by default, will be appended at the end).
   * @returns {[Sector]} Added Sector
   * @example
   * const sector = styleManager.addSector('mySector',{
   *   name: 'My sector',
   *   open: true,
   *   properties: [{ name: 'My property'}]
   * }, { at: 0 });
   * // With `at: 0` we place the new sector at the beginning of the list
   * */
  addSector(id: string, sector: SectorProperties, options: AddOptions = {}) {
    let result = this.getSector(id);

    if (!result) {
      sector.id = id;
      result = this.sectors.add(sector, options);
    }

    return result;
  }

  /**
   * Get sector by id.
   * @param {String} id  Sector id
   * @returns {[Sector]|null}
   * @example
   * const sector = styleManager.getSector('mySector');
   * */
  getSector(id: string, opts: { warn?: boolean } = {}) {
    const res = this.sectors.where({ id })[0];
    !res && opts.warn && this._logNoSector(id);
    return res || null;
  }

  /**
   * Get all sectors.
   * @param {Object} [opts={}] Options
   * @param {Boolean} [opts.visible] Returns only visible sectors
   * @returns {Array<[Sector]>}
   * @example
   * const sectors = styleManager.getSectors();
   * */
  getSectors<T extends { array?: boolean; visible?: boolean }>(opts: T = {} as T) {
    const { sectors } = this;
    const res = sectors && sectors.models ? (opts.array ? [...sectors.models] : sectors) : [];
    return (opts.visible ? res.filter(s => s.isVisible()) : res) as T['array'] extends true
      ? Sector[]
      : T['visible'] extends true
      ? Sector[]
      : Sectors;
  }

  /**
   * Remove sector by id.
   * @param  {String} id Sector id
   * @returns {[Sector]} Removed sector
   * @example
   * const removed = styleManager.removeSector('mySector');
   */
  removeSector(id: string) {
    return this.getSectors().remove(this.getSector(id, { warn: true }));
  }

  /**
   * Add new property to the sector.
   * @param {String} sectorId Sector id.
   * @param {Object} property Property definition. Check the [base available properties](property.html#properties) + others based on the `type` of your property.
   * @param {Object} [opts={}] Options
   * @param {Number} [opts.at] Position index (by default, will be appended at the end).
   * @returns {[Property]|null} Added property or `null` in case the sector doesn't exist.
   * @example
   * const property = styleManager.addProperty('mySector', {
   *   label: 'Minimum height',
   *   property: 'min-height',
   *   type: 'select',
   *   default: '100px',
   *   options: [
   *    { id: '100px', label: '100' },
   *    { id: '200px', label: '200' },
   *   ],
   * }, { at: 0 });
   */
  addProperty(sectorId: string, property: PropertyTypes, opts: AddOptions = {}): Property | undefined {
    const sector = this.getSector(sectorId, { warn: true });
    let prop = null;
    if (sector) prop = sector.addProperty(property, opts);

    return prop;
  }

  /**
   * Get the property.
   * @param {String} sectorId Sector id.
   * @param {String} id Property id.
   * @returns {[Property]|undefined}
   * @example
   * const property = styleManager.getProperty('mySector', 'min-height');
   */
  getProperty(sectorId: string, id: string): Property | undefined {
    const sector = this.getSector(sectorId, { warn: true });
    let prop;

    if (sector) {
      prop = sector.properties.filter(prop => prop.get('property') === id || prop.get('id') === id)[0];
    }

    return prop;
  }

  /**
   * Get all properties of the sector.
   * @param {String} sectorId Sector id.
   * @returns {Collection<[Property]>|undefined} Collection of properties
   * @example
   * const properties = styleManager.getProperties('mySector');
   */
  getProperties(sectorId: string) {
    let props;
    const sector = this.getSector(sectorId, { warn: true });
    if (sector) props = sector.properties;

    return props;
  }

  /**
   * Remove the property.
   * @param {String} sectorId Sector id.
   * @param {String} id Property id.
   * @returns {[Property]|null} Removed property
   * @example
   * const property = styleManager.removeProperty('mySector', 'min-height');
   */
  removeProperty(sectorId: string, id: string) {
    const props = this.getProperties(sectorId);
    return props ? props.remove(this.getProperty(sectorId, id)!) : null;
  }

  /**
   * Select new target.
   * The target could be a Component, CSSRule, or a CSS selector string.
   * @param {[Component]|[CSSRule]|String} target
   * @returns {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
   * @example
   * // Select the first button in the current page
   * const wrapperCmp = editor.Pages.getSelected().getMainComponent();
   * const btnCmp = wrapperCmp.find('button')[0];
   * btnCmp && styleManager.select(btnCmp);
   *
   * // Set as a target the CSS selector
   * styleManager.select('.btn > span');
   */
  select(
    target: StyleTarget | string | (StyleTarget | string)[],
    opts: { stylable?: boolean; component?: Component } = {}
  ) {
    const { em } = this;
    const trgs = isArray(target) ? target : [target];
    const { stylable } = opts;
    const cssc = em.Css;
    let targets: StyleTarget[] = [];

    trgs.filter(Boolean).forEach(target => {
      let model = target;

      if (isString(target)) {
        const rule = cssc.getRule(target) || cssc.setRule(target);
        !isUndefined(stylable) && rule.set({ stylable });
        model = rule;
      }

      targets.push(model as StyleTarget);
    });

    const component = opts.component || targets.filter(t => isComponent(t)).reverse()[0];
    targets = targets.map(t => this.getModelToStyle(t));
    const state = em.getState();
    const lastTarget = targets.slice().reverse()[0];
    const lastTargetParents = this.getParentRules(lastTarget, {
      state,
      // @ts-ignore
      component,
    });
    let stateTarget = this.__getStateTarget();

    // Handle the creation and update of the state rule, if enabled.
    em.skip(() => {
      // @ts-ignore
      if (state && lastTarget?.getState?.()) {
        const style = lastTarget.getStyle();
        if (!stateTarget) {
          stateTarget = cssc.getAll().add({
            selectors: 'gjs-selected',
            style,
            shallow: true,
            important: true,
          }) as unknown as CssRule;
        } else {
          stateTarget.setStyle(style);
        }
      } else if (stateTarget) {
        cssc.remove(stateTarget);
        stateTarget = undefined;
      }
    });

    this.model.set({
      targets,
      lastTarget,
      lastTargetParents,
      stateTarget,
      component,
    });
    this.__upProps(opts);

    return targets;
  }

  /**
   * Get the last selected target.
   * By default, the Style Manager shows styles of the last selected target.
   * @returns {[Component]|[CSSRule]|null}
   */
  getSelected(): StyleTarget | undefined {
    return this.model.get('lastTarget');
  }

  /**
   * Get the array of selected targets.
   * @returns {Array<[Component]|[CSSRule]>}
   */
  getSelectedAll() {
    return this.model.get('targets') as StyleTarget[];
  }

  /**
   * Get parent rules of the last selected target.
   * @returns {Array<[CSSRule]>}
   */
  getSelectedParents(): CssRule[] {
    return this.model.get('lastTargetParents') || [];
  }

  __getStateTarget(): CssRule | undefined {
    return this.model.get('stateTarget');
  }

  /**
   * Update selected targets with a custom style.
   * @param {Object} style Style object
   * @param {Object} [opts={}] Options
   * @example
   * styleManager.addStyleTargets({ color: 'red' });
   */
  addStyleTargets(style: StyleProps, opts: any) {
    this.getSelectedAll().map(t => t.addStyle(style, opts));
    const target = this.getSelected();

    // Trigger style changes on selected components
    target && this.__emitCmpStyleUpdate(style);

    // Update state rule
    const targetState = this.__getStateTarget();
    target && targetState?.setStyle(target.getStyle(), opts);
  }

  /**
   * Return built-in property definition
   * @param {String} prop Property name.
   * @returns {Object|null} Property definition.
   * @example
   * const widthPropDefinition = styleManager.getBuiltIn('width');
   */
  getBuiltIn(prop: string) {
    return this.builtIn.get(prop);
  }

  /**
   * Get all the available built-in property definitions.
   * @returns {Object}
   */
  getBuiltInAll() {
    return this.builtIn.props;
  }

  /**
   * Add built-in property definition.
   * If the property exists already, it will extend it.
   * @param {String} prop Property name.
   * @param {Object} definition Property definition.
   * @returns {Object} Added property definition.
   * @example
   * const sector = styleManager.addBuiltIn('new-property', {
   *  type: 'select',
   *  default: 'value1',
   *  options: [{ id: 'value1', label: 'Some label' }, ...],
   * })
   */
  addBuiltIn(prop: string, definition: Omit<PropertyProps, 'property'> & { proeperty?: 'string' }) {
    return this.builtIn.add(prop, definition);
  }

  /**
   * Get what to style inside Style Manager. If you select the component
   * without classes the entity is the Component itself and all changes will
   * go inside its 'style' property. Otherwise, if the selected component has
   * one or more classes, the function will return the corresponding CSS Rule
   * @param  {Model} model
   * @return {Model}
   * @private
   */
  getModelToStyle(model: any, options: { skipAdd?: boolean; useClasses?: boolean } = {}) {
    const { em } = this;
    const { skipAdd } = options;

    if (em && model?.toHTML) {
      const config = em.getConfig();
      const um = em.UndoManager;
      const cssC = em.Css;
      const sm = em.Selectors;
      const smConf = sm ? sm.getConfig() : {};
      const state = !config.devicePreviewMode ? em.get('state') : '';
      const classes = model.get('classes');
      const valid = classes.getStyleable();
      const hasClasses = valid.length;
      const useClasses = !smConf.componentFirst || options.useClasses;
      const addOpts = { noCount: 1 };
      const opts = { state, addOpts };
      let rule;

      // I stop undo manager here as after adding the CSSRule (generally after
      // selecting the component) and calling undo() it will remove the rule from
      // the collection, therefore updating it in style manager will not affect it
      // #268
      um.stop();

      if (hasClasses && useClasses) {
        const deviceW = em.getCurrentMedia();
        rule = cssC.get(valid, state, deviceW);

        if (!rule && !skipAdd) {
          rule = cssC.add(valid, state, deviceW, {}, addOpts);
        }
      } else if (config.avoidInlineStyle) {
        const id = model.getId();
        rule = cssC.getIdRule(id, opts);
        !rule && !skipAdd && (rule = cssC.setIdRule(id, {}, opts));
        if (model.is('wrapper')) {
          // @ts-ignore
          rule!.set('wrapper', 1, addOpts);
        }
      }

      rule && (model = rule);
      um.start();
    }

    return model;
  }

  getParentRules(target: StyleTarget, { state, component }: { state?: string; component?: Component } = {}) {
    const { em } = this;
    let result: CssRule[] = [];

    if (em && target) {
      const sel = component;
      const cssC = em.Css;
      const cssGen = em.CodeManager.getGenerator('css');
      // @ts-ignore
      const cmp = target.toHTML ? target : target.getComponent();
      const optsSel = { combination: true, array: true };
      let cmpRules = [];
      let otherRules = [];
      let rules = [];

      // Componente related rule
      if (cmp) {
        cmpRules = cssC.getRules(`#${cmp.getId()}`);
        otherRules = sel ? cssC.getRules(sel.getSelectors().getFullName(optsSel) as string) : [];
        rules = otherRules.concat(cmpRules);
      } else {
        cmpRules = sel ? cssC.getRules(`#${sel.getId()}`) : [];
        otherRules = cssC.getRules(target.getSelectors().getFullName(optsSel) as string);
        rules = cmpRules.concat(otherRules);
      }

      const all = rules
        .filter(rule => (!isUndefined(state) ? rule.get('state') === state : 1))
        .sort(cssGen.sortRules)
        .reverse();

      // Slice removes rules not related to the current device
      result = all.slice(all.indexOf(target as CssRule) + 1);
    }

    return result;
  }

  /**
   * Add new property type
   * @param {string} id Type ID
   * @param {Object} definition Definition of the type.
   * @example
   * styleManager.addType('my-custom-prop', {
   *    // Create UI
   *    create({ props, change }) {
   *      const el = document.createElement('div');
   *      el.innerHTML = '<input type="range" class="my-input" min="10" max="50"/>';
   *      const inputEl = el.querySelector('.my-input');
   *      inputEl.addEventListener('change', event => change({ event }));
   *      inputEl.addEventListener('input', event => change({ event, partial: true }));
   *      return el;
   *    },
   *    // Propagate UI changes up to the targets
   *    emit({ props, updateStyle }, { event, partial }) {
   *      const { value } = event.target;
   *      updateStyle(`${value}px`, { partial });
   *    },
   *    // Update UI (eg. when the target is changed)
   *    update({ value, el }) {
   *      el.querySelector('.my-input').value = parseInt(value, 10);
   *    },
   *    // Clean the memory from side effects if necessary (eg. global event listeners, etc.)
   *    destroy() {}
   *})
   */
  addType<T>(id: string, definition: CustomPropertyView<T>) {
    this.properties.addType(id, definition);
  }

  /**
   * Get type
   * @param {string} id Type ID
   * @return {Object} Type definition
   */
  getType(id: string) {
    return this.properties.getType(id);
  }

  /**
   * Get all types
   * @return {Array}
   */
  getTypes() {
    return this.properties.getTypes();
  }

  /**
   * Create new UI property from type (Experimental)
   * @param {string} id Type ID
   * @param  {Object} [options={}] Options
   * @param  {Object} [options.model={}] Custom model object
   * @param  {Object} [options.view={}] Custom view object
   * @return {PropertyView}
   * @private
   * @example
   * const propView = styleManager.createType('number', {
   *  model: {units: ['px', 'rem']}
   * });
   * propView.render();
   * propView.model.on('change:value', ...);
   * someContainer.appendChild(propView.el);
   */
  createType(id: string, { model = {}, view = {} } = {}) {
    const { config } = this;
    const type = this.getType(id);

    if (type) {
      return new type.view({
        model: new type.model(model),
        config,
        ...view,
      });
    }
  }

  /**
   * Render sectors and properties
   * @return  {HTMLElement}
   * @private
   * */
  render() {
    const { config, em, SectView } = this;
    const el = SectView && SectView.el;
    this.SectView = new SectorsView({
      el,
      em,
      config,
      module: this,
      collection: this.sectors,
    });
    return this.SectView.render().el;
  }

  _logNoSector(sectorId: string) {
    const { em } = this;
    em && em.logWarning(`'${sectorId}' sector not found`);
  }

  __emitCmpStyleUpdate(style: StyleProps, opts: { components?: Component | Component[] } = {}) {
    const { em } = this;
    const event = 'component:styleUpdate';

    // Ignore partial updates
    if (!style.__p) {
      const cmp = opts.components || em.getSelectedAll();
      const cmps = Array.isArray(cmp) ? cmp : [cmp];
      const newStyle = { ...style };
      delete newStyle.__p;
      const styleKeys = Object.keys(newStyle);
      const optsToPass = { style: newStyle };

      cmps.forEach(component => {
        em.trigger(event, component, optsToPass);
        styleKeys.forEach(key => em.trigger(`${event}:${key}`, component, optsToPass));
      });
    }
  }

  __upProps(opts = {}) {
    const lastTarget = this.getSelected();
    if (!lastTarget) return;

    const { sectors } = this;
    const component = this.model.get('component');
    const lastTargetParents = this.getSelectedParents();
    const style = lastTarget.getStyle();
    const parentStyles = lastTargetParents.map(p => ({
      target: p,
      style: p.getStyle(),
    }));

    sectors.map(sector => {
      sector.getProperties().map(prop => {
        this.__upProp(prop, style, parentStyles, opts);
      });
    });

    // Update sectors/properties visibility
    sectors.forEach(sector => {
      const props = sector.getProperties();
      props.forEach(prop => {
        const isVisible = prop.__checkVisibility({
          target: lastTarget,
          component,
          // @ts-ignore
          sectors,
        });
        prop.set('visible', isVisible);
      });
      const sectorVisible = props.some(p => p.isVisible());
      sector.set('visible', sectorVisible);
    });
  }

  __upProp(prop: any, style: StyleProps, parentStyles: any[], opts: any) {
    const name = prop.getName();
    const value = style[name];
    const hasVal = propDef(value);
    const isStack = prop.getType() === 'stack';
    const isComposite = prop.getType() === 'composite';
    const opt = { ...opts, __up: true };
    const canUpdate = !isComposite && !isStack;
    let newLayers = isStack ? prop.__getLayersFromStyle(style) : [];
    let newProps = isComposite ? prop.__getPropsFromStyle(style) : {};
    let newValue = hasVal ? value : null;
    let parentTarget: any = null;

    if ((isStack && newLayers === null) || (isComposite && newProps === null)) {
      const method = isStack ? '__getLayersFromStyle' : '__getPropsFromStyle';
      const parentItem = parentStyles.filter(p => prop[method](p.style) !== null)[0];

      if (parentItem) {
        newValue = parentItem.style[name];
        parentTarget = parentItem.target;
        const val = prop[method](parentItem.style);
        if (isStack) {
          newLayers = val;
        } else {
          newProps = val;
        }
      }
    } else if (!hasVal) {
      newValue = null;
      const parentItem = parentStyles.filter(p => propDef(p.style[name]))[0];

      if (parentItem) {
        newValue = parentItem.style[name];
        parentTarget = parentItem.target;
      }
    }

    prop.__setParentTarget(parentTarget);
    canUpdate && prop.__getFullValue() !== newValue && prop.upValue(newValue, opt);
    isStack && prop.__setLayers(newLayers || []);
    if (isComposite) {
      const props = prop.getProperties();

      // Detached has to be treathed as separate properties
      if (prop.isDetached()) {
        const newStyle = prop.__getPropsFromStyle(style, { byName: true }) || {};
        const newParentStyles = parentStyles.map(p => ({
          ...p,
          style: prop.__getPropsFromStyle(p.style, { byName: true }) || {},
        }));
        props.map((pr: any) => this.__upProp(pr, newStyle, newParentStyles, opts));
      } else {
        prop.__setProperties(newProps || {}, opt);
        prop.getProperties().map((pr: any) => pr.__setParentTarget(parentTarget));
      }
    }
  }

  destroy() {
    [this.properties, this.sectors].forEach(coll => {
      coll.reset();
      coll.stopListening();
    });
    this.SectView?.remove();
    this.model.stopListening();
    this.upAll.cancel();
  }
}
