import { get as lodashGet } from 'lodash';
import { isPlainObject } from '@felce/lowcode-utils';
import {
  IPublicTypeEngineOptions,
  IPublicModelEngineConfig,
  IPublicModelPreference,
} from '@felce/lowcode-types';
import { getLogger } from './utils/logger';
import Preference from './utils/preference';

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: '设备类型',
  },
  deviceClassName: {
    type: 'string',
    default: undefined,
    description: '指定初始化的 deviceClassName，挂载到画布的顶层节点上',
  },
  locale: {
    type: 'string',
    default: 'zh-CN',
    description: '语言',
  },
  renderEnv: {
    type: 'string',
    enum: ['react', '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[], e: MouseEvent) => 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 上',
  },
  hideSettingsTabsWhenOnlyOneItem: {
    type: 'boolean',
    description: '是否在只有一个 item 的时候隐藏设置 tabs',
  },
  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 的地址',
  },
  // 与 react-renderer 的 appHelper 一致，https://lowcode-engine.cn/site/docs/guide/expand/runtime/renderer#apphelper
  appHelper: {
    type: 'object',
    description: '定义 utils 和 constants 等对象',
  },
  requestHandlersMap: {
    type: 'object',
    description: '数据源引擎的请求处理器映射',
  },
  thisRequiredInJSE: {
    type: 'boolean',
    description: 'JSExpression 是否只支持使用 this 来访问上下文变量',
  },
  enableStrictNotFoundMode: {
    type: 'boolean',
    description: '当开启组件未找到严格模式时，渲染模块不会默认给一个容器组件',
  },
  focusNodeSelector: {
    type: 'function',
    description: '配置指定节点为根组件',
  },
  enableAutoOpenFirstWindow: {
    type: 'boolean',
    description: '应用级设计模式下，自动打开第一个窗口',
    default: true,
  },
  enableWorkspaceMode: {
    type: 'boolean',
    description: '是否开启应用级设计模式',
    default: false,
  },
  workspaceEmptyComponent: {
    type: 'function',
    description: '应用级设计模式下，窗口为空时展示的占位组件',
  },
  enableContextMenu: {
    type: 'boolean',
    description: '是否开启右键菜单',
    default: false,
  },
  hideComponentAction: {
    type: 'boolean',
    description: '是否隐藏设计器辅助层',
    default: false,
  },
};

const getStrictModeValue = (
  engineOptions: IPublicTypeEngineOptions,
  defaultValue: boolean,
): boolean => {
  if (!engineOptions || !isPlainObject(engineOptions)) {
    return defaultValue;
  }
  if (
    engineOptions.enableStrictPluginMode === undefined ||
    engineOptions.enableStrictPluginMode === null
  ) {
    return defaultValue;
  }
  return engineOptions.enableStrictPluginMode;
};

export interface IEngineConfig extends IPublicModelEngineConfig {
  /**
   * if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions.
   *
   * @param {IPublicTypeEngineOptions} engineOptions
   */
  setEngineOptions(engineOptions: IPublicTypeEngineOptions): void;

  notifyGot(key: string): void;

  setWait(key: string, resolve: (data: any) => void, once?: boolean): void;

  delWait(key: string, fn: any): void;
}

export class EngineConfig implements IEngineConfig {
  private config: { [key: string]: any } = {};

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

  /**
   * used to store preferences
   *
   */
  readonly preference: IPublicModelPreference;

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

  /**
   * 判断指定 key 是否有值
   * @param key
   */
  has(key: string): boolean {
    return this.config[key] !== undefined;
  }

  /**
   * 获取指定 key 的值
   * @param key
   * @param defaultValue
   */
  get(key: string, defaultValue?: any): any {
    return lodashGet(this.config, key, defaultValue);
  }

  /**
   * 设置指定 key 的值
   * @param key
   * @param value
   */
  set(key: string, value: any) {
    this.config[key] = value;
    this.notifyGot(key);
  }

  /**
   * 批量设值，set 的对象版本
   * @param config
   */
  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 {IPublicTypeEngineOptions} engineOptions
   */
  setEngineOptions(engineOptions?: IPublicTypeEngineOptions) {
    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 as any)[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);
    }
  }

  /**
   * 获取指定 key 的值，若此时还未赋值，则等待，若已有值，则直接返回值
   *  注：此函数返回 Promise 实例，只会执行（fullfill）一次
   * @param key
   * @returns
   */
  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);
    });
  }

  /**
   * 获取指定 key 的值，函数回调模式，若多次被赋值，回调会被多次调用
   * @param key
   * @param fn
   * @returns
   */
  onGot(key: string, fn: (data: any) => void): () => void {
    const val = this.config?.[key];
    if (val !== undefined) {
      fn(val);
    }
    this.setWait(key, fn);
    return () => {
      this.delWait(key, fn);
    };
  }

  notifyGot(key: string): void {
    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);
    }
  }

  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 }]);
    }
  }

  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);
    }
  }

  getPreference(): IPublicModelPreference {
    return this.preference;
  }
}

export const engineConfig = new EngineConfig();
