"use strict";

var ReplaceSupers = require("../../helpers/replace-supers");
var nameMethod    = require("../../helpers/name-method");
var defineMap     = require("../../helpers/define-map");
var messages      = require("../../../messages");
var util          = require("../../../util");
var t             = require("../../../types");

exports.check = t.isClass;

exports.ClassDeclaration = function (node, parent, scope, file) {
  return new ClassTransformer(node, file, scope, true).run();
};

exports.ClassExpression = function (node, parent, scope, file) {
  if (!node.id) {
    if (t.isProperty(parent) && parent.value === node && !parent.computed && t.isIdentifier(parent.key)) {
      // var o = { foo: class {} };
      node.id = parent.key;
    }

    if (t.isVariableDeclarator(parent) && t.isIdentifier(parent.id)) {
      // var foo = class {};
      node.id = parent.id;
    }
  }

  return new ClassTransformer(node, file, scope, false).run();
};

/**
 * Description
 *
 * @param {Node} node
 * @param {File} file
 * @param {Scope} scope
 * @param {Boolean} isStatement
 */

function ClassTransformer(node, file, scope, isStatement) {
  this.isStatement = isStatement;
  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 || 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 () {
  var superName = this.superName;
  var className = this.className;
  var file      = this.file;

  //

  var body = this.body = [];

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

  if (this.node.id) {
    constructor = t.functionDeclaration(className, [], constructorBody);
    body.push(constructor);
  } else {
    constructor = t.functionExpression(null, [], constructorBody);
    body.push(t.variableDeclaration("var", [
      t.variableDeclarator(className, constructor)
    ]));
  }
  this.constructor = constructor;

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

  //

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

    if (!t.isIdentifier(superName)) {
      superName = this.scope.generateUidBasedOnNode(superName, this.file);
    }

    closureParams.push(superName);

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

  this.buildBody();

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

  var init;

  if (body.length === 1) {
    // only a constructor so no need for a closure container
    init = t.toExpression(constructor);
  } else {
    body.push(t.returnStatement(className));
    init = t.callExpression(
      t.functionExpression(null, closureParams, t.blockStatement(body)),
      closureArgs
    );
  }

  if (this.isStatement) {
    return t.variableDeclaration("let", [
      t.variableDeclarator(className, init)
    ]);
  } else {
    return init;
  }
};

/**
 * Description
 */

ClassTransformer.prototype.buildBody = function () {
  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 replaceSupers = new ReplaceSupers({
        methodNode: node,
        className:  this.className,
        superName:  this.superName,
        isStatic:   node.static,
        isLoose:    this.isLoose,
        scope:      this.scope,
        file:       this.file
      }, true);
      replaceSupers.replace();

      if (node.key.name === "constructor") {
        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.isFalsyExpression(superName)) {
    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;

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

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

  if (instanceProps || staticProps) {
    staticProps = staticProps || t.literal(null);

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

    body.push(t.expressionStatement(
      t.callExpression(this.file.addHelper("prototype-properties"), args)
    ));
  }
};

/**
 * Push a method to its respective mutatorMap.
 *
 * @param {Node} node MethodDefinition
 */

ClassTransformer.prototype.pushMethod = function (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 className = this.className;
      if (!node.static) className = t.memberExpression(className, t.identifier("prototype"));
      methodName = t.memberExpression(className, 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);
  defineMap.push(mutatorMap, methodName, "enumerable", node.computed, false);
};

/**
 * Description
 *
 * @param {Node} node
 */

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

  var key;

  if (node.static) {
    key = t.memberExpression(this.className, 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.
 *
 * @param {Node} method MethodDefinition
 */

ClassTransformer.prototype.pushConstructor = function (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;
  construct.body.body             = construct.body.body.concat(fn.body.body);
};
