/* eslint-disable no-use-before-define */
/* eslint-disable @typescript-eslint/no-use-before-define */
import * as path from 'path';
import { LoadedModules } from './loader';
import { IOptions } from './index';
import { evalCode } from './util';
import { init } from './core_module';

type ModuleFunction = (require, exports, module: Module) => void;

export function defineModules(allModules: LoadedModules, opts: IOptions) {
  init(opts.runtime);
  const definedModules: { [id: string]: Module } = {};
  opts.runtime[opts.defineFuncName] = function (id: string, factory: ModuleFunction) {
    definedModules[id].factory = factory;
  };
  Object.entries(allModules).map(([id, modData]) => {
    const mod = new Module(id, modData.url);
    definedModules[id] = mod;
    try {
      mod.defineModule(modData.code, opts.defineFuncName, opts.runtime);
    } catch (e) {
      opts.onError(e);
    }
  });
  Object.entries(definedModules).map(([id, mod]) => {
    Object.entries(allModules[id].requires).map(([requirePath, resolvedPath]) => {
      mod.dependencies[requirePath] = definedModules[resolvedPath];
    });
  });
  return definedModules;
}

// https://nodejs.org/api/modules.html#modules_the_module_object
export class Module {
  id: string; // resolved file path

  url: string;

  exports: any;

  /**
   * The definition function of the module
   */
  factory: ModuleFunction;

  dependencies: { [requirePath: string]: Module } = {};

  constructor(id: string, url: string) {
    this.id = id;
    this.url = url;
  }

  execute() {
    if (this.exports) {
      // console.warn('Using cached exports for ' + self.id)
      return this.exports;
    }

    this.exports = {};
    this.factory.call(this.exports, this.require, this.exports, this);
    return this.exports;
  }

  require = (path: string) => {
    const dep = this.dependencies[path];
    if (dep) {
      return dep.execute();
    }
    console.warn(`Dependeny ${path} not loaded properly for ${this.id}`);
  };

  /**
   * Register the module
   */
  defineModule(srcCode: string, defineFuncName: string, runtime: Window) {
    const { id } = this;
    const ext = path.extname(id).substr(1);
    let source = srcCode;
    if (loaders[ext]) {
      source = loaders[ext](source);
    }
    const code = `${defineFuncName}("${id}", function(require, exports, module) {
${source}
})//# sourceURL=${id}`;
    if (runtime.document) {
      evalCode(code, runtime.document);
    } else {
      console.warn('script runtime error, runtime do not has document.');
    }
    if (typeof this.factory !== 'function') {
      throw new Error(`Lowcode syntax error of module ${this.url}`);
    }
  }
}

const loaders = {
  json(src) {
    return `module.exports=${src}`;
  },
  css: textLoader,
  txt: textLoader,
};

export function registerExtension(ext: string, transform: (srcCode: string) => string) {
  if (loaders[ext]) {
    console.error(`Duplicate transform for ${ext}`);
    return;
  }
  loaders[ext] = transform;
}

function textLoader(src) {
  return `module.exports=${JSON.stringify(src)}`;
}
