// Although `Symbol` is widely supported these days, we can safely fall
// back to using a non-enumerable string property without violating any
// assumptions elsewhere in the implementation.
var useSymbol = typeof Symbol === "function";

// Used to mark `tuple.prototype` so that all objects that inherit from
// any `tuple.prototype` object (there could be more than one) will test
// positive according to `tuple.isTuple`.
var brand = useSymbol
  ? Symbol.for("immutable-tuple")
  : "@@__IMMUTABLE_TUPLE__@@";

// Used to save a reference to the globally shared `UniversalWeakMap` that
// stores all known `tuple` objects.
var globalKey = useSymbol
  ? Symbol.for("immutable-tuple-root")
  : "@@__IMMUTABLE_TUPLE_ROOT__@@";

// Convenient helper for defining hidden immutable properties.
function def(obj, name, value, enumerable) {
  Object.defineProperty(obj, name, {
    value: value,
    enumerable: !! enumerable,
    writable: false,
    configurable: false
  });
  return value;
}

var freeze = Object.freeze || function (obj) {
  return obj;
};

function isObjRef(value) {
  switch (typeof value) {
  case "object":
    if (value === null) {
      return false;
    }
  case "function":
    return true;
  default:
    return false;
  }
}

// The `mustConvertThisToArray` value is true when the corresponding
// `Array` method does not attempt to modify `this`, which means we can
// pass a `tuple` object as `this` without first converting it to an
// `Array`.
function forEachArrayMethod(fn) {
  function call(name, mustConvertThisToArray) {
    var desc = Object.getOwnPropertyDescriptor(Array.prototype, name);
    fn(name, desc, !! mustConvertThisToArray);
  }

  call("every");
  call("filter");
  call("find");
  call("findIndex");
  call("forEach");
  call("includes");
  call("indexOf");
  call("join");
  call("lastIndexOf");
  call("map");
  call("reduce");
  call("reduceRight");
  call("slice");
  call("some");
  call("toLocaleString");
  call("toString");

  // The `reverse` and `sort` methods are usually destructive, but for
  // `tuple` objects they return a new `tuple` object that has been
  // appropriately reversed/sorted.
  call("reverse", true);
  call("sort", true);

  // Make `[...someTuple]` work.
  call(useSymbol && Symbol.iterator || "@@iterator");
}

// A map data structure that holds object keys weakly, yet can also hold
// non-object keys, unlike the native `WeakMap`.
var UniversalWeakMap = function UniversalWeakMap() {
  // Since a `WeakMap` cannot hold primitive values as keys, we need a
  // backup `Map` instance to hold primitive keys. Both `this._weakMap`
  // and `this._strongMap` are lazily initialized.
  this._weakMap = null;
  this._strongMap = null;
  this.data = null;
};

// Since `get` and `set` are the only methods used, that's all I've
// implemented here.

UniversalWeakMap.prototype.get = function get (key) {
  var map = this._getMap(key, false);
  if (map) {
    return map.get(key);
  }
};

UniversalWeakMap.prototype.set = function set (key, value) {
  this._getMap(key, true).set(key, value);
  // An actual `Map` or `WeakMap` would return `this` here, but
  // returning the `value` is more convenient for the `tuple`
  // implementation.
  return value;
};

UniversalWeakMap.prototype._getMap = function _getMap (key, canCreate) {
  if (! canCreate) {
    return isObjRef(key) ? this._weakMap : this._strongMap;
  }
  if (isObjRef(key)) {
    return this._weakMap || (this._weakMap = new WeakMap);
  }
  return this._strongMap || (this._strongMap = new Map);
};

// See [`universal-weak-map.js`](universal-weak-map.html).
// See [`util.js`](util.html).
// If this package is installed multiple times, there could be mutiple
// implementations of the `tuple` function with distinct `tuple.prototype`
// objects, but the shared pool of `tuple` objects must be the same across
// all implementations. While it would be ideal to use the `global`
// object, there's no reliable way to get the global object across all JS
// environments without using the `Function` constructor, so instead we
// use the global `Array` constructor as a shared namespace.
var root = Array[globalKey] || def(Array, globalKey, new UniversalWeakMap, false);

function lookup() {
  return lookupArray(arguments);
}

function lookupArray(array) {
  var node = root;

  // Because we are building a tree of *weak* maps, the tree will not
  // prevent objects in tuples from being garbage collected, since the
  // tree itself will be pruned over time when the corresponding `tuple`
  // objects become unreachable. In addition to internalization, this
  // property is a key advantage of the `immutable-tuple` package.
  var len = array.length;
  for (var i = 0; i < len; ++i) {
    var item = array[i];
    node = node.get(item) || node.set(item, new UniversalWeakMap);
  }

  // Return node.data rather than node itself to prevent tampering with
  // the UniversalWeakMap tree.
  return node.data || (node.data = Object.create(null));
}

// See [`lookup.js`](lookup.html).
// See [`util.js`](util.html).
// When called with any number of arguments, this function returns an
// object that inherits from `tuple.prototype` and is guaranteed to be
// `===` any other `tuple` object that has exactly the same items. In
// computer science jargon, `tuple` instances are "internalized" or just
// "interned," which allows for constant-time equality checking, and makes
// it possible for tuple objects to be used as `Map` or `WeakMap` keys, or
// stored in a `Set`.
function tuple() {
  var arguments$1 = arguments;

  var node = lookup.apply(null, arguments);

  if (node.tuple) {
    return node.tuple;
  }

  var t = Object.create(tuple.prototype);

  // Define immutable items with numeric indexes, and permanently fix the
  // `.length` property.
  var argc = arguments.length;
  for (var i = 0; i < argc; ++i) {
    t[i] = arguments$1[i];
  }

  def(t, "length", argc, false);

  // Remember this new `tuple` object so that we can return the same object
  // earlier next time.
  return freeze(node.tuple = t);
}

// Since the `immutable-tuple` package could be installed multiple times
// in an application, there is no guarantee that the `tuple` constructor
// or `tuple.prototype` will be unique, so `value instanceof tuple` is
// unreliable. Instead, to test if a value is a tuple, you should use
// `tuple.isTuple(value)`.
def(tuple.prototype, brand, true, false);
function isTuple(that) {
  return !! (that && that[brand] === true);
}

tuple.isTuple = isTuple;

function toArray(tuple) {
  var array = [];
  var i = tuple.length;
  while (i--) { array[i] = tuple[i]; }
  return array;
}

// Copy all generic non-destructive Array methods to `tuple.prototype`.
// This works because (for example) `Array.prototype.slice` can be invoked
// against any `Array`-like object.
forEachArrayMethod(function (name, desc, mustConvertThisToArray) {
  var method = desc && desc.value;
  if (typeof method === "function") {
    desc.value = function () {
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      var result = method.apply(
        mustConvertThisToArray ? toArray(this) : this,
        args
      );
      // Of course, `tuple.prototype.slice` should return a `tuple` object,
      // not a new `Array`.
      return Array.isArray(result) ? tuple.apply(void 0, result) : result;
    };
    Object.defineProperty(tuple.prototype, name, desc);
  }
});

// Like `Array.prototype.concat`, except for the extra effort required to
// convert any tuple arguments to arrays, so that
// ```
// tuple(1).concat(tuple(2), 3) === tuple(1, 2, 3)
// ```
var ref = Array.prototype;
var concat = ref.concat;
tuple.prototype.concat = function () {
  var args = [], len = arguments.length;
  while ( len-- ) args[ len ] = arguments[ len ];

  return tuple.apply(void 0, concat.apply(toArray(this), args.map(
    function (item) { return isTuple(item) ? toArray(item) : item; }
  )));
};

export default tuple;
export { tuple, lookup, lookupArray };
