const NormMetric = Union{Euclidean,Chebyshev,Cityblock,Minkowski,WeightedEuclidean,WeightedCityblock,WeightedMinkowski,Mahalanobis}

"""
    HyperSphere{N,T}

Bounding sphere used by `BallTree` nodes to represent their spatial region.

# Fields
- `center::SVector{N,T}`: The center point of the sphere in N-dimensional space
- `r::T`: The radius of the sphere in the same units as the input data

A point `p` is inside the sphere if `distance(p, center) <= r` using the tree's metric.

# Example
```julia
tree = BallTree(data)
root = treeroot(tree)
region = treeregion(root)  # Returns a HyperSphere

# Access center and radius
println("Center: ", region.center)
println("Radius: ", region.r)
println("Dimension: ", length(region.center))
```
"""
struct HyperSphere{N,T}
    center::SVector{N,T}
    r::T
end

HyperSphere(center::SVector{N,T1}, r) where {N, T1} = HyperSphere(center, convert(T1, r))
HyperSphere(center::AbstractVector, r) = HyperSphere(SVector{length(center)}(center), r)

@inline function intersects(m::Metric,
                            s1::HyperSphere{N},
                            s2::HyperSphere{N}) where {N}
    d = evaluate(m, s1.center, s2.center)
    return d, d <= s1.r + s2.r
end

@inline function intersects(m::MinkowskiMetric,
                            s1::HyperSphere{N},
                            s2::HyperSphere{N}) where {N}
    d = evaluate_maybe_end(m, s1.center, s2.center, false)
    return d, d <= eval_pow(m, s1.r + s2.r)
end

@inline function encloses(m::Metric,
                          s1::HyperSphere{N},
                          s2::HyperSphere{N}) where {N}
    evaluate(m, s1.center, s2.center) + s1.r <= s2.r
end


@inline function encloses_fast(d, m::Metric,
                          s1::HyperSphere{N},
                          s2::HyperSphere{N}) where {N}
     if s1.r > s2.r
        return false
    else
        return d + s1.r <= s2.r
    end
end

@inline function encloses_fast(d, m::MinkowskiMetric,
                          s1::HyperSphere{N},
                          s2::HyperSphere{N}) where {N}
    if s1.r > s2.r
        return false
    else
        return d <= eval_pow(m, s2.r - s1.r)
    end
end

@inline function interpolate(::NormMetric,
                             c1::V,
                             c2::V,
                             x,
                             d) where {V <: AbstractVector}
    alpha = x / d
    center = (1 - alpha) * c1 + alpha * c2
    return center, true
end

@inline function interpolate(::Metric,
                             c1::V,
                             ::V,
                             ::Any,
                             ::Any) where {V <: AbstractVector}
    return c1, false
end

# Computes a bounding sphere for a set of points
function create_bsphere(data::AbstractVector{V}, metric::Metric, indices::Vector{Int}, range) where {V}
    T = get_T(eltype(V))
    # Compute the center from the finite points only: points at infinity would
    # poison the centroid with NaNs (Inf - Inf) and silently corrupt pruning
    # (issue #78). They still contribute an infinite radius below, which keeps
    # the sphere valid and merely disables pruning for this subtree.
    acc = zero(SVector{length(V), T})
    n_finite = 0
    for i in range
        p = data[indices[i]]
        if all(isfinite, p)
            acc += p
            n_finite += 1
        end
    end
    center = n_finite == 0 ? acc : acc * (one(T) / n_finite)
    # float: metrics like Hamming return integer distances which have no eps
    r = float(maximum(evaluate(metric, data[indices[i]], center) for i in range))
    if isfinite(r) # eps(Inf) is NaN
        r += eps(r)
    end
    return HyperSphere(center, r)
end

# Creates a bounding sphere from two other spheres
function create_bsphere(m::Metric,
                        s1::HyperSphere{N,T},
                        s2::HyperSphere{N,T}) where {N, T}
    if encloses(m, s1, s2)
        return HyperSphere(s2.center, s2.r)
    elseif encloses(m, s2, s1)
        return HyperSphere(s1.center, s1.r)
    end

    # Compute the distance x along a geodesic from s1.center to s2.center
    # where the new center should be placed (note that 0 <= x <= d because
    # neither s1 nor s2 contains the other)
    dist = evaluate(m, s1.center, s2.center)
    x = (s2.r - s1.r + dist) / 2
    center, is_exact_center = interpolate(m, s1.center, s2.center, x, dist)
    if is_exact_center
        rad = (s2.r + s1.r + dist) / 2
    else
        rad = max(s1.r + evaluate(m, s1.center, center), s2.r + evaluate(m, s2.center, center))
    end

    return HyperSphere(SVector{N,T}(center), rad)
end

function distance_to_sphere(metric::Metric, point, sphere::HyperSphere)
    dist = evaluate(metric, point, sphere.center) - sphere.r
    return max(zero(eltype(dist)), dist)
end

@inline function get_min_max_distance(m::Metric, s1::HyperSphere, s2::HyperSphere)
    dist = evaluate(m, s1.center, s2.center)
    min_d = max(zero(dist), dist - (s1.r + s2.r))
    max_d = dist + s1.r + s2.r
    return min_d, max_d
end
