import { loadCmdModule } from '../cmd-loader';
import * as Babel from '@babel/standalone';
import { ICodeResource } from '@cloudbase/cals';
import { runtime } from '../index';

export interface IEvaledModule extends ICodeResource {
  exports: any; // eval result, the cmd exportsx
}

export interface WidgetsContext {
  _parentId: string;
  _childrenId: string[];
  _visible: boolean;
  parent: WidgetsContext;
  children: WidgetsContext[];
  [key: string]: any;
}

/**
 * The eval result of lowcode
 */
export interface LowcodeContext {
  state: { [page: string]: Object };
  /**
   * functions defined as getters
   */
  computed: any;
  /**
   * The raw computed functions
   */
  computedFuncs: { [page: string]: { [fnName: string]: () => any } };
  lifeCycle: { [page: string]: { [fnName: string]: () => any } };
  common: { [page: string]: { [fnName: string]: () => any } };
  handler: { [page: string]: { [fnName: string]: () => any } };
  renderer?: { [page: string]: { [fnName: string]: () => any } };
  widgets: Record<string, any>;
  node: Record<string, any>;

  this: any;
  props: any;
}

/**
 * Each type of module has parser
 */
export interface LowCodeModuleParser {
  /**
   * called when parse begin
   * @param context The context to share data across parsers
   */
  onStart(context: LowcodeContext);
  parseModule(pageId: string, mod: IEvaledModule): void;
  onEnd(context: LowcodeContext);
}

let cancelPrevModLoader;

export interface IEvalCodeModulOptions {
  dependencies: any;
  onCodeError?: (e: Error, file: string) => void;
  onDependencyMiss?: (name: string, version: string) => void;
  runtimeWindow: any;
  disableAutoCancelPrev?: boolean;
  externals?: {
    [key: string]: string;
  };
}

export async function evalCodeCodules(
  modulesByScope: { [pageId: string]: ICodeResource[] },
  opts: IEvalCodeModulOptions,
) {
  // @ts-ignore
  const runtimeWindow = opts.runtimeWindow || runtime.previewWindow;

  if (!runtimeWindow) {
    return;
  }
  opts.onCodeError = opts.onCodeError || defaultCodeErrorHandler;
  const entries: string[] = [];
  const preload = {};
  const externalMap = opts.externals || {};
  for (const nodeModule in externalMap) {
    preload[nodeModule] = transformCode(`module.exports=${externalMap[nodeModule]}`);
  }

  for (const scope in modulesByScope) {
    // eslint-disable-line
    const modules = modulesByScope[scope];
    modules.forEach((m) => {
      const file = getModuleFilePath(scope, m.path);
      try {
        preload[file] = transformCode(m.code);
        entries.push(file);
      } catch (e) {
        opts.onCodeError(e, file);
        runtimeWindow.console.error(`${file}: ${e}`);
      }
    });
  }

  if (!opts.disableAutoCancelPrev && cancelPrevModLoader) {
    cancelPrevModLoader();
  }

  const { promise, cancel } = loadCmdModule(entries, {
    // const { promise, cancel } = loadEsModule(entries, {
    preload,
    dependencies: opts.dependencies,
    onDependencyMiss: opts.onDependencyMiss,
    runtime: runtimeWindow,
    onModuleError: (e) => {
      runtimeWindow.console.error(e.stack);
    },
    onError: (e) => {
      runtimeWindow?.console?.error?.(e.stack);
    },
  });
  !opts.disableAutoCancelPrev && (cancelPrevModLoader = cancel);
  const loaded = await promise;
  const result: { [scope: string]: IEvaledModule[] } = {};
  for (const scope in modulesByScope) {
    // eslint-disable-line
    const modules = modulesByScope[scope];
    result[scope] = modules.map((m) => {
      const file = getModuleFilePath(scope, m.path);
      return { ...m, exports: loaded[file] };
    });
  }
  return result;
}

function transformCode(srcCode: string) {
  // return srcCode;
  // eslint-disable-next-line no-undef
  return Babel.transform(srcCode, {
    presets: ['env', 'react'],
  }).code;
}

function getModuleFilePath(pageId: string, path: string) {
  const file = `/${path}`;
  if (pageId === 'global') {
    return file.replace('/global', '');
  }
  return file;
}

function defaultCodeErrorHandler(e: Error, file: string) {
  console.error('Lowcode syntax error', file, e);
}

export function getDefaultModule(targetModule) {
  if (!targetModule) {
    return targetModule;
  }
  const keys = Object.keys(targetModule);
  if (keys.length === 1 && keys[0] === 'default') {
    return targetModule.default;
  }
  return targetModule;
}
