import { tag } from "./helpers.js"
import { _Set, Vec } from "./classes.js"

/** @namespace */
const type_checks = {}

/** 
 * **[Global]** Checks if a value is a number or a big int
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isnum = tag({ keep_global: true }, x => typeof x === "number" || typeof x === "bigint")

/** 
 * **[Global]** Same as the built in [Number.isInteger](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger)
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isint = tag({ keep_global: true }, Number.isInteger)

/** 
 * **[Global]** Checks if a value is a string
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isstr = tag({ keep_global: true }, x => typeof x === "string")

/** 
 * **[Global]** Checks if a value is an array (alias for Array.isArray)
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
// Can't use Array.isArray directly because the regex won't pick up on it being niladic
type_checks.isarr = tag({ keep_global: true }, x => Array.isArray(x))

/** 
 * **[Global]** Checks if a value is a boolean
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isbool = tag({ keep_global: true }, x => typeof x === "boolean")

/** 
 * **[Global]** Checks if a value is a function
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isfunc = tag({ keep_global: true }, x => typeof x === "function")

/**
 * **[Global]** Checks if a value is an object
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isobj = tag({ keep_global: true }, x => typeof x === "object")

/** 
 * **[Global]** Gets the type of a value using typeof
 * @function
 * @param {any} value Value to get the type of
 * @returns {boolean}
 */
type_checks.type = tag({ keep_global: true }, x => typeof x)

/** 
 * **[Global]** Checks if a type is iterable
 * @function
 * @param {any} value Value to check
 * @returns {boolean}
 */
type_checks.isiter = tag({ keep_global: true }, x => typeof x[Symbol.iterator] === "function")

/** @namespace */
const type_casts = {}

/**
 * Casts a value to a number using the `Number` constructor. If `value` is a string
 * then a number will be extracted with regex before casting.
 * @param {any} value
 * @returns {number}
 */
type_casts.num = value => {
	if (type_checks.isstr(value)) {
		const match = value.match(/-?\d+(\.\d+)?/)[0]
		return match ? Number(match) : null
	}

	return Number(value)
}

/**
 * Casts a value to an integer using parseInt. If `value` is a string an int
 * will be extracted with regex before casting).
 * @param {any} value Value to cast
 * @returns {number}
 */
type_casts.int = value => parseInt(type_casts.num(value))

/**
 * Casts a value to a string. If `value` is an array, it will be joined by "".
 * @param {any} value Value to cast
 */
type_casts.str = value => {
	const str = value?.toString()

	if (str === null) return "null"
	if (str === undefined) return "undefined"
	if (value instanceof Vec)
		return `Vec(${value.x}, ${value.y})`
	if (type_checks.isarr(value))
		return value.join("")
	if (type_checks.isobj(value))
		return str === "[object Object]" ? JSON.stringify(value) : str

	return str
}

/**
 * Casts a value to an array. If `value` is a number, an array of length `value`
 * will be created
 * @param {any} value Value to cast
 * @returns {Array}
 */
type_casts.arr = value => {
	if (type_checks.isnum(value)) return Array(value)
	if (type_checks.isiter(value)) return [...value]
	return Array.from(value)
}

/**
 * Casts a value to a boolean (alias for Boolean constructor)
 * @param {any} value Value to cast
 * @returns {boolean}
 */
type_casts.bool = value => Boolean(value)

/** @namespace */
const math_utils = {}

/** 
 * Casts `a` and `b` using `Number` constructor, then adds them using `+` operator
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
math_utils.add = (a, b) => Number(a) + Number(b)

/** 
 * Casts `a` and `b` using `Number` constructor, then subtracts them using `-` operator
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
math_utils.sub = (a, b) => Number(a) - Number(b)

/** 
 * Casts `a` and `b` using `Number` constructor, then multiplies them using `*` operator
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
math_utils.mul = (a, b) => Number(a) * Number(b)

/** 
 * Casts `a` and `b` using `Number` constructor, then divides them using `/` operator
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
math_utils.div = (a, b) => Number(a) / Number(b)

/** 
 * Casts `a` and `b` using `Number` constructor, then calculates `a % b` 
 * (note: this is not true modulo, just remainder after division)
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
math_utils.mod = (a, b) => Number(a) % Number(b)

/** 
 * Calculates `b ** m` (optionally modulo `m`)
 * @param {number} b
 * @param {number} e
 * @param {number} [m]
 * @return {number}
 */
math_utils.exp = (b, e, m = null) => {
	if (m) {
		// naive solution but it does the job
		if (m == 1) return 0
		let ret = 1
		for (let i = 0; i < e; i++)
			ret = (ret * b) % m
		return ret
	} else return b ** e
}

/**
 * Returns the absolute difference between `a` and `b`
 * @param {number} a
 * @param {number} b
 * @return {number}
 */
math_utils.absdiff = (a, b) => Math.abs(a - b)

/**
 * Checks if `a` is greater than `b`
 * @param {number} a
 * @param {number} b
 * @returns {boolean}
 */
math_utils.gt = (a, b) => a > b

/**
 * Checks if `a` is less than `b`
 * @param {number} a
 * @param {number} b
 * @returns {boolean}
 */
math_utils.lt = (a, b) => a < b

/**
 * Checks if `a` is greater than or equal to `b`
 * @param {number} a
 * @param {number} b
 * @returns {boolean}
 */
math_utils.gte = (a, b) => a >= b

/**
 * Checks if `a` is less than or equal to `b`
 * @param {number} a
 * @param {number} b
 * @returns {boolean}
 */
math_utils.lte = (a, b) => a <= b

/**
 * Checks if `n` is odd
 * @param {number} n
 * @returns {boolean}
 */
math_utils.odd = n => n % 2 === 1

/**
 * Checks if `n` is even
 * @param {number} n
 * @returns {boolean}
 */
math_utils.even = n => n % 2 === 0

/**
 * Calculates the absolute value of `n`
 * @param {number} n
 * @returns {number}
 */
math_utils.abs = n => Math.abs(n)

/**
 * Calculates the square root of `n`
 * @param {number} n
 * @returns {number}
 */
math_utils.sqrt = n => Math.sqrt(n)

/**
 * Calculates the sign of `n`
 * @param {number} n
 * @returns {number}
 */
math_utils.sign = n => Math.sign(n)

/**
 * Rounds `n` down to the nearest integer
 * @param {number} n
 * @returns {number}
 */
math_utils.floor = n => Math.floor(n)

/**
 * Rounds `n` up to the nearest integer
 * @param {number} n
 * @returns {number}
 */
math_utils.ceil = n => Math.ceil(n)

/**
 * Rounds `n` to the nearest integer
 * @param {number} n
 * @returns {number}
 */
math_utils.round = n => Math.round(n)

/**
 * Wraps `index` around `length` (for indexing into to arrays, strings, etc.)
 * @param {number} index
 * @param {number} length
 * @returns {number}
 */
math_utils.wrapindex = (index, length) => (index % length + length) % length

/**
 * Clamps `n` between `min` and `max`
 * @param {number} n
 * @param {number} min
 * @param {number} max
 * @returns {number}
 */
math_utils.clamp = (n, min, max) => Math.min(Math.max(n, min), max)

/**
 * Checks if `n` is in the range [`min`, `max`] (inclusive)
 * @param {number} n
 * @param {number} min
 * @param {number} max
 * @returns {boolean}
 */
math_utils.inrange = (n, min, max) => n >= min && n <= max

/**
 * Calculates the minimum of an iterable using the builtin [Math.min](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min)
 * @param {Iterable.<*>} iter The iterable to find the minimum of
 * @returns {number}
 * @example min([2, 1, 3]) // 1
 */
math_utils.min = iter => Math.min(...iter)


/**
 * Calculates the maximum of an iterable using the builtin [Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max)
 * @param {Iterable.<*>} iter The iterable to find the maximum of
 * @returns {number}
 * @example max([2, 1, 3]) // 3
 */
math_utils.max = iter => Math.max(...iter)

/**
 * Functions which operate on iterable types
 * @namespace
 */
const iter_utils = {}

/**
 * Gets the first `n` elements of an iterable value and returns them in an array
 * @template T
 * @param {Iterable.<T>} iter The iterable to get elements from
 * @param {number} n The number of elements to get
 * @returns {T[]}
 */
iter_utils.firstn = (iter, n) => [...iter].slice(0, n)

/**
 * Gets the last `n` elements of an iterable value and returns them in an array
 * @template T
 * @param {Iterable.<T>} iter The iterable to get elements from
 * @param {number} n The number of elements to get
 * @returns {T[]}
 */
iter_utils.lastn = (iter, n) => !n ? [] : [...iter].slice(-n)

/**
 * Gets the first element of an iterable value
 * @param {Iterable.<*>} iter The iterable to get the first element from
 * @returns {any}
 */
iter_utils.first = iter => [...iter][0]

/**
 * Gets the last element of an iterable value
 * @param {Iterable.<*>} iter The iterable to get the last element from
 * @returns {any}
 */
iter_utils.last = iter => {
	const array = [...iter]
	return array[array.length - 1]
}

/**
 * Removes the nth element from an iterable value
 * @template T
 * @param {Iterable.<T>} iter The iterable to remove an element from
 * @param {number} index The index of the element to remove
 * @returns {T[]} The resulting array
 */
iter_utils.dropnth = (iter, index) => {
	const array = [...iter]
	array.splice(math_utils.wrapindex(index, array.length), 1)
	return array
};

/**
 * Sums the elements of an iterable value
 * @param {Iterable.<*>} iter The iterable to sum
 * @returns {number|string}
 */
iter_utils.sum = iter => [...iter].reduce((acc, cur) => acc + cur)

/**
 * Calculates the product of all values in an iterable
 * @param {Iterable.<*>} iter The iterable
 * @returns {number}
 */
iter_utils.prod = iter => [...iter].reduce((acc, cur) => acc * cur)

/**
 * Reverses the elements of an iterable (not in place)
 * @template T
 * @param {Iterable.<T>} iter The iterable to reverse
 * @returns {T[]} The reversed array
 */
iter_utils.rev = iter => [...iter].reverse()

/**
 * **[Global]** Zips multiple iterable into a single array of arrays, each
 * containing a single element from each iterable. The result will only be as
 * long as the shortest input.
 * @template T
 * @function
 * @param {...Iterable.<T[]>} iters The iterables to zip
 * @returns {T[][]}
 * @example zip([1, 2, 3], [4, 5, 6]) // [[1, 4], [2, 5], [3, 6]]
 * @example zip([1, 2]) // [[1], [2]]
 */
iter_utils.zip = tag({ keep_global: true }, (...iters) => {
	if (!iters.length) return [];

	const smallest = Math.min(...iters.map(x => x.length))
	const ret = [];

	for (let i = 0; i < smallest; i++) {
		ret.push(iters.map(x => x[i]));
	}

	return ret;
})

/**
 * Creates a new {@link _Set} from an iterable. Same as {@link constructors.set}.
 * @param {Iterable.<*>} iter The iterable to create a set from
 * @returns {_Set}
 */
iter_utils.uniq = iter => new _Set(iter)

/**
 * Sorts an iterable given a comparison function (same as [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort))
 * but not in place
 * @template T
 * @param {Iterable.<T>} iter The iterable to sort
 * @param {function(T, T): number} compare_fn The comparison function
 * @returns {T[]} The sorted array
 */
iter_utils.sorted = (iter, compare_fn) => [...iter].sort(compare_fn)

/**
 * Sorts an iterable (of numbers) in ascending order
 * @template T
 * @param {Iterable.<T>} iter The iterable to sort
 * @returns {T[]} The sorted array
 */
iter_utils.nsort = iter => [...iter].sort((a, b) => a - b)

/**
 * Sorts an iterable (of numbers) in descending order
 * @param {Iterable.<*>} iter The iterable to sort
 * @returns {T[]} The sorted array
 */
iter_utils.rsort = iter => [...iter].sort((a, b) => b - a)

/**
 * Sorts an iterable in ascending order by the result of applying a given
 * function to each element
 * @template T
 * @param {Iterable.<T>} iter The iterable to sort
 * @param {function(T)} func The function to apply to each element
 * @returns {T[]} The sorted array
 */
iter_utils.sortby = (iter, func) => iter.sort((a, b) => func(a) - func(b))

/**
 * Sorts an iterable in descending order by the result of applying a given
 * function to each element
 * @template T
 * @param {Iterable.<T>} iter The iterable to sort
 * @param {function(T)} func The function to apply to each element
 * @returns {T[]} The sorted array
 */
iter_utils.rsortby = (iter, func) => iter.sort((a, b) => func(b) - func(a))

/** 
 * Same as the built in [Array.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
 * @template T
 * @param {Iterable.<T>} iter The iterable to iterate over
 * @param {function(T, number, Iterable.<T>)} callback The callback to call for each element (see the MDN link for more info)
 * @returns {Iterable.<T>} The original iterable
 */
iter_utils.for = (iter, callback) => {
	[...iter].forEach(callback)
	return iter
}

/** 
 * Same as the built in [Array.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)
 * @function
 * @template T
 * @param {Iterable.<T>} iter The iterable to iterate over
 * @param {function(T, number, Iterable.<T>)} callback The callback to call for each element (see the MDN link for more info)
 * @returns {Iterable.<T>} The original iterable
 */
iter_utils.each = iter_utils.for

/**
 * Same as the built in [Math.min](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min)
 * but it takes an iterable as a single argument
 * @param {Iterable.<*>} iter The iterable to find the minimum of
 * @returns {number}
 */
iter_utils.min = iter => Math.min(...iter)

/**
 * Same as the built in [Math.max](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max)
 * but it takes an iterable as a single argument
 * @param {Iterable.<*>} iter The iterable to find the maximum of
 * @returns {number}
 */
iter_utils.max = iter => Math.max(...iter)

/**
 * Same as the built in [Array.push](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push)
 * @template T
 * @param {T[]} array The array to append to
 * @param {...T} items The items to append
 * @returns {T[]} The resulting array
 */
iter_utils.append = (array, ...items) => (array.push(...items), array)

/**
 * Same as the built in [Array.unshift](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift)
 * @template T
 * @param {T[]} array The array to prepend to
 * @param {...T} items The items to prepend
 * @returns {T[]} The resulting array
 */
iter_utils.prepend = (array, ...items) => (array.unshift(...items), array)

/**
 * Checks if all items in an iterable are equal
 * @param {Iterable.<*>} iter The iterable to check
 * @returns {boolean}
 */
iter_utils.alleq = iter => {
	const array = [...iter]
	return array.every(item => misc_utils.eq(item, array[0]))
}

/**
 * Creates a list of sublists of length `size` each of which is a sliding
 * window looking into `iter` starting at index `0` moving along by 1 each
 * time. If `loop` is true, there will be some extra windows at the end which
 * wrap around to the start.
 * @template T
 * @param {Iterable.<T[]>} iter The iterable to create sliding windows from
 * @param {number} size The size of the sliding window
 * @param {boolean} [wrap=false] Whether to include windows that wrap around
 * @returns {T[][]}
 * @example [1, 2, 3, 4, 5].sliding(3) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
 * @example [1, 2, 3].sliding(2, true) // [[1, 2], [2, 3], [3, 1]]
 */
iter_utils.sliding = (iter, size = 2, wrap = false) => {
	const array = [...iter]
	if (array.length < size) return []
	const x = wrap ? array.concat(array.slice(0, size - 1)) : array
	return x.slice(0, -size + 1).map((_, i) => x.slice(i, i + size))
}

/**
 * Creates a list of contiguous non-overlapping chunks of `iter`, each of
 * length `size`
 * @template T
 * @param {Iterable.<*>} iter The iterable to create chunks from
 * @param {number} [size=2] The size of each chunk
 * @returns {T[][]}
 * @example [1, 2, 3, 4, 5, 6].chunks() // [[1, 2], [3, 4], [5, 6]]
 * @example [1, 2, 3, 4, 5, 6].chunks(3) // [[1, 2, 3], [4, 5, 6]]
 */
iter_utils.chunks = (iter, size = 2) => {
	const ret = []
	for (let i = 0; i < iter.length; i += size) {
		ret.push(iter.slice(i, i + size))
	}
	return ret
}

/**
 * Swaps the elements at indices `index_a` and `index_b` in `array`. Only works
 * on arrays so that it can be done in place.
 * @template T
 * @param {T[]} array The array to swap elements in
 * @param {number} index_a The index of the first element to swap
 * @param {number} index_b The index of the second element to swap
 * @returns {T[]} The resulting array
 */
iter_utils.swap = (array, index_a, index_b) => {
	[array[index_a], array[index_b]] = [array[index_b], array[index_a]]
	return array
}

/**
 * Generates all permutations of an iterable
 * @template T
 * @param {Iterable.<T>} iter
 * @returns {T[][]}
 */
iter_utils.perms = iter => {
	const array = [...iter]
	if (!array.length) return []

	const output = []

	const generate = (k, array) => {
		if (k === 1) {
			output.push(array.slice())
			return
		}

		generate(k - 1, array)

		for (let i = 0; i < k - 1; i++) {
			if (k % 2 === 0) iter_utils.swap(array, i, k - 1)
			else iter_utils.swap(array, 0, k - 1)
			generate(k - 1, array)
		}
	}

	generate(array.length, array)
	return output
}

/**
 * Return a list with every `n`th element of `iter` starting at index `start`
 * @template T
 * @param {Iterable.<T>} iter The iterable to get elements from
 * @param {number} n The step size
 * @param {number} [start=0] The starting index
 * @returns {T[]}
 */
iter_utils.everynth = (iter, n, start = 0) => {
	const ret = [];
	for (let i = start; i < iter.length; i += n)
		ret.push(iter[i])
	return ret
}

/**
 * Gets the element at `index` in `iter`. Indices wrap around using
 * {@link math_utils.wrapindex}. If `index` is a {@link Vec}, `iter` will be
 * treated as a grid, or if `index` is a list of integers, a list of elements
 * at those indices will be returned.
 * @function
 * @template T
 * @param {Iterable.<T|Iterable.<T>>} iter The iterable to get elements from
 * @param {number|Vec|number[]} index The index/indicies to get
 * @returns {T}
 */
iter_utils.at = tag({ attach_to: [Array] }, (iter, index) => {
	const array = [...iter]

	if (type_checks.isint(index)) {
		return array[math_utils.wrapindex(index, array.length)];
	} else if (index instanceof Vec) {
		return iter?.[index.y]?.[index.x]
	} else if (type_checks.isarr(index) && index.every(type_checks.isint)) {
		return index.map(i => iter_utils.at(array, i));
	} else {
		throw new Error(`Invalid index/indicies ${JSON.stringify(index)}`);
	}
})

const count = (iter, value, string_like_match = false, overlapping = false) => {
	const array = [...iter]
	if (typeof value === "function") return array.filter(value).length

	string_like_match = type_checks.isstr(iter) || string_like_match

	if (type_checks.isiter(value) && string_like_match) {
		const to_find = [...value]
		let count = 0

		for (let i = 0; i < array.length;) {
			if (misc_utils.eq(array.slice(i, i + to_find.length), to_find)) {
				count++
				i += overlapping ? 1 : to_find.length
			} else i++
		}

		return count
	}

	return array.filter(x => misc_utils.eq(x, value)).length
}

/**
 * Counts the number of times `value` appears in `iter`. If `value` is a
 * function, it's will do the same as `.filter(f).length`, if `iter` is a
 * string or `string_like_match` is explicitly set to `true`, `["a", "b"]` will
 * match `abcd` or ["a", "b", "c", "d"] as well as something like 
 * ["arstneio", ["a", "b], 4].
 * @param {Iterable.<*>} iter The iterable to count values in
 * @param {any} value The value to count
 * @param {boolean} [string_like_match=false] (See description)
 * @returns {number}
 * @example [4, 2, 3, 1, 2].count(2) // 2
 * @example "abababa".count("aba") // 2
 * @example [3, 2, 1, 2, 1].count([2, 1]) // 0
 * @example [3, 2, 1, 2, 1].count([2, 1], true) // 2
 */
iter_utils.count = (...args) => count(...args)

/**
 * Same as `iter_utils.count` but counts overlapping matches
 * @param {Iterable.<*>} iter The iterable to count values in
 * @param {any} value The value to count
 * @param {boolean} [string_like_match=false] (See description)
 * @returns {number}
 * @example "abababa".countol("aba") // 3
 * @example [1, 2, 1, 2, 1].countol([1, 2, 1]) // 2
 */
iter_utils.countol = (iter, value, string_like_match = false) => count(iter, value, string_like_match, true)

/**
 * Same as the built in [Array.filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)
 * but inverted. For consistency with builtins like `Array.filter` and `Array.map` which _don't_ work on any iterable. This must also be called on an array.
 * @template T
 * @param {T[]} array The iterable to filter
 * @param {function(T): boolean} filter_fn The filter function
 * @returns {T[]}
 */
iter_utils.filterout = (array, filter_fn) => array.filter((...args) => !filter_fn(...args))

/**
 * Checks if there are no elements in `array` that satisfy `fn`. `fn` is passed
 * to the built in [Array.some](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some)
 * so refer to those docs if needed.
 * @template T
 * @param {T[]} array The array to check
 * @param {function(T, number, T[]): boolean} fn The function to check
 * @returns {boolean}
 */
iter_utils.none = (array, fn) => !array.some(fn)

/**
 * Gets the differences between consecutive elements of an iterable
 * @param {Iterable.<number>} iter The iterable to get deltas from
 * @returns {number[]}
 * @example [1, 2, 9, -4].deltas // [1, 7, -13]
 */
iter_utils.deltas = iter => {
	const array = [...iter];

	if (array.length <= 1) return [];

	const ret = [];
	for (let i = 0; i < array.length - 1; i++) {
		ret.push(array[i + 1] - array[i]);
	}
	return ret;
}

/**
 * Splits an iterable into `n` pieces. Rounds up if the split is uneven.
 * @template T
 * @param {Iterable.<T>} iter The iterable to split
 * @param {number} n The number of pieces to split into
 * @returns {T[][]}
 * @example [1, 2, 3, 4, 5, 6].pieces(3) // [[1, 2], [3, 4], [5, 6]]
 * @example [1, 2, 3].pieces(2) // [[1, 2], [3]]
 */
iter_utils.pieces = (iter, n) => {
	if (n <= 0) return []
	const array = [...iter], ret = [], step = Math.ceil(array.length / n);
	for (let i = 0; i < array.length; i += step) {
		ret.push(array.slice(i, i + step))
	}
	return ret
}

/**
 * Returns list of indices where the function `fn` returns true. `fn` receives
 * the value, index and the iterable as arguments, in that order.
 * @template T
 * @param {Iterable.<T>} iter
 * @param {function(T, number, Iterable.<T>): boolean} fn
 * @returns {number[]}
 * @example [1, 2, 3, 4, 5].iwhere(x => x % 2 === 0) // [1, 3]
 */
iter_utils.iwhere = (iter, fn) => [...iter]
	.map((value, index) => [value, index])
	.filter(([value, index]) => fn(value, index, iter))
	.map(([, index]) => index)

/**
 * Returns a list of sublists containing consecutive elements of `iter`
 * @template T
 * @param {Iterable.<T>} iter The iterable to group
 * @returns {T[][]}
 */
iter_utils.seqs = iter => {
	const ret = []
	for (const value of iter) {
		if (ret[ret.length - 1]?.[ret[ret.length - 1].length - 1] === value) {
			ret[ret.length - 1].push(value)
		} else {
			ret.push([value])
		}
	}
	return ret
}

/**
 * Checks if an iterable includes a value. Uses {@link iter_utils.count}.
 * @template T
 * @param {Iterable.<T>} iter The iterable to check
 * @param {T} value The value to check for
 * @param {number} [start_index=0] The index to start checking from
 * @returns {boolean}
 */
iter_utils.includes = tag({ attach_to: [Array] }, (iter, value, start_index) => {
	if (typeof iter === "string") return iter.includes(value, start_index)
	return [...iter].filter(x => misc_utils.eq(x, value)).length > 0
})


/** Alias for {@link iter_utils.includes}
 * @template T
 * @param {Iterable.<T>} iter The iterable to check
 * @param {T} value The value to check for
 * @param {number} [start_index=0] The index to start checking from
 * @returns {boolean}
 */
iter_utils.incl = iter_utils.includes

/**
 * Deep flattens an iterable
 * @template T
 * @param {Iterable.<T>} iter The iterable to flatten
 * @returns {T[]}
 * @example [[1, [2]], [[[[3, [4, 5]]]]], 6].flat // [1, 2, 3, 4, 5, 6]
 */
iter_utils.flat = iter => [...iter].flat(Infinity)

/**
 * Flattens an iterable to a certain depth. Alias for the builtin [Array.flat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat).
 * @template T
 * @param {Iterable.<T>} iter The iterable to flatten
 * @param {number} [depth=1] The depth to flatten to
 * @returns {T[]}
 */
iter_utils.nflat = (iter, depth = 1) => [...iter].flat(depth)

/** @namespace */
const string_utils = {}

/**
 * Splits a string into an array of characters
 * @param {string} str The string to split
 * @returns {string[]}
 */
string_utils.chars = str => str.split("")

/**
 * Splits a string on spaces and trims each word
 * @param {string} str The string to split
 * @returns {string[]}
 */
string_utils.words = str => str.split(" ").map(s => s.trim())

/**
 * Splits a string on "\n"
 * @param {string} str The string to split
 * @returns {string[]}
 */
string_utils.lines = str => str.split("\n")

/**
 * Splits a string on "\n\n"
 * @param {string} str The string to split
 * @returns {string[]}
 */
string_utils.groups = str => str.split("\n\n")

/**
 * Splits a string on commas and trims each element
 * @param {string} str The string to split
 * @returns {string[]}
 */
string_utils.csv = str => str.split(",").map(s => s.trim())

/**
 * Checks if a character is a digit
 * @param {string} char The character to check
 * @returns {boolean}
 */
string_utils.isdigit = char => /^\d$/.test(char)

/**
 * Extracts all integers from a string and returns them in an array
 * @param {string} str The string to extract integers from
 * @returns {number[]}
 */
string_utils.ints = str => str.match(/-?\d+/g).map(type_casts.int)

/**
 * Extracts all numbers from a string and returns them in an array
 * @param {string} str The string to extract numbers from
 * @returns {number[]}
 */
string_utils.nums = str => str.match(/\d+(\.\d+)?/g).map(type_casts.num)

/**
 * Gets the ASCII code a character
 * @param {string} char The character to get the code of
 * @returns {number}
 */
string_utils.chcode = char => char.charCodeAt(0)

/**
 * Converts a string to upper case
 * @param {string} str The string to convert
 * @returns {string}
 */
string_utils.upper = str => str.toUpperCase()

/**
 * Converts a string to lower case
 * @param {string} str The string to convert
 * @returns {string}
 */
string_utils.lower = str => str.toLowerCase()

/** @namespace */
const logic_utils = {}

/**
 * Returns `a && b`. I.e. if `a` is truthy it returns `b`, otherwise it short
 * circuits and returns `a`
 * @param {any} a
 * @param {any} b
 * @returns {any}
 */
logic_utils.and = (a, b) => a && b

/**
 * Returns `a || b`. I.e. if `a` is truthy it short circuits and returns `a`,
 * otherwise it returns `b`
 * @param {any} a
 * @param {any} b
 * @returns {any}
 */
logic_utils.or = (a, b) => a || b

/**
 * Returns logical NOT of `a`
 * @param {any} a
 * @returns {boolean}
 */
logic_utils.not = a => !a

/**
 * Returns logical XOR of `a` and `b`. I.e. returns true if exactly one of `a`
 * and `b` are truthy, otherwise returns false
 * @param {any} a
 * @param {any} b
 * @returns {boolean}
 */
logic_utils.xor = (a, b) => !!(!a ^ !b)

/** @namespace */
const grid_utils = {}

/**
 * Creates a grid from a string where each row is separated by a newline and
 * each cell is seperated by `delim`.
 * @param {string} str The string to create a grid from
 * @param {string} delim The delimiter to split cells by
 * @returns {string[][]}
 */
grid_utils.gridd = (str, delim) => str.trim().split("\n").filter(x => x).map(row => row.split(delim).filter(x => x))

/**
 * Calls with {@link grid_utils.gridd} with an empty delimiter
 * @param {string} str The string to create a grid from
 * @returns {string[][]}
 */
grid_utils.grid = str => grid_utils.gridd(str, "")

/**
 * Uses {@link grid_utils.grid} then maps each cell to a number
 * @param {string} str The string to create a grid from
 * @returns {number[][]}
 */
grid_utils.digitgrid = str => grid_utils.grid(str).map(row => row.map(cell => +cell))

/**
 * Maps a function over each cell in a grid
 * @template T
 * @template K
 * @param {T[][]} grid The grid to map over
 * @param {function(T, Vec, T[][]): K} fn The function to map
 * @returns {K[][]}
 */
grid_utils.gmap = (grid, fn) => grid.map((row, y) => row.map((cell, x) => fn(cell, new Vec(x, y), grid)))

/**
 * Finds {@link Vec}s in a grid which satisfy the given function
 * @template T
 * @param {T[][]} grid The grid to search
 * @param {function(T, Vec, T[][]): boolean} fn The function to filter with
 * @returns {Vec[]}
 */
grid_utils.vecswhere = (grid, fn) => grid.flatMap(
	(row, y) => row
		.map((cell, x) => [cell, new Vec(x, y)])
		.filter(args => fn(...args, grid))
		.map(([, vec]) => vec)
)

/**
 * Rotates a grid 90 degrees clockwise `times` times
 * @param {T[][]} grid The grid to rotate
 * @param {number} [times=1] The number of times to rotate
 * @returns {T[][]}
 */
grid_utils.nrotate = (grid, times = 1) => {
	if (!grid.length) return []
	let ret = grid.map(row => row.slice())
	for (let i = 0; i < times; i++) {
		ret = ret[0].map((_, i) => ret.map(row => row[i]).reverse())
	}
	return ret
}

/**
 * Rotates a grid 90 degrees clockwise once (alias for `grid_utils.nrotate(grid, 1)`)
 * @param {T[][]} grid The grid to rotate
 * @returns {T[][]}
 */
grid_utils.rotate = grid => grid_utils.nrotate(grid, 1)

/**
 * Gets diagonals of a grid (top right to bottom left). Assumes the grid is rectangular.
 * @param {T[][]} grid The grid to get diagonals from
 * @returns {T[][]}
 * @example diagonals([
 *     [1, 2, 3],
 *     [4, 5, 6],
 *     [7, 8, 9]
 * ]) // [[1], [2, 4], [3, 5, 7], [6, 8], [9]]
 */
grid_utils.diagonals = grid => {
	const ret = []

	for (let r = 0; r < grid.length; r++) {
		for (let c = 0; c < grid[0].length; c++) {
			ret[r + c] ??= []
			ret[r + c].push(grid[r][c])
		}
	}

	return ret
}

/**
 * Gets diagonals of a grid in the other direction to {@link grid_utils.diagonals}
 * (so top left to bottom right). Assumes the grid is rectangular.
 * @param {T[][]} grid The grid to get diagonals from
 * @returns {T[][]}
 * @example antidiagonals([
 *     [1, 2, 3],
 *     [4, 5, 6],
 *     [7, 8, 9]
 * ]) // [[3], [2, 6], [1, 5, 9], [4, 8], [7]]
 */
grid_utils.antidiagonals = grid => {
	const ret = [], width = grid[0]?.length

	for (let r = 0; r < grid.length; r++) {
		for (let c = 0; c < width; c++) {
			ret[r + (width - 1 - c)] ??= []
			ret[r + (width - 1 - c)].push(grid[r][c])
		}
	}

	return ret
}

/**
 * **[Global]** Creates a grid of size `width` by `height` filled with `fill`.
 * Fill defaults to `null`.
 * @template T
 * @param {number} width The width of the grid
 * @param {number} height The height of the grid
 * @param {T} [fill=null] The value to fill the grid with
 * @returns {T[][]}
 */
grid_utils.emptygrid = tag(
	{ keep_global: true },
	(width, height, fill = null) => Array(height).fill().map(() => Array(width).fill(fill))
)

/**
 * **[Global]** Prints a grid and returns it. If the shortest cell is only 1 character long
 * then the grid will be printed without spaces between cells.
 * @function
 * @template T
 * @param {T[][]} grid
 * @returns {T[][]}
 */
grid_utils.gprint = tag({ keep_global: true }, grid => {
	const longest_cell = Math.max(...grid.flatMap(
		row => row.map(cell => cell.toString().length)
	))

	console.log(grid.map(row => row.map(
		cell => cell.toString().padEnd(longest_cell)
	).join(longest_cell > 1 ? " " : "")).join("\n"))

	return grid
})

/**
 * Alias for {@link grid_utils.gprint}
 * @template T
 * @param {T[][]} grid
 * @returns {T[][]}
 */
grid_utils.gpr = grid => grid_utils.gprint(grid)

/**
 * Sets value at the given vector in a grid. If the cell doesn't exist, it will be created.
 * @template T
 * @param {T[][]} grid The grid to set the value in
 * @param {Vec} vec The vector to set the value at
 * @param {T} value The value to set
 * @returns {T[][]} The modified grid
 */
grid_utils.gset = (grid, vec, value) => {
	const row = grid[vec.y]
	if (!row) grid[vec.y] = []
	grid[vec.y][vec.x] = value
	return grid
}

/**
 * Returns the first vector in a grid (searching top to bottom, left to right)
 * where the function `fn` is truthy for that cell. `fn` also receives the
 * position of each cell and a reference to the grid.
 * @template T
 * @param {T[][]} grid The grid to search
 * @param {function(T, Vec, T[][]): boolean} fn The function to search with
 * @returns {Vec}
 */
grid_utils.vecwhere = (grid, fn) => {
	for (let y = 0; y < grid.length; y++) {
		for (let x = 0; x < grid[y].length; x++) {
			if (fn(grid[y][x], new Vec(x, y), grid)) return new Vec(x, y)
		}
	}
	return null
}

/**
 * Returns the first cell in a grid (searching top to bottom, left to right)
 * where the function `fn` is truthy for that cell. `fn` also receives the
 * position of each cell and a reference to the grid.
 * @template T
 * @param {T[][]} grid The grid to search
 * @param {function(T, Vec, T[][]): boolean} fn The function to search with
 * @returns {T}
 */
grid_utils.gfind = (grid, fn) => {
	for (let y = 0; y < grid.length; y++) {
		for (let x = 0; x < grid[y].length; x++) {
			if (fn(grid[y][x], new Vec(x, y), grid)) return grid[y][x]
		}
	}
	return null
}

/** @namespace */
const object_utils = {}

/**
 * Same as the built in [Object.keys](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys)
 * @param {object} obj The object to get keys from
 * @returns {string[]}
 */
object_utils.keys = obj => Object.keys(obj)

/**
 * Same as the built in [Object.values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values)
 * @param {object} obj The object to get values from
 * @returns {any[]}
 */
object_utils.values = obj => Object.values(obj)

/**
 * Same as the built in [Object.entries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries)
 * @param {object} obj The object to get entries from
 * @returns {any[]} An array of key-value tuples
 */
object_utils.entries = obj => Object.entries(obj)

/** @namespace */
const misc_utils = {}

/**
 * **[Global]** Checks `a` and `b` are equal. In theory this works for any two values,
 * whether they're primitives, arrays or some other object.
 * @function
 * @param {any} a
 * @param {any} b
 * @returns {boolean}
 */
misc_utils.eq = tag({ keep_global: true }, (a, b) => {
	if (Array.isArray(a) && Array.isArray(b)) {
		if (a.length !== b.length) return false;
		for (let i = 0; i < a.length; i++)
			if (!misc_utils.eq(a[i], b[i])) return false;
		return true;
	} else if (typeof a === "object" && typeof b === "object") {
		return JSON.stringify(a) === JSON.stringify(b);
	} else return a === b;
})

/** 
 * **[Global]** Checks `a` and `b` are not equal (by negating `misc_utils.eq`)
 * @function
 * @param {any} a
 * @param {any} b
 * @returns {boolean}
 */
misc_utils.neq = tag({ keep_global: true }, (a, b) => !eq(a, b))

/**
 * **[Global]** Creates the range `[a, b)` in an array with an optional step size
 * @function
 * @param {number} a End of the range (if `b` is not provided)
 * @param {number} [b=0] End of the range (if provided, in which case `a` is the start)
 * @param {number} [step=1]
 * @returns {number[]}
 * @example range(4) // [0, 1, 2, 3]
 * @example range(2, 5) // [2, 3, 4]
 * @example range(1, 10, 2) // [1, 3, 5, 7, 9]
 */
misc_utils.range = tag({ keep_global: true }, function(a, b = 0, step = 1) {
	let start = 0, end = a
	if (arguments.length > 1) {
		start = a
		end = b
	}
	const ret = [];
	for (let n = start; n < end; n += step)
		ret.push(n)
	return ret
})

/**
 * **[Global]** Same as `console.log` its arg(s)
 * @function
 * @template T
 * @param {...T} values Values to print
 * @returns {T|T[]}
 */
misc_utils.print = tag({ keep_global: true }, (...values) => {
	console.log(...values)
	return values.length > 1 ? values : values[0]
})

/**
 * **[Global]** Same as `misc_utils.print` but niladic (so that it becomes a getter)
 * @template T
 * @param {T} value Value to print
 * @returns {T}
 */
misc_utils.pr = value => misc_utils.print(value)

/**
 * **[Global]** Tries to get the length property of `value` otherwise tries to get `size`
 * @function
 * @param {any} value Value to get the length/size of
 * @returns {?number}
 */
misc_utils.len = value => value?.length ?? value?.size

/**
 * Wraps a value in a singleton list
 * @function
 * @template T
 * @param {T} value Value to wrap
 * @returns {T[]}
 */
misc_utils.wrap = value => [value]

/**
 * Finds the number of corners in an area
 * @param {_Set<Vec>} area The area to find corners in
 * @returns {number}
 */
misc_utils.corners = area => {
	const xs = [...area].map(v => v.x)
	const ys = [...area].map(v => v.y)

	let corners = 0

	for (let x = Math.min(...xs) - 1; x <= Math.max(...xs); x++) {
		for (let y = Math.min(...ys) - 1; y <= Math.max(...ys); y++) {
			const subgrid = [new Vec(x, y), new Vec(x + 1, y), new Vec(x, y + 1), new Vec(x + 1, y + 1)]
			const in_area = subgrid.map(v => area.has(v))
			const count_in_area = in_area.filter(x => x).length
			if (count_in_area == 1 || count_in_area == 3) corners++
			else if (
				misc_utils.eq(in_area, [true, false, false, true]) ||
				misc_utils.eq(in_area, [false, true, true, false])
			) corners += 2
		}
	}

	return corners
}

/**
 * **[Global]** Exits the program (alias for [process.exit](https://nodejs.org/api/process.html#process_process_exit_code))
 * @param {number|string} [exit_code=0]
 * @returns {void}
 */
misc_utils.exit = tag({ keep_global: true }, (exit_code = process.exitCode ?? 0) => process.exit(exit_code))

/** @namespace */
const constructors = {}

/**
 * **[Global]** Calls `new _Set(iter)`. See the documentation for `{@link _Set}` for more info
 * @function
 * @param {Iterable.<*>} iter The iterable to create the set from
 * @returns {_Set}
 */
constructors.S = tag({ keep_global: true }, (iter = []) => new _Set(iter))

/**
 * **[Global]** Calls `new Vec(x, y)`. See the documentation for `{@link Vec}` for more info
 * @function
 * @param {number} x The x component of the vector
 * @param {number} y The y component of the vector
 * @returns {Vec}
 */
constructors.V = tag({ keep_global: true }, (x, y) => new Vec(x, y))

export default {
	...type_checks,
	...type_casts,
	...math_utils,
	...string_utils,
	...iter_utils,
	...object_utils,
	...logic_utils,
	...grid_utils,
	...misc_utils,
	...constructors
}