use :node;

var Node = module.require('../Node').Node; var extend = require('util')._extend;

fn SuperExpression()

extends Node {

this.type = 'SuperExpression';

}

SuperExpression.prototype.codegen = () -> {

if !super.codegen() {
  return;
}

// Raise an error if we are getting a syntax  
// such as: super?.test
if this.parent.type == 'NullPropagatingExpression' {
  Node.getErrorManager().error({
    type: "InvalidSuperReference",
    message: "cannot refer to super before the ?. operator",
    loc: this.loc
  });

  return { type: 'ThisExpression' };
}

// The only supported use case for super is 
// member expression (e.g: super.x)
if this.parent.type != 'MemberExpression' {
  Node.getErrorManager().error({
    type: "InvalidUsageForSuper",
    message: "invalid usage of super keyword",
    loc: this.loc
  });

  return { type: 'ThisExpression' };
}

// Find a parent function that extends another function
// (has the extends keyword) or a prototype assignment
var parentNode = this;
var node = this;
while (node && !node.inheritsFrom &&
  (node.type != 'AssignmentExpression' || ((!node.left.property || node.left.property.name != 'prototype') && 
    (!node.left.object.property || node.left.object.property.name != 'prototype')))) {
  parentNode = node;
  node = node.parent;
}

// Raise an error if there's no parent function
if !node {   
  Node.getErrorManager().error({
    type: "InvalidContextForSuper",
    message: "invalid context for super keyword",
    loc: this.loc
  });

  return { type: 'ThisExpression' };
}

// Add _self variable to a context.
var addSelf = (context) -> {
  var selfId = {
    "type": "Identifier",
    "name": "_self",
    "codeGenerated": "true"
  };

  if (!context.node.__isSelfDefined) {
    context.node.body.splice(0, 0, {
      "type": "VariableDeclaration",
      "declarations": [{
        "type": "VariableDeclarator",
        "id": selfId,
        "init": {
          type: "ThisExpression"
        }
      }],
      "kind": "let"
    });

    context.node.__isSelfDefined = true;
  }

  return selfId;
};

// Modify the call expression to look like:
// _fn.call(_self)
var mutateCallExpression = (id, parent, selfNode) -> {
  selfNode.codeGenerated = true;

  parent.object = id;
  parent.property = ::Object.create({
    "codeGenerated": "true",
    "type": "Identifier",
    "name": "call"
  });

  parent.parent.arguments.splice(0, 0, selfNode);
};

// If we are inside a prototype function, change 
// something like: super.x() to Base.prototype.x.call(_self);
if node.type == 'AssignmentExpression' {
  var identifier = node.left.object;
  var context;

  if node.left.property.name != 'prototype' {
    identifier = identifier.object;
    context = { node: node.right.body };
  } else {
    // Make sure that the syntax is something like:
    // Fn.prototype = { ... }
    if node.right.type != 'ObjectExpression' {
      Node.getErrorManager().error({
        type: "InvalidUsageForSuper",
        message: "invalid usage of super keyword",
        loc: this.loc
      });

      return { type: 'ThisExpression' };
    }

    var contextNode = this;
    while (contextNode.parent? && contextNode.parent != node.right) {
      contextNode = contextNode.parent;
    }

    // Make sure that the syntax is something like:
    // Fn.prototype = { test: fn () { ... } }
    if contextNode?.type != 'Property' ||
       contextNode?.value?.type != 'FunctionExpression' {
      Node.getErrorManager().error({
        type: "InvalidUsageForSuper",
        message: "invalid usage of super keyword",
        loc: this.loc
      });

      return { type: 'ThisExpression' };
    }

    context = { node: contextNode.value.body };
  }

  var selfNode;
  if context.node == this.getContext().node {
    selfNode = { type: 'ThisExpression' };
  } else {
    selfNode = addSelf(context);
  }

  // If this is a call expression, e.g: super.test(), 
  // Turn this into Base.prototype.test.call(_self)
  if this.parent.parent.type == 'CallExpression' {
    var memberExpression = {
      type: 'MemberExpression',
      computed: false,
      object: {
      type: 'MemberExpression',
        computed: false,
        object: this.parent.getDefinedIdentifier(identifier.name).parent.inheritsFrom.callee,
        property: {
          type: 'Identifier',
          name: 'prototype'
        }
      },
      property: this.parent.parent.callee.property
    };

    mutateCallExpression(memberExpression, this.parent, selfNode);
    return memberExpression;
  }

  // Otherwise, just return (this|_self).property
  return selfNode;
}

var superContext = parentNode.getContext(); 

// Find the most left side in the member expression
// For super.a.b.c.d, the most left side would be super
var lastObject = this.parent;
while (lastObject.object.type == 'MemberExpression') {
  lastObject = lastObject.object;
}

// Change it to a this expression
lastObject.object = {
  type: 'ThisExpression'
};

var id = {
  "type": "Identifier",
  "name": "_" + this.parent.property.name
};

// Declare a new variable in the function that extends
// with a reference to our property
if !this.parent.isIdentifierDefined(id.name) {
  superContext.node.body.splice(superContext.position, 0, {
    "codeGenerated": "true",
    "type": "VariableDeclaration",
    "declarations": [{
      "type": "VariableDeclarator",
      "id": id,
      "init": extend({}, lastObject)
    }],
    "kind": "let"
  });

  superContext.node.defineIdentifier(id);
}

// If we are in a call expression, change it to:
// _fn.call(_self)
if this.parent.parent.type == 'CallExpression' {
  mutateCallExpression(id, this.parent, addSelf(superContext));
} else {
  // Otherwise, just refer to the variable
  this.parent.type = 'Identifier';
  ::Object.defineProperty(this.parent, 'name', { 
    value: id.name, 
    enumerable: true 
  });  
}

return id;

};

SuperExpression.prototype.hasCallExpression = () -> false;

exports.SuperExpression = SuperExpression;