Source: core.js

/**
 * Core data types
 *
 * @private
 *
 */
const MAP_TYPE = 1;
const ARRAY_TYPE = 2;
const OBJECT_TYPE = 3;
const LIST_TYPE = 4;
const SET_TYPE = 5;
const LAZY_ITERABLE_TYPE = 6;
const BOOLEAN_TYPE = 7;
const NUMBER_TYPE = 8;
const STRING_TYPE = 9;
const FUNCTION_TYPE = 10;

/**
 * Internal function which returns a new empty collection based on type
 *
 * @private
 * @func
 * @param {*}
 * @return {*}
 *
 */
function emptyOfType(type) {
  switch (type) {
    case MAP_TYPE:
      return new Map();
    case ARRAY_TYPE:
      return [];
    case OBJECT_TYPE:
      return {};
    case LIST_TYPE:
      return new List();
    case SET_TYPE:
      return new Set();
    case LAZY_ITERABLE_TYPE:
      return lazy(function* () {
        return;
      });
  }
  return null;
}

/**
 * Internal function which returns data type based by checking the object
 *
 * @private
 * @func
 * @param {*}
 * @return {number}
 * @example
 *
 * typeConst([1, 2, 3]);
 * // => 2
 *
 * typeConst({name: "George", occupation: "Cat"});
 * // => 3
 *
 */
function typeConst(obj) {
  if (obj instanceof Map) {
    return MAP_TYPE;
  }

  if (obj instanceof Set) {
    return SET_TYPE;
  }

  if (obj instanceof List) {
    return LIST_TYPE;
  }

  if (obj instanceof Array) {
    return ARRAY_TYPE;
  }

  if (obj instanceof LazyIterable) {
    return LAZY_ITERABLE_TYPE;
  }

  if (obj instanceof Object) {
    return OBJECT_TYPE;
  }

  return null;
}

/**
 * Internal function which returns primitive type by checking x
 *
 * @private
 * @func
 * @param {*}
 * @return {number}
 * @example
 *
 * typePrimitive(false);
 * // => 7
 *
 * typePrimitive(1);
 * // => 8
 *
 */
function typePrimitive(x) {
  if (typeof x === "boolean") {
    return BOOLEAN_TYPE;
  }

  if (typeof x === "number") {
    return NUMBER_TYPE;
  }

  if (typeof x === "string") {
    return STRING_TYPE;
  }

  if (typeof x === "function") {
    return FUNCTION_TYPE;
  }

  return null;
}

/**
 * creates something similar to a Clojure's List structure using JavaScript arrays
 *
 * It's useful to have a seperate structure because the lists behave differently to
 * vectors in some functions
 *
 * Exported for tests, not intended for direct use
 *
 * @private
 * @class
 * @param {...*}
 * @return {List}
 * @example
 *
 * new List(1, 2, 3);
 * // => List(3) [1, 2, 3]
 *
 */
export class List extends Array {
  constructor(...args) {
    super();
    this.push(...args);
  }
}

/**
 * checks if x is a List
 *
 * @func
 * @param {*}
 * @return {boolean}
 *
 * isList(new List(1, 2, 3));
 * // => true
 *
 * isList("hello");
 * // => false
 *
 */
export function isList(x) {
  return typeConst(x) === LIST_TYPE;
}

/**
 * creates a new List containing args
 *
 * @func
 * @param {...*}
 * @return {List}
 * @example
 *
 * list("a", "b", "c");
 * // => List(3) ["a", "b", "c"]
 *
 * list(1, 2, 3);
 * // => List(3) [1, 2, 3]
 *
 */
export function list(...args) {
  return new List(...args);
}

/**
 * returns a lazy sequence of the concatenation of elements in colls
 *
 * @func
 * @param {...Array}
 * @return {LazyIterable}
 * @example
 *
 * [...concat([1, 2], [3, 4])];
 * // => [1, 2, 3, 4]
 *
 * [...concat(["a", "b"], null, [1, [2, 3], 4])];
 * // => ["a", "b", 1, [2, 3], 4]
 *
 */
export function concat(...colls) {
  return lazy(function* () {
    for (const coll of colls) {
      yield* iterable(coll);
    }
  });
}

/**
 * returns a LazyIterable of applying concat to the result of
 * applying map to f and colls
 *
 * @func
 * @param {function}
 * @param {...Array}
 * @return {LazyIterable}
 * @example
 *
 * [...mapcat(reverse, [[3, 2, 1, 0], [6, 5, 4], [9, 8, 7]])];
 * // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 *
 * [...mapcat(list, ["a", "b", "c"], [1, 2, 3])];
 * // => ["a", 1, "b", 2, "c", 3]
 *
 */
export function mapcat(f, ...colls) {
  return concat(...map(f, ...colls));
}

/**
 * returns true if the seq function is supported for x
 *
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {boolean}
 * @example
 *
 * isSeqable("hello");
 * // => true
 *
 */
export function isSeqable(x) {
  return (
    typeof x === "string" ||
    x === null ||
    x === undefined ||
    Symbol.iterator in x
  );
}

/**
 * Internal function whichereturns an iterable of x, even if it's empty
 * allowing for nil punning
 *
 * @private
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {Array|List|Map|Set|Object|List|string}
 * @example
 *
 * iterable(null);
 * // => []
 *
 * iterable("abc");
 * // => "abc"
 *
 */
export function iterable(x) {
  if (x === null || x === undefined) {
    return [];
  }

  if (isSeqable(x)) {
    return x;
  }

  return Object.entries(x);
}

const IIterable = Symbol("Iterable");

const IIterableIterator = Symbol.iterator;

/**
 * Internal funtion used for creating lazy sequences
 *
 * @private
 * @func
 * @param {Array|Object}
 * @return {Object}
 * @example
 *
 * iterator([1, 2, 3]);
 * // => Object [Array Iterator] {}
 *
 */
function iterator(coll) {
  return coll[Symbol.iterator]();
}

/**
 * takes a collection and returns an iterable of that collection, or nil if
 * it's empty.
 *
 * @func
 * @param {*}
 * @return {Array|null}
 * @example
 *
 * seq([1, 2]);
 * // => [1, 2]
 *
 * seq(null);
 * // => null
 *
 */
export function seq(x) {
  var iter = iterable(x);

  if (iter.length === 0 || iter.size === 0) {
    return null;
  }

  return iter;
}

/**
 * Enables lazy evaluation of sequences
 *
 * @private
 *
 */
class LazyIterable {
  constructor(gen) {
    this.gen = gen;
  }
  [IIterable] = true;
  [Symbol.iterator]() {
    return this.gen();
  }
}

/**
 * Internal function which returns a new instance of LazyIterable
 *
 * @private
 * @func
 * @params {function}
 * @return {LazyIterable}
 *
 */
function lazy(f) {
  return new LazyIterable(f);
}

/**
 * returns a new LazyIterable where x is the first item and
 * coll is the rest
 *
 * @func
 * @param {*}
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...cons(1, [2, 3, 4, 5, 6])];
 * // => [1, 2, 3, 4, 5, 6]
 *
 * [...cons([1, 2], [4, 5, 6])];
 * // => [[1, 2], 4, 5, 6]
 *
 */
export function cons(x, coll) {
  return lazy(function* () {
    yield x;
    yield* iterable(coll);
  });
}

/**
 * applies a given function to each element of a collection
 *
 * @func
 * @param {function}
 * @param {...Array|List|Map|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...map(inc, [1, 2, 3, 4, 5])];
 * // => [2, 3, 4, 5, 6]
 *
 * [...map(last, {x: 1, y: 2, z: 3})];
 * // => [1, 2, 3]
 *
 */
export function map(f, ...colls) {
  switch (colls.length) {
    case 0:
      throw new Error("map with one argument is not supported");
    case 1:
      return lazy(function* () {
        for (const x of iterable(colls[0])) {
          yield f(x);
        }
      });
    default:
      return lazy(function* () {
        const iters = colls.map((coll) => iterator(iterable(coll)));

        while (true) {
          let args = [];
          for (const i of iters) {
            const nextVal = i.next();

            if (nextVal.done) {
              return;
            }

            args.push(nextVal.value);
          }

          yield f(...args);
        }
      });
  }
}

/**
 * returns a lazy sequence of the items in coll for which
 * pred(item) returns true
 *
 * @func
 * @param {function} predicate
 * @param {Array|List|Set|null} collection
 * @example
 *
 * [...filter(isEven, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])];
 * // => [0, 2, 4, 6, 8, 10]
 *
 */
export function filter(pred, coll) {
  return lazy(function* () {
    for (const x of iterable(coll)) {
      if (pred(x)) {
        yield x;
      }
    }
  });
}

/**
 * returns an array of the items in coll for which
 * pred(item) returns true
 *
 * @param {function} predicate check
 * @param {Array|List|Set|null} collection
 * @return {Array}
 * @example
 *
 * filterv(isEven, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
 * // => [0, 2, 4, 6, 8, 10]
 *
 */
export function filterv(pred, coll) {
  return [...filter(pred, coll)];
}

/**
 * returns a lazy sequence of the items in coll for which
 * pred(item) returns false
 *
 * @func
 * @param {function} predicate check
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {LazyIterable}
 *
 * [...remove(isEven, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])];
 * // => [1, 3, 5, 7, 9]
 *
 */
export function remove(pred, coll) {
  return filter(complement(pred), coll);
}

/**
 * returns an array of the result of applying f to 0 and the first
 * item of coll, followed by applying f to 1 and the second item in the coll etc
 *
 * @func
 * @param {function}
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * mapIndexed((index, item) => [index, item], "foobar");
 * => [
 *      [0, "f"],
 *      [1, "o"],
 *      [2, "o"],
 *      [3, "b"],
 *      [4, "a"],
 *      [5, "r"],
 *    ];
 *
 */
export function mapIndexed(f, coll) {
  let ret = [];
  let i = 0;

  for (const x of iterable(coll)) {
    ret.push(f(i, x));
    i++;
  }

  return ret;
}

/**
 * returns a LazyIterable collection containing a possibly empty seq of the items
 * after the first
 *
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...rest([1, 2, 3])];
 * // => [2, 3]
 *
 * [...rest(null)];
 * // => []
 *
 */
export function rest(coll) {
  return lazy(function* () {
    var first = true;

    for (const x of iterable(coll)) {
      if (first) {
        first = false;
      } else {
        yield x;
      }
    }
  });
}

/**
 * returns the first item of collection
 *
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {*}
 * @example
 *
 * first([1, 2, 3]);
 * // => 1
 *
 * first("abc");
 * // => "a"
 *
 */
export function first(coll) {
  var [first] = iterable(coll);

  return first ?? null;
}

/**
 * returns the second item of a collection
 *
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {*}
 * @example
 *
 * second([1, 2, 3]);
 * // => 2
 *
 */
export function second(coll) {
  var [_, v] = iterable(coll);

  return v ?? null;
}

/**
 * the same as first(first(coll))
 *
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {*}
 * @example
 *
 * ffirst({name: "George", weight: 100})
 * // => "name"
 *
 */
export function ffirst(coll) {
  return first(first(coll));
}

/**
 * returns the last item in a collection
 *
 * @func
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {*}
 * @example
 *
 * last([1, 2, 3, 4, 5]);
 * // => 5
 *
 * last({one: 1, two: 2, three: 3});
 * // => ["three", 3]
 *
 */
export function last(coll) {
  coll = iterable(coll);

  switch (typeConst(coll)) {
    case ARRAY_TYPE:
      return coll[coll.length - 1] ?? null;
    default:
      let lastEl;
      for (const x of coll) {
        lastEl = x;
      }
      return lastEl;
  }
}

/**
 * Internal class for reduce()
 *
 * @private
 *
 */
class Reduced {
  value;
  constructor(x) {
    this.value = x;
  }
  _deref() {
    return this.value;
  }
}

/**
 * wraps x so that reduce will terminate with the value x
 *
 * @func
 * @param {*}
 * @return {*}
 * @example
 *
 * reduce((a, b) => {
 *   if ((a + b) > 20) {
 *     return reduced("Done early!");
 *   } else {
 *     return a + b;
 *   }
 * }, range(10));
 * // => "Done early!"
 *
 */
export function reduced(x) {
  return new Reduced(x);
}

/**
 * returns true if x is the result of a call to reduced
 *
 * @func
 * @param {function}
 * @return {boolean}
 * @example
 *
 * isReduced("foo");
 * // => false
 *
 * isReduced(reduced("foo"));
 * // => true
 *
 */
export function isReduced(x) {
  return x instanceof Reduced;
}

/**
 * iterates over a collection and applies a function to each
 * element, returning a single result
 *
 * @func
 * @param {function}
 * @return {Array|List|Map|Set|Object|string}
 * @example
 *
 * reduce(plus, [1, 2, 3, 4, 5]);
 * // => 15
 *
 * reduce(plus, [1]);
 * // => 1
 *
 */
export function reduce(f, arg1, arg2) {
  let coll, val;

  if (arg1.length === 0 && arg2 === undefined) {
    return f();
  }

  if (arg2 === undefined) {
    // reduce(f, coll)
    let iter = iterable(arg1)[Symbol.iterator]();
    val = iter.next().value;
    coll = iter;
  } else {
    // reduce(f, val, coll)
    val = arg1;
    coll = iterable(arg2);
  }

  if (val instanceof Reduced) {
    return val.value;
  }

  for (const x of coll) {
    val = f(val, x);

    if (val instanceof Reduced) {
      val = val.value;
      break;
    }
  }

  return val;
}

/**
 * returns a lazy sequence of the intermediate values of the reduction
 *
 * @func
 * @param {function}
 * @param {Array|List}
 * @param {Array|List}
 * @return {LazyIterable}
 * @example
 *
 * [...reductions(plus, [1, 2, 3, 4, 5])];
 * // => [1, 3, 6, 10, 15]
 *
 * [...reductions(conj, [], list(1, 2, 3))];
 * // => [[], [1], [1, 2], [1, 2, 3]]
 *
 */
export function reductions(f, init, coll) {
  if (coll === undefined) {
    coll = iterable(init);
    init = coll[0];
    coll = coll.slice(1);
  }

  return lazy(function* () {
    let acc = init === undefined ? 0 : init;

    yield acc;

    for (const item of coll) {
      acc = f(acc, item);
      yield acc;
    }
  });
}

/**
 * MUTATOR: adds a value to a structure by mutating the original
 *
 * @func
 * @param {Array|Map|Object}
 * @param {number|string}
 * @param {*} value
 * @return {Array}
 * @example
 *
 * var arrData = [1, 2, 5, 6, 8, 9];
 * mutAssoc(someData, 0, 77);
 * // => [77, 2, 5, 6, 8, 9]
 *
 */
export function mutAssoc(coll, key, val, ...kvs) {
  if (kvs.length % 2 !== 0) {
    throw new Error(
      "Illegal argument: assoc expects an odd number of arguments."
    );
  }

  switch (typeConst(coll)) {
    case MAP_TYPE:
      coll.set(key, val);

      for (var i = 0; i < kvs.length; i += 2) {
        coll.set(kvs[i], kvs[i + 1]);
      }
      break;
    case ARRAY_TYPE:
    case OBJECT_TYPE:
      coll[key] = val;

      for (var i = 0; i < kvs.length; i += 2) {
        coll[kvs[i]] = kvs[i + 1];
      }
      break;
    default:
      throw new Error(
        "Illegal argument: assoc! expects a Map, Array or Object as the first argument."
      );
  }

  return coll;
}

/**
 * returns a new structure with a modified values
 *
 * @func
 * @param {Array|Map|Object}
 * @param {number|string}
 * @param {*} value
 * @return {Array}
 * @example
 *
 * assoc([1, 2, 5, 6, 8, 9], 0, 77);
 * // => [77, 2, 5, 6, 8, 9]
 *
 */
export function assoc(coll, key, val, ...kvs) {
  if (!coll) {
    coll = {};
  }

  switch (typeConst(coll)) {
    case MAP_TYPE:
      return mutAssoc(new Map(coll.entries()), key, val, ...kvs);
    case ARRAY_TYPE:
      return mutAssoc([...coll], key, val, ...kvs);
    case OBJECT_TYPE:
      return mutAssoc({ ...coll }, key, val, ...kvs);
    default:
      throw new Error(
        "Illegal argument: assoc expects a Map, Array or Object as the first argument."
      );
  }
}

/**
 * Internal function
 * allows for modification (mutation or copy) of nested structures
 *
 * @private
 *
 */
function assocInWith(f, fname, coll, keys, val) {
  var baseType = typeConst(coll);

  if (
    baseType !== MAP_TYPE &&
    baseType !== ARRAY_TYPE &&
    baseType !== OBJECT_TYPE
  ) {
    throw new Error(
      `Illegal argument: ${fname} expects a Map, Array or Object as the first argument.`
    );
  }

  const chain = [coll];
  var lastInChain = coll;

  for (var i = 0; i < keys.length - 1; i += 1) {
    var k = keys[i];
    var chainVal;

    if (lastInChain instanceof Map) {
      chainVal = lastInChain.get(k);
    } else {
      chainVal = lastInChain[k];
    }

    if (!chainVal) {
      chainVal = emptyOfType(baseType);
    }

    chain.push(chainVal);

    lastInChain = chainVal;
  }

  chain.push(val);

  for (var i = chain.length - 2; i >= 0; i -= 1) {
    chain[i] = f(chain[i], keys[i], chain[i + 1]);
  }

  return chain[0];
}

/**
 * MUTATOR: associates a value in a nested structure by mutating value
 *
 * @func
 * @param {Array|May|object}
 * @return {Array}
 * @example
 *
 * var pets = [
 *   { name: "George", age: 12 },
 *   { name: "Lola", age: 11 },
 * ];
 * mutAssocIn(pets, [0, "age"], 13);
 * pets
 * // => [
 * //      { name: "George", age: 13 },
 * //      { name: "Lola", age: 11 },
 * //    ];
 *
 */
export function mutAssocIn(coll, keys, val) {
  return assocInWith(mutAssoc, "assocIn!", coll, keys, val);
}

/**
 * assocIn() associates a value in a nested structure. It returns a new structure
 *
 * @func
 * @param {Array|May|object}
 * @return {Array}
 * @example
 *
 * assocIn([{name: "George", age: 12}, {name: "Lola", age: 11}], [0, "age"], 13);
 * // => [
 * //      { name: "George", age: 13 },
 * //      { name: "Lola", age: 11 },
 * //    ];
 *
 */
export function assocIn(coll, keys, val) {
  return assocInWith(assoc, "assocIn", coll, keys, val);
}

/**
 * MUTATOR: removes item(s) from an object by key name
 *
 * @func
 * @param {Object} object
 * @param {...string} keys
 * @return {Object}
 * @example
 *
 * var dissocObj = {name: "George", salary: "Biscuits"};
 * mutDissoc(dissocObj, "name", "salary");
 * // => {}
 * dissocObj
 * // => {}
 *
 */
export function mutDissoc(obj, ...keys) {
  for (const key of keys) {
    delete obj[key];
  }

  return obj;
}

/**
 * returns a copy of an object with item(s) removed by key name
 *
 * @func
 * @param {Object} object
 * @param {...string} keys
 * @return {Object}
 * @example
 *
 * dissoc({name: "George", salary: "Biscuits"}, "name", "salary");
 * // => {}
 *
 */
export function dissoc(obj, ...keys) {
  let obj2 = { ...obj };

  for (const key of keys) {
    delete obj2[key];
  }

  return obj2;
}

/**
 * takes a set of functions and returns a fn that is the composition
 * of those fns
 *
 * @func
 * @param {...function}
 * @return {*}
 * @example
 *
 * comp(isZero)(5);
 * // => false
 *
 * comp(str, plus)(8, 8, 8);
 * // => "24"
 *
 */
export function comp(...fs) {
  if (fs.length === 0) {
    return identity;
  } else if (fs.length === 1) {
    return fs[0];
  }

  var [f, ...more] = fs.slice().reverse();

  return function (...args) {
    var x = f(...args);
    for (const g of more) {
      x = g(x);
    }

    return x;
  };
}

/**
 * MUTATOR: mutConj(oin) adds to a structure by mutation. The position of the addition
 * depends on the structure type
 *
 * @func
 * @param {Set|Array|List|Map|Object}
 * @param {*}
 * @return {Set|Array|List|Map|Object}
 * @example
 *
 * mutConj([1, 2, 3], 4);
 * // => [1, 2, 3, 4]
 *
 * mutConj({name: "George", coat: "Tabby"}, {age: 12, nationality: "British"})
 * // => {name: "George", coat: "Tabby", age: 12, nationality: "British"}
 *
 */
export function mutConj(...xs) {
  if (xs.length === 0) {
    return vector();
  }

  var [coll, ...rest] = xs;

  if (coll === null || coll === undefined) {
    coll = [];
  }

  switch (typeConst(coll)) {
    case SET_TYPE:
      for (const x of rest) {
        coll.add(x);
      }
      break;
    case LIST_TYPE:
      coll.unshift(...rest.reverse());
      break;
    case ARRAY_TYPE:
      coll.push(...rest);
      break;
    case MAP_TYPE:
      for (const x of rest) {
        if (!(x instanceof Array))
          iterable(x).forEach((kv) => {
            coll.set(kv[0], kv[1]);
          });
        else coll.set(x[0], x[1]);
      }
      break;
    case OBJECT_TYPE:
      for (const x of rest) {
        if (!(x instanceof Array)) {
          Object.assign(coll, x);
        } else {
          coll[x[0]] = x[1];
        }
      }
      break;
    default:
      throw new Error(
        "Illegal argument: conj! expects a Set, Array, List, Map, or Object as the first argument."
      );
  }

  return coll;
}

/**
 * conj() (conjoin) adds to a structure and returns a copy. The position of the
 * addition depends on the structure type
 *
 * @func
 * @param {Set|Array|List|Map|Object}
 * @param {*}
 * @return {Set|Array|List|Map|Object}
 * @example
 *
 * conj([1, 2, 3], 4);
 * // => [1, 2, 3, 4]
 *
 * conj([1, 2, 3], 4, 5);
 * // => [1, 2, 3, 4, 5]
 *
 */
export function conj(...xs) {
  if (xs.length === 0) {
    return vector();
  }

  let [coll, ...rest] = xs;

  if (coll === null || coll === undefined) {
    coll = [];
  }

  switch (typeConst(coll)) {
    case SET_TYPE:
      return new Set([...coll, ...rest]);
    case LIST_TYPE:
      return new List(...rest.reverse(), ...coll);
    case ARRAY_TYPE:
      return [...coll, ...rest];
    case MAP_TYPE:
      const m = new Map(coll);

      for (const x of rest) {
        if (!(x instanceof Array))
          iterable(x).forEach((kv) => {
            m.set(kv[0], kv[1]);
          });
        else m.set(x[0], x[1]);
      }

      return m;
    case LAZY_ITERABLE_TYPE:
      return lazy(function* () {
        yield* rest;
        yield* coll;
      });
    case OBJECT_TYPE:
      const coll2 = { ...coll };

      for (const x of rest) {
        if (!(x instanceof Array)) {
          Object.assign(coll2, x);
        } else {
          coll2[x[0]] = x[1];
        }
      }

      return coll2;
    default:
      throw new Error(
        "Illegal argument: conj expects a Set, Array, List, Map, or Object as the first argument."
      );
  }
}

/**
 * MUTATOR: removes item(s) from a set via mutation
 *
 * @func
 * @param {Set}
 * @return {Set}
 * @example
 *
 * var disjSet = new Set(["a", "b", "c"]);
 * mutDisj(disjSet, "b");
 * // => Set(2) { "a", "c" }
 *
 */
export function mutDisj(set, ...xs) {
  for (const x of xs) {
    set.delete(x);
  }
  return set;
}

/**
 * returns a new copy of a set with item(s) removed
 *
 * @func
 * @param {Set}
 * @return {Set}
 * @example
 *
 * disj(new Set(["a", "b", "c"]), "b");
 * // => Set(2) { "a", "c" }
 *
 */
export function disj(set, ...xs) {
  let set1 = new Set([...set]);
  return mutDisj(set1, ...xs);
}

/**
 * returns true if key is present in the given collection,
 * otherwise false. For arrays the key is the index.
 *
 * @func
 * @param {Array|Map|Set|List|Object}
 * @return {boolean}
 * @example
 *
 * itContains({name: "George", salary: "Biscuits"}, "name");
 * // => true
 *
 */
export function itContains(coll, val) {
  switch (typeConst(coll)) {
    case SET_TYPE:
    case MAP_TYPE:
      return coll.has(val);
    case undefined:
      return false;
    default:
      return val in coll;
  }
}

/**
 * returns the sum of numbers
 *
 * @func
 * @param {...number}
 * @return {number}
 * @example
 *
 * plus(1, 2, 3);
 * // => 6
 *
 */
export function plus(...xs) {
  return xs.reduce((x, y) => x + y, 0);
}

/**
 * returns the subtraction of numbers
 *
 * @func
 * @param {...number}
 * @return {number}
 * @example
 *
 * minus(5, 1, 2);
 * // => 2
 *
 */
export function minus(...xs) {
  return xs.reduce((x, y) => x - y);
}

/**
 * returns the division of numbers
 *
 * @func
 * @param {...number}
 * @return {number}
 * @example
 *
 * divide(6, 3);
 * // => 2
 *
 * divide(10);
 * // => 0.1
 *
 * divide(6, 3, 2);
 * // => 1
 *
 */
export function divide(...xs) {
  if (xs.length === 0) {
    throw new Error(`Illegal arity: 0 args passed to divide`);
  }

  if (xs.length === 1) {
    return 1 / xs;
  }

  return xs.reduce((x, y) => x / y);
}

/**
 * returns the quotient of dividing numerator by denominator
 *
 * @func
 * @param {number}
 * @return {number
 * @example
 *
 * quot(10, 3);
 * // => 3
 *
 * quot(11, 3);
 * // => 3
 *
 * quot(12, 3);
 * // => 4
 *
 */
export function quot(a, b) {
  return Math.floor(a / b);
}

/**
 * returns the product of numbers
 *
 * @func
 * @param {number}
 * @return {number}
 * @example
 *
 * multiply();
 * // => 1
 *
 * multiply(6);
 * // => 6
 *
 * multiply(2, 3);
 * // => 6
 *
 * multiply(2, 3, 4);
 * // => 24
 *
 */
export function multiply(...xs) {
  return xs.reduce((x, y) => x * y, 1);
}

/**
 * greater than (>) returns true if numbers are in decreasing order, otherwise false
 *
 * @func
 * @param {...number}
 * @return {boolean}
 * @example
 *
 * gt(1, 2);
 * // => false
 *
 * gt(2, 1);
 * // => true
 *
 * gt(6, 5, 4, 3, 2)
 * // => true
 *
 */
export function gt(...xs) {
  for (let i = 0; i < xs.length - 1; i++) {
    if (xs[i] <= xs[i + 1]) {
      return false;
    }
  }
  return true;
}

/**
 * less than (<) returns true if numbers are in decreasing order, otherwise false
 *
 * @func
 * @param {number}
 * @return {boolean}
 * @example
 *
 * lt(1, 2);
 * // => true
 *
 * lt(2, 1);
 * // => false
 *
 * lt(2, 3, 4, 5, 6);
 * // => true
 *
 */
export function lt(...xs) {
  for (let i = 0; i < xs.length - 1; i++) {
    if (xs[i] >= xs[i + 1]) {
      return false;
    }
  }

  return true;
}

/**
 * returns its argument
 *
 * @func
 * @param {*}
 * @return {*}
 * @example
 *
 * identity([1]);
 * // => [1]
 *
 */
export function identity(x) {
  return x;
}

/**
 * returns a lazy sequence of the first item in each coll,
 * then the second etc
 *
 * @func
 * @param {Array|Map|Set|List|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...interleave(["a", "b", "c"], [1, 2, 3])];
 * // => ["a", 1, "b", 2, "c", 3]
 *
 */
export function interleave(...colls) {
  return lazy(function* () {
    const iters = colls.map((coll) => iterator(iterable(coll)));

    while (true) {
      let res = [];
      for (const i of iters) {
        const nextVal = i.next();

        if (nextVal.done) {
          return;
        }

        res.push(nextVal.value);
      }

      yield* res;
    }
  });
}

/*
 * interpose() returns a lazy sequence of the elements of coll separated
 * by sep
 *
 * @func
 * @param {Array|Map|Set|List|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...interpose(", ", ["one", "two", "three"])];
 * // => ["one", ", ", "two", ", ", "three"]
 *
 */
export function interpose(sep, coll) {
  return drop(1, interleave(repeat(sep), coll));
}

/**
 * returns a map containing only those entries in the map whose key is in keys
 *
 * @func
 * @param {Array|Map|Set|Object|List|string|null}
 * @param {Array}
 * @return {Object}
 * @example
 *
 * selectKeys({a: 1, b: 2}, ["a"]);
 * // => {a: 1}
 *
 */
export function selectKeys(coll, keys) {
  const type = typeConst(coll);

  const ret = emptyOfType(type);

  for (const key of keys) {
    const val = get(coll, key);

    if (val !== undefined && val !== null) {
      mutAssoc(ret, key, val);
    }
  }

  return ret;
}

/**
 * Internal function
 * _partition() is a helper function for partition and partitionAll
 *
 * @private
 *
 */
function _partition(n, step, pad, coll, all) {
  return lazy(function* () {
    let p = [];
    let i = 0;

    for (let x of iterable(coll)) {
      if (i < n) {
        p.push(x);

        if (p.length === n) {
          yield p;
          p = step < n ? p.slice(step) : [];
        }
      }

      i++;

      if (i === step) {
        i = 0;
      }
    }

    if (p.length > 0) {
      if (p.length === n || all) {
        yield p;
      } else if (pad.length) {
        p.push(...pad.slice(0, n - p.length));
        yield p;
      }
    }
  });
}

/**
 * returns a lazy sequence of n items each at offsets step apart
 *
 * @func
 * @param {number}
 * @param {Array|Map|Set|Object|List|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...partition(4, range(20))];
 * // => [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15], [16, 17, 18, 19]]
 *
 */
export function partition(n, ...args) {
  let step = n;
  let pad = [];
  let coll = args[0];

  if (args.length === 2) {
    [step, coll] = args;
  } else if (args.length > 2) {
    [step, pad, coll] = args;
  }

  return _partition(n, step, pad, coll, false);
}

/**
 * returns a lazy sequence of arrays like partition but
 * may include partitions with fewer than n items at the end
 *
 * @func
 * @param {number}
 * @param {Array|List|Map|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * [...partitionAll(4, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])];
 * // => [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9]]
 *
 */
export function partitionAll(n, ...args) {
  let step = n;
  let coll = args[0];

  if (args.length === 2) {
    [step, coll] = args;
  }

  return _partition(n, step, [], coll, true);
}

/**
 * returns an empty coll of the same category as coll or null
 *
 * @func
 * @param {Array|Map|Set|List|Object|null}
 * @return {Array|Map|Set|List|Object|null}
 * @example
 *
 * empty(list(1, 2));
 * // => List(0) []
 *
 */
export function empty(coll) {
  const type = typeConst(coll);

  return emptyOfType(type);
}

/**
 * returns an object that consists of the rest of the maps conjed onto the first
 *
 * @func
 * @param {...Array|Map|Set|List|Object|string|null}
 * @return {Array|Map|Set|List|Object}
 * @example
 *
 * merge({a: 1, b: 2, c: 3}, {b: 9, d: 4});
 * // => { a: 1, b: 9, c: 3, d: 4 }
 *
 */
export function merge(...args) {
  const firstArg = args[0];
  let obj;

  if (first === null || firstArg === undefined) {
    obj = {};
  } else {
    obj = into(empty(firstArg), firstArg);
  }
  return mutConj(obj, ...args.slice(1));
}

/**
 * returns a new coll containing values from colls conjoined
 *
 * @func
 * @param {...Array|Map|Set|Object|string|null}
 * @return {Array|Map|Set|Object}
 * @example
 *
 * into([], [1, 2, 3]);
 * // => [1, 2, 3]
 *
 */
export function into(...args) {
  switch (args.length) {
    case 0:
      return [];
    case 1:
      return args[0];
    default:
      return conj(args[0] ?? [], ...iterable(args[1]));
  }
}

/**
 * returns a number one greater than n
 *
 * @func
 * @param {number}
 * @return {number}
 * @example
 *
 * inc(5);
 * // => 6
 *
 */
export function inc(n) {
  return n + 1;
}

/**
 * returns a number one less than n
 *
 * @func
 * @param {number}
 * @return {number}
 * @example
 *
 * dec(5);
 * // => 4
 *
 */
export function dec(n) {
  return n - 1;
}

/**
 * a wrapper for console.log()
 *
 * @func
 * @param {*}
 * @return {undefined}
 * @example
 *
 * println("Hello", "world");
 * (out) Hello world
 * // => undefined
 *
 */
export function println(...args) {
  console.log(...args);
}

/**
 * returns the value at the index
 *
 * @func
 * @param {Array|List|string}
 * @return {*}
 * @example
 *
 * nth(["a", "b", "c", "d"], 0);
 * // => "a"
 *
 */
export function nth(coll, index, notFound = null) {
  let i = 0;

  for (let element of coll) {
    if (i === index) {
      return element;
    }
    i++;
  }

  return notFound;
}

/**
 * returns the value mapped to key, notFound or null if the key is not present
 *
 * @func
 * @param {Array|List|Map|Set|Object|string}
 * @key {number|string}
 * @return {*}
 * @example
 *
 * get([1, 2, 3], 1);
 * // => 2
 *
 */
export function get(coll, key, notFound = null) {
  let val;

  switch (typeConst(coll)) {
    case SET_TYPE:
      if (coll.has(key)) {
        val = key;
        break;
      }
    case MAP_TYPE:
      val = coll.get(key);
      break;
    case undefined:
      break;
    default:
      val = coll[key];
      break;
  }

  if (val !== undefined) {
    return val;
  }

  return notFound;
}

/**
 * returns a string for single values and a concatenation of multiple values
 *
 * @func
 * @param {...*}
 * @return {string}
 * @example
 *
 * str(1);
 * // => "1"
 *
 * str(1, 2, 3);
 * // => "123"
 *
 */
export function str(...xs) {
  return xs.join("");
}

/**
 * returns true if x is logical false, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * not(true);
 * // => false
 *
 */
export function not(x) {
  return !x;
}

/*
 * returns true if x is null, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isNil(null);
 * // => true
 *
 */
export function isNil(x) {
  return x === null;
}

/**
 * Internal function for prStr
 *
 * @private
 *
 */
function _prStr(x) {
  return JSON.stringify(x, (_key, val) => {
    switch (typeConst(val)) {
      case SET_TYPE:
      case LAZY_ITERABLE_TYPE:
        return [...val];
      case MAP_TYPE:
        return Object.fromEntries(val);
      default:
        return val;
    }
  });
}

/**
 * turns a collection into a string and returns it
 *
 * @func
 * @param {...*}
 * @return {string}
 * @example
 *
 * prStr([1, 2, 3, 4, 5]);
 * // => "[1,2,3,4,5]"
 *
 * prStr({name: "George", salary: "Biscuits"});
 * // => "{"name":"George","salary":"Biscuits"}"
 *
 */
export function prStr(...xs) {
  return xs.map(_prStr).join("");
}

/**
 * turns a collection into a string and prints to console
 * returns undefined
 *
 * @func
 * @param {...*}
 * @return {undefined}
 * @example
 *
 * prn([1, 2, 3, 4, 5]);
 * (out) "[1,2,3,4,5]"
 * // => undefined
 *
 * prn({name: "George", salary: "Biscuits"});
 * (out) "{"name":"George","salary":"Biscuits"}"
 * // => undefined
 *
 */
export function prn(...xs) {
  println(prStr(...xs));
}

/**
 * Internal function for Atoms
 *
 * @private
 *
 */
function Atom(init) {
  this.value = init;
  this._deref = () => this.value;
  this._mutReset = (x) => (this.value = x);
}

/**
 * Atoms provide a way to manage state
 *
 * @func
 * @param {*} value
 * @return {Atom}
 * @example
 *
 * var myAtom = atom(0);
 * myAtom;
 * // => Atom {
 * //      value: 0,
 * //      _deref: [Function (anonymous)],
 * //      _mutReset: [Function (anonymous)]
 * //   }
 *
 * deref(myAtom);
 * // => 0
 *
 * mutSwap(myAtom, inc);
 * // => 1
 *
 * mutSwap(myAtom, function(n) { return (n + n) * 2 });
 * // => 4
 *
 * mutReset(myAtom, 0);
 *
 * deref(myAtom);
 * // => 0
 *
 */
export function atom(init) {
  return new Atom(init);
}

/**
 * returns the value stored in an atom
 *
 * @func
 * @param {Atom}
 * @return {*}
 * @example
 *
 * var myAtom = atom(0);
 * mutSwap(myAtom, inc);
 * deref(myAtom);
 * // => 1
 *
 */
export function deref(ref) {
  return ref._deref();
}

/**
 * MUTATOR: resets the value stored in an atom
 *
 * @func
 * @param {Array|Map|Set|Object|string|null}
 * @return {*}
 * @example
 *
 * var myAtom = atom(0);
 * mutSwap(myAtom, inc);
 * deref(myAtom);
 * // => 1
 *
 * mutReset(myAtom, 0);
 * deref(myAtom);
 * // => 0
 *
 */
export function mutReset(atom, val) {
  atom._mutReset(val);
}

/**
 * MUTATOR: automatically changes the value of an existing atom by applying f
 * and returns the new value
 *
 * @func
 * @param {Atom}
 * @param {function}
 * @param {...*}
 * @return {*}
 * @example
 *
 * var myAtom = atom(0);
 * mutSwap(myAtom, inc);
 * // => 1
 *
 */
export function mutSwap(atom, f, ...args) {
  const val = f(deref(atom), ...args);
  mutReset(atom, val);

  return val;
}

/**
 * returns a lazy sequence of numbers from start to end
 *
 * @func
 * @param {number}
 * @param {number}
 * @param {number}
 * @return {LazyIterable}
 * @example
 *
 * [...range(10)];
 * // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 *
 * [...range(-5, 5)];
 * // => [-5, -4, -3, -2, -1, 0,  1,  2,  3,  4]
 *
 */
export function range(begin, end, step) {
  return lazy(function* () {
    let b = begin;
    let e = end;
    let s = step;

    if (end === undefined) {
      b = 0;
      e = begin;
    }

    let i = b || 0;
    s = step || 1;

    while (e === undefined || i < e) {
      yield i;
      i += s;
    }
  });
}

/*
 * returns the match, if any, of string to pattern using RegExp.prototype.exec()
 *
 * @func
 * @param {RegExp}
 * @param {string}
 * @return {Array|string}
 * @example
 *
 * reMatches(/hello, world/gi, "hello, world");
 * // => "hello, world"
 *
 * reMatches(/quick\s(?<color>brown).+?(jumps)/dgi, "The Quick Brown Fox Jumps Over The Lazy Dog");
 * // => ["Quick Brown Fox Jumps", "Brown", "Jumps"]
 *
 */
export function reMatches(re, s) {
  let matches = re.exec(s);

  if (matches.length === 1) {
    if (matches && s === matches[0]) {
      return matches[0];
    }
  }

  if (matches.length > 1) {
    return matches.map(function (match) {
      return match;
    });
  }

  return null;
}

/**
 * returns an array of the items in an array from start to end
 *
 * @func
 * @param {Array} array
 * @param {number} start
 * @param {number} end
 * @return {Array}
 * @example
 *
 * subvec([1, 2, 3, 4, 5, 6, 7], 2);
 * // => [3, 4, 5, 6, 7]
 *
 * subvec([1, 2, 3, 4, 5, 6, 7], 2, 4);
 * // => [3, 4]
 *
 */
export function subvec(arr, start, end) {
  return arr.slice(start, end);
}

/**
 * returns a new array containing args
 *
 * @func
 * @param {...*}
 * @return {Array}
 * @example
 *
 * vector();
 * // => []
 *
 * vector(1, 2, 3);
 * // => [1, 2, 3]
 *
 */
export function vector(...args) {
  return args;
}

/**
 * checks if x is an array
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isVector([1, 2, 3]);
 * // => true
 *
 * isVector("hello");
 * // => false
 *
 */
export function isVector(x) {
  return typeConst(x) === ARRAY_TYPE;
}

/**
 * returns the results of map() in an array rather than a lazy sequence
 *
 * @func
 * @param {function}
 * @param {...Array|List|Map|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * mapv(inc, [1, 2, 3, 4, 5]);
 * // => [2, 3, 4, 5, 6]
 *
 */
export function mapv(...args) {
  return [...map(...args)];
}

/**
 * creates a new array containing iterable of coll
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * vec(null);
 * []
 *
 * vec({a: 1, b: 2, c: 3});
 * [["a", 1], ["b", 2], ["c", 3]]
 *
 */
export function vec(coll) {
  return [...iterable(coll)];
}

/**
 * returns a set of the distinct elements of coll
 *
 * @func
 * @param {Array|Map|List|Object|string|null}
 * @return {*}
 * @example
 *
 * set([1, 2, 3, 4, 5]);
 * // => Set(5) { 1, 2, 3, 4, 5 }
 *
 * set("abcd")
 * // => Set(4) { 'a', 'b', 'c', 'd' }
 *
 */
export function set(coll) {
  return new Set(iterable(coll));
}

/**
 * applies f to the argument list formed by prepending intervening
 * arguments to args
 *
 * @func
 * @param {function}
 * @param {...*}
 * @return {string}
 * @example
 *
 * apply(str, ["str1", "str2", "str3"]);
 * // => "str1str2str3"
 *
 */
apply(str, [1, 2]);
export function apply(f, ...args) {
  var xs = args.slice(0, args.length - 1);
  var coll = args[args.length - 1];

  return f(...xs, ...coll);
}

/**
 * isEven() returns true if x is even
 *
 * @func
 * @param {number}
 * @return {boolean}
 * @example
 *
 * isEven(2);
 * // => true
 *
 * isEven(null);
 * throws error
 *
 */
export function isEven(x) {
  if (typeof x !== "number") {
    throw new Error(`Illegal argument: ${x} is not a number`);
  }

  return x % 2 === 0;
}

/**
 * returns true if x is odd
 *
 * @func
 * @param {number}
 * @return {boolean}
 * @example
 *
 * isOdd(3);
 * // => true
 *
 * isOdd(null);
 * throws error
 *
 */
export function isOdd(x) {
  return not(isEven(x));
}

/**
 * takes a fn f and returns a fn that takes the same arguments
 * as f, has the same effects, if any, and returns the opposite truth value
 *
 * @func
 * @param {function}
 * @return {function}
 * @example
 *
 * var testIsOdd = complement(isEven);
 * testIsOdd(3);
 * // => true
 *
 */
export function complement(f) {
  return (...args) => not(f(...args));
}

/**
 * constantly returns a function that takes any number of arguments and returns x
 *
 * @func
 * @param {*}
 * @return {function}
 * @example
 *
 * var boring = constantly(10);
 * boring()
 * // => 10
 *
 * boring("hello");
 * // => 10
 *
 */
export function constantly(x) {
  return (..._) => x;
}

/**
 * returns a lazy sequence of args
 *
 * @func
 * @param {...*}
 * @return {LazyIterable}
 * @example
 *
 * [...repeat(5, "x")];
 * // => ["x", "x", "x", "x", "x"]
 *
 */
export function repeat(...args) {
  if (args.length === 0 || args.length > 2) {
    throw new Error(`Invalid arity: ${args.length}`);
  }

  return {
    [IIterable]: true,
    [IIterableIterator]:
      args.length == 1
        ? function* () {
            let x = args[0];
            while (true) {
              yield x;
            }
          }
        : function* () {
            let [n, x] = args;
            for (var i = 0; i < n; i++) {
              yield x;
            }
          },
  };
}

/**
 * returns a lazy sequence of the first n items in coll, or
 * all items if there are fewer than n
 *
 * @func
 * @param {number}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...take(3, [1, 2, 3, 4, 5, 6])];
 * // => [1, 2, 3]
 *
 * [...take(3, [1, 2])];
 * // => [1, 2]
 *
 */
export function take(n, coll) {
  return lazy(function* () {
    let i = n - 1;

    for (const x of iterable(coll)) {
      if (i-- >= 0) {
        yield x;
      }

      if (i < 0) {
        return;
      }
    }
  });
}

/**
 * returns a lazy sequence of successive items from coll
 * while pred(item) returns true
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...takeWhile(isNeg, [-2, -1, 0, 1, 2, 3])];
 * // => [-2, -1]
 *
 */
export function takeWhile(pred, coll) {
  return lazy(function* () {
    for (const x of iterable(coll)) {
      if (pred(x)) {
        yield x;
      } else {
        return;
      }
    }
  });
}

/**
 * returns a lazy sequence of every nth item in coll
 *
 * @func
 * @param {number}
 * @param {Array|Map|List|Set|Object|string}
 * @return {LazyIterable}
 * @example
 *
 * [...takeNth(2, range(10))];
 * // => [0, 2, 4, 6, 8]
 *
 */
export function takeNth(n, coll) {
  if (n <= 0) {
    return repeat(first(coll));
  }

  return lazy(function* () {
    let i = 0;

    for (let x of iterable(coll)) {
      if (i % n === 0) {
        yield x;
      }
      i++;
    }
  });
}

/**
 * takes a function f and fewer than normal arguments to f. It returns a
 * fn that takes a variable number of additional args. When called, the
 * returned function calls f with args plus additional args
 *
 * @func
 * @param {function}
 * @param {...*}
 * @return {function}
 * @example
 *
 * var hundredPlus = partial(plus, 100);
 * hundredPlus(5);
 * // => 105
 *
 */
export function partial(f, ...xs) {
  return function (...args) {
    return f(...xs, ...args);
  };
}

/**
 * returns a lazy sequence of repetitions from items in coll
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...take(5, cycle(["a", "b"]))];
 * // => ["a", "b", "a", "b", "a"]
 *
 * [...take(10, cycle(range(0, 3)))];
 * // => [0, 1, 2, 0, 1, 2, 0, 1, 2, 0]
 *
 */
export function cycle(coll) {
  return lazy(function* () {
    while (true) {
      yield* coll;
    }
  });
}

/**
 * returns an array with items in reverse order
 *
 * @func
 * @param {Array|Map|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * reverse([1, 2, 3]);
 * // => [3, 2, 1]
 *
 * reverse("hello");
 * // => ["o", "l", "l", "e", "h"]
 *
 */
export function reverse(coll) {
  if (coll instanceof Array) {
    return coll.reverse();
  } else {
    return [...coll].reverse();
  }
}

/**
 * returns a sorted sequence of the items in coll
 *
 * @func
 * @param {function}
 * @param {Array|List|Set|string}
 * @return {Array}
 * @example
 *
 * sort([3, 4, 1, 2]);
 * // => [1, 2, 3, 4]
 *
 * sort((a, b) => b - a, [3, 4, 1, 2]);
 * // => [4, 3, 2, 1]
 *
 */
export function sort(f, coll) {
  if (coll === undefined) {
    coll = f;
    f = undefined;
  }
  return [...coll].sort(f);
}

/**
 * returns a random permutation of coll
 *
 * @func
 * @param {Array|List|Set|string}
 * @return {Array}
 * @example
 *
 * shuffle([1, 2, 3, 4]);
 * // => [2, 1, 3, 4]
 *
 */
export function shuffle(coll) {
  return [...coll].sort(function () {
    return Math.random() - 0.5;
  });
}

/**
 * returns the first true value of pred(x) for any x in coll,
 * otherwise null
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {*}
 * @example
 *
 * some(isEven, [1, 2, 3, 4]);
 * // => true
 *
 */
export function some(pred, coll) {
  for (const x of iterable(coll)) {
    const res = pred(x);

    if (res) {
      return res;
    }
  }

  return null;
}

/**
 * randInt() returns a random integer between 0 and n
 *
 * @func
 * @param {number}
 * @return {number}
 * @example
 *
 * randInt(30);
 * // => 27
 *
 * randInt(30);
 * // => 3
 *
 */
export function randInt(n) {
  return Math.floor(Math.random() * n);
}

/**
 * returns true if x is true, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isTrue(1 > 0);
 * // => true
 *
 */
export function isTrue(x) {
  return x === true;
}

/**
 * returns true if x is false, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isFalse(1 > 0);
 * // => false
 *
 */
export function isFalse(x) {
  return x === false;
}

/**
 * returns true if x is not null or undefined, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isSome(1 < 5);
 * // => true
 *
 */
export function isSome(x) {
  return not(x === null || x === undefined);
}

/**
 * returns x coerced to a boolean
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * booleans("hello");
 * // => true
 *
 * booleans(0)
 * // => false
 *
 */
export function booleans(x) {
  return !!x;
}

/**
 * returns true if x is zero, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isZero(3);
 * // => false
 *
 */
export function isZero(x) {
  return x === 0;
}

/**
 * returns true if x is less than zero, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isNeg(-5);
 * // => true
 *
 */
export function isNeg(x) {
  return x < 0;
}

/**
 * returns true if x is greater than zero, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 * isPos(5);
 *
 * // => true
 *
 */
export function isPos(x) {
  return x > 0;
}

/**
 * returns a lazy sequence of all but the first n items in coll
 *
 * @func
 * @param {number}
 * @param {...Array|List|Set|Object|string|null}
 * @return {LazySequence}
 * @example
 *
 * [...drop(-1, [1, 2, 3, 4])];
 * // => [1, 2, 3, 4]
 *
 * [...drop(2, [1, 2, 3, 4])];
 * // => [3, 4]
 *
 */
export function drop(n, xs) {
  return lazy(function* () {
    let iter = iterator(iterable(xs));

    for (let x = 0; x < n; x++) {
      iter.next();
    }

    yield* iter;
  });
}

/**
 * returns a lazy sequence of the items in coll starting from
 * the first item for which pred(value) returns false
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * [...dropWhile((x) => 3 > x, [1, 2, 3, 4, 5, 6])];
 * // => [3, 4, 5, 6]
 *
 * [...dropWhile((x) => 3 >= x, [1, 2, 3, 4, 5, 6])];
 * // => [4, 5, 6]
 *
 */
export function dropWhile(pred, xs) {
  return lazy(function* () {
    let iter = iterator(iterable(xs));

    while (true) {
      let nextItem = iter.next();

      if (nextItem.done) {
        break;
      }

      let value = nextItem.value;

      if (!pred(value)) {
        yield value;
        break;
      }
    }
    yield* iter;
  });
}

/**
 * returns a lazy sequnce of the elements of coll with duplicates removed
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...distinct([1, 2, 1, 3, 1, 4, 1, 5])];
 * // => [1, 2, 3, 4, 5]
 *
 * [...distinct(["a", "a", "b", "c"])];
 * // => ["a", "b", "c"]
 *
 * apply(str, distinct("tattoo"));
 * // => "tao"
 *
 */
export function distinct(coll) {
  return lazy(function* () {
    let seen = new Set();
    for (const x of iterable(coll)) {
      if (!seen.has(x)) {
        yield x;
      }
      seen.add(x);
    }
    return;
  });
}

/**
 * returns true if no two of the arguments are equal, false otherwise
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isDistinct(1, 2, 3);
 * // => true
 *
 * isDistinct(1, 2, 3, 3);
 * // => false
 *
 * isDistinct(["A", "A", "c"]);
 * // => true
 *
 */
export function isDistinct(...xs) {
  return xs.every((el, i) => {
    return xs.indexOf(el) === i;
  });
}

/**
 * returns a new structure with a value updated by f
 *
 * @func
 * @param {Array|Map|Object}
 * @param {*}
 * @param {function}
 * @param {...*}
 * @return {Array|Map|Object}
 * @example
 *
 * update([1, 2, 3], 0, inc);
 * // => [2, 2, 3]
 *
 * update([], 0, function(item) { return str("foo", item); });
 * // => ["foo"]
 *
 */
export function update(coll, key, f, ...args) {
  return assoc(coll, key, f(get(coll, key), ...args));
}

/**
 * MUTATOR: updates a collection with a value updated by f
 *
 * @func
 * @param {Array|Map|Object}
 * @return {Array|Map|Object}
 * @example
 *
 * var pet = {name: "George", age: 11};
 * mutUpdate(pet, "age", inc);
 * pet
 * // => {name: 'George', age: 12}
 *
 */
export function mutUpdate(coll, key, f, ...args) {
  const val = get(coll, key);

  return mutAssoc(coll, key, f(val, ...args));
}

/**
 * returns an object of the elements of coll keyed by the
 * result of f on each element
 *
 * @func
 * @param {function}
 * @param {Array|List|Set|null}
 * @return {Object}
 * @example
 *
 * groupBy(isOdd, range(10));
 * // => {false: [0, 2, 4, 6, 8], true: [1, 3, 5, 7, 9]}
 *
 */
export function groupBy(f, coll) {
  const res = {};

  for (const x of iterable(coll)) {
    const key = f(x);
    mutUpdate(res, key, fnil(mutConj, []), x);
  }

  return res;
}

/**
 * returns an object from distinct items in coll
 * to the number of times they appear
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Object}
 * @example
 *
 * frequencies(["a", "b", "a", "a"]);
 * // => {a: 3, b: 1}
 *
 */
export function frequencies(coll) {
  const res = {};
  const uf = fnil(inc, 0);

  for (const o of iterable(coll)) {
    mutUpdate(res, o, uf);
  }

  return res;
}

/**
 * returns a sequence of all but the last item in coll
 *
 * butlast([1, 2, 3]);
 * // => [1, 2]
 *
 */
export function butlast(coll) {
  let x = [...iterable(coll)];
  x.pop();

  return x.length > 0 ? x : null;
}

/**
 * returns a lazy sequence of all but the last n items in coll
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...dropLast([1, 2, 3, 4])];
 * // => [1, 2, 3]
 *
 */
export function dropLast(...args) {
  let [n, coll] = args.length > 1 ? args : [1, args[0]];

  return map((x, _) => x, coll, drop(n, coll));
}

/**
 * returns an array of [take(n, coll), drop(n, coll)]
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * splitAt(2, [1, 2, 3, 4, 5]);
 * // => [[1, 2], [3, 4, 5]]
 *
 */
export function splitAt(n, coll) {
  return [[...take(n, coll)], [...drop(n, coll)]];
}

/**
 * returns an array of [takeWhile(pred, coll), dropWhile(pred, coll)]
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Array}
 * @example
 *
 * splitWith(isOdd, [1, 3, 5, 6, 7, 9]);
 * // => [[1, 3, 5], [6, 7, 9]]
 *
 */
export function splitWith(pred, coll) {
  return [[...takeWhile(pred, coll)], [...dropWhile(pred, coll)]];
}

/**
 * returns the number of items in the collection
 *
 * @func
 * @param {*}
 * @return {number}
 * @example
 *
 * count([1, 2, 3]);
 * // => 3
 *
 */
export function count(coll) {
  if (!coll) {
    return 0;
  }

  const len = coll.length || coll.size;

  if (typeof len === "number") {
    return len;
  }

  let ret = 0;

  for (const x of iterable(coll)) {
    ret++;
  }

  return ret;
}

/**
 * returns a selected value from a nested structure
 *
 * @func
 * @param {Array|Map|List|Set|Object}
 * @param {Array}
 * @param {*}
 * @return {*}
 * @example
 *
 * var pet = {
 *   name: "George",
 *   profile: {
 *     name: "George V",
 *     address: { city: "London", street: "Tabby Lane" },
 *   },
 * }
 *
 * getIn(pet, ["profile", "name"]);
 * // => "George V"
 *
 */
export function getIn(coll, path, notFound = null) {
  let entry = coll;

  for (const item of path) {
    entry = get(entry, item);
  }

  if (entry === undefined || entry === null) {
    return notFound;
  }

  return entry;
}

/**
 * updates a value in a nested structure
 *
 * @func
 * @param {Array|Map|List|Set|Object}
 * @param {Array}
 * @param {function}
 * @param {...*}
 * @return {Array|Map|List|Set|Object}
 *
 * @example
 * var pets = [{name: "George", age: 11}, {name: "Lola", age: 10}];
 * updateIn(pets, [1, "age"], inc);
 * // => [{name: "George", age: 11}, {name: "Lola", age: 11}]
 *
 */
export function updateIn(coll, path, f, ...args) {
  return assocIn(coll, path, f(getIn(coll, path), ...args));
}

/**
 * takes a function f, and returns a function that calls f, replacing
 * a null first argument to f with the supplied value x
 *
 * @func
 * @param {function}
 * @param {*}
 * @return {function}
 * @example
 *
 * function sayHello(name) {
 *   return str("Hello ", name);
 * }
 *
 * var sayHelloWithDefaults = fnil(sayHello, "world");
 *
 * sayHelloWithDefaults(null);
 * // => "Hello world"
 *
 * sayHelloWithDefaults("universe");
 * // => "Hello universe"
 *
 */
export function fnil(f, ...xs) {
  return function (...args) {
    for (let i = 0; i < args.length; i++) {
      if (args[i] === null || args[i] === undefined) {
        args[i] = xs[i] || xs[xs.length - 1];
      }
    }

    return apply(f, args);
  };
}

/**
 * returns true if pred(x) is true for every x in coll,
 * otherwise false
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {boolean}
 * @example
 *
 * isEvery(isEven, [2, 4, 6]);
 * // => true
 *
 * isEvery(isEven, [1, 2, 3]);
 * // => false
 *
 */
export function isEvery(pred, coll) {
  for (let x of iterable(coll)) {
    if (!pred(x)) {
      return false;
    }
  }

  return true;
}

/**
 * returns false if pred(x) is true for every x in coll,
 * otherwise true
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {boolean}
 * @example
 *
 * isNotEvery(isEven, [2, 4, 6]);
 * // => false
 *
 * isNotEvery(isEven, [1, 2, 3]);
 * // => true
 *
 */
export function isNotEvery(pred, coll) {
  return !isEvery(pred, coll);
}

/**
 * returns false if pred(x) is true for any x in coll, otherwise true
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {boolean}
 * @example
 *
 * isNotAny(isOdd, [2, 4, 6]);
 * // => true
 *
 */
export function isNotAny(pred, coll) {
  return !some(pred, coll);
}

/**
 * Internal function
 * _repeatedly() is a helper for repeatedly()
 *
 * @private
 *
 */
function _repeatedly(f) {
  return lazy(function* () {
    while (true) {
      yield f();
    }
  });
}

/**
 * takes a function of no args and returns it with n calls to it
 *
 * @func
 * @param {number}
 * @return {function}
 * @return {LazyIterable}
 * @example
 *
 * [...repeatedly(5, () => randInt(11))];
 * // => [7, 7, 7, 6, 4]
 *
 */
export function repeatedly(n, f) {
  if (f === undefined) {
    f = n;
    n = undefined;
  }

  const res = _repeatedly(f);

  if (n) {
    return take(n, res);
  } else {
    return res;
  }
}

/**
 * returns a lazy sequence of the non-nil results of f(item)
 *
 * @func
 * @param {function}
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {LazyIterable}
 * @example
 *
 * [...keep(isEven, range(1, 10))];
 * // => [false, true, false, true, false, true, false, true, false]
 *
 */
export function keep(pred, coll) {
  return lazy(function* () {
    for (const x of iterable(coll)) {
      const res = pred(x);

      if (res !== null && res !== undefined) {
        yield res;
      }
    }
  });
}

/**
 * turns collection with any elements equal to a key in coll1 replaced with the
 * corresponding val in coll1
 *
 * @func
 * @param {*}
 * @param {*}
 * @return {*}
 * @example
 *
 * replace(["zeroth", "first", "second", "third", "fourth"], [0, 2, 4, 0])
 * // => ["zeroth", "second", "fourth", "zeroth"]
 *
 */
export function replace(coll1, coll2) {
  let mapf = coll2 instanceof Array ? mapv : map;

  return mapf((x) => {
    const repl = coll1[x];

    if (repl !== undefined) {
      return repl;
    } else {
      return x;
    }
  }, coll2);
}

/**
 * returns true if coll has no items
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {boolean}
 * @example
 *
 * isEmpty(list());
 * // => true
 *
 */
export function isEmpty(coll) {
  return seq(coll) ? false : true;
}

/**
 * evaluates test. If false evaluates and returns then or
 * otherwise (if supplied)
 *
 * @func
 * @param {*}
 * @param {*}
 * @param {*}
 * @return {*}
 * @example
 *
 * ifNot(isEmpty([1, 2]), first([1, 2]));
 * // => 1
 *
 * ifNot(isEmpty([]), first([1, 2]));
 * // => null
 *
 */
export function ifNot(test, then, otherwise = null) {
  if (!test) {
    return then;
  }

  return otherwise;
}

/**
 * checks if x is an instance of c
 *
 * @func
 * @param {Class}
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isInstance(String, "hello");
 * // => true
 *
 * isInstance(Number, 5);
 * // => true
 *
 * isInstance(Number, "5");
 * // => false
 *
 */
export function isInstance(c, x) {
  var a = typeConst(x);
  var b = typePrimitive(x);

  if (c === Boolean && b === BOOLEAN_TYPE) {
    return typeof x === "boolean";
  }

  if (c === Number && b === NUMBER_TYPE) {
    return typeof x === "number";
  }

  if (c === String && b === STRING_TYPE) {
    return typeof x === "string";
  }

  if (c === Function && b === FUNCTION_TYPE) {
    return typeof x === "function";
  }

  if (
    a === MAP_TYPE ||
    a === ARRAY_TYPE ||
    a === OBJECT_TYPE ||
    a === LIST_TYPE ||
    a === SET_TYPE ||
    a === LAZY_ITERABLE_TYPE
  ) {
    return x instanceof c;
  }

  return false;
}

/**
 * keys() returns all keys from a collection
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Array|null}
 * @example
 *
 * keys({a: 1, b: 2, c: 3});
 * // => ["a", "b", "c"]
 *
 * keys({});
 * // => null
 *
 * keys(null);
 * // => null
 *
 */
export function keys(x) {
  if (isEmpty(x)) {
    return null;
  }

  return Object.keys(x);
}

/**
 * returns all values from an object
 *
 * @func
 * @param {Array|Map|List|Set|Object|string|null}
 * @return {Array|null}
 * @example
 *
 * vals({a: 1, b: 2, c: 3});
 * // => [1, 2, 3]
 *
 * vals({});
 * // => null
 *
 * vals(null);
 * // => null
 *
 */
export function vals(x) {
  if (isEmpty(x)) {
    return null;
  }

  return Object.values(x);
}

/**
 * thread first threads x through the fns. Inserts x as the second item in the first
 * function. It will do the same for following functions.
 *
 * @func
 * @param {*}
 * @param {...function}
 * @return {*}
 * @example
 *
 * tf("3", parseInt);
 * // => 3
 *
 */
export function tf(x, ...fns) {
  return fns.reduce((acc, fn) => fn(acc), x);
}

/**
 * thread last threads x through the fns. Inserts x as the last item in the first
 * function. It will do the same for following functions
 *
 * @func
 * @param {*}
 * @param {...function}
 * @return {*}
 * @example
 *
 * tl("3", parseInt);
 * // => 3
 *
 */
export function tl(x, ...fns) {
  return fns.reduceRight((acc, fn) => fn(acc), x);
}

/**
 * evaluates the test and returns then if true, otherwise if false
 *
 * @func
 * @param {*}
 * @param {*}
 * @param {*}
 * @return {*}
 * @example
 *
 * iff(1 > 2, "hello", "world");
 * "world"
 *
 * iff(3 > 2, str("hello", " world"), "world");
 * "hello world"
 *
 * iff(1 * 2 === 2, (() => 3 * 2)(), 7);
 * 6
 *
 */
export function iff(test, then, otherwise) {
  return test ? then : otherwise;
}

/**
 * takes a set of test / expression pairs. It evaluates each test one at a
 * time returning the result for the first true test
 *
 * @func
 * @param {...Array}
 * @return {function
 * @example
 *
 * function posNegOrZero(n) {
 *   return cond(
 *     [() => n < 0, () => "negative"],
 *     [() => n > 0, () => "positive"],
 *     [() => "else", () => "zero"]
 *   )();
 * }
 * posNegOrZero(5);
 * // => "positive"
 *
 */
export function cond(...xs) {
  return function (...args) {
    for (let i = 0; i < xs.length; i++) {
      const [pred, val] = xs[i];

      if (pred(...args)) {
        return val(...args);
      }
    }

    return null;
  };
}

/**
 * groups variables in a single scope
 *
 * @func
 * @param {Array}
 * @param {function}
 * @return {*}
 * @example
 *
 * lett([["x", 3], ["y", 4], ["z", 55]], (xs) => {
 *   return (xs.x * xs.y) + xs.z;
 * });
 *
 */
export function lett(bindings, f) {
  return (() => {
    const scope = {};

    for (let i = 0; i < bindings.length; i++) {
      scope[first(bindings[i])] = last(bindings[i]);
    }

    return f(scope);
  })();
}

/**
 * evaluates expressions from left to right and returns the first
 * argument that is falsy or the last arg if all args are truthy
 *
 * @func
 * @param {...*}
 * @return {*}
 * @example
 *
 * and(true, true);
 * // => true
 *
 * and([], []);
 * // => []
 *
 * and(0, 1);
 * // => 1
 *
 */
export function and(...args) {
  for (let i = 0; i < args.length; i++) {
    if (args[i] === null || args[i] === undefined || args[i] === false) {
      return args[i];
    }
  }

  return args[args.length - 1];
}

/**
 * returns the first argument that is truthy or the last argument
 * if all arguments are falsy
 *
 * @func
 * @param {...*}
 * @return {*}
 * @example
 *
 * or(true, false, false);
 * // => true
 *
 * or(null, null);
 * // => null
 *
 * or(false, 42);
 * // => 42
 *
 */
export function or(...args) {
  for (let i = 0; i < args.length; i++) {
    if (args[i] !== false && args[i] !== undefined && args[i] !== null) {
      return args[i];
    }
  }

  return args[args.length - 1];
}

/**
 * returns true if x is a number
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isNumber(1);
 * // => true
 *
 * isNumber("1");
 * // => false
 *
 * isNumber(+"1");
 * // => true
 *
 */
export function isNumber(x) {
  return typeof x === "number";
}

/**
 * returns true if x is an integer
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isInteger(1);
 * // => true
 *
 * isInteger(1.0);
 * // => true
 *
 * isInteger(3.4);
 * // => false
 *
 */
export function isInteger(x) {
  return Number.isInteger(x);
}

/**
 * returns true if x is a number
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isFloat(1);
 * // => true
 *
 * isFloat("1");
 * // => false
 *
 * isFloat(+"1");
 * // => true
 *
 */
export function isFloat(x) {
  return isNumber(x);
}

/**
 * returns true if x is a string
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isString(5);
 * // => false
 *
 * isString(true);
 * // => false
 *
 */
export function isString(x) {
  return typeof x === "string";
}

/**
 * returns true if x is a Map
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isMap([]);
 * // => false
 *
 * isMap(new Map());
 * // => true
 *
 * isMap({});
 * // => false
 *
 */
export function isMap(x) {
  return typeConst(x) === MAP_TYPE;
}

/**
 * returns true if x is an Object
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isObject([]);
 * // => false
 *
 * isObject(list());
 * // => false
 *
 * isObject({a: 2});
 * // => true
 *
 */
export function isObject(x) {
  return typeConst(x) === OBJECT_TYPE;
}

/**
 * returns true if x is a Set
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isSet("abc");
 * false
 *
 * isSet({});
 * false
 *
 * isSet(set());
 * true
 *
 */
export function isSet(x) {
  return typeConst(x) === SET_TYPE;
}

/**
 * returns true if x is a function
 *
 * @func
 * @param {*}
 * @return {boolean}
 * @example
 *
 * isFn(() => 1 + 1);
 * // => true
 *
 * isFn(isNil);
 * // => true
 *
 * isFn(1);
 * // => false
 *
 */
export function isFn(x) {
  return typePrimitive(x) === FUNCTION_TYPE;
}

/**
 * Internal function
 * _eq() is a helper for eq()
 *
 * @private
 *
 */
function _eq(x, y) {
  if (typeof y === "undefined") {
    return true;
  }

  if (isInstance(Set, x) && isInstance(Set, y)) {
    if (x.size !== y.size) {
      return false;
    }

    for (let v of x.values()) {
      if (!y.has(v)) {
        return false;
      }
    }

    return true;
  }

  if (isInstance(Map, x) && isInstance(Map, y)) {
    if (x.size !== y.size) {
      return false;
    }

    for (let [k, v] of x.entries()) {
      if (!y.has(k) || !eq(v, y.get(k))) {
        return false;
      }
    }

    return true;
  }

  if (isInstance(List, x) || isInstance(List, y)) {
    try {
      return eq([...x], [...y]);
    } catch (error) {
      return false;
    }
  }

  if (isInstance(Array, x) && isInstance(Array, y)) {
    if (x.length !== y.length) {
      return false;
    }

    for (let i = 0; i < x.length; i++) {
      try {
        if (!eq(x[i], y[i])) {
          return false;
        }
      } catch (error) {
        return false;
      }
    }

    return true;
  }

  if (isInstance(Object, x) && isInstance(Object, y)) {
    let xk = Object.keys(x);
    let yk = Object.keys(y);

    if (xk.length !== yk.length) {
      return false;
    }

    for (let i = 0; i < xk.length; i++) {
      let k = xk[i];

      try {
        if (!eq(x[k], y[k])) {
          return false;
        }
      } catch (error) {
        return false;
      }
    }

    return true;
  }

  return x === y;
}

/**
 * compares x, y and args. Allows comparison of nested arrays and objects
 *
 * @func
 * @param {*}
 * @param {*}
 * @param {...*}
 * @return {boolean}
 * @example
 *
 * eq(5);
 * // => true
 *
 * eq(1, 2);
 * // => false
 *
 * eq(1, 1, 1);
 * // => true
 *
 * eq([1, 2], [1, 2], [1, 2]);
 * // => true
 *
 * eq([1, 2, [3, 4, [{a: "b"}]]], [1, 2, [3, 4, [{a: "b"}]]]);
 * // => true
 *
 */
export function eq(x, y, ...args) {
  if (not(isEmpty(args))) {
    let compare = [x, y, ...args];
    let firstv = first(compare);

    for (let i = 0; i < compare.length; i++) {
      if (!_eq(firstv, compare[i])) {
        return false;
      }
    }

    return true;
  }

  return _eq(x, y);
}

/**
 * the same as not(eq(x, y))
 *
 * @func
 * @param {*}
 * @param {*}
 * @param {...*}
 * @return {boolean}
 * @example
 *
 * notEq(1);
 * // => false
 *
 * notEq(1, 2);
 * // => true
 *
 * notEq([1, 2], [3, 4]);
 * // => true
 *
 */
export function notEq(x, y, ...args) {
  return not(eq(x, y, ...args));
}

/**
 * coerces x to an integer
 *
 * @func
 * @param {string|number}
 * @return {number}
 * @example
 *
 * int(1);
 * // => 1
 *
 * int(1.2);
 * // => 1
 *
 */
export function int(x) {
  return parseInt(x);
}

/**
 * returns a Symbol with the given name
 *
 * @func
 * @param {string}
 * @return {Symbol}
 * @example
 *
 * symbol("foo");
 * Symbol(foo)
 *
 */
export function symbol(name) {
  return Symbol(name);
}

/**
 * returns the name of a Symbol
 *
 * @func
 * @param {Symbol}
 * @return {string}
 * @example
 *
 * name(symbol("foo"));
 * // => "foo"
 *
 */
export function name(symbol) {
  return symbol.toString().slice(7, -1);
}

/**
 * returns function which returns true if all predicates are true, false otherwise
 *
 * @func
 * @param {...function}
 * @return {function}
 * @example
 *
 * everyPred(isNumber, isOdd)(3, 9, 1);
 * // true
 *
 * everyPred(isNumber, isEven)(3, 9, 1);
 * // false
 *
 * everyPred(isNumber, isOdd, isPos)(3, 9, 1);
 * // true
 *
 */
export function everyPred(...predicates) {
  return function (...args) {
    for (let i = 0; i < predicates.length; i++) {
      if (!predicates[i](...args)) {
        return false;
      }
    }
    return true;
  };
}

/**
 * returns a lazy sequence of successive matches of pattern in a string
 *
 * @func
 * @param {RegExp}
 * @param {string}
 * @return {LazyIterable}
 * @example
 *
 * [...reSeq(/\d/g, "test 5.9.2")];
 * // => ["5", "9", "2"]
 *
 * [...reSeq(/\w+/g, "this is the story all about how")];
 * // => ["this", "is", "the",  "story", "all",  "about", "how"]
 *
 * [...reSeq(/(\S+):(\d+)/g, " RX pkts:18 err:5 drop:48")];
 * // => [["pkts:18", "pkts", "18"], ["err:5", "err", "5"], ["drop:48", "drop", "48"]]
 *
 */
export function reSeq(pattern, string) {
  let match;

  return lazy(function* () {
    while ((match = pattern.exec(string))) {
      if (match.length === 1) {
        yield match[0];
      }

      if (match.length > 1) {
        yield match.map(function (match) {
          return match;
        });
      }
    }
  });
}

/**
 * coerces x to a float
 *
 * @func
 * @param {string|number}
 * @return {number}
 * @example
 *
 * float("1.5");
 * // => 1.5
 *
 * float(1);
 * // => 1
 *
 * float(1.4442);
 * // => 1.4442
 *
 */
export function float(x) {
  return parseFloat(x);
}

/**
 * returns the class of x
 *
 * @func
 * @param {*}
 * @return {Class}
 * @example
 *
 * classOf("hello");
 * // => [Function: String]
 *
 * classOf(false);
 * // => [Function: Boolean]
 *
 * classOf(1);
 * // => [Function: Number]
 *
 * classOf(function() {});
 * // => [Function: Function]
 *
 * classOf(new Map());
 * // => [Function: Map]
 *
 * classOf([]);
 * // => [Function: Array]
 *
 * classOf({});
 * // => [Function: Object]
 *
 * classOf(new List());
 * // => [class List extends Array]
 *
 * classOf(new Set);
 * // => [Function: Set]
 *
 * classOf(new LazyIterable);
 * // => [class LazyIterable]
 *
 */
export function classOf(x) {
  var a = typeConst(x);
  var b = typePrimitive(x);

  if (b === BOOLEAN_TYPE) {
    return Boolean;
  }

  if (b === NUMBER_TYPE) {
    return Number;
  }

  if (b === STRING_TYPE) {
    return String;
  }

  if (b === FUNCTION_TYPE) {
    return Function;
  }

  if (a === MAP_TYPE) {
    return Map;
  }

  if (a === ARRAY_TYPE) {
    return Array;
  }

  if (a === OBJECT_TYPE) {
    return Object;
  }

  if (a === LIST_TYPE) {
    return List;
  }

  if (a === SET_TYPE) {
    return Set;
  }

  if (a === LAZY_ITERABLE_TYPE) {
    return LazyIterable;
  }

  return null;
}

/**
 * returns a lazy sequence of x, f(x), f(f(x)) etc
 *
 * @func
 * @param {function}
 * @param {number}
 * @return {LazyIterable}
 * @example
 *
 * [...take(3, iterate(inc, 5))];
 * // => [5, 6, 7]
 *
 * [...take(10, iterate(partial(plus, 2), 0))];
 * // => [0,  2,  4,  6,  8, 10, 12, 14, 16, 18]
 *
 * [...take(20, iterate(partial(plus, 2), 0))];
 * // => [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38]
 *
 * var powersOfTwo = iterate(partial(multiply, 2), 1);
 * nth(powersOfTwo, 10);
 * // => 1024
 *
 * [...take(10, powersOfTwo)];
 * // => [1,  2, 4, 8, 16, 32, 64, 128, 256, 512]
 *
 * var fib = map(first, iterate(([a, b]) => [b, a + b], [0, 1]));
 * [...take(10, fib)];
 * // => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
 *
 */
export function iterate(f, x) {
  return lazy(function* () {
    let result = x;

    while (true) {
      yield result;
      result = f(result);
    }
  });
}

/**
 * Returns the greatest of the numbers
 *
 * @func
 * @param {...number}
 * @return {number}
 * @example
 *
 * max(1, 2, 3, 4, 5);
 * // => 5
 *
 * apply(max, [1, 2, 3, 4, 3]);
 * // => 4
 *
 */
export function max(...args) {
  let val = args[0];

  for (let i = 0; i < args.length; i++) {
    if (args[i] > val) {
      val = args[i];
    }
  }

  return val;
}

/**
 * Returns the least of the numbers
 *
 * @func
 * @param {...number}
 * @return {number}
 * @example
 *
 * min(1, 2, 3, 4, 5);
 * // => 1
 *
 * min(5, 4, 3, 2, 1);
 * // => 1
 *
 */
export function min(...args) {
  let val = args[0];

  for (let i = 0; i < args.length; i++) {
    if (args[i] < val) {
      val = args[i];
    }
  }

  return val;
}

/**
 * returns an object with keys mapped to corresponding values
 *
 * @func
 * @param {Array}
 * @param {Array}
 * @return {Object}
 * @example
 *
 * zipmap(["a", "b", "c"], [1, 2, 3]);
 * // => {a: 1, b: 2, c: 3}
 *
 */
export function zipmap(k, v) {
  const result = {};
  const length = Math.min(k.length, v.length);

  for (let i = 0; i < length; i++) {
    result[k[i]] = v[i];
  }

  return result;
}