
// Store values with indices to handle duplicates uniquely
private struct Entry<T: BinaryFloatingPoint>: Comparable {
    let index: Int
    let value: T
    static func < (lhs: Entry<T>, rhs: Entry<T>) -> Bool {
        lhs.value == rhs.value ? lhs.index < rhs.index : lhs.value < rhs.value
    }
}

extension Sequence {

    func firstElement() -> Element? {
        for element in self {
            return element
        }
        return nil
    }

    func minMax<E>(by converting: (Element) -> E) -> (min: E, max: E)? where E: Comparable {
        guard let first = firstElement() else {
            return nil
        }
        var minimum = converting(first)
        var maximum = minimum
        for location in dropFirst() {
            let value = converting(location)
            if value < minimum {
                minimum = value
            } else if value > maximum {
                maximum = value
            }
        }
        return (minimum, maximum)
    }

    /// Applies a centered median filter to the sequence.
    /// - Parameters:
    ///   - windowSize: The number of samples in the median filter window (should be odd for symmetric centering).
    ///   - transform: Closure to transform each element into a numeric value.
    /// - Returns: An array of filtered elements (same type as input).
    func medianFiltered<T: BinaryFloatingPoint>(windowSize: Int, transform: (Element) -> T) -> [Element] {
        precondition(windowSize > 0, "Window size must be greater than zero")
        let input = Array(self)
        guard !input.isEmpty else { return [] }

        var result: [Element] = []
        result.reserveCapacity(input.count)

        let halfWindow = windowSize / 2

        for i in 0..<input.count {
            let start = Swift.max(0, i - halfWindow)
            let end = Swift.min(input.count - 1, i + halfWindow)
            var window: [Entry<T>] = []

            for j in start...end {
                window.append(Entry(index: j, value: transform(input[j])))
            }

            window.sort()

            // Median position
            let medianIndex = window.count / 2
            let medianValue = window[medianIndex].value

            // Choose the element closest to the median
            let closest = input[start...end]
                .min(by: { abs(Double(transform($0) - medianValue)) < abs(Double(transform($1) - medianValue)) })!

            result.append(closest)
        }

        return result
    }

    /// Default version when Element itself is BinaryFloatingPoint
    func medianFiltered(windowSize: Int) -> [Element] where Element: BinaryFloatingPoint {
        return self.medianFiltered(windowSize: windowSize, transform: { $0 })
    }

    /// Iterate over adjacent pairs of elements in the sequence, applying a transform closure.
    /// - Parameter transform: A closure that takes two consecutive elements and returns a value of type T.
    /// - Returns: An array of transformed values.
    func adjacentPairs<T>(_ transform: (Element, Element) -> T) -> [T] {
        var result: [T] = []
        var iterator = self.makeIterator()
        guard var prev = iterator.next() else { return [] }
        while let current = iterator.next() {
            result.append(transform(prev, current))
            prev = current
        }
        return result
    }
}

// MARK: - Helpers

extension Array where Element: Comparable {
    /// Binary search returning the index where `predicate` fails (insertion point).
    func binarySearch(predicate: (Element) -> Bool) -> Int {
        var low = 0
        var high = count
        while low < high {
            let mid = (low + high) / 2
            if predicate(self[mid]) {
                low = mid + 1
            } else {
                high = mid
            }
        }
        return low
    }

    /// Binary search exact element index if present.
    func binarySearchExact(_ element: Element) -> Int? {
        var low = 0
        var high = count - 1
        while low <= high {
            let mid = (low + high) / 2
            if self[mid] == element { return mid }
            else if self[mid] < element { low = mid + 1 }
            else { high = mid - 1 }
        }
        return nil
    }
}
