/**
  React Native Decompiler
  Copyright (C) 2020 Richard Fu and contributors

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License
  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import { performance } from 'perf_hooks';
import { NodePath } from '@babel/traverse';
import { PluginConstructor, Plugin } from './plugin';
import Module from './module';
import CmdArgs from './interfaces/cmdArgs';

export default class Router<T extends Plugin, TConstructor extends PluginConstructor<T>> {
  static traverseTimeTaken = 0;
  static recrawlTimeTaken = 0;
  static timeTaken: { [index: string]: number } = {};

  readonly maxPass: number;
  private readonly module: Module;
  private readonly moduleList: Module[];
  private readonly list: T[];
  private readonly listConstructors: TConstructor[];
  private readonly args: CmdArgs;

  constructor(list: TConstructor[], module: Module, moduleList: Module[], args: CmdArgs) {
    this.listConstructors = list;
    this.args = args;
    this.list = list.map((PluginToLoad) => {
      if (this.args.performance && Router.timeTaken[PluginToLoad.name] == null) {
        Router.timeTaken[PluginToLoad.name] = 0;
      }
      return new PluginToLoad(args, module, moduleList);
    });
    this.maxPass = Math.max(...this.list.map((plugin) => plugin.pass));

    this.module = module;
    this.moduleList = moduleList;
  }

  runPass = (pass: number): void => {
    if (this.module.failedToDecompile) return;
    try {
      const passPlugins = this.list.map((plugin, index) => ({ plugin, index })).filter(({ plugin }) => plugin.pass === pass);

      if (this.args.debug === this.module.moduleId) {
        this.runDebugPass(passPlugins.map(({ plugin }) => plugin));
      }

      let startTime = performance.now();
      const visitorFunctions: { [index: string]: ((path: NodePath<unknown>) => void)[] } = {};

      passPlugins.forEach(({ plugin, index }) => {
        if (plugin.evaluate) {
          this.performanceTrack(this.listConstructors[index].name, () => plugin.evaluate && plugin.evaluate(this.module.rootPath, this.runPlugin));
        } else if (plugin.getVisitor) {
          // disable some eslint rules from object mapping
          /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */
          const visitor: any = plugin.getVisitor(this.runPlugin);
          Object.keys(visitor).forEach((key) => {
            if (!visitorFunctions[key]) {
              visitorFunctions[key] = [];
            }
            if (this.args.performance) {
              visitorFunctions[key].push((path: NodePath<unknown>) => {
                Router.traverseTimeTaken += performance.now() - startTime;
                this.performanceTrack(this.listConstructors[index].name, () => visitor[key](path));
                startTime = performance.now();
              });
            } else {
              visitorFunctions[key].push(visitor[key]);
            }
          });
        } else {
          throw new Error('Plugin does not have getVisitor nor evaluate');
        }
      });

      const visitor: any = {};
      Object.keys(visitorFunctions).forEach((key) => {
        visitor[key] = this.processVisit(visitorFunctions[key]);
      });
      /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return */
      if (Object.keys(visitor).length > 0) {
        startTime = performance.now();
        this.module.rootPath.traverse(visitor);
        Router.traverseTimeTaken += performance.now() - startTime;
      }

      passPlugins.forEach(({ plugin, index }) => {
        this.performanceTrack(this.listConstructors[index].name, () => plugin.afterPass && plugin.afterPass(this.runPlugin));
      });
    } catch (e) {
      console.error(`An error occured parsing module ${this.module.moduleId}, it will be outputted as is!`);
      console.error(e);
      this.module.failedToDecompile = true;
    }
  };

  private runDebugPass = (passPlugins: Plugin[]): void => {
    let lastCode = '';
    passPlugins.forEach((plugin) => {
      if (plugin.evaluate) {
        plugin.evaluate(this.module.rootPath, this.runPlugin);
      } else if (plugin.getVisitor) {
        this.module.rootPath.traverse(plugin.getVisitor(this.runPlugin));
      } else {
        throw new Error('Plugin does not have getVisitor nor evaluate');
      }
    });
    passPlugins.forEach((plugin) => {
      if (plugin.afterPass) {
        plugin.afterPass(this.runPlugin);
      }
      console.log('after', plugin.name ?? 'unknown_name:');
      const newCode = this.module.debugToCode();
      if (lastCode !== newCode) {
        console.log(newCode);
        lastCode = newCode;
      } else {
        console.log('No change');
      }
    });
  };

  private performanceTrack = (key: string, cb: () => void): void => {
    if (!this.args.performance) {
      cb();
    } else {
      const startTime = performance.now();
      cb();
      Router.timeTaken[key] += performance.now() - startTime;
    }
  };

  private processVisit = (plugins: ((path: NodePath<unknown>) => void)[]) => (path: NodePath<unknown>): void => {
    plugins.forEach((fn) => fn(path));
  };

  private runPlugin = (PluginToRun: PluginConstructor): void => {
    const plugin = new PluginToRun(this.args, this.module, this.moduleList);
    if (plugin.evaluate) {
      plugin.evaluate(this.module.rootPath, this.runPlugin);
    } else if (plugin.getVisitor) {
      this.module.rootPath.traverse(plugin.getVisitor(this.runPlugin));
    } else {
      throw new Error('Plugin does not have getVisitor nor evaluate');
    }
    if (plugin.afterPass) {
      plugin.afterPass(this.runPlugin);
    }
  };
}