import { Component } from 'react';
import { EventEmitter } from 'events';
import { fromJS, Iterable, Map as IMMap } from 'immutable';
import logger from '@ali/vu-logger';
import { cloneDeep, isDataEqual, combineInitial, Transducer } from '@ali/ve-utils';
import I18nUtil from '@ali/ve-i18n-util';
import { getSetter } from '@ali/lowcode-editor-core';
import { editor } from './editor';
import { OldPropConfig, DISPLAY_TYPE } from './bundle/upgrade-metadata';
import { uniqueId } from '@ali/lowcode-utils';

type IPropConfig = OldPropConfig;

// 1: chain -1: start 0: discard
const CHAIN_START = -1;
const CHAIN_HAS_REACH = 0;

export enum PROP_VALUE_CHANGED_TYPE {
  /**
   * normal set value
   */
  SET_VALUE = 'SET_VALUE',
  /**
   * value changed caused by sub-prop value change
   */
  SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE',
}

/**
 * Dynamic setter will use 've.plugin.setterProvider' to
 * calculate setter type in runtime
 */
let dynamicSetterProvider: any;

export interface IHotDataMap extends IMMap<string, any> {
  value: any;
  hotValue: any;
}

export interface ISetValueOptions {
  disableMutator?: boolean;
  type?: PROP_VALUE_CHANGED_TYPE;
}

export interface IVariableSettable {
  useVariable?: boolean;
  variableValue: string;
  isUseVariable: () => boolean;
  isSupportVariable: () => boolean;
  setVariableValue: (value: string) => void;
  setUseVariable: (flag?: boolean) => void;
  getVariableValue: () => string;
  onUseVariableChange: (func: (data: { isUseVariable: boolean }) => any) => void;
}

export default class Prop implements IVariableSettable {

  /**
   * Setters predefined as default options
   * can by selected by user for every prop
   *
   * @static
   * @memberof Prop
   */
  public static INSET_SETTER = {};

  public id: string;
  public emitter: EventEmitter;

  public inited: boolean;
  public i18nLink: any;
  public loopLock: boolean;

  public props: any;
  public parent: any;

  public config: IPropConfig;
  public initial: any;
  public initialData: any;

  public expanded: boolean;
  public useVariable?: boolean;

  /**
   * value to be saved in schema it is usually JSON serialized
   * prototype.js can config Transducer.toNative to generate value
   */
  public value: any;
  /**
   * value to be used in VisualDesigner more flexible
   * prototype.js can config Transducer.toHot to generate hotValue
   */
  public hotValue: any;
  /**
   * 启用变量之后，变量表达式字符串值
   */
  public variableValue: string;
  public hotData: IMMap<string, IHotDataMap>;
  public defaultValue: any;
  public transducer: any;
  public inGroup: boolean;

  constructor(parent: any, config: IPropConfig, data?: any) {
    if (parent.isProps) {
      this.props = parent;
      this.parent = null;
    } else {
      this.props = parent.getProps();
      this.parent = parent;
    }

    this.id = uniqueId('prop');

    if (typeof config.setter === 'string') {
      config.setter = getSetter(config.setter)?.component as any;
    }
    this.config = config;
    this.emitter = new EventEmitter();
    this.emitter.setMaxListeners(100);
    this.initialData = data;
    this.useVariable = false;

    dynamicSetterProvider = editor.get('ve.plugin.setterProvider');

    this.beforeInit();
  }

  public getId() {
    return this.id;
  }

  public isTab() {
    return this.getDisplay() === 'tab';
  }

  public isGroup() {
    return false;
  }

  public beforeInit() {
    if (IMMap.isMap(this.initialData)) {
      this.value = this.initialData.get('value');
      if (this.value && typeof this.value.toJS === 'function') {
        this.value = this.value.toJS();
      }
      this.hotData = this.initialData;
    } else {
      this.value = this.initialData;
    }

    this.resolveValue();

    let defaultValue = null;
    if (this.config.defaultValue !== undefined) {
      defaultValue = this.config.defaultValue;
    } else if (typeof this.config.initialValue !== 'function') {
      defaultValue = this.config.initialValue;
    }
    this.defaultValue = defaultValue;
    this.transducer = new Transducer(this, this.config);
    this.initial = combineInitial(this, this.config);
  }

  public resolveValue() {
    if (this.value && this.value.type === 'variable') {
      const { value, variable } = this.value;
      this.value = value;
      this.variableValue = variable;
      this.useVariable = this.isSupportVariable();
    } else {
      this.useVariable = false;
    }
  }

  public init(defaultValue?: any) {
    if (this.inited) { return; }

    this.value = this.initial(this.value,
      this.defaultValue != null ? this.defaultValue : defaultValue);

    if (this.hotData) {
      const tempVal = this.hotData.get('value');
      // if we create a prop from runtime data, we don't need initial() or set with defaultValue process
      // but if we got an empty value, we fill with the initial() process and default value
      if (Iterable.isIterable(tempVal)) {
        this.value = tempVal.toJS() || this.value;
      } else {
        this.value = tempVal || this.value;
      }
      this.resolveValue();
    }

    this.i18nLink = I18nUtil.attach(this, this.value,
      ((val: any) => { this.setValue(val, false, true); }) as any);

    // call config.accessor
    const value = this.getValue();

    if (this.hotData) {
      this.hotValue = this.hotData.get('hotValue');
      if (this.hotValue && Iterable.isIterable(this.hotValue)) {
        this.hotValue = this.hotValue.toJS();
      }
    } else {
      try {
        this.hotValue = this.transducer.toHot(value);
      } catch (e) {
        logger.log('ERROR_PROP_VALUE');
        logger.warn('属性初始化错误：', this);
      }

      this.hotData = fromJS({
        hotValue: this.hotValue,
        value: this.getMixValue(value),
      });
    }
    this.inited = true;
  }

  public isInited() {
    return this.inited;
  }

  public getHotData() {
    return this.hotData;
  }

  public getProps() {
    return this.props;
  }

  public getNode(): any {
    return this.getProps().getNode();
  }

  /**
   * 获得属性名称
   *
   * @returns {string}
   */
  public getName(): string {
    const ns = this.parent ? `${this.parent.getName()}.` : '';
    return ns + this.config.name;
  }

  public getKey() {
    return this.config.name;
  }

  /**
   * 获得属性标题
   *
   * @returns {string}
   */
  public getTitle() {
    return this.config.title || this.getName();
  }

  public getTip() {
    return this.config.tip || null;
  }

  public getValue(disableCache?: boolean, options?: {
    disableAccessor?: boolean;
  }) {
    const accessor = this.config.accessor;
    if (accessor && (!options || !options.disableAccessor)) {
      const value = accessor.call(this as any, this.value);
      if (!disableCache) {
        this.value = value;
      }
      return value;
    }
    return this.value;
  }

  public getMixValue(value?: any) {
    if (value == null) {
      value = this.getValue();
    }
    if (this.isUseVariable()) {
      value = {
        type: 'variable',
        value,
        variable: this.getVariableValue(),
      };
    }
    return value;
  }

  public toData() {
    return cloneDeep(this.getMixValue());
  }

  public getDefaultValue() {
    return this.defaultValue;
  }

  public getHotValue() {
    return this.hotValue;
  }

  public getConfig<K extends keyof IPropConfig>(configName?: K): IPropConfig[K] | IPropConfig {
    if (configName) {
      return this.config[configName];
    }

    return this.config;
  }

  public sync() {
    if (this.props.hasReach(this)) {
      return;
    }

    const sync = this.config.sync;
    if (sync) {
      const value = sync.call(this as any, this.getValue(true));
      if (value !== undefined) {
        this.setValue(value);
      }
    } else {
      // sync 的时候不再需要调用经过 accessor 处理之后的值了
      // 这里之所以需要 setValue 是为了过 getValue() 中的 accessor 修饰函数
      this.setValue(this.getValue(true), false, false, {
        disableMutator: true,
      });
    }
  }

  public isUseVariable() {
    return this.useVariable || false;
  }

  public isSupportVariable() {
    return this.config.supportVariable || false;
  }

  public setVariableValue(value: string) {
    if (!this.isUseVariable()) { return; }

    const state = this.props.chainReach(this);
    if (state === CHAIN_HAS_REACH) {
      return;
    }

    this.variableValue = value;

    if (this.modify()) {
      this.valueChange();
      this.props.syncPass(this);
    }

    if (state === CHAIN_START) {
      this.props.endChain();
    }
  }

  public setUseVariable(flag: boolean = false) {
    if (this.useVariable === flag) { return; }

    const state = this.props.chainReach(this);
    if (state === CHAIN_HAS_REACH) {
      return;
    }

    this.useVariable = flag;
    this.expanded = true;

    if (this.modify()) {
      this.valueChange();
      this.props.syncPass(this);
    }

    if (state === CHAIN_START) {
      this.props.endChain();
    }

    this.emitter.emit('ve.prop.useVariableChange', { isUseVariable: flag });
    if (this.config.useVariableChange) {
      this.config.useVariableChange.call(this as any, { isUseVariable: flag });
    }
  }

  public getVariableValue() {
    return this.variableValue;
  }

  /**
   * @param value
   * @param isHotValue 是否为设计器热状态值
   * @param force 是否强制触发更新
   */
  public setValue(value: any, isHotValue?: boolean, force?: boolean, extraOptions?: ISetValueOptions) {
    const state = this.props.chainReach(this);
    if (state === CHAIN_HAS_REACH) {
      return;
    }

    const preValue = this.value;
    const preHotValue = this.hotValue;

    if (isHotValue) {
      this.hotValue = value;
      this.value = this.transducer.toNative(this.hotValue);
    } else {
      if (!isDataEqual(value, this.value)) {
        this.hotValue = this.transducer.toHot(value);
      }
      this.value = value;
    }

    this.i18nLink = I18nUtil.attach(this, this.value, ((val: any) => this.setValue(val, false, true)) as any);

    const mutator = this.config.mutator;

    if (!extraOptions) {
      extraOptions = {};
    }

    if (mutator && !extraOptions.disableMutator) {
      mutator.call(this as any, this.value);
    }

    if (this.modify(force)) {
      this.valueChange(extraOptions);
      this.props.syncPass(this);
    }

    if (state === CHAIN_START) {
      this.props.endChain();
    }
  }

  public setHotValue(hotValue: any, options?: ISetValueOptions) {
    try {
      this.setValue(hotValue, true, false, options);
    } catch (e) {
      logger.log('ERROR_PROP_VALUE');
      logger.warn('属性值设置错误：', e, hotValue);
    }
  }

  /**
   * 验证是否存在变更
   * @param force 是否强制返回已变更
   */
  public modify(force?: boolean) {
    const hotData = this.hotData.merge(fromJS({
      hotValue: this.getHotValue(),
      value: this.getMixValue(),
    }));

    if (!force && hotData.equals(this.hotData)) {
      return false;
    }

    this.hotData = hotData;

    (this.parent || this.props).modify(this.getName());

    return true;
  }

  public setHotData(hotData: IMMap<string, IHotDataMap>, options?: ISetValueOptions) {
    if (!IMMap.isMap(hotData)) {
      return;
    }
    this.hotData = hotData;
    let value = hotData.get('value');
    if (value && typeof value.toJS === 'function') {
      value = value.toJS();
    }
    let hotValue = hotData.get('hotValue');
    if (hotValue && typeof hotValue.toJS === 'function') {
      hotValue = hotValue.toJS();
    }

    const preValue = value;
    const preHotValue = hotValue;

    this.value = value;
    this.hotValue = hotValue;
    this.resolveValue();

    if (!options || !options.disableMutator) {
      const mutator = this.config.mutator;
      if (mutator) {
        mutator.call(this as any, value);
      }
    }

    this.valueChange();
  }

  public valueChange(options?: ISetValueOptions) {
    if (this.loopLock) { return; }

    this.emitter.emit('valuechange', options);
    if (this.parent) {
      this.parent.valueChange(options);
    }
  }

  public getDisplay() {
    return this.config.display || this.config.fieldStyle || 'block';
  }

  public isHidden() {
    if (!this.isInited() || this.getDisplay() === DISPLAY_TYPE.NONE || this.isDisabled()) {
      return true;
    }

    let hidden = this.config.hidden;
    if (typeof hidden === 'function') {
      hidden = hidden.call(this as any, this.getValue());
    }
    return hidden === true;
  }

  public isDisabled() {
    let disabled = this.config.disabled;
    if (typeof disabled === 'function') {
      disabled = disabled.call(this as any, this.getValue());
    }
    return disabled === true;
  }

  public isIgnore() {
    if (this.isDisabled()) { return true; }

    let ignore = this.config.ignore;
    if (typeof ignore === 'function') {
      ignore = ignore.call(this as any, this.getValue());
    }
    return ignore === true;
  }

  public isExpand() {
    if (this.expanded == null) {
      this.expanded = !(this.config.collapsed || this.config.fieldCollapsed);
    }
    return this.expanded;
  }

  public toggleExpand() {
    if (this.expanded) {
      this.expanded = false;
    } else {
      this.expanded = true;
    }
    this.emitter.emit('expandchange', this.expanded);
  }

  public getSetter() {
    if (dynamicSetterProvider) {
      const setter = dynamicSetterProvider.call(this, this, this.getNode().getPrototype());
      if (setter) {
        return setter;
      }
    }
    const setterConfig = this.config.setter;
    if (typeof setterConfig === 'function' && !(setterConfig.prototype instanceof Component)) {
      return (setterConfig as any).call(this, this.getValue());
    }
    if (Array.isArray(setterConfig)) {
      let item;
      for (item of setterConfig) {
        if (item.condition?.call(this, this.getValue())) {
          return item.setter;
        }
      }
      return setterConfig[0].setter;
    }
    return setterConfig;
  }

  public getSetterData(): any {
    if (Array.isArray(this.config.setter)) {
      let item;
      for (item of this.config.setter) {
        if (item.condition?.call(this, this.getValue())) {
          return item;
        }
      }
      return this.config.setter[0];
    }
    return { };
  }

  public destroy() {
    if (this.i18nLink) {
      this.i18nLink.detach();
    }
  }

  public onValueChange(func: () => any) {
    this.emitter.on('valuechange', func);
    return () => {
      this.emitter.removeListener('valuechange', func);
    };
  }

  public onExpandChange(func: () => any) {
    this.emitter.on('expandchange', func);
    return () => {
      this.emitter.removeListener('expandchange', func);
    };
  }

  public onUseVariableChange(func: (data: { isUseVariable: boolean }) => any) {
    this.emitter.on('ve.prop.useVariableChange', func);
    return () => {
      this.emitter.removeListener('ve.prop.useVariableChange', func);
    };
  }
}
