use :node;

var Node = module.require('../Node').Node;

fn NullCoalescingExpression(left, right)

extends Node {

this.type = 'NullCoalescingExpression';

this.left = left;
this.left.parent = this;

this.right = right;
this.right.parent = this;

}

NullCoalescingExpression.prototype.codegen = () -> {

if !super.codegen() {
  return;
}

var leftType = this.left.type;

this.left = this.left.codegen();
this.right = this.right.codegen();

var context = this.getContext();
var addUndefinedCheck = true;

// If the left expression is a function call (e.g: f() ?? 5)
// then store its value in a separate variable to avoid
// calling the function twice.
if leftType == 'NullPropagatingExpression' ||
   leftType == 'NullCoalescingExpression' ||
   this.left.hasCallExpression?() {

  var id = {
    "type": "Identifier",
    "name": NullCoalescingExpression.getNextLeftName()
  };

  context.node.body.splice(context.position, 0, {
    "type": "VariableDeclaration",
    "declarations": [
      {
        "type": "VariableDeclarator",
        "id": id,
        "init": this.left
      }
    ],
    "kind": "let",
    "codeGenerated": true
  });

  this.left = id;
  addUndefinedCheck = false;
}

var test = {
  "type": "BinaryExpression",
  "operator": "==",
  "left": this.left,
  "right": {
    "type": "Literal",
    "value": null,
    "raw": "null"
  }
};

if this.left.type != 'Identifier' {
  addUndefinedCheck = false;
}

if addUndefinedCheck {
  test = {
    "type": "LogicalExpression",
    "operator": "||",
    "left": {
      "type": "BinaryExpression",
      "operator": "===",
      "left": {
        "type": "UnaryExpression",
        "operator": "typeof",
        "argument": this.left,
        "prefix": true
      },
      "right": {
        "type": "Literal",
        "value": "undefined",
        "raw": "'undefined'"
      }
    },
    "right": test
  };
}

// If the null coalescing operator is an expression
// statement child, the generated JS should be an if statement.
if this.parent?.type == 'ExpressionStatement' {

  if !this.right.hasCallExpression() {
    this.parent.type = 'EmptyStatement';
    return this;
  }

  this.parent.type = 'IfStatement';
  this.parent.test = test;

  this.parent.consequent = {
    "type": "BlockStatement",
    "body": [
      {
        "type": "ExpressionStatement",
        "expression": this.right
      }
    ]
  };
} else {
  // Otherwise - if the null coalescing operator is 
  // inside an expression, the generated JS should 
  // look like:
  // 
  // typeof left === 'undefined' || left === null ? right : left

  return {
    "type": "ConditionalExpression",
    "test": test,
    "consequent": this.right,
    "alternate": this.left
  };
}

};

NullCoalescingExpression.prototype.hasCallExpression = () -> {

return this.left?.hasCallExpression() ||
       this.right?.hasCallExpression();

};

NullCoalescingExpression.getNextLeftName = () -> {

if (!this.nullCoalescingIndex?) { 
  this.nullCoalescingIndex = 0; 
}

return "nullCoalescing" + this.nullCoalescingIndex++;

};

NullCoalescingExpression.resetVariableNames = () -> {

this.nullCoalescingIndex = 0;

};

exports.NullCoalescingExpression = NullCoalescingExpression;