import { getSafeProperty, isSafeProperty, setSafeProperty } from './customs.js';
import { isMap, isObject } from './is.js';

/**
 * A map facade on a bare object.
 *
 * The small number of methods needed to implement a scope,
 * forwarding on to the SafeProperty functions. Over time, the codebase
 * will stop using this method, as all objects will be Maps, rather than
 * more security prone objects.
 */
export class ObjectWrappingMap {
  constructor(object) {
    this.wrappedObject = object;
    this[Symbol.iterator] = this.entries;
  }
  keys() {
    return Object.keys(this.wrappedObject).filter(key => this.has(key)).values();
  }
  get(key) {
    return getSafeProperty(this.wrappedObject, key);
  }
  set(key, value) {
    setSafeProperty(this.wrappedObject, key, value);
    return this;
  }
  has(key) {
    return isSafeProperty(this.wrappedObject, key) && key in this.wrappedObject;
  }
  entries() {
    return mapIterator(this.keys(), key => [key, this.get(key)]);
  }
  forEach(callback) {
    for (var key of this.keys()) {
      callback(this.get(key), key, this);
    }
  }
  delete(key) {
    if (isSafeProperty(this.wrappedObject, key)) {
      delete this.wrappedObject[key];
    }
  }
  clear() {
    for (var key of this.keys()) {
      this.delete(key);
    }
  }
  get size() {
    return Object.keys(this.wrappedObject).length;
  }
}

/**
 * Create a map with two partitions: a and b.
 * The set with bKeys determines which keys/values are read/written to map b,
 * all other values are read/written to map a
 *
 * For example:
 *
 *   const a = new Map()
 *   const b = new Map()
 *   const p = new PartitionedMap(a, b, new Set(['x', 'y']))
 *
 * In this case, values `x` and `y` are read/written to map `b`,
 * all other values are read/written to map `a`.
 */
export class PartitionedMap {
  /**
   * @param {Map} a
   * @param {Map} b
   * @param {Set} bKeys
   */
  constructor(a, b, bKeys) {
    this.a = a;
    this.b = b;
    this.bKeys = bKeys;
    this[Symbol.iterator] = this.entries;
  }
  get(key) {
    return this.bKeys.has(key) ? this.b.get(key) : this.a.get(key);
  }
  set(key, value) {
    if (this.bKeys.has(key)) {
      this.b.set(key, value);
    } else {
      this.a.set(key, value);
    }
    return this;
  }
  has(key) {
    return this.b.has(key) || this.a.has(key);
  }
  keys() {
    return new Set([...this.a.keys(), ...this.b.keys()])[Symbol.iterator]();
  }
  entries() {
    return mapIterator(this.keys(), key => [key, this.get(key)]);
  }
  forEach(callback) {
    for (var key of this.keys()) {
      callback(this.get(key), key, this);
    }
  }
  delete(key) {
    return this.bKeys.has(key) ? this.b.delete(key) : this.a.delete(key);
  }
  clear() {
    this.a.clear();
    this.b.clear();
  }
  get size() {
    return [...this.keys()].length;
  }
}

/**
 * Create a new iterator that maps over the provided iterator, applying a mapping function to each item
 */
function mapIterator(it, callback) {
  return {
    next: () => {
      var n = it.next();
      return n.done ? n : {
        value: callback(n.value),
        done: false
      };
    }
  };
}

/**
 * Creates an empty map, or whatever your platform's polyfill is.
 *
 * @returns an empty Map or Map like object.
 */
export function createEmptyMap() {
  return new Map();
}

/**
 * Creates a Map from the given object.
 *
 * @param { Map | { [key: string]: unknown } | undefined } mapOrObject
 * @returns
 */
export function createMap(mapOrObject) {
  if (!mapOrObject) {
    return createEmptyMap();
  }
  if (isMap(mapOrObject)) {
    return mapOrObject;
  }
  if (isObject(mapOrObject)) {
    return new ObjectWrappingMap(mapOrObject);
  }
  throw new Error('createMap can create maps from objects or Maps');
}

/**
 * Unwraps a map into an object.
 *
 * @param {Map} map
 * @returns { [key: string]: unknown }
 */
export function toObject(map) {
  if (map instanceof ObjectWrappingMap) {
    return map.wrappedObject;
  }
  var object = {};
  for (var key of map.keys()) {
    var value = map.get(key);
    setSafeProperty(object, key, value);
  }
  return object;
}

/**
 * Copies the contents of key-value pairs from each `objects` in to `map`.
 *
 * Object is `objects` can be a `Map` or object.
 *
 * This is the `Map` analog to `Object.assign`.
 */
export function assign(map) {
  for (var _len = arguments.length, objects = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    objects[_key - 1] = arguments[_key];
  }
  for (var args of objects) {
    if (!args) {
      continue;
    }
    if (isMap(args)) {
      for (var key of args.keys()) {
        map.set(key, args.get(key));
      }
    } else if (isObject(args)) {
      for (var _key2 of Object.keys(args)) {
        map.set(_key2, args[_key2]);
      }
    }
  }
  return map;
}