module Static

import IfElse: ifelse
using SciMLPublic: @public

export StaticInt, StaticFloat64, StaticSymbol, True, False, StaticBool, NDIndex
export dynamic, is_static, known, static, static_promote

@public OptionallyStaticRange,
    OptionallyStaticUnitRange, OptionallyStaticStepRange, SUnitRange, SOneTo
@public eachop, eachop_tuple, reduce_tup, eq, ne, gt, ge, le, lt, mul, add

import PrecompileTools: @recompile_invalidations

@recompile_invalidations begin
    import CommonWorldInvalidations
end

"""
    StaticSymbol(value) -> StaticSymbol

Represent a symbol in the type parameter so that `value` is available to dispatch and
constant propagation.

# Arguments

- `value`: A `Symbol`, or a value accepted by `Symbol`. Passing a `StaticSymbol` returns it
  unchanged.

# Returns

- `StaticSymbol{S}`: A zero-field value whose type parameter `S` is the converted symbol.

# Examples

```julia
julia> using Static

julia> name = StaticSymbol(:state)
static(:state)

julia> known(name)
:state
```
"""
struct StaticSymbol{s}
    StaticSymbol{s}() where {s} = new{s::Symbol}()
    StaticSymbol(s::Symbol) = new{s}()
    StaticSymbol(@nospecialize x::StaticSymbol) = x
    StaticSymbol(x) = StaticSymbol(Symbol(x))
    StaticSymbol(x, y) = StaticSymbol(Symbol(x, y))
    @generated function StaticSymbol(::StaticSymbol{X}, ::StaticSymbol{Y}) where {X, Y}
        return StaticSymbol(Symbol(X, Y))
    end
    StaticSymbol(x, y, z...) = StaticSymbol(StaticSymbol(x, y), z...)
end

Base.Symbol(@nospecialize(s::StaticSymbol)) = known(s)

abstract type StaticInteger{N} <: Number end

"""
    StaticBool(value::Bool) -> Union{True, False}

Convert a runtime boolean to its static representation.

# Arguments

- `value::Bool`: The boolean value to encode in the result type.

# Returns

- [`True`](@ref) when `value` is `true`, otherwise [`False`](@ref).

# Examples

```julia
julia> using Static

julia> StaticBool(true)
True()

julia> StaticBool(false)
False()
```
"""
abstract type StaticBool{bool} <: StaticInteger{bool} end

"""
    True()

The static representation of `true`.

# Examples

```julia
julia> using Static

julia> True() === static(true)
true

julia> dynamic(True())
true
```
"""
struct True <: StaticBool{true} end

"""
    False()

The static representation of `false`.

# Examples

```julia
julia> using Static

julia> False() === static(false)
true

julia> dynamic(False())
false
```
"""
struct False <: StaticBool{false} end

StaticBool{true}() = True()
StaticBool{false}() = False()
StaticBool(x::StaticBool) = x
function StaticBool(x::Bool)
    if x
        return True()
    else
        return False()
    end
end

"""
    StaticInt(N::Int)::StaticInt{N}

Represent an integer in the type parameter while retaining the arithmetic and indexing
behavior of a number. Use `StaticInt(N)` instead of `Val(N)` when the value must participate
in numeric operations.

# Arguments

- `N::Int`: The integer value to encode in the result type.

# Returns

- `StaticInt{N}`: A zero-field numeric value with `N` available at compile time.

# Examples

```julia
julia> using Static

julia> n = StaticInt(3)
static(3)

julia> n + static(2)
static(5)

julia> dynamic(n)
3
```
"""
struct StaticInt{N} <: StaticInteger{N}
    StaticInt{N}() where {N} = new{N::Int}()
    StaticInt(N::Int) = new{N}()
    StaticInt(@nospecialize N::StaticInt) = N
    StaticInt(::Val{N}) where {N} = StaticInt(N)
end

const IntType = Union{StaticInt, Int}
IntType(x::Integer) = Int(x)
IntType(@nospecialize x::Union{Int, StaticInt}) = x

Base.isinteger(@nospecialize x::StaticInteger) = true

include("float.jl")

const StaticNumber{N} = Union{StaticInt{N}, StaticBool{N}, StaticFloat64{N}}

Base.promote_rule(::Type{Bool}, T::Type{<:StaticNumber}) = promote_rule(Bool, eltype(T))

Base.getindex(x::Tuple, ::StaticInt{N}) where {N} = getfield(x, N)

Base.getindex(x::NamedTuple, ::StaticSymbol{N}) where {N} = getfield(x, N)
function Base.getindex(x::NamedTuple, symbols::Tuple{StaticSymbol, Vararg{StaticSymbol}})
    return x[map(known, symbols)]
end

Base.zero(@nospecialize(::StaticInt)) = StaticInt{0}()

Base.to_index(x::StaticInt) = known(x)
function Base.checkindex(::Type{Bool}, inds::AbstractUnitRange, ::StaticNumber{N}) where {N}
    return checkindex(Bool, inds, N)
end
function Base.checkindex(
        ::Type{Bool}, inds::Base.IdentityUnitRange,
        ::StaticFloat64{N}
    ) where {N}
    return checkindex(Bool, inds.indices, N)
end

ifelse(::True, @nospecialize(x), @nospecialize(y)) = x
ifelse(::False, @nospecialize(x), @nospecialize(y)) = y

const Zero = StaticInt{0}
const One = StaticInt{1}
const FloatOne = StaticFloat64{one(Float64)}
const FloatZero = StaticFloat64{zero(Float64)}

const StaticType{T} = Union{StaticNumber{T}, StaticSymbol{T}}

StaticInt(x::False) = Zero()
StaticInt(x::True) = One()
Base.Bool(::True) = true
Base.Bool(::False) = false

Base.eltype(@nospecialize(T::Type{<:StaticFloat64})) = Float64
Base.eltype(@nospecialize(T::Type{<:StaticInt})) = Int
Base.eltype(@nospecialize(T::Type{<:StaticBool})) = Bool

"""
    NDIndex(i, j, k...)   -> I
    NDIndex((i, j, k...)) -> I

A multidimensional scalar index whose coordinates may independently be runtime `Int`
values or compile-time [`StaticInt`](@ref) values. `NDIndex` interoperates with Julia's
Cartesian indexing operations while preserving static coordinates.

# Arguments

- `i, j, k...`: Integer coordinates. Nested tuples and Cartesian indices are flattened.
- `(i, j, k...)`: A tuple containing the coordinates.

# Returns

- `NDIndex`: An index containing one coordinate per dimension.

# Examples

```julia
julia> using Static

julia> i = NDIndex(static(1), 2, static(3))
NDIndex(static(1), 2, static(3))

julia> i[static(1)]
static(1)

julia> i[1]
1
```
"""
struct NDIndex{N, I <: Tuple{Vararg{Union{StaticInt, Int}, N}}} <:
    Base.AbstractCartesianIndex{N}
    index::I

    NDIndex{N}(i::Tuple{Vararg{Union{StaticInt, Int}, N}}) where {N} = new{N, typeof(i)}(i)
    NDIndex{N}(index::Tuple) where {N} = _ndindex(static(N), _flatten(index...))
    NDIndex{N}(index...) where {N} = NDIndex{N}(index)

    NDIndex{0}(::Tuple{}) = new{0, Tuple{}}(())
    NDIndex{0}() = NDIndex{0}(())

    NDIndex(i::Tuple{Vararg{Union{StaticInt, Int}, N}}) where {N} = new{N, typeof(i)}(i)
    NDIndex(i::Vararg{Union{StaticInt, Int}, N}) where {N} = NDIndex(i)

    NDIndex(index::Tuple) = NDIndex(_flatten(index...))
    NDIndex(index...) = NDIndex(index)
end

_ndindex(n::StaticInt{N}, i::Tuple{Vararg{Union{Int, StaticInt}, N}}) where {N} = NDIndex(i)
function _ndindex(n::StaticInt{N}, i::Tuple{Vararg{Any, M}}) where {N, M}
    M > N && throw(ArgumentError("input tuple of length $M, requested $N"))
    return NDIndex(_fill_to_length(i, n))
end
_fill_to_length(x::Tuple{Vararg{Any, N}}, n::StaticInt{N}) where {N} = x
@inline function _fill_to_length(x::Tuple{Vararg{Any, M}}, n::StaticInt{N}) where {M, N}
    return _fill_to_length((x..., static(1)), n)
end

_flatten(i::StaticInt{N}) where {N} = (i,)
_flatten(i::Integer) = (Int(i),)
_flatten(i::Base.AbstractCartesianIndex) = _flatten(Tuple(i)...)
@inline _flatten(i::StaticInt, I...) = (i, _flatten(I...)...)
@inline _flatten(i::Integer, I...) = (Int(i), _flatten(I...)...)
@inline function _flatten(i::Base.AbstractCartesianIndex, I...)
    return (_flatten(Tuple(i)...)..., _flatten(I...)...)
end
Base.Tuple(@nospecialize(x::NDIndex)) = getfield(x, :index)

"""
    known(value_or_type)

Return the value encoded by a static value or type. Return `nothing` when no value is known
from the type alone.

`known` ensures that the type of the returned value is always inferred, even if the
compiler fails to infer the exact value.

# Arguments

- `value_or_type`: A value or type to inspect. Tuples are inspected element by element.

# Returns

- The encoded `Int`, `Bool`, `Float64`, `Symbol`, or tuple for supported static inputs.
- `nothing` for a dynamic type whose value cannot be determined from its type.

# Examples

```julia
julia> using Static

julia> known(StaticInt{1})
1

julia> known(Int) === nothing
true

julia> known(typeof((static(1), static(:x))))
(1, :x)
```

See also: [`static`](@ref), [`is_static`](@ref), [`dynamic`](@ref).
"""
known(@nospecialize(x)) = known(typeof(x))
_get_known(::Type{T}, dim::StaticInt{D}) where {T, D} = known(field_type(T, dim))
known(@nospecialize(T::Type{<:Tuple})) = eachop(_get_known, nstatic(Val(fieldcount(T))), T)
known(T::DataType) = nothing
known(@nospecialize(T::Type{<:NDIndex})) = known(T.parameters[2])
known(::Union{True, Type{True}}) = true
known(::Union{False, Type{False}}) = false
known(::Union{Val{V}, Type{Val{V}}}) where {V} = V
known(::Union{StaticInt{N}, Type{StaticInt{N}}}) where {N} = _return_int(N)
_return_int(x::Int) = x
known(::Union{StaticSymbol{S}, Type{StaticSymbol{S}}}) where {S} = _return_symbol(S)
_return_symbol(x::Symbol) = x
known(::Union{StaticFloat64{F}, Type{StaticFloat64{F}}}) where {F} = _return_float(F)
_return_float(x::Float64) = x

"""
    static(x)

Convert `x` to a static representation whose value is encoded in its type. Return `x`
unchanged when it is already static.

# Arguments

- `x`: A boolean, integer, real number convertible to `Float64`, symbol-like value, tuple,
  `Val`, or `CartesianIndex` to convert.

# Returns

- A [`StaticBool`](@ref), [`StaticInt`](@ref), [`StaticFloat64`](@ref),
  [`StaticSymbol`](@ref), [`NDIndex`](@ref), or tuple of static values.

# Throws

- `ErrorException`: If no static representation is defined for the input type.

# Examples

```julia
julia> using Static

julia> static(1)
static(1)

julia> static(true)
True()

julia> static(:x)
static(:x)

julia> static((1, true))
(static(1), True())
```

See also: [`is_static`](@ref), [`known`](@ref).
"""
static(@nospecialize(x::Union{StaticSymbol, StaticNumber})) = x
static(x::Integer) = StaticInt(x)
function static(x::Union{AbstractFloat, Complex, Rational, AbstractIrrational})
    return StaticFloat64(Float64(x))
end
static(x::Bool) = StaticBool(x)
static(x::Union{Symbol, AbstractChar, AbstractString}) = StaticSymbol(x)
static(x::Tuple{Vararg{Any}}) = map(static, x)
static(::Val{V}) where {V} = static(V)
static(x::CartesianIndex) = NDIndex(static(Tuple(x)))
function static(x::X) where {X}
    Base.issingletontype(X) && return x
    error("There is no static alternative for type $(typeof(x)).")
end

"""
    is_static(value_or_type) -> Union{True, False}

Return a static boolean indicating whether the input's value is fully encoded by its type.
For tuples, every element type must be static.

# Arguments

- `value_or_type`: A value or type to inspect.

# Returns

- [`True`](@ref) when the type is static, otherwise [`False`](@ref).

# Examples

```julia
julia> using Static

julia> is_static(StaticInt{2})
True()

julia> is_static(Int)
False()
```

See also: [`static`](@ref), [`known`](@ref).
"""
is_static(@nospecialize(x)) = is_static(typeof(x))
is_static(@nospecialize(x::Type{<:StaticType})) = True()
is_static(@nospecialize(x::Type{<:Val})) = True()
_tuple_static(::Type{T}, i) where {T} = is_static(field_type(T, i))
_all_static_flags(::Tuple{}) = True()
_all_static_flags(t::Tuple) = first(t) === True() ? _all_static_flags(Base.tail(t)) : False()
@inline function is_static(@nospecialize(T::Type{<:Tuple}))
    return _all_static_flags(eachop(_tuple_static, nstatic(Val(fieldcount(T))), T))
end
is_static(T::DataType) = False()

"""
    dynamic(x)

Return the ordinary runtime representation of a static value. Return `x` unchanged when it
is not static.

`dynamic` ensures that the type of the returned value is always inferred, even if the
compiler fails to infer the exact value.

# Arguments

- `x`: A static scalar, tuple, [`NDIndex`](@ref), or arbitrary dynamic value.

# Returns

- The encoded runtime value, a tuple converted elementwise, a `CartesianIndex` for an
  `NDIndex`, or the unchanged dynamic input.

# Examples

```julia
julia> dynamic(static(1))
1

julia> dynamic(1)
1

julia> dynamic((static(1), static(:x)))
(1, :x)
```

See also: [`known`](@ref), [`static`](@ref).
"""
@inline dynamic(@nospecialize x::StaticInt) = known(x)
@inline dynamic(@nospecialize x::StaticFloat64) = known(x)
@inline dynamic(@nospecialize x::StaticSymbol) = known(x)
@inline dynamic(@nospecialize x::Union{True, False}) = known(x)
@inline dynamic(@nospecialize x::Tuple) = map(dynamic, x)
dynamic(@nospecialize(x::NDIndex)) = CartesianIndex(dynamic(Tuple(x)))
dynamic(@nospecialize x) = x

"""
    static_promote(x, y)
    static_promote(x::AbstractRange{<:Integer}, y::AbstractRange{<:Integer})

Combine equal values while preferring the representation that carries more static
information. Integer ranges are combined field by field so static starts, steps, and stops
are preserved independently. `nothing` acts as missing information and promotes to the
other input.

# Arguments

- `x`, `y`: Values that must be equal, equal integer ranges, or `nothing`.

# Returns

- The equal value with the most static information available from either input.

# Throws

- `ErrorException`: If both inputs provide unequal values.

# Examples

```julia
julia> using Static

julia> static_promote(static(1), 1)
static(1)

julia> static_promote(static(1):10, 1:static(10))
static(1):static(10)

julia> static_promote(nothing, 3)
3
```
"""
@inline static_promote(x::StaticType{X}, ::StaticType{X}) where {X} = x
@noinline function static_promote(::StaticType{X}, ::StaticType{Y}) where {X, Y}
    error("$X and $Y are not equal")
end
Base.@propagate_inbounds function static_promote(::StaticType{N}, x) where {N}
    return static(static_promote(N, x))
end
Base.@propagate_inbounds function static_promote(x, ::StaticType{N}) where {N}
    return static(static_promote(N, x))
end
Base.@propagate_inbounds static_promote(x, y) = _static_promote(x, y)
Base.@propagate_inbounds function _static_promote(x, y)
    @boundscheck x === y || error("$x and $y are not equal")
    return x
end
_static_promote(::Nothing, ::Nothing) = nothing
_static_promote(x, ::Nothing) = x
_static_promote(::Nothing, y) = y

@inline function static_promote(
        x0::AbstractRange{<:Integer},
        y0::AbstractRange{<:Integer},
    )
    x = OptionallyStaticStepRange(x0)
    y = OptionallyStaticStepRange(y0)
    fst = static_promote(getfield(x, :start), getfield(y, :start))
    stp = static_promote(getfield(x, :step), getfield(y, :step))
    lst = static_promote(getfield(x, :stop), getfield(y, :stop))
    if isa(stp, One)
        return _OptionallyStaticUnitRange(fst, lst)
    else
        return _OptionallyStaticStepRange(fst, stp, lst)
    end
end
function static_promote(x::Base.Slice, y::Base.Slice)
    return Base.Slice(static_promote(x.indices, y.indices))
end

Base.@propagate_inbounds function _promote_shape(
        a::Tuple{A, Vararg{Any}},
        b::Tuple{B, Vararg{Any}}
    ) where {A, B}
    return (
        static_promote(getfield(a, 1), getfield(b, 1)),
        _promote_shape(Base.tail(a), Base.tail(b))...,
    )
end
_promote_shape(::Tuple{}, ::Tuple{}) = ()
Base.@propagate_inbounds function _promote_shape(::Tuple{}, b::Tuple{B}) where {B}
    return (static_promote(static(1), getfield(b, 1)),)
end
Base.@propagate_inbounds function _promote_shape(a::Tuple{A}, ::Tuple{}) where {A}
    return (static_promote(static(1), getfield(a, 1)),)
end
Base.@propagate_inbounds function Base.promote_shape(
        a::Tuple{StaticInt, Vararg{Union{Int, StaticInt}}},
        b::Tuple{Vararg{Union{Int, StaticInt}}}
    )
    return _promote_shape(a, b)
end
Base.@propagate_inbounds function Base.promote_shape(
        a::Tuple{Int, Vararg{Union{Int, StaticInt}}},
        b::Tuple{StaticInt, Vararg{Union{Int, StaticInt}}}
    )
    return _promote_shape(a, b)
end
Base.@propagate_inbounds function Base.promote_shape(
        a::Tuple{},
        b::Tuple{StaticInt, Vararg{Union{Int, StaticInt}}}
    )
    return _promote_shape(a, b)
end

function Base.promote_rule(
        @nospecialize(T1::Type{<:StaticNumber}),
        @nospecialize(T2::Type{<:StaticNumber})
    )
    return promote_rule(eltype(T1), eltype(T2))
end
function Base.promote_rule(
        ::Type{<:Base.TwicePrecision{R}},
        @nospecialize(T::Type{<:StaticNumber})
    ) where {R <: Number}
    return promote_rule(Base.TwicePrecision{R}, eltype(T))
end
function Base.promote_rule(
        @nospecialize(T1::Type{<:StaticNumber}),
        T2::Type{<:Union{Rational, AbstractFloat, Signed}}
    )
    return promote_rule(T2, eltype(T1))
end

Base.:(~)(::StaticInteger{N}) where {N} = static(~N)

Base.inv(x::StaticInteger{N}) where {N} = one(x) / x

Base.zero(@nospecialize T::Type{<:StaticInt}) = StaticInt(0)
Base.zero(@nospecialize T::Type{<:StaticBool}) = False()

Base.one(@nospecialize T::Type{<:StaticBool}) = True()
Base.one(@nospecialize T::Type{<:StaticInt}) = StaticInt(1)

@inline Base.iszero(::Union{StaticInt{0}, StaticFloat64{0.0}, False}) = true
@inline Base.iszero(@nospecialize x::Union{StaticInt, True, StaticFloat64}) = false

@inline Base.isone(::Union{One, True, StaticFloat64{1.0}}) = true
@inline Base.isone(@nospecialize x::Union{StaticInt, False, StaticFloat64}) = false

@inline Base.iseven(@nospecialize x::StaticNumber) = iseven(known(x))
@inline Base.isodd(@nospecialize x::StaticNumber) = isodd(known(x))

Base.AbstractFloat(x::StaticNumber) = StaticFloat64(x)

Base.abs(::StaticNumber{N}) where {N} = static(abs(N))
Base.abs2(::StaticNumber{N}) where {N} = static(abs2(N))
Base.sign(::StaticNumber{N}) where {N} = static(sign(N))

Base.widen(@nospecialize(x::StaticNumber)) = widen(known(x))

function Base.convert(::Type{T}, @nospecialize(N::StaticNumber)) where {T <: Number}
    return convert(T, known(N))
end

# Convert a runtime value to a specific StaticInt{N} type
# Only succeeds if x == N, otherwise throws an error
function Base.convert(::Type{StaticInt{N}}, x::Number) where {N}
    x == N || throw(ArgumentError("cannot convert $x to StaticInt{$N}"))
    return StaticInt{N}()
end

# Disambiguation methods for StaticNumber -> StaticInt{N}
function Base.convert(::Type{StaticInt{N}}, @nospecialize(x::StaticNumber)) where {N}
    known(x) == N || throw(ArgumentError("cannot convert $(known(x)) to StaticInt{$N}"))
    return StaticInt{N}()
end

# Convert a runtime value to a specific StaticBool type
function Base.convert(::Type{True}, x::Number)
    x == true || throw(ArgumentError("cannot convert $x to True"))
    return True()
end
function Base.convert(::Type{False}, x::Number)
    x == false || throw(ArgumentError("cannot convert $x to False"))
    return False()
end

# Disambiguation methods for StaticNumber -> True/False
function Base.convert(::Type{True}, @nospecialize(x::StaticNumber))
    known(x) == true || throw(ArgumentError("cannot convert $(known(x)) to True"))
    return True()
end
function Base.convert(::Type{False}, @nospecialize(x::StaticNumber))
    known(x) == false || throw(ArgumentError("cannot convert $(known(x)) to False"))
    return False()
end

#Base.Bool(::StaticInt{N}) where {N} = Bool(N)

Base.Integer(@nospecialize(x::StaticInt)) = x
(::Type{T})(x::StaticInteger) where {T <: Real} = T(known(x))
function (@nospecialize(T::Type{<:StaticNumber}))(
        x::Union{
            AbstractFloat,
            AbstractIrrational, Integer,
            Rational,
        }
    )
    return static(convert(eltype(T), x))
end

@inline Base.:(-)(::StaticNumber{N}) where {N} = static(-N)
Base.:(*)(::Union{AbstractFloat, AbstractIrrational, Integer, Rational}, y::Zero) = y
Base.:(*)(x::Zero, ::Union{AbstractFloat, AbstractIrrational, Integer, Rational}) = x
Base.:(*)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(X * Y)
Base.:(/)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(X / Y)
Base.:(-)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(X - Y)
Base.:(+)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(X + Y)

@generated Base.sqrt(::StaticNumber{N}) where {N} = :($(static(sqrt(N))))

function Base.div(::StaticNumber{X}, ::StaticNumber{Y}, m::RoundingMode) where {X, Y}
    return static(div(X, Y, m))
end
Base.div(x::Real, ::StaticInteger{Y}, m::RoundingMode) where {Y} = div(x, Y, m)
Base.div(::StaticNumber{X}, y::Real, m::RoundingMode) where {X} = div(X, y, m)
Base.div(x::StaticBool, y::False) = throw(DivideError())
Base.div(x::StaticBool, y::True) = x

Base.rem(@nospecialize(x::StaticNumber), T::Type{<:Integer}) = rem(known(x), T)
Base.rem(::StaticNumber{X}, ::StaticNumber{Y}) where {X, Y} = static(rem(X, Y))
Base.rem(x::Real, ::StaticInteger{Y}) where {Y} = rem(x, Y)
Base.rem(::StaticInteger{X}, y::Real) where {X} = rem(X, y)
Base.mod(::StaticNumber{X}, ::StaticNumber{Y}) where {X, Y} = static(mod(X, Y))
Base.mod(x::Real, ::StaticNumber{Y}) where {Y} = mod(x, Y)
Base.mod(::StaticNumber{X}, y::Real) where {X} = mod(X, y)

Base.:(==)(::StaticNumber{X}, ::StaticNumber{Y}) where {X, Y} = ==(X, Y)

Base.:(<)(::StaticNumber{X}, ::StaticNumber{Y}) where {X, Y} = <(X, Y)

Base.isless(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = isless(X, Y)
Base.isless(::StaticInteger{X}, y::Real) where {X} = isless(X, y)
Base.isless(x::Real, ::StaticInteger{Y}) where {Y} = isless(x, Y)

Base.min(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(min(X, Y))
Base.min(::StaticInteger{X}, y::Number) where {X} = min(X, y)
Base.min(x::Number, ::StaticInteger{Y}) where {Y} = min(x, Y)

Base.max(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(max(X, Y))
Base.max(::StaticInteger{X}, y::Number) where {X} = max(X, y)
Base.max(x::Number, ::StaticInteger{Y}) where {Y} = max(x, Y)

Base.minmax(::StaticNumber{X}, ::StaticNumber{Y}) where {X, Y} = static(minmax(X, Y))

Base.:(<<)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(<<(X, Y))
Base.:(<<)(::StaticInteger{X}, n::Integer) where {X} = <<(X, n)
Base.:(<<)(x::Integer, ::StaticInteger{N}) where {N} = <<(x, N)

Base.:(>>)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(>>(X, Y))
Base.:(>>)(::StaticInteger{X}, n::Integer) where {X} = >>(X, n)
Base.:(>>)(x::Integer, ::StaticInteger{N}) where {N} = >>(x, N)

Base.:(>>>)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(>>>(X, Y))
Base.:(>>>)(::StaticInteger{X}, n::Integer) where {X} = >>>(X, n)
Base.:(>>>)(x::Integer, ::StaticInteger{N}) where {N} = >>>(x, N)

Base.:(&)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(X & Y)
Base.:(&)(::StaticInteger{X}, y::Union{Integer, Missing}) where {X} = X & y
Base.:(&)(x::Union{Integer, Missing}, ::StaticInteger{Y}) where {Y} = x & Y
Base.:(&)(x::Bool, y::True) = x
Base.:(&)(x::Bool, y::False) = y
Base.:(&)(x::True, y::Bool) = y
Base.:(&)(x::False, y::Bool) = x

Base.:(|)(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(|(X, Y))
Base.:(|)(::StaticInteger{X}, y::Union{Integer, Missing}) where {X} = X | y
Base.:(|)(x::Union{Integer, Missing}, ::StaticInteger{Y}) where {Y} = x | Y
Base.:(|)(x::Bool, y::True) = y
Base.:(|)(x::Bool, y::False) = x
Base.:(|)(x::True, y::Bool) = x
Base.:(|)(x::False, y::Bool) = y

Base.xor(::StaticInteger{X}, ::StaticInteger{Y}) where {X, Y} = static(xor(X, Y))
Base.xor(::StaticInteger{X}, y::Union{Integer, Missing}) where {X} = xor(X, y)
Base.xor(x::Union{Integer, Missing}, ::StaticInteger{Y}) where {Y} = xor(x, Y)

Base.:(!)(::True) = False()
Base.:(!)(::False) = True()

Base.real(@nospecialize(x::StaticNumber)) = x
Base.real(@nospecialize(T::Type{<:StaticNumber})) = eltype(T)
Base.imag(@nospecialize(x::StaticNumber)) = zero(x)

@inline field_type(T::Type, f::Union{Int, Symbol}) = fieldtype(T, f)
@inline field_type(::Type{T}, ::StaticInt{N}) where {T, N} = fieldtype(T, N)
@inline field_type(::Type{T}, ::StaticSymbol{S}) where {T, S} = fieldtype(T, S)

@inline Base.exponent(::StaticNumber{M}) where {M} = static(exponent(M))

Base.:(^)(::StaticFloat64{x}, y::Float64) where {x} = exp2(log2(x) * y)

Base.:(+)(x::True) = One()
Base.:(+)(x::False) = Zero()

# from `^(x::Bool, y::Bool) = x | !y`
Base.:(^)(x::StaticBool, y::False) = True()
Base.:(^)(x::StaticBool, y::True) = x
Base.:(^)(x::Integer, y::False) = one(x)
Base.:(^)(x::Integer, y::True) = x
Base.:(^)(x::BigInt, y::False) = one(x)
Base.:(^)(x::BigInt, y::True) = x

@inline function Base.ntuple(f::F, ::StaticInt{N}) where {F, N}
    (N >= 0) || throw(ArgumentError(string("tuple length should be ≥ 0, got ", N)))
    return ntuple(f, Val(N))
end

@inline function invariant_permutation(@nospecialize(x::Tuple), @nospecialize(y::Tuple))
    if y === x === ntuple(static, StaticInt(nfields(x)))
        return True()
    else
        return False()
    end
end

@inline nstatic(::Val{N}) where {N} = ntuple(StaticInt, Val(N))

permute(@nospecialize(x::Tuple), @nospecialize(perm::Val)) = permute(x, static(perm))
@inline function permute(@nospecialize(x::Tuple), @nospecialize(perm::Tuple))
    if invariant_permutation(nstatic(Val(nfields(x))), perm) === False()
        return eachop(getindex, perm, x)
    else
        return x
    end
end

"""
    eachop(op, iterator::Tuple, args...) -> Tuple

Apply `op(args..., index)` to each `index` in `iterator`, preserving one inferred tuple
element per call. This is a developer API for implementing operations over statically known
dimensions.

# Arguments

- `op`: The function to invoke for each index.
- `iterator::Tuple`: The indices to pass as the final argument to `op`.
- `args...`: Leading arguments forwarded unchanged to each invocation.

# Returns

- `Tuple`: The ordered results of applying `op` to every index.

# Examples

```julia
julia> using Static

julia> Static.eachop(getindex, (static(3), static(1)), (:a, :b, :c))
(:c, :a)
```
"""
@inline function eachop(op::F, itr::Tuple{T, Vararg{Any}}, args::Vararg{Any}) where {F, T}
    return (op(args..., first(itr)), eachop(op, Base.tail(itr), args...)...)
end
eachop(::F, ::Tuple{}, args::Vararg{Any}) where {F} = ()

"""
    eachop_tuple(op, iterator::Tuple, arg, args...) -> Type{<:Tuple}

Construct the tuple type whose element at each position is
`op(arg, args..., iterator[position])`. This is a developer API for generating inferred
tuple types from static indices.

# Arguments

- `op`: A function that returns a type for each static index.
- `iterator::Tuple`: Static indices controlling the generated tuple elements.
- `arg`: The first argument passed to `op`. Put a tuple type here rather than in `args` to
  preserve inference.
- `args...`: Additional arguments forwarded to `op` before the index.

# Returns

- `Type{<:Tuple}`: A concrete tuple type containing each computed type.

# Examples

```julia
julia> using Static

julia> field_at(T, i) = fieldtype(T, dynamic(i));

julia> Static.eachop_tuple(field_at, (static(1), static(2)), Tuple{String, Float64})
Tuple{String, Float64}
```
"""
eachop_tuple(op, itr, arg, args...) = _eachop_tuple(op, itr, arg, args)
@generated function _eachop_tuple(op, ::I, arg, args::A) where {A, I}
    t = Expr(:curly, Tuple)
    narg = length(A.parameters)
    for p in I.parameters
        call_expr = Expr(:call, :op, :arg)
        if narg > 0
            for i in 1:narg
                push!(call_expr.args, :(getfield(args, $i)))
            end
        end
        push!(call_expr.args, :(StaticInt{$(p.parameters[1])}()))
        push!(t.args, call_expr)
    end
    return Expr(:block, Expr(:meta, :inline), t)
end

#=
    find_first_eq(x, collection::Tuple)

Finds the position in the tuple `collection` that is exactly equal (i.e. `===`) to `x`.
If `x` and `collection` are static (`is_static`) and `x` is in `collection` then the return
value is a `StaticInt`.
=#
@generated function find_first_eq(x::X, itr::I) where {X, N, I <: Tuple{Vararg{Any, N}}}
    # we avoid incidental code gen when evaluated a tuple of known values by iterating
    #  through `I.parameters` instead of `known(I)`.
    index = known(X) === nothing ? nothing : findfirst(==(X), I.parameters)
    if index !== nothing
        return :($(static(index)))
    end
    result = :(nothing)
    for d in N:-1:1
        result = :(dynamic(x) == dynamic(getfield(itr, $d)) ? $d : $result)
    end
    return result
end

function Base.invperm(p::Tuple{StaticInt, Vararg{StaticInt, N}}) where {N}
    return map(Base.Fix2(find_first_eq, p), ntuple(static, StaticInt(N + 1)))
end

"""
    reduce_tup(f, values::Tuple)

Reduce a nonempty tuple using a compile-time balanced evaluation tree. This developer API
can provide better inference and inlining than a generic reduction when tuple length is
known.

# Arguments

- `f`: A binary function used to combine tuple elements.
- `values::Tuple`: The nonempty tuple to reduce.

# Returns

- The result of reducing `values` with `f` while preserving the tuple's inferred types.

# Examples

```julia
julia> using Static

julia> Static.reduce_tup(+, (static(1), static(2), static(3)))
static(6)

julia> Static.reduce_tup(max, (2, 5, 3))
5
```
"""
@generated function reduce_tup(f::F, inds::Tuple{Vararg{Any, N}}) where {F, N}
    q = Expr(:block, Expr(:meta, :inline, :propagate_inbounds))
    if N == 1
        push!(q.args, :(inds[1]))
        return q
    end
    syms = Vector{Symbol}(undef, N)
    i = 0
    for n in 1:N
        syms[n] = iₙ = Symbol(:i_, (i += 1))
        push!(q.args, Expr(:(=), iₙ, Expr(:ref, :inds, n)))
    end
    W = 1 << (8sizeof(N) - 2 - leading_zeros(N))
    while W > 0
        _N = length(syms)
        for _ in (2W):W:_N
            for w in 1:W
                new_sym = Symbol(:i_, (i += 1))
                push!(q.args, Expr(:(=), new_sym, Expr(:call, :f, syms[w], syms[w + W])))
                syms[w] = new_sym
            end
            deleteat!(syms, (1 + W):(2W))
        end
        W >>>= 1
    end
    return q
end

# This method assumes that `f` retrieves compile time information and `g` is the fall back
# for the corresponding dynamic method. If the `f(x)` doesn't return `nothing` that means
# the value is known and compile time and returns `static(f(x))`.
@inline function maybe_static(f::F, g::G, x) where {F, G}
    L = f(x)
    if L === nothing
        return g(x)
    else
        return static(L)
    end
end

"""
    eq(x, y) -> Union{Bool, True, False}
    eq(x) -> Function

Compare values with `==`, returning a [`StaticBool`](@ref) when both inputs are static. The
one-argument form creates a function equivalent to `y -> eq(y, x)`.

# Arguments

- `x`, `y`: Values to compare.

# Returns

- `True` or `False` for two static inputs, `Bool` otherwise, or a comparison function from
  the one-argument form.

# Examples

```julia
julia> using Static

julia> Static.eq(static(2), static(2))
True()

julia> Static.eq(2)(2)
true
```
"""
eq(x::X, y::Y) where {X, Y} = ifelse(is_static(X) & is_static(Y), static, identity)(x == y)
eq(x) = Base.Fix2(eq, x)

"""
    ne(x, y) -> Union{Bool, True, False}
    ne(x) -> Function

Compare values with `!=`, returning a [`StaticBool`](@ref) when both inputs are static. The
one-argument form creates a function equivalent to `y -> ne(y, x)`.

# Arguments

- `x`, `y`: Values to compare.

# Returns

- `True` or `False` for two static inputs, `Bool` otherwise, or a comparison function from
  the one-argument form.

# Examples

```julia
julia> using Static

julia> Static.ne(static(2), static(3))
True()

julia> Static.ne(2)(2)
false
```
"""
ne(x::X, y::Y) where {X, Y} = !eq(x, y)
ne(x) = Base.Fix2(ne, x)

"""
    gt(x, y) -> Union{Bool, True, False}
    gt(x) -> Function

Compare values with `>`, returning a [`StaticBool`](@ref) when both inputs are static. The
one-argument form creates a function equivalent to `y -> gt(y, x)`.

# Arguments

- `x`, `y`: Values to compare.

# Returns

- `True` or `False` for two static inputs, `Bool` otherwise, or a comparison function from
  the one-argument form.

# Examples

```julia
julia> using Static

julia> Static.gt(static(3), static(2))
True()

julia> Static.gt(2)(3)
true
```
"""
gt(x::X, y::Y) where {X, Y} = ifelse(is_static(X) & is_static(Y), static, identity)(x > y)
gt(x) = Base.Fix2(gt, x)

"""
    ge(x, y) -> Union{Bool, True, False}
    ge(x) -> Function

Compare values with `>=`, returning a [`StaticBool`](@ref) when both inputs are static. The
one-argument form creates a function equivalent to `y -> ge(y, x)`.

# Arguments

- `x`, `y`: Values to compare.

# Returns

- `True` or `False` for two static inputs, `Bool` otherwise, or a comparison function from
  the one-argument form.

# Examples

```julia
julia> using Static

julia> Static.ge(static(2), static(2))
True()

julia> Static.ge(2)(1)
false
```
"""
ge(x::X, y::Y) where {X, Y} = ifelse(is_static(X) & is_static(Y), static, identity)(x >= y)
ge(x) = Base.Fix2(ge, x)

"""
    le(x, y) -> Union{Bool, True, False}
    le(x) -> Function

Compare values with `<=`, returning a [`StaticBool`](@ref) when both inputs are static. The
one-argument form creates a function equivalent to `y -> le(y, x)`.

# Arguments

- `x`, `y`: Values to compare.

# Returns

- `True` or `False` for two static inputs, `Bool` otherwise, or a comparison function from
  the one-argument form.

# Examples

```julia
julia> using Static

julia> Static.le(static(2), static(3))
True()

julia> Static.le(2)(1)
true
```
"""
le(x::X, y::Y) where {X, Y} = ifelse(is_static(X) & is_static(Y), static, identity)(x <= y)
le(x) = Base.Fix2(le, x)

"""
    lt(x, y) -> Union{Bool, True, False}
    lt(x) -> Function

Compare values with `<`, returning a [`StaticBool`](@ref) when both inputs are static. The
one-argument form creates a function equivalent to `y -> lt(y, x)`.

# Arguments

- `x`, `y`: Values to compare.

# Returns

- `True` or `False` for two static inputs, `Bool` otherwise, or a comparison function from
  the one-argument form.

# Examples

```julia
julia> using Static

julia> Static.lt(static(2), static(3))
True()

julia> Static.lt(2)(3)
false
```
"""
lt(x::X, y::Y) where {X, Y} = ifelse(is_static(X) & is_static(Y), static, identity)(x < y)
lt(x) = Base.Fix2(lt, x)

"""
    mul(x) -> Function

Create a function equivalent to `y -> y * x`. This developer API is useful when composing
inferred arithmetic transformations.

# Arguments

- `x`: The fixed right-hand factor.

# Returns

- A callable `Base.Fix2` that multiplies its argument by `x`.

# Examples

```julia
julia> using Static

julia> Static.mul(static(3))(static(2))
static(6)
```
"""
mul(x) = Base.Fix2(*, x)

"""
    add(x) -> Function

Create a function equivalent to `y -> y + x`. This developer API is useful when composing
inferred arithmetic transformations.

# Arguments

- `x`: The fixed right-hand addend.

# Returns

- A callable `Base.Fix2` that adds `x` to its argument.

# Examples

```julia
julia> using Static

julia> Static.add(static(3))(static(2))
static(5)
```
"""
add(x) = Base.Fix2(+, x)

const Mul{X} = Base.Fix2{typeof(*), X}
const Add{X} = Base.Fix2{typeof(+), X}

Base.:∘(::Add{StaticInt{X}}, ::Add{StaticInt{Y}}) where {X, Y} = Base.Fix2(+, static(X + Y))
Base.:∘(x::Mul{Int}, ::Add{StaticInt{0}}) = x
Base.:∘(x::Mul{StaticInt{X}}, ::Add{StaticInt{0}}) where {X} = x
Base.:∘(x::Mul{StaticInt{0}}, ::Add{StaticInt{0}}) = x
Base.:∘(x::Mul{StaticInt{1}}, ::Add{StaticInt{0}}) = x
Base.:∘(x::Mul{StaticInt{0}}, y::Add{StaticInt{Y}}) where {Y} = x
Base.:∘(::Mul{StaticInt{1}}, y::Add{StaticInt{Y}}) where {Y} = y
Base.:∘(x::Mul{StaticInt{0}}, y::Add{Int}) = x
Base.:∘(::Mul{StaticInt{1}}, y::Add{Int}) = y
Base.:∘(::Mul{StaticInt{X}}, ::Mul{StaticInt{Y}}) where {X, Y} = Base.Fix2(*, static(X * Y))
Base.:∘(x::Mul{StaticInt{0}}, y::Mul{Int}) = x
Base.:∘(::Mul{Int}, y::Mul{StaticInt{0}}) = y
Base.:∘(::Mul{StaticInt{1}}, y::Mul{Int}) = y
Base.:∘(x::Mul{Int}, ::Mul{StaticInt{1}}) = x

# length
Base.length(@nospecialize(x::NDIndex))::Int = length(Tuple(x))
Base.length(::Type{<:NDIndex{N}}) where {N} = N

# indexing
Base.@propagate_inbounds function Base.getindex(x::NDIndex{N, T}, i::Int)::Int where {N, T}
    return Int(getfield(Tuple(x), i))
end
Base.@propagate_inbounds function Base.getindex(
        x::NDIndex{N, T},
        i::StaticInt{I}
    ) where {N, T, I}
    return getfield(Tuple(x), I)
end

# Base.get(A::AbstractArray, I::CartesianIndex, default) = get(A, I.I, default)
# eltype(::Type{T}) where {T<:CartesianIndex} = eltype(fieldtype(T, :I))

Base.setindex(x::NDIndex, i, j) = NDIndex(Base.setindex(Tuple(x), i, j))

# equality
Base.:(==)(@nospecialize(x::NDIndex), @nospecialize(y::NDIndex)) = ==(Tuple(x), Tuple(y))

# zeros and ones
Base.zero(@nospecialize(x::NDIndex)) = zero(typeof(x))
function Base.zero(@nospecialize(T::Type{<:NDIndex}))
    return NDIndex(ntuple(_ -> static(0), Val(length(T))))
end
Base.oneunit(@nospecialize(x::NDIndex)) = oneunit(typeof(x))
function Base.oneunit(@nospecialize(T::Type{<:NDIndex}))
    return NDIndex(ntuple(_ -> static(1), Val(length(T))))
end

@inline function Base.IteratorsMD.split(i::NDIndex, V::Val)
    i, j = Base.IteratorsMD.split(Tuple(i), V)
    return NDIndex(i), NDIndex(j)
end

# arithmetic, min/max
@inline Base.:(-)(@nospecialize(i::NDIndex)) = NDIndex(map(-, Tuple(i)))
@inline function Base.:(+)(@nospecialize(i1::NDIndex), @nospecialize(i2::NDIndex))
    return NDIndex(map(+, Tuple(i1), Tuple(i2)))
end
@inline function Base.:(-)(@nospecialize(i1::NDIndex), @nospecialize(i2::NDIndex))
    return NDIndex(map(-, Tuple(i1), Tuple(i2)))
end
@inline function Base.min(@nospecialize(i1::NDIndex), @nospecialize(i2::NDIndex))
    return NDIndex(map(min, Tuple(i1), Tuple(i2)))
end
@inline function Base.max(@nospecialize(i1::NDIndex), @nospecialize(i2::NDIndex))
    return NDIndex(map(max, Tuple(i1), Tuple(i2)))
end
@inline function Base.:(*)(a::Integer, @nospecialize(i::NDIndex))
    return NDIndex(map(x -> a * x, Tuple(i)))
end
@inline Base.:(*)(@nospecialize(i::NDIndex), a::Integer) = *(a, i)

Base.CartesianIndex(@nospecialize(x::NDIndex)) = dynamic(x)

# comparison
@inline function Base.isless(@nospecialize(x::NDIndex), @nospecialize(y::NDIndex))
    return Bool(_isless(static(0), Tuple(x), Tuple(y)))
end

function lt(@nospecialize(x::NDIndex), @nospecialize(y::NDIndex))
    return _isless(static(0), Tuple(x), Tuple(y))
end

_final_isless(c::Int) = c === 1
_final_isless(::StaticInt{N}) where {N} = static(false)
_final_isless(::StaticInt{1}) = static(true)
_isless(c::C, x::Tuple{}, y::Tuple{}) where {C} = _final_isless(c)
function _isless(c::C, x::Tuple, y::Tuple) where {C}
    return _isless(icmp(c, x, y), Base.front(x), Base.front(y))
end
icmp(::StaticInt{0}, x::Tuple, y::Tuple) = icmp(last(x), last(y))
icmp(::StaticInt{N}, x::Tuple, y::Tuple) where {N} = static(N)
function icmp(cmp::Int, x::Tuple, y::Tuple)
    if cmp === 0
        return icmp(Int(last(x)), Int(last(y)))
    else
        return cmp
    end
end
icmp(a, b) = _icmp(lt(a, b), a, b)
_icmp(x::StaticBool, a, b) = ifelse(x, static(1), __icmp(eq(a, b)))
_icmp(x::Bool, a, b) = ifelse(x, 1, __icmp(a == b))
__icmp(x::StaticBool) = ifelse(x, static(0), static(-1))
__icmp(x::Bool) = ifelse(x, 0, -1)

#  Necessary for compatibility with Base
# In simple cases, we know that we don't need to use axes(A). Optimize those
# until Julia gets smart enough to elide the call on its own:
@inline function Base.to_indices(A, inds, I::Tuple{NDIndex, Vararg{Any}})
    return to_indices(A, inds, (Tuple(I[1])..., Base.tail(I)...))
end
# But for arrays of CartesianIndex, we just skip the appropriate number of inds
@inline function Base.to_indices(
        A, inds,
        I::Tuple{AbstractArray{NDIndex{N, J}}, Vararg{Any}}
    ) where {
        N,
        J,
    }
    _, indstail = Base.IteratorsMD.split(inds, Val(N))
    return (Base.to_index(A, I[1]), to_indices(A, indstail, Base.tail(I))...)
end

function Base.show(
        @nospecialize(io::IO), @nospecialize(
            x::Union{
                StaticNumber, StaticSymbol, NDIndex,
            }
        )
    )
    return show(io, MIME"text/plain"(), x)
end
function Base.show(
        @nospecialize(io::IO), ::MIME"text/plain", @nospecialize(
            x::Union{
                StaticNumber, StaticSymbol,
            }
        )
    )
    print(io, "static(" * repr(known(typeof(x))) * ")")
    return nothing
end
function Base.show(@nospecialize(io::IO), m::MIME"text/plain", @nospecialize(x::NDIndex))
    print(io, "NDIndex")
    show(io, m, Tuple(x))
    return nothing
end

include("ranges.jl")

end
