fus 2.4.1
import "./main" all

# If the element is a number or string, it will be more convenient
# to use the element itself without a selector.
_elementOrUseSelector: (element, selector) -> if selector'ok then selector(element) else element

_ratioToIndex'export: (arr, ratio) ->
    r: Math.round(ratio * arr.length - 0.5)
    if r ≤ 0 # `≤` can correct -0
        0
    else if r > arr.length - 1
        arr.length - 1
    else
        r

_ratioToLength'export: (arr, ratio) ->
    r: Math.round(ratio * (arr.length + 1) - 0.5)
    if r ≤ 0 # `≤` can correct -0
        0
    else if r > arr.length
        arr.length
    else
        r

_reverseToIndex'export: (arr, reverseIndex) ->
    arr.length - 1 - reverseIndex

_positionToIndex'export: (arr, pos) ->
    if pos is Number
        if 0 < pos < 1
            pos: {Ratio: pos}
        else if -1 < pos < 0
            pos: {Reverse: {Ratio: -pos}}
        else if pos < 0
            pos: {Reverse: -pos - 1}
    if pos is Number
        pos
    else if pos'ok and pos.Reverse'ok and pos.Reverse.Ratio'ok
        arr.._reverseToIndex(arr.._ratioToIndex(pos.Reverse.Ratio))
    else if pos'ok and pos.Reverse'ok
        arr.._reverseToIndex(pos.Reverse)
    else if pos'ok and pos.Ratio'ok
        arr.._ratioToIndex(pos.Ratio)
    else
        fail()

_amountToLength'export: (arr, amount) ->
    if amount is Number
        if 0 < amount < 1
            amount: {Ratio: amount}
    if amount is Number
        amount
    else if amount'ok and amount.Ratio'ok
        arr.._ratioToLength(amount.Ratio)
    else
        fail()

clone'export: arr -> arr.slice()

isEmpty'export: arr -> arr.length = 0

bound'export: arr -> arr.length - 1

portion'export: (arr, startIndex, length, endIndex) ->
    startIndex: arr.._positionToIndex(startIndex)
    length: arr.._amountToLength(length) if length'ok
    endIndex: arr.._positionToIndex(endIndex) if endIndex'ok
    arr.slice(startIndex, length'ok ? startIndex + length | (endIndex'ok ? endIndex + 1 | void))

at'export: (arr, index) ->
    index: arr.._positionToIndex(index)

    # useful for validating element operations like `first`, `last`
    assert(Number.isInteger(index) and 0 ≤ index < arr.length)

    arr.(index)

atOrNull'export: (arr, index) ->
    try
        arr..at(index)
    catch
        null
atOrVoid'export: (arr, index) ->
    try
        arr..at(index)
    catch
        void

# TODO: In TC39, there's no `contains` but `includes`. I keep `contains` for old code.
includes'export: (arr, value) -> value in arr
contains'export: includes

# TODO: performance
first'export: (arr, predicate) ->
    queryResult: predicate'ok ? arr.filter(predicate) | arr
    queryResult..at(0)

firstOrNull'export: (arr, predicate) ->
    try
        arr..first(predicate)
    catch
        null
firstOrVoid'export: (arr, predicate) ->
    try
        arr..first(predicate)
    catch
        void

# TODO: performance
last'export: (arr, predicate) ->
    queryResult: predicate'ok ? arr.filter(predicate) | arr
    queryResult..at(queryResult.length - 1)

lastOrNull'export: (arr, predicate) ->
    try
        arr..last(predicate)
    catch
        null
lastOrVoid'export: (arr, predicate) ->
    try
        arr..last(predicate)
    catch
        void

single'export: (arr, predicate) ->
    queryResult: predicate'ok ? arr.filter(predicate) | arr
    assert(queryResult.length = 1)
    queryResult..at(0)

# In Microsoft LINQ it still throws if matched elements > 1, because I think
# it shouldn't throw. I want it more consistent.
singleOrNull'export: (arr, predicate) ->
    try
        arr..single(predicate)
    catch
        null
singleOrVoid'export: (arr, predicate) ->
    try
        arr..single(predicate)
    catch
        void

# If array length is 1, then `reduce` will return the single element. That's exactly what
# `withMax` and `withMin` are for, so we don't need to copy what we did in `sum` method.
withMax'export: (arr, selector) -> arr.reduce((a, b, index) ->
    _elementOrUseSelector(a, selector) > _elementOrUseSelector(b, selector) ? a | b
)
withMin'export: (arr, selector) -> arr.reduce((a, b, index) ->
    _elementOrUseSelector(a, selector) < _elementOrUseSelector(b, selector) ? a | b
)

max'export: (arr, selector) -> _elementOrUseSelector(arr..withMax(selector), selector)
min'export: (arr, selector) -> _elementOrUseSelector(arr..withMin(selector), selector)

sum'export: (arr, selector) ->
    if arr.length = 0
        0
    else if arr.length = 1
        _elementOrUseSelector(arr..first(), selector)
    else
        arr.reduce((a, b, index) ->
            (index = 1 ? _elementOrUseSelector(a, selector) | a)
            + _elementOrUseSelector(b, selector)
        )

average'export: (arr, selector) ->
    if arr.length = 0
        fail()
    else
        arr..sum(selector) / arr.length

median'export: (arr, selector) ->
    sorted: arr..sort(selector)
    a: sorted..at(0.5 - Number.EPSILON)
    b: sorted..at(0.5 + Number.EPSILON)
    m: _elementOrUseSelector(a, selector)
    n: _elementOrUseSelector(b, selector)
    (m + n) / 2

product'export: (arr, selector) ->
    if arr.length = 0
        1
    else if arr.length = 1
        _elementOrUseSelector(arr..first(), selector)
    else
        arr.reduce((a, b, index) ->
            (index = 1 ? _elementOrUseSelector(a, selector) | a)
            * _elementOrUseSelector(b, selector)
        )

# These methods use sorting. For `keySelector`, note that the keys of all elements must be either
# all numbers, all booleans, or all strings. ====================[

# Why don't use {key: ..., value: ...}, but a non-intuitive array for the key-value pair?
# Because ECMAScript 6th's Map constructor only accepts the array form to denote a key-value pair.
# I don't want to break the consistency.
group'export: (arr, keySelector, valueSelector) ->
    if arr..isEmpty()
        []
    else
        sorted: arr..sort(keySelector)
        results: []
        comparedKey: _elementOrUseSelector(sorted..first(), keySelector)
        elements: []
        sorted.forEach(m ->
            key: _elementOrUseSelector(m, keySelector)
            if key ≠ comparedKey
                results.push[
                    comparedKey
                    _elementOrUseSelector(elements, valueSelector)
                ]
                comparedKey: key
                elements: []
            elements.push(m)
        )
        results.push[
            comparedKey
            _elementOrUseSelector(elements, valueSelector)
        ]
        results

_sort'export: (arr, keySelector, isDescending) ->
    arr..clone().sort((a, b) ->
        a1: _elementOrUseSelector(a, keySelector)
        b1: _elementOrUseSelector(b, keySelector)
        if a1 < b1 then (isDescending ? 1 | -1)
        else if a1 > b1 then (isDescending ? -1 | 1)
        else 0
    )
sort'export: (arr, keySelector) -> arr.._sort(keySelector, false)
sortDescending'export: (arr, keySelector) -> arr.._sort(keySelector, true)
# ]========================================

reverse'export: arr -> arr..clone().reverse()

except'export: (arr, array, equalityComparer: (a, b) -> a = b) ->
    arr.filter(m ->
        not array.some(n -> equalityComparer(n, m))
    )
distinct'export: (arr, equalityComparer: (a, b) -> a = b) ->
    r: []
    arr.forEach(m ->
        r.push(m) if not r.some(n -> equalityComparer(n, m))
    )
    r
union'export: (arr, array, equalityComparer) -> arr.concat(array)..distinct(equalityComparer)
intersect'export: (arr, array, equalityComparer: (a, b) -> a = b) ->
    r: []
    arr..distinct(equalityComparer).forEach(m ->
        r.push(m) if array.some(n -> equalityComparer(n, m))
    )
    r

flatten'export: (arr, level) ->
    if level ≤ 0
        fail()
    else
        r: []
        canContinue: false
        arr.forEach(m ->
            if m is Array
                canContinue: true
                m.forEach(n -> r.push(n))
            else
                r.push(m)
        )
        if canContinue
            if level'ok
                if level = 1
                    r
                else
                    r..flatten(level - 1)
            else
                r..flatten()
        else
            r

forEach'export: (arr, callback) ->
    arr.every (element, index) ->
        callback(element, index) = break ? false | true
    void

asyncForEach'export: (arr, callback) ->
    promise: Promise.resolve()
    arr.forEach (element, index) ->
        promise: promise.then(value ->
            if value = break
                break
            else
                callback(element, index)
        )
    promise.then(<>) # `forEach` returns void so in its async form we also need to return void

asyncSome'export: (arr, callback) ->
    promise: Promise.resolve(false)
    arr.forEach (element, index) ->
        promise: promise.then(value ->
            if value
                true
            else
                callback(element, index)
        )
    promise.then(m -> m ? true | false)

asyncEvery'export: (arr, callback) ->
    promise: Promise.resolve(true)
    arr.forEach (element, index) ->
        promise: promise.then(value ->
            if not value
                false
            else
                callback(element, index)
        )
    promise.then(m -> m ? true | false)

asyncMap'export: (arr, callback) ->
    r: []
    if arr.length = 0
        Promise.resolve(r)
    else
        promise: Promise.resolve()
        arr.forEach (element, index) ->
            promise: promise.then(value ->
                r.push(value) if index > 0
                callback(element, index)
            )
        promise.then(value ->
            r.push(value)
            r
        )

toObject'export: arr ->
    r: {}
    arr.forEach(element ->
        r.(element.0): element.1
    )
    r

join'export: (arr, args...) ->
    args: args.0 if args.0 is Array
    if args.length ≤ 1
        arr.join(args.0)
    else
        arr.map(m -> m..join(args.slice(0, -1))).join(args..last())

randomOne'export: arr -> arr.(Math..randomInt(arr.length))
random'export: (arr, count) -> arr..clone()..takeRandom(count)
takeRandomOne'export: arr ->
    index: Math..randomInt(arr.length)
    r: arr.(index)
    arr..removeAt(index)
    r
takeRandom'export: (arr, count) ->
    count ifnull: arr.length
    count: arr.._amountToLength(count)
    repeat(count, <> arr..takeRandomOne())

removeAt'export: (arr, index) ->
    arr.splice(index, 1)
    arr

# remove the first, not all
remove'export: (arr, element) ->
    index: arr.indexOf(element)
    assert(index > -1)
    arr..removeAt(index)

# TODO: performance
removeAll'export: (arr, element) ->
    loop --
        index: arr.indexOf(element)
        if index = -1
            break
        else
            arr..removeAt(index)
    arr

# remove the first, not all
removeMatch'export: (arr, predicate) ->
    index: arr.findIndex(predicate)
    assert(index > -1)
    arr..removeAt(index)

# TODO: performance
removeAllMatch'export: (arr, predicate) ->
    loop --
        index: arr.findIndex(predicate)
        if index = -1
            break
        else
            arr..removeAt(index)
    arr

insertBefore'export: (arr, element, elements...) ->
    index: arr.indexOf(element)
    assert(index > -1)
    arr..insertBeforeIndex(index, elements...)

insertBeforeIndex'export: (arr, index, elements...) ->
    arr.splice(index, 0, elements...)
    arr

insertAfter'export: (arr, element, elements...) ->
    index: arr.indexOf(element)
    assert(index > -1)
    arr..insertBeforeIndex(index + 1, elements...)
