/**
  React Native Decompiler
  Copyright (C) 2020-2022 Richard Fu, Numan 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 { Visitor, NodePath } from '@babel/traverse';
import {
  VariableDeclarator,
  isCallExpression,
  isIdentifier,
  isMemberExpression,
  isNumericLiteral,
  arrayPattern,
  identifier,
  Identifier,
} from '@babel/types';
import { Plugin } from '../../plugin';
import Module from '../../module';
import CmdArgs from '../../interfaces/cmdArgs';

interface VariableDeclaratorData {
  path: NodePath<VariableDeclarator>;
  varName: string;
  varStart: number;
  couldBeDestructure: boolean;
  destructureBindingStart?: number;
  destructureArrayBindingStart?: number;
  couldBeArrayAccess: boolean;
  arrayAccessBindingStart?: number;
  arrayAccessVal?: number;
}

/**
 * Converts Babel array destructuring to the native one
 */
export default class ArrayDestructureEvaluator extends Plugin {
  readonly pass = 2;

  private readonly destructureUsed: boolean;
  private readonly variableDeclarators: VariableDeclaratorData[] = [];
  private destructureFunction?: NodePath<VariableDeclarator>;
  private destructureFunctionStart?: number;

  constructor(args: CmdArgs, module: Module, moduleList: Module[]) {
    super(args, module, moduleList);

    const destructureDependency = moduleList.find((mod) => mod?.moduleName === '@babel/runtime/helpers/slicedToArray');
    this.destructureUsed = destructureDependency?.moduleId != null && module.dependencies.includes(destructureDependency?.moduleId);
  }

  getVisitor(): Visitor {
    if (!this.destructureUsed) return {};

    return {
      VariableDeclarator: (path) => {
        if (!isIdentifier(path.node.id) || path.node.id.start == null) return;

        const variableDeclaratorData: VariableDeclaratorData = {
          path,
          couldBeDestructure: false,
          couldBeArrayAccess: false,
          varName: path.node.id.name,
          varStart: path.node.id.start,
        };

        if (isCallExpression(path.node.init) && isIdentifier(path.node.init.callee)
          && path.node.init.arguments.length === 2 && isIdentifier(path.node.init.arguments[0]) && isNumericLiteral(path.node.init.arguments[1])) {
          variableDeclaratorData.couldBeDestructure = true;
          variableDeclaratorData.destructureBindingStart = path.scope.getBindingIdentifier(path.node.init.callee.name)?.start ?? undefined;
          variableDeclaratorData.destructureArrayBindingStart = path.scope.getBindingIdentifier(path.node.init.arguments[0].name)?.start ?? undefined;
        }
        if (isMemberExpression(path.node.init) && isIdentifier(path.node.init.object) && isNumericLiteral(path.node.init.property)) {
          variableDeclaratorData.couldBeArrayAccess = true;
          variableDeclaratorData.arrayAccessBindingStart = path.scope.getBindingIdentifier(path.node.init.object.name)?.start ?? undefined;
          variableDeclaratorData.arrayAccessVal = path.node.init.property.value;
        }

        this.variableDeclarators.push(variableDeclaratorData);

        const callExpression = path.get('init');
        if (!callExpression.isCallExpression()) return;

        const moduleDependency = this.getModuleDependency(callExpression);
        if (moduleDependency?.moduleName === '@babel/runtime/helpers/slicedToArray') {
          this.destructureFunction = path;
          this.destructureFunctionStart = path.node.id.start;
        }
      },
    };
  }

  afterPass(): void {
    if (this.destructureFunctionStart == null) return;

    this.variableDeclarators.forEach((data) => {
      if (!data.couldBeDestructure) return;
      if (data.destructureBindingStart !== this.destructureFunctionStart) return;

      const sourceArray = this.variableDeclarators.find((srcData) => srcData.varStart === data.destructureArrayBindingStart);
      const arrayUsages = this.variableDeclarators.filter((arrData) => arrData.arrayAccessBindingStart === data.varStart);
      if (!sourceArray || !arrayUsages.length) return;

      const arrayPatternElements: (Identifier | null)[] = [];
      arrayUsages.forEach((usage) => {
        if (usage.arrayAccessVal == null) throw new Error();
        arrayPatternElements[usage.arrayAccessVal] = identifier(usage.varName);
      });
      for (let i = 0; i < arrayPatternElements.length; i += 1) {
        if (arrayPatternElements[i] === undefined) {
          arrayPatternElements[i] = null;
        }
      }

      sourceArray.path.node.id = arrayPattern(arrayPatternElements);

      if (!this.destructureFunction?.removed) {
        this.destructureFunction?.remove();
      }
      if (!data.path.removed) {
        data.path.remove();
      }
      arrayUsages.forEach((usageData) => (usageData.path.removed ? null : usageData.path.remove()));
    });
  }
}