import { ok } from "assert";
import { ObfuscateOrder } from "../order";
import { ComputeProbabilityMap } from "../probability";
import Template from "../templates/template";
import {
  BinaryExpression,
  CallExpression,
  ExpressionStatement,
  ForStatement,
  FunctionExpression,
  Identifier,
  Literal,
  MemberExpression,
  ReturnStatement,
  UpdateExpression,
  VariableDeclaration,
  VariableDeclarator,
} from "../util/gen";
import { clone, prepend } from "../util/insert";
import { getRandomInteger } from "../util/random";
import Transform from "./transform";

var Hash = function (s) {
  var a = 1,
    c = 0,
    h,
    o;
  if (s) {
    a = 0;
    for (h = s.length - 1; h >= 0; h--) {
      o = s.charCodeAt(h);
      a = ((a << 6) & 268435455) + o + (o << 14);
      c = a & 266338304;
      a = c !== 0 ? a ^ (c >> 21) : a;
    }
  }
  return ~~String(a).slice(0, 3);
};

var HashTemplate = Template(
  `
  var {name} = function(arr) {
    var s = arr.map(x=>x+"").join(''), a = 1, c = 0, h, o;
    if (s) {
        a = 0;
        for (h = s.length - 1; h >= 0; h--) {
            o = s.charCodeAt(h);
            a = (a<<6&268435455) + o + (o<<14);
            c = a & 266338304;
            a = c!==0?a^c>>21:a;
        }
    }
    return ~~String(a).slice(0, 3);
};`
);

/**
 * Shuffles arrays initial order of elements.
 *
 * "Un-shuffles" the array at runtime.
 */
export default class Shuffle extends Transform {
  hashName: string;
  constructor(o) {
    super(o, ObfuscateOrder.Shuffle);
  }

  match(object, parents) {
    return (
      object.type == "ArrayExpression" &&
      !parents.find((x) => x.$dispatcherSkip)
    );
  }

  transform(object, parents) {
    return () => {
      if (object.elements.length < 3) {
        // Min: 4 elements
        return;
      }

      function isAllowed(e) {
        return (
          e.type == "Literal" &&
          { number: 1, boolean: 1, string: 1 }[typeof e.value]
        );
      }

      // Only arrays with only literals
      var illegal = object.elements.find((x) => !isAllowed(x));

      if (illegal) {
        return;
      }

      var mapped = object.elements.map((x) => x.value);

      var mode = ComputeProbabilityMap(this.options.shuffle, (x) => x, mapped);
      if (mode) {
        var shift = getRandomInteger(
          1,
          Math.min(60, object.elements.length * 6)
        );

        var expr = Literal(shift);
        var name = this.getPlaceholder();

        if (mode == "hash") {
          var str = mapped.join("");
          shift = Hash(str);

          if (!this.hashName) {
            prepend(
              parents[parents.length - 1],
              HashTemplate.single({
                name: (this.hashName = this.getPlaceholder()),
              })
            );
          }

          for (var i = 0; i < shift; i++) {
            object.elements.push(object.elements.shift());
          }

          var shiftedHash = Hash(
            object.elements.map((x) => x.value + "").join("")
          );

          expr = BinaryExpression(
            "-",
            CallExpression(Identifier(this.hashName), [Identifier(name)]),
            Literal(shiftedHash - shift)
          );
        } else {
          for (var i = 0; i < shift; i++) {
            object.elements.push(object.elements.shift());
          }
        }

        var code = [];

        var iName = this.getPlaceholder();

        var inPlace = false;
        var inPlaceName;
        var inPlaceBody;
        var inPlaceIndex;

        var varDeclarator = parents[0];
        if (varDeclarator.type == "VariableDeclarator") {
          var varDec = parents[2];
          if (varDec.type == "VariableDeclaration") {
            var body = parents[3];
            if (
              varDec.declarations.length == 1 &&
              Array.isArray(body) &&
              varDeclarator.id.type === "Identifier" &&
              varDeclarator.init === object
            ) {
              inPlaceIndex = body.indexOf(varDec);
              inPlaceBody = body;
              inPlace = inPlaceIndex !== -1;
              inPlaceName = varDeclarator.id.name;
            }
          }
        }

        if (mode !== "hash") {
          code.push(
            Template(`
            for ( var x = 16; x%4 === 0; x++) {
              var z = 0;
              ${
                inPlace ? `${inPlaceName} = ${name}` : name
              } = ${name}.concat((function(){
                z++;
                if(z === 1){
                  return [];
                }

                for( var i = ${getRandomInteger(5, 105)}; i; i-- ){
                  ${name}.unshift(${name}.pop());
                }
                return [];
              })());
            }
            `).single()
          );
        }

        code.push(
          ForStatement(
            VariableDeclaration(VariableDeclarator(iName, expr)),
            Identifier(iName),
            UpdateExpression("--", Identifier(iName), false),
            [
              // ${name}.unshift(${name}.pop());
              ExpressionStatement(
                CallExpression(
                  MemberExpression(
                    Identifier(name),
                    Identifier("unshift"),
                    false
                  ),
                  [
                    CallExpression(
                      MemberExpression(
                        Identifier(name),
                        Identifier("pop"),
                        false
                      ),
                      []
                    ),
                  ]
                )
              ),
            ]
          )
        );

        if (inPlace) {
          var varDeclarator = parents[0];
          ok(i != -1);

          inPlaceBody.splice(
            inPlaceIndex + 1,
            0,
            VariableDeclaration(
              VariableDeclarator(name, Identifier(varDeclarator.id.name))
            ),
            ...code
          );
        }

        if (!inPlace) {
          this.replace(
            object,
            CallExpression(
              FunctionExpression(
                [Identifier(name)],
                [...code, ReturnStatement(Identifier(name))]
              ),
              [clone(object)]
            )
          );
        }
      }
    };
  }
}
