"use strict";

var _interopRequireWildcard = function (obj) { return obj && obj.__esModule ? obj : { "default": obj }; };

var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };

var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };

exports.ClassDeclaration = ClassDeclaration;
exports.ClassExpression = ClassExpression;
exports.__esModule = true;

var ReplaceSupers = _interopRequire(require("../../helpers/replace-supers"));

var nameMethod = _interopRequireWildcard(require("../../helpers/name-method"));

var defineMap = _interopRequireWildcard(require("../../helpers/define-map"));

var messages = _interopRequireWildcard(require("../../../messages"));

var util = _interopRequireWildcard(require("../../../util"));

var traverse = _interopRequire(require("../../../traversal"));

var t = _interopRequireWildcard(require("../../../types"));

var check = t.isClass;

exports.check = check;

function ClassDeclaration(node, parent, scope, file) {
  return t.variableDeclaration("let", [t.variableDeclarator(node.id, t.toExpression(node))]);
}

function ClassExpression(node, parent, scope, file) {
  return new ClassTransformer(node, parent, scope, file).run();
}

var verifyConstructorVisitor = traverse.explode({
  MethodDefinition: {
    enter: function enter() {
      this.skip();
    }
  },

  Property: {
    enter: function enter(node) {
      if (node.method) this.skip();
    }
  },

  CallExpression: {
    enter: function enter(node, parent, scope, state) {
      if (t.isIdentifier(node.callee, { name: "super" })) {
        state.hasBareSuper = true;

        if (!state.hasSuper) {
          throw state.file.errorWithNode(node, "super call is only allowed in derived constructor");
        }
      }
    }
  },

  ThisExpression: {
    enter: function enter(node, parent, scope, state) {
      if (state.hasSuper && !state.hasBareSuper) {
        throw state.file.errorWithNode(node, "'this' is not allowed before super()");
      }
    }
  }
});

var ClassTransformer = (function () {

  /**
   * Description
   */

  function ClassTransformer(node, parent, scope, file) {
    _classCallCheck(this, ClassTransformer);

    this.parent = parent;
    this.scope = scope;
    this.node = node;
    this.file = file;

    this.hasInstanceMutators = false;
    this.hasStaticMutators = false;

    this.instanceMutatorMap = {};
    this.staticMutatorMap = {};

    this.hasConstructor = false;
    this.className = node.id;
    this.classRef = node.id || scope.generateUidIdentifier("class");

    this.superName = node.superClass || t.identifier("Function");
    this.hasSuper = !!node.superClass;

    this.isLoose = file.isLoose("es6.classes");
  }

  /**
   * Description
   *
   * @returns {Array}
   */

  ClassTransformer.prototype.run = function run() {
    var superName = this.superName;
    var className = this.className;
    var classBody = this.node.body.body;
    var classRef = this.classRef;
    var file = this.file;

    //

    var body = this.body = [];

    //

    var constructorBody = t.blockStatement([t.expressionStatement(t.callExpression(file.addHelper("class-call-check"), [t.thisExpression(), classRef]))]);

    var constructor;

    if (this.className) {
      constructor = t.functionDeclaration(this.className, [], constructorBody);
      body.push(constructor);
    } else {
      constructor = t.functionExpression(null, [], constructorBody);
    }

    this.constructor = constructor;

    //

    var closureParams = [];
    var closureArgs = [];

    //

    if (this.hasSuper) {
      closureArgs.push(superName);

      superName = this.scope.generateUidBasedOnNode(superName, this.file);
      closureParams.push(superName);

      this.superName = superName;
      body.push(t.expressionStatement(t.callExpression(file.addHelper("inherits"), [classRef, superName])));
    }

    //

    this.buildBody();

    if (this.className) {
      // named class with only a constructor
      if (body.length === 1) return t.toExpression(body[0]);
    } else {
      // infer class name if this is a nameless class expression
      constructor = nameMethod.bare(constructor, this.parent, this.scope);

      body.unshift(t.variableDeclaration("var", [t.variableDeclarator(classRef, constructor)]));

      t.inheritsComments(body[0], this.node);
    }

    //

    body.push(t.returnStatement(classRef));

    return t.callExpression(t.functionExpression(null, closureParams, t.blockStatement(body)), closureArgs);
  };

  /**
   * Description
   */

  ClassTransformer.prototype.buildBody = function buildBody() {
    var constructor = this.constructor;
    var className = this.className;
    var superName = this.superName;
    var classBody = this.node.body.body;
    var body = this.body;

    for (var i = 0; i < classBody.length; i++) {
      var node = classBody[i];
      if (t.isMethodDefinition(node)) {
        var isConstructor = !node.computed && t.isIdentifier(node.key, { name: "constructor" }) || t.isLiteral(node.key, { value: "constructor" });
        if (isConstructor) this.verifyConstructor(node);

        var replaceSupers = new ReplaceSupers({
          methodNode: node,
          objectRef: this.classRef,
          superRef: this.superName,
          isStatic: node["static"],
          isLoose: this.isLoose,
          scope: this.scope,
          file: this.file
        }, true);

        replaceSupers.replace();

        if (isConstructor) {
          this.pushConstructor(node);
        } else {
          this.pushMethod(node);
        }
      } else if (t.isPrivateDeclaration(node)) {
        this.closure = true;
        body.unshift(node);
      } else if (t.isClassProperty(node)) {
        this.pushProperty(node);
      }
    }

    // we have no constructor, we have a super, and the super doesn't appear to be falsy
    if (!this.hasConstructor && this.hasSuper && t.evaluateTruthy(superName, this.scope) !== false) {
      var helperName = "class-super-constructor-call";
      if (this.isLoose) helperName += "-loose";
      constructor.body.body.push(util.template(helperName, {
        CLASS_NAME: className,
        SUPER_NAME: this.superName
      }, true));
    }

    var instanceProps;
    var staticProps;
    var classHelper = "create-class";

    if (this.hasInstanceMutators) {
      instanceProps = defineMap.toClassObject(this.instanceMutatorMap);
    }

    if (this.hasStaticMutators) {
      staticProps = defineMap.toClassObject(this.staticMutatorMap);
    }

    if (instanceProps || staticProps) {
      if (defineMap.hasComputed(this.instanceMutatorMap) || defineMap.hasComputed(this.staticMutatorMap)) {
        if (instanceProps) instanceProps = defineMap.toComputedObjectFromClass(instanceProps);
        if (staticProps) staticProps = defineMap.toComputedObjectFromClass(staticProps);
        classHelper = "create-computed-class";
      }

      if (!instanceProps) instanceProps = t.literal(null);

      var args = [this.classRef, instanceProps];
      if (staticProps) args.push(staticProps);

      body.push(t.expressionStatement(t.callExpression(this.file.addHelper(classHelper), args)));
    }
  };

  /**
   * Description
   */

  ClassTransformer.prototype.verifyConstructor = function verifyConstructor(node) {
    return; // enable this for the next major

    var state = {
      hasBareSuper: false,
      hasSuper: this.hasSuper,
      file: this.file
    };

    traverse(node, verifyConstructorVisitor, this.scope, state);

    if (!state.hasBareSuper && this.hasSuper) {
      throw this.file.errorWithNode(node, "Derived constructor must call super()");
    }
  };

  /**
   * Push a method to its respective mutatorMap.
   */

  ClassTransformer.prototype.pushMethod = function pushMethod(node) {
    var methodName = node.key;

    var kind = node.kind;

    if (kind === "") {
      nameMethod.property(node, this.file, this.scope);

      if (this.isLoose) {
        // use assignments instead of define properties for loose classes

        var classRef = this.classRef;
        if (!node["static"]) classRef = t.memberExpression(classRef, t.identifier("prototype"));
        methodName = t.memberExpression(classRef, methodName, node.computed);

        var expr = t.expressionStatement(t.assignmentExpression("=", methodName, node.value));
        t.inheritsComments(expr, node);
        this.body.push(expr);
        return;
      }

      kind = "value";
    }

    var mutatorMap = this.instanceMutatorMap;
    if (node["static"]) {
      this.hasStaticMutators = true;
      mutatorMap = this.staticMutatorMap;
    } else {
      this.hasInstanceMutators = true;
    }

    defineMap.push(mutatorMap, methodName, kind, node.computed, node);
  };

  /**
   * Description
   */

  ClassTransformer.prototype.pushProperty = function pushProperty(node) {
    if (!node.value) return;

    var key;

    if (node["static"]) {
      key = t.memberExpression(this.classRef, node.key);
      this.body.push(t.expressionStatement(t.assignmentExpression("=", key, node.value)));
    } else {
      key = t.memberExpression(t.thisExpression(), node.key);
      this.constructor.body.body.unshift(t.expressionStatement(t.assignmentExpression("=", key, node.value)));
    }
  };

  /**
   * Replace the constructor body of our class.
   */

  ClassTransformer.prototype.pushConstructor = function pushConstructor(method) {
    if (method.kind) {
      throw this.file.errorWithNode(method, messages.get("classesIllegalConstructorKind"));
    }

    var construct = this.constructor;
    var fn = method.value;

    this.hasConstructor = true;

    t.inherits(construct, fn);
    t.inheritsComments(construct, method);

    construct._ignoreUserWhitespace = true;
    construct.params = fn.params;

    t.inherits(construct.body, fn.body);
    construct.body.body = construct.body.body.concat(fn.body.body);
  };

  return ClassTransformer;
})();