import { ComponentType } from 'react';
import { get as lodashGet } from 'lodash';
import { isPlainObject } from '@alilc/lowcode-utils';
import { getLogger } from './utils/logger';

const logger = getLogger({ level: 'log', bizName: 'config' });

// this default behavior will be different later
const STRICT_PLUGIN_MODE_DEFAULT = true;

// used in strict mode, when only options in this VALID_ENGINE_OPTIONS can be accepted
// type and description are only used for developer`s assistance, won`t affect runtime
const VALID_ENGINE_OPTIONS = {
  enableCondition: {
    type: 'boolean',
    description: '是否开启 condition 的能力，默认在设计器中不管 condition 是啥都正常展示',
  },
  designMode: {
    type: 'string',
    enum: ['design', 'live'],
    default: 'design',
    description: '设计模式，live 模式将会实时展示变量值',
  },
  device: {
    type: 'string',
    enum: ['default', 'mobile', 'any string value'],
    default: 'default',
    description: '设备类型',
  },
  locale: {
    type: 'string',
    default: 'zh_CN',
    description: '语言',
  },
  renderEnv: {
    type: 'string',
    enum: ['react', 'rax', 'any string value'],
    default: 'react',
    description: '渲染器类型',
  },
  deviceMapper: {
    type: 'object',
    description: '设备类型映射器，处理设计器与渲染器中 device 的映射',
  },
  enableStrictPluginMode: {
    type: 'boolean',
    default: STRICT_PLUGIN_MODE_DEFAULT,
    description: '开启严格插件模式，默认值: STRICT_PLUGIN_MODE_DEFAULT , 严格模式下，插件将无法通过engineOptions传递自定义配置项',
  },
  enableReactiveContainer: {
    type: 'boolean',
    default: false,
    description: '开启拖拽组件时，即将被放入的容器是否有视觉反馈',
  },
  disableAutoRender: {
    type: 'boolean',
    default: false,
    description: '关闭画布自动渲染，在资产包多重异步加载的场景有效',
  },
  disableDetecting: {
    type: 'boolean',
    default: false,
    description: '关闭拖拽组件时的虚线响应，性能考虑',
  },
  customizeIgnoreSelectors: {
    type: 'function',
    default: undefined,
    description: '定制画布中点击被忽略的 selectors, eg. (defaultIgnoreSelectors: string[]) => string[]',
  },
  disableDefaultSettingPanel: {
    type: 'boolean',
    default: false,
    description: '禁止默认的设置面板',
  },
  disableDefaultSetters: {
    type: 'boolean',
    default: false,
    description: '禁止默认的设置器',
  },
  enableCanvasLock: {
    type: 'boolean',
    default: false,
    description: '打开画布的锁定操作',
  },
  enableLockedNodeSetting: {
    type: 'boolean',
    default: false,
    description: '容器锁定后，容器本身是否可以设置属性，仅当画布锁定特性开启时生效',
  },
  stayOnTheSameSettingTab: {
    type: 'boolean',
    default: false,
    description: '当选中节点切换时，是否停留在相同的设置 tab 上',
  },
  loadingComponent: {
    type: 'ComponentType',
    default: undefined,
    description: '自定义 loading 组件',
  },
  supportVariableGlobally: {
    type: 'boolean',
    default: false,
    description: '设置所有属性支持变量配置',
  },
  visionSettings: {
    type: 'object',
    description: 'Vision-polyfill settings',
  },
  simulatorUrl: {
    type: 'array',
    description: '自定义 simulatorUrl 的地址',
  },
};
export interface EngineOptions {
  /**
   * 是否开启 condition 的能力，默认在设计器中不管 condition 是啥都正常展示
   */
  enableCondition?: boolean;
  /**
   * @todo designMode 无法映射到文档渲染模块
   *
   * @see https://yuque.antfin.com/ali-lowcode/docs/hk2ogo#designMode
   *
   * 设计模式，live 模式将会实时展示变量值，默认值：'design'
   */
  designMode?: 'design' | 'live';
  /**
   * 设备类型，默认值：'default'
   */
  device?: 'default' | 'mobile' | string;
  /**
   * 语言，默认值：'zh_CN'
   */
  locale?: string;
  /**
   * 渲染器类型，默认值：'react'
   */
  renderEnv?: 'react' | 'rax' | string;
  /**
   * 设备类型映射器，处理设计器与渲染器中 device 的映射
   */
  deviceMapper?: {
    transform: (originalDevice: string) => string;
  };
  /**
   * 开启严格插件模式，默认值: STRICT_PLUGIN_MODE_DEFAULT , 严格模式下，插件将无法通过engineOptions传递自定义配置项
   * enable strict plugin mode, default value: false
   * under strict mode, customed engineOption is not accepted.
   */
   enableStrictPluginMode?: boolean;
  /**
   * 开启拖拽组件时，即将被放入的容器是否有视觉反馈，默认值：false
   */
  enableReactiveContainer?: boolean;
  /**
   * 关闭画布自动渲染，在资产包多重异步加载的场景有效，默认值：false
   */
  disableAutoRender?: boolean;
  /**
   * 关闭拖拽组件时的虚线响应，性能考虑，默认值：false
   */
  disableDetecting?: boolean;
  /**
   * 定制画布中点击被忽略的 selectors，默认值：undefined
   */
  customizeIgnoreSelectors?: (defaultIgnoreSelectors: string[]) => string[];
  /**
   * 禁止默认的设置面板，默认值：false
   */
  disableDefaultSettingPanel?: boolean;
  /**
   * 禁止默认的设置器，默认值：false
   */
  disableDefaultSetters?: boolean;
  /**
   * 打开画布的锁定操作，默认值：false
   */
  enableCanvasLock?: boolean;
  /**
   * 容器锁定后，容器本身是否可以设置属性，仅当画布锁定特性开启时生效， 默认值为：false
   */
  enableLockedNodeSetting?: boolean;
  /**
   * 当选中节点切换时，是否停留在相同的设置 tab 上，默认值：false
   */
  stayOnTheSameSettingTab?: boolean;
  /**
   * 自定义 loading 组件
   */
  loadingComponent?: ComponentType;
  /**
   * 设置所有属性支持变量配置，默认值：false
   */
  supportVariableGlobally?: boolean;
  /**
   * 设置 simulator 相关的 url，默认值：undefined
   */
  simulatorUrl?: string[];
  /**
   * Vision-polyfill settings
   */
  visionSettings?: {
    // 是否禁用降级 reducer，默认值：false
    disableCompatibleReducer?: boolean;
    // 是否开启在 render 阶段开启 filter reducer，默认值：false
    enableFilterReducerInRenderStage?: boolean;
  };
}

const getStrictModeValue = (engineOptions: EngineOptions, defaultValue: boolean): boolean => {
  if (!engineOptions || !isPlainObject(engineOptions)) {
    return defaultValue;
  }
  if (engineOptions.enableStrictPluginMode === undefined
    || engineOptions.enableStrictPluginMode === null) {
    return defaultValue;
  }
  return engineOptions.enableStrictPluginMode;
};
export class EngineConfig {
  private config: { [key: string]: any } = {};

  private waits = new Map<
  string,
  Array<{
    once?: boolean;
    resolve: (data: any) => void;
  }>
  >();

  constructor(config?: { [key: string]: any }) {
    this.config = config || {};
  }

  has(key: string): boolean {
    return this.config[key] !== undefined;
  }

  get(key: string, defaultValue?: any): any {
    return lodashGet(this.config, key, defaultValue);
  }

  set(key: string, value: any) {
    this.config[key] = value;
    this.notifyGot(key);
  }

  setConfig(config: { [key: string]: any }) {
    if (config) {
      Object.keys(config).forEach((key) => {
        this.set(key, config[key]);
      });
    }
  }

  /**
   * if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions.
   *
   * @param {EngineOptions} engineOptions
   * @memberof EngineConfig
   */
  setEngineOptions(engineOptions: EngineOptions) {
    if (!engineOptions || !isPlainObject(engineOptions)) {
      return;
    }
    const strictMode = getStrictModeValue(engineOptions, STRICT_PLUGIN_MODE_DEFAULT) === true;
    if (strictMode) {
      const isValidKey = (key: string) => {
        const result = (VALID_ENGINE_OPTIONS as any)[key];
        return !(result === undefined || result === null);
      };
      Object.keys(engineOptions).forEach((key) => {
        if (isValidKey(key)) {
          this.set(key, engineOptions[key]);
        } else {
          logger.warn(`failed to config ${key} to engineConfig, only predefined options can be set under strict mode, predefined options: ${VALID_ENGINE_OPTIONS}`);
        }
      });
    } else {
      this.setConfig(engineOptions as any);
    }
  }

  onceGot(key: string): Promise<any> {
    const val = this.config[key];
    if (val !== undefined) {
      return Promise.resolve(val);
    }
    return new Promise((resolve) => {
      this.setWait(key, resolve, true);
    });
  }

  onGot(key: string, fn: (data: any) => void): () => void {
    const val = this.config?.[key];
    if (val !== undefined) {
      fn(val);
      return () => {};
    } else {
      this.setWait(key, fn);
      return () => {
        this.delWait(key, fn);
      };
    }
  }

  private notifyGot(key: string) {
    let waits = this.waits.get(key);
    if (!waits) {
      return;
    }
    waits = waits.slice().reverse();
    let i = waits.length;
    while (i--) {
      waits[i].resolve(this.get(key));
      if (waits[i].once) {
        waits.splice(i, 1);
      }
    }
    if (waits.length > 0) {
      this.waits.set(key, waits);
    } else {
      this.waits.delete(key);
    }
  }

  private setWait(key: string, resolve: (data: any) => void, once?: boolean) {
    const waits = this.waits.get(key);
    if (waits) {
      waits.push({ resolve, once });
    } else {
      this.waits.set(key, [{ resolve, once }]);
    }
  }

  private delWait(key: string, fn: any) {
    const waits = this.waits.get(key);
    if (!waits) {
      return;
    }
    let i = waits.length;
    while (i--) {
      if (waits[i].resolve === fn) {
        waits.splice(i, 1);
      }
    }
    if (waits.length < 1) {
      this.waits.delete(key);
    }
  }
}

export const engineConfig = new EngineConfig();
