"use strict";

function isPureAndUndefined(rval) {
  if (rval.isIdentifier() && rval.node.name === "undefined") {
    return true;
  }
  if (!rval.isPure()) {
    return false;
  }
  var evaluation = rval.evaluate();
  return evaluation.confident === true && evaluation.value === undefined;
}

function getLoopParent(path, scopeParent) {
  var parent = path.findParent(function (p) {
    return p.isLoop() || p === scopeParent;
  });
  // don't traverse higher than the function the var is defined in.
  return parent === scopeParent ? null : parent;
}

function getFunctionParent(path, scopeParent) {
  var parent = path.findParent(function (p) {
    return p.isFunction();
  });
  // don't traverse higher than the function the var is defined in.
  return parent === scopeParent ? null : parent;
}

function getFunctionReferences(path, scopeParent) {
  var references = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : new Set();

  for (var func = getFunctionParent(path, scopeParent); func; func = getFunctionParent(func, scopeParent)) {
    var id = func.node.id;
    var binding = id && func.scope.getBinding(id.name);

    if (!binding) {
      continue;
    }

    binding.referencePaths.forEach(function (path) {
      if (!references.has(path)) {
        references.add(path);
        getFunctionReferences(path, scopeParent, references);
      }
    });
  }
  return references;
}

function hasViolation(declarator, scope, start) {
  var binding = scope.getBinding(declarator.node.id.name);
  if (!binding) {
    return true;
  }

  var scopeParent = declarator.getFunctionParent();

  var violation = binding.constantViolations.some(function (v) {
    // return 'true' if we cannot guarantee the violation references
    // the initialized identifier after
    var violationStart = v.node.start;
    if (violationStart === undefined || violationStart < start) {
      return true;
    }

    var references = getFunctionReferences(v, scopeParent);
    var _iteratorNormalCompletion = true;
    var _didIteratorError = false;
    var _iteratorError = undefined;

    try {
      for (var _iterator = references[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
        var ref = _step.value;

        if (ref.node.start === undefined || ref.node.start < start) {
          return true;
        }
      }
    } catch (err) {
      _didIteratorError = true;
      _iteratorError = err;
    } finally {
      try {
        if (!_iteratorNormalCompletion && _iterator.return) {
          _iterator.return();
        }
      } finally {
        if (_didIteratorError) {
          throw _iteratorError;
        }
      }
    }

    for (var loop = getLoopParent(declarator, scopeParent); loop; loop = getLoopParent(loop, scopeParent)) {
      if (loop.node.end === undefined || loop.node.end > violationStart) {
        return true;
      }
    }
  });

  return violation;
}

module.exports = function () {
  return {
    name: "transform-remove-undefined",
    visitor: {
      ReturnStatement(path) {
        if (path.node.argument !== null) {
          if (isPureAndUndefined(path.get("argument"))) {
            path.node.argument = null;
          }
        }
      },

      VariableDeclaration(path) {
        switch (path.node.kind) {
          case "const":
            break;
          case "let":
            var _iteratorNormalCompletion2 = true;
            var _didIteratorError2 = false;
            var _iteratorError2 = undefined;

            try {
              for (var _iterator2 = path.get("declarations")[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
                var declarator = _step2.value;

                if (isPureAndUndefined(declarator.get("init"))) {
                  declarator.node.init = null;
                }
              }
            } catch (err) {
              _didIteratorError2 = true;
              _iteratorError2 = err;
            } finally {
              try {
                if (!_iteratorNormalCompletion2 && _iterator2.return) {
                  _iterator2.return();
                }
              } finally {
                if (_didIteratorError2) {
                  throw _iteratorError2;
                }
              }
            }

            break;
          case "var":
            var start = path.node.start;
            if (start === undefined) {
              // This is common for plugin-generated nodes
              break;
            }
            var scope = path.scope;
            var _iteratorNormalCompletion3 = true;
            var _didIteratorError3 = false;
            var _iteratorError3 = undefined;

            try {
              for (var _iterator3 = path.get("declarations")[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
                var _declarator = _step3.value;

                if (isPureAndUndefined(_declarator.get("init")) && !hasViolation(_declarator, scope, start)) {
                  _declarator.node.init = null;
                }
              }
            } catch (err) {
              _didIteratorError3 = true;
              _iteratorError3 = err;
            } finally {
              try {
                if (!_iteratorNormalCompletion3 && _iterator3.return) {
                  _iterator3.return();
                }
              } finally {
                if (_didIteratorError3) {
                  throw _iteratorError3;
                }
              }
            }

            break;
        }
      }
    }
  };
};