import Template from "../../templates/template";
import Transform from "../transform";
import { ObfuscateOrder } from "../../order";
import {
  Node,
  Location,
  CallExpression,
  Identifier,
  Literal,
  FunctionDeclaration,
  ReturnStatement,
  MemberExpression,
  SwitchStatement,
  SwitchCase,
} from "../../util/gen";
import { prepend } from "../../util/insert";
import { getIdentifierInfo } from "../../util/identifiers";
import { getRandomInteger } from "../../util/random";
import { reservedIdentifiers, reservedKeywords } from "../../constants";
import { ComputeProbabilityMap } from "../../probability";

class GlobalAnalysis extends Transform {
  notGlobals: Set<string>;
  globals: { [name: string]: Location[] };

  constructor(o) {
    super(o);

    this.globals = Object.create(null);
    this.notGlobals = new Set();
  }

  match(object: Node, parents: Node[]) {
    return object.type == "Identifier" && !reservedKeywords.has(object.name);
  }

  transform(object: Node, parents: Node[]) {
    // no touching `import()` or `import x from ...`
    var importIndex = parents.findIndex(
      (x) => x.type == "ImportExpression" || x.type == "ImportDeclaration"
    );
    if (importIndex !== -1) {
      if (
        parents[importIndex].source === (parents[importIndex - 1] || object)
      ) {
        return;
      }
    }

    var info = getIdentifierInfo(object, parents);
    if (!info.spec.isReferenced) {
      return;
    }

    // Add to globals
    if (!this.notGlobals.has(object.name)) {
      if (!this.globals[object.name]) {
        this.globals[object.name] = [];
      }

      this.globals[object.name].push([object, parents]);
    }

    if (info.spec.isDefined || info.spec.isModified) {
      delete this.globals[object.name];

      this.notGlobals.add(object.name);
    }

    var assignmentIndex = parents.findIndex(
      (x) => x.type == "AssignmentExpression"
    );
    var updateIndex = parents.findIndex((x) => x.type == "UpdateExpression");

    if (
      (assignmentIndex != -1 &&
        parents[assignmentIndex].left ===
          (parents[assignmentIndex - 1] || object)) ||
      updateIndex != -1
    ) {
      var memberIndex = parents.findIndex((x) => x.type == "MemberExpression");
      if (
        memberIndex == -1 ||
        memberIndex > (assignmentIndex == -1 ? assignmentIndex : updateIndex)
      ) {
        delete this.globals[object.name];

        this.notGlobals.add(object.name);
      }
    }
  }
}

/**
 * Global Concealing hides global variables being accessed.
 *
 * - Any variable that is not defined is considered "global"
 */
export default class GlobalConcealing extends Transform {
  globalAnalysis: GlobalAnalysis;
  globalVar: string;

  constructor(o) {
    super(o, ObfuscateOrder.GlobalConcealing);

    this.globalAnalysis = new GlobalAnalysis(o);
    this.before.push(this.globalAnalysis);

    this.globalVar = null;
  }

  match(object: Node, parents: Node[]) {
    return object.type == "Program";
  }

  transform(object: Node, parents: Node[]) {
    return () => {
      var globals: { [name: string]: Location[] } = this.globalAnalysis.globals;
      this.globalAnalysis.notGlobals.forEach((del) => {
        delete globals[del];
      });

      reservedIdentifiers.forEach((x) => {
        delete globals[x];
      });

      Object.keys(globals).forEach((x) => {
        if (this.globalAnalysis.globals[x].length < 1) {
          delete globals[x];
        } else if (
          !ComputeProbabilityMap(this.options.globalConcealing, (x) => x, x)
        ) {
          delete globals[x];
        }
      });

      // this.log(Object.keys(globals).join(', '))

      if (Object.keys(globals).length > 0) {
        var used = new Set();

        // 1. Make getter function

        this.globalVar = this.getPlaceholder();
        // "window" or "global" in node
        var global =
          this.options.globalVariables.values().next().value || "window";
        var callee = this.getPlaceholder();

        // Returns global variable or fall backs to `this`
        var functionDeclaration = Template(`
        function ${callee}(){
          try {
            return ${global};
          } catch (e){
            return this;
          }
        }`).single();

        // 2. Replace old accessors

        var variableDeclaration = Template(`
        var ${this.globalVar} = ${callee}.call(this);
        `).single();

        var globalFn = this.getPlaceholder();

        var newNames = Object.create(null);

        Object.keys(globals).forEach((name) => {
          var locations: Location[] = globals[name];
          var state;
          do {
            state = getRandomInteger(-1000, 1000 + used.size);
          } while (used.has(state));
          used.add(state);

          newNames[name] = state;

          locations.forEach(([node, parents]) => {
            if (!parents.find((x) => x.$dispatcherSkip)) {
              this.replace(
                node,
                CallExpression(Identifier(globalFn), [Literal(state)])
              );
            }
          });
        });

        // Adds all global variables to the switch statement
        // this.options.globalVariables.forEach((name) => {
        //   if (!newNames[name]) {
        //     var state;
        //     do {
        //       state = getRandomInteger(
        //         -1000,
        //         1000 + used.size + this.options.globalVariables.size
        //       );
        //     } while (used.has(state));
        //     used.add(state);

        //     newNames[name] = state;
        //   }
        // });

        prepend(
          object,
          FunctionDeclaration(
            globalFn,
            [Identifier("index")],
            [
              SwitchStatement(
                Identifier("index"),
                Object.keys(newNames).map((name) => {
                  var code = newNames[name];

                  return SwitchCase(Literal(code), [
                    ReturnStatement(
                      MemberExpression(
                        Identifier(this.globalVar),
                        Literal(name),
                        true
                      )
                    ),
                  ]);
                })
              ),
            ]
          )
        );

        prepend(object, variableDeclaration);
        prepend(object, functionDeclaration);
      }
    };
  }
}
