import { createApp, type App, onMounted, onUnmounted } from 'vue';
import {
  EngineRenderer,
  SetterValueTypes,
  buildInSetters
} from '../components';
import { Skeleton } from './Skeleton';
import { Simulator } from './Simulator';
import { Renderer } from './renderer';
import { AssetsJSON, PorjectSchema } from './types';
import { mergeConfig, IConfig, ISetterConfig } from './config';
import {
  emitter,
  Emitter,
  EVENT_PROJECT_CREATE_FILE,
  EVENT_PROJECT_REMOVE_FILE,
  EVENT_PROJECT_UPDATE_FILE,
  EVENT_NODE_CHANGE,
  EVENT_PROJECT_DATASOURCES_CHANGE,
  EVENT_ASSETS_TEMPLATE_REMOVE,
  EVENT_ASSETS_TEMPLATE_ADD,
  EVENT_PROJECT_UPDATE
} from './emitter';
import { Project } from './models';
import { Assets } from './Assets';
import { Service } from './Service';
import { StorageService } from './StorageService';
import { logger } from '../utils';

/**
 * 引擎实例化参数对象
 */
export interface EngineOptions {
  config?: IConfig;
  assets?: AssetsJSON | (() => Promise<AssetsJSON>);
  project?: PorjectSchema;
  service?: Service;
}

/**
 * 引擎类
 */
export class Engine {
  // 引擎配置
  config: IConfig;
  // 引擎应用实例
  private app: App;
  // 引擎骨架
  public skeleton: Skeleton;
  // 自定义事件处理器
  public emitter: Emitter = emitter;
  // 模拟器
  public simulator: Simulator;
  // 资产配置
  public assets: Assets;
  // 项目实体
  public project: Project;
  // 数据持久化服务
  public service: Service;

  static registerSetters(setters: ISetterConfig[]) {
    setters.forEach((setter) => {
      const { name, component, dataType } = setter;
      buildInSetters[name] = component;
      if (SetterValueTypes[dataType]) {
        SetterValueTypes[dataType].push(name);
      } else {
        logger.warn('注册Setter失败，未知的数据类型：', dataType);
      }
    });
  }

  // 初始化引擎
  constructor(options: EngineOptions, el?: any) {
    const { config = {}, assets, project, service } = options;
    const assetsLoader =
      typeof assets === 'function'
        ? (assets as () => Promise<AssetsJSON>)
        : () => Promise.resolve(assets || {});
    this.config = mergeConfig(config);
    this.skeleton = new Skeleton(this.config.widgets || []);
    this.service = service || new StorageService();
    this.assets = new Assets({ loader: assetsLoader });
    this.project = new Project(project);
    this.simulator = new Simulator({
      renderer: new Renderer(this.config.handlers),
      assets: this.assets,
      project: this.project
    });
    this.app = createApp(EngineRenderer, {
      engine: this
    });
    this.loadProject(project);
    this.bindListeners();
    Engine.registerSetters(this.config.setters || []);
    if (el) {
      onMounted(() => {
        this.mount(el.value || el);
      });
    }
    onUnmounted(() => {
      this.dispose();
    });
  }

  mount(el: Element) {
    this.app.mount(el);
  }

  dispose() {
    this.emitter.all.clear();
    this.simulator.dispose();
    this.app.unmount();
  }

  private async loadProject(porjectSchema?: PorjectSchema) {
    const { project, service, skeleton, config } = this;
    const scheam = await service.loadProject(this, porjectSchema);
    if (scheam) {
      project.load(scheam);
    }
    if (config.activeApp) {
      skeleton.regions.apps.setCurrentWidget(config.activeApp);
    }
  }

  private bindListeners() {
    const { service, emitter } = this;
    emitter.on(EVENT_PROJECT_CREATE_FILE, (file) =>
      service.createFile(this, file)
    );
    emitter.on(EVENT_PROJECT_UPDATE_FILE, (file) =>
      service.updateFile(this, file)
    );
    emitter.on(EVENT_PROJECT_REMOVE_FILE, (file) =>
      service.removeFile(this, file)
    );
    emitter.on(EVENT_NODE_CHANGE, (node) =>
      service.updateFile(this, node.root)
    );
    emitter.on(EVENT_PROJECT_DATASOURCES_CHANGE, (schema) =>
      service.updateProject(this, schema)
    );
    emitter.on(EVENT_NODE_CHANGE, (node) =>
      service.saveHistory(this, node.root)
    );
    emitter.on(EVENT_ASSETS_TEMPLATE_REMOVE, (name) =>
      service.removeTemplate(this, name)
    );
    emitter.on(EVENT_ASSETS_TEMPLATE_ADD, (desc) =>
      service.addTemplate(this, desc)
    );
    emitter.on(EVENT_PROJECT_UPDATE, (project: Project) => {
      const schema = project.toSchema();
      service.updateProject(this, schema);
    });
  }
}
