"use strict";

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

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

var CountedSet = require("./counted-set");
var isLabelIdentifier = require("./is-label-identifier");

var newIssueUrl = "https://github.com/babel/babili/issues/new";

/**
 * ScopeTracker
 *   references: Map<Scope, CountedSet<String> >
 *   bindings: Map<Scope, Map<String, Binding> >
 */
module.exports = function () {
  function ScopeTracker() {
    _classCallCheck(this, ScopeTracker);

    this.references = new Map();
    this.bindings = new Map();
  }

  /**
   * Register a new Scope and initiliaze it with empty sets
   * @param {Scope} scope
   */


  _createClass(ScopeTracker, [{
    key: "addScope",
    value: function addScope(scope) {
      if (!this.references.has(scope)) {
        this.references.set(scope, new CountedSet());
      }
      if (!this.bindings.has(scope)) {
        this.bindings.set(scope, new Map());
      }
    }

    /**
     * Add reference to all Scopes between and including the ReferencedScope
     * and Binding's Scope
     * @param {Scope} scope
     * @param {Binding} binding
     * @param {String} name
     */

  }, {
    key: "addReference",
    value: function addReference(scope, binding, name) {
      var parent = scope;
      do {
        this.references.get(parent).add(name);
        if (!binding) {
          throw new Error(`Binding Not Found for ${name} during scopeTracker.addRefernce. ` + `Please report at ${newIssueUrl}`);
        }
        if (binding.scope === parent) break;
      } while (parent = parent.parent);
    }

    /**
     * has a Reference in the given {Scope} or a child Scope
     *
     * Refer {addReference} to know why the following call will be valid
     * for detecting references in child Scopes
     *
     * @param {Scope} scope
     * @param {String} name
     */

  }, {
    key: "hasReference",
    value: function hasReference(scope, name) {
      return this.references.get(scope).has(name);
    }

    /**
     * Update reference count in all scopes between and including the
     * Referenced Scope and the Binding's Scope
     *
     * @param {Scope} scope
     * @param {Binding} binding
     * @param {String} oldName
     * @param {String} newName
     */

  }, {
    key: "updateReference",
    value: function updateReference(scope, binding, oldName, newName) {
      var parent = scope;
      do {
        var ref = this.references.get(parent);

        ref.delete(oldName);
        ref.add(newName);

        if (!binding) {
          // Something went wrong - panic
          throw new Error("Binding Not Found during scopeTracker.updateRefernce " + `while updating "${oldName}" to "${newName}". ` + `Please report at ${newIssueUrl}`);
        }

        if (binding.scope === parent) break;
      } while (parent = parent.parent);
    }

    /**
     * has either a Binding or a Reference
     * @param {Scope} scope
     * @param {Binding} binding
     * @param {String} name
     */

  }, {
    key: "hasBindingOrReference",
    value: function hasBindingOrReference(scope, binding, name) {
      return this.hasReference(scope, name) || this.hasBinding(scope, name);
    }

    /**
     * For a Binding visit all places where the Binding is used and detect
     * if the newName {next} can be used in all these places
     *
     * 1. binding's own scope
     * 2. constant violations' scopes
     * 3. referencePaths' scopes
     *
     * @param {Binding} binding
     * @param {String} next
     */

  }, {
    key: "canUseInReferencedScopes",
    value: function canUseInReferencedScopes(binding, next) {
      var tracker = this;

      if (tracker.hasBindingOrReference(binding.scope, binding, next)) {
        return false;
      }

      for (var i = 0; i < binding.constantViolations.length; i++) {
        var violation = binding.constantViolations[i];
        if (tracker.hasBindingOrReference(violation.scope, binding, next)) {
          return false;
        }
      }

      for (var _i = 0; _i < binding.referencePaths.length; _i++) {
        var ref = binding.referencePaths[_i];
        if (!ref.isIdentifier()) {
          var canUse = true;
          ref.traverse({
            ReferencedIdentifier(path) {
              if (path.node.name !== next) return;
              if (tracker.hasBindingOrReference(path.scope, binding, next)) {
                canUse = false;
              }
            }
          });
          if (!canUse) {
            return canUse;
          }
        } else if (!isLabelIdentifier(ref)) {
          if (tracker.hasBindingOrReference(ref.scope, binding, next)) {
            return false;
          }
        }
      }

      return true;
    }

    /**
     * Add a binding to Tracker in binding's own Scope
     * @param {Binding} binding
     */

  }, {
    key: "addBinding",
    value: function addBinding(binding) {
      if (!binding) {
        return;
      }

      var bindings = this.bindings.get(binding.scope);
      var existingBinding = bindings.get(binding.identifier.name);

      if (existingBinding && existingBinding !== binding) {
        throw new Error(`scopeTracker.addBinding: ` + `Binding "${existingBinding.identifier.name}" already exists. ` + `Trying to add "${binding.identifier.name}" again.`);
      }

      bindings.set(binding.identifier.name, binding);
    }

    /**
     * Moves Binding from it's own Scope to {toScope}
     *
     * required for fixup-var-scope
     *
     * @param {Binding} binding
     * @param {Scope} toScope
     */

  }, {
    key: "moveBinding",
    value: function moveBinding(binding, toScope) {
      this.bindings.get(binding.scope).delete(binding.identifier.name);
      this.bindings.get(toScope).set(binding.identifier.name, binding);
    }

    /**
     * has a Binding in the current {Scope}
     * @param {Scope} scope
     * @param {String} name
     */

  }, {
    key: "hasBinding",
    value: function hasBinding(scope, name) {
      return this.bindings.get(scope).has(name);
    }

    /**
     * Update the ScopeTracker on rename
     * @param {Scope} scope
     * @param {String} oldName
     * @param {String} newName
     */

  }, {
    key: "renameBinding",
    value: function renameBinding(scope, oldName, newName) {
      var bindings = this.bindings.get(scope);
      bindings.set(newName, bindings.get(oldName));
      bindings.delete(oldName);
    }
  }]);

  return ScopeTracker;
}();