"""
    OptionallyStaticUnitRange(start, stop) <: AbstractUnitRange{Int}

Represent a unit range whose first and last values may independently be runtime `Int`
values or compile-time [`StaticInt`](@ref) values. Range syntax constructs this type when
either endpoint is static.

# Arguments

- `start`: The first integer in the range.
- `stop`: The inclusive upper endpoint. It may be less than `start` for an empty range.

# Returns

- `OptionallyStaticUnitRange`: An `AbstractUnitRange{Int}` preserving any static endpoint.

# Examples

```julia
julia> using Static

julia> range = static(1):10
static(1):10

julia> (first(range), last(range), length(range))
(1, 10, 10)
```

Use the standard range accessors instead of relying on the concrete field layout.
"""
struct OptionallyStaticUnitRange{F <: IntType, L <: IntType} <:
    AbstractUnitRange{Int}
    start::F
    stop::L

    global function _OptionallyStaticUnitRange(start::IntType, stop::IntType)
        return new{typeof(start), typeof(stop)}(start, stop)
    end
end

"""
    OptionallyStaticStepRange(start, step, stop) <: OrdinalRange{Int,Int}

Represent an integer range whose start, step, and stop may independently carry static
information. The constructor normalizes `stop` to the final reachable value, matching
Julia's ordinary stepped-range behavior.

# Arguments

- `start`: The first integer in the range.
- `step`: The nonzero increment between values.
- `stop`: An inclusive bound used to determine the final reachable value.

# Returns

- `OptionallyStaticStepRange`: An `OrdinalRange{Int, Int}` preserving static inputs.

# Throws

- `ArgumentError`: If `step` is zero.

# Examples

```julia
julia> using Static

julia> static(2):static(2):10
static(2):static(2):10

julia> Tuple(static(2):static(2):10)
(2, 4, 6, 8, 10)
```
"""
struct OptionallyStaticStepRange{
        F <: IntType, S <: IntType,
        L <: IntType,
    } <: OrdinalRange{Int, Int}
    start::F
    step::S
    stop::L

    global function _OptionallyStaticStepRange(
            @nospecialize(start::IntType),
            @nospecialize(step::IntType),
            @nospecialize(stop::IntType)
        )
        return new{typeof(start), typeof(step), typeof(stop)}(start, step, stop)
    end
end
@noinline function OptionallyStaticStepRange(
        @nospecialize(start::IntType),
        ::StaticInt{0},
        @nospecialize(stop::IntType)
    )
    throw(ArgumentError("step cannot be zero"))
end
# we don't need to check the `stop` if we know it acts like a unit range
function OptionallyStaticStepRange(
        @nospecialize(start::IntType),
        step::StaticInt{1},
        @nospecialize(stop::IntType)
    )
    return _OptionallyStaticStepRange(start, step, stop)
end
function OptionallyStaticStepRange(
        @nospecialize(start::IntType),
        @nospecialize(step::StaticInt),
        @nospecialize(stop::IntType)
    )
    return _OptionallyStaticStepRange(start, step, _steprange_last(start, step, stop))
end
function OptionallyStaticStepRange(start, step, stop)
    return OptionallyStaticStepRange(IntType(start), IntType(step), IntType(stop))
end
function OptionallyStaticStepRange(
        @nospecialize(start::IntType),
        step::Int,
        @nospecialize(stop::IntType)
    )
    return if step === 0
        throw(ArgumentError("step cannot be zero"))
    else
        _OptionallyStaticStepRange(start, step, _steprange_last(start, step, stop))
    end
end
OptionallyStaticStepRange(@nospecialize x::OptionallyStaticStepRange) = x
function OptionallyStaticStepRange(x::Union{Base.Slice, Base.IdentityUnitRange})
    return OptionallyStaticStepRange(x.indices)
end
function OptionallyStaticStepRange(x::Base.OneTo)
    return _OptionallyStaticStepRange(static(1), static(1), Int(last(x)))
end
function OptionallyStaticStepRange(x::OptionallyStaticUnitRange)
    return _OptionallyStaticStepRange(getfield(x, :start), static(1), getfield(x, :stop))
end
function OptionallyStaticStepRange(x::AbstractUnitRange)
    return _OptionallyStaticStepRange(Int(first(x)), static(1), Int(last(x)))
end
function OptionallyStaticStepRange(x::AbstractRange)
    return _OptionallyStaticStepRange(Int(first(x)), Int(step(x)), Int(last(x)))
end

# to make StepRange constructor inlineable, so optimizer can see `step` value
@inline function _steprange_last(start::StaticInt, step::StaticInt, stop::StaticInt)
    return StaticInt(_steprange_last(Int(start), Int(step), Int(stop)))
end
@inline function _steprange_last(
        start::Union{StaticInt, Int},
        step::Union{StaticInt, Int},
        stop::Union{StaticInt, Int}
    )
    return _steprange_last(Int(start), Int(step), Int(stop))
end
@inline function _steprange_last(start::Int, step::Int, stop::Int)
    if stop === start
        return stop
    elseif step > 0
        if stop > start
            return stop - rem(stop - start, step)
        else
            return start - 1
        end
    else
        if stop > start
            return start + 1
        else
            return stop + rem(start - stop, -step)
        end
    end
end

OptionallyStaticUnitRange(@nospecialize x::OptionallyStaticUnitRange) = x
OptionallyStaticUnitRange(x::Base.OneTo) = OptionallyStaticUnitRange(static(1), Int(last(x)))
function OptionallyStaticUnitRange(x::Union{Base.Slice, Base.IdentityUnitRange})
    return OptionallyStaticUnitRange(x.indices)
end
function OptionallyStaticUnitRange(x::OptionallyStaticStepRange)
    assert_unit_step(step(x))
    return _OptionallyStaticUnitRange(getfield(x, :start), getfield(x, :stop))
end
function OptionallyStaticUnitRange(x::AbstractRange)
    assert_unit_step(step(x))
    return _OptionallyStaticUnitRange(first(x), last(x))
end
function OptionallyStaticUnitRange{F, L}(x::AbstractRange) where {F, L}
    return OptionallyStaticUnitRange(x)
end
function OptionallyStaticUnitRange(start::IntType, stop::IntType)
    return _OptionallyStaticUnitRange(start, stop)
end
function OptionallyStaticUnitRange(start, stop)
    return OptionallyStaticUnitRange(IntType(start), IntType(stop))
end
function OptionallyStaticUnitRange{StaticInt{F}, StaticInt{L}}() where {F, L}
    return _OptionallyStaticUnitRange(StaticInt{F}(), StaticInt{L}())
end
function assert_unit_step(s::Int)
    s == 1 && return nothing
    errmsg(x) = throw(ArgumentError(LazyString("step must be 1, got ", s))) # avoid GC frame
    return errmsg(s)
end

"""
    SUnitRange(start::Int, stop::Int)
    SUnitRange{F, L}()

Construct a [`OptionallyStaticUnitRange`](@ref) whose endpoints are both known statically.

# Arguments

- `start::Int`: The statically encoded first value.
- `stop::Int`: The statically encoded inclusive endpoint.

# Returns

- `SUnitRange{start, stop}`: A fully static unit range.

# Examples

```julia
julia> using Static

julia> Static.SUnitRange(2, 4)
static(2):static(4)
```
"""
const SUnitRange{F, L} = OptionallyStaticUnitRange{StaticInt{F}, StaticInt{L}}
SUnitRange(start::Int, stop::Int) = SUnitRange{start, stop}()

"""
    SOneTo(n::Int)
    SOneTo{N}()

Construct a one-based unit range whose upper endpoint is known statically. This is the
static analogue of `Base.OneTo` and is useful for statically sized axes.

# Arguments

- `n::Int`: The statically encoded inclusive endpoint.

# Returns

- `SOneTo{n}`: The range `static(1):static(n)`.

# Examples

```julia
julia> using Static

julia> Static.SOneTo(3)
static(1):static(3)
```
"""
const SOneTo{L} = SUnitRange{1, L}
SOneTo(n::Int) = SOneTo{n}()
Base.oneto(::StaticInt{N}) where {N} = SOneTo{N}()

"""
    OptionallyStaticRange{F, L}

A union of [`OptionallyStaticUnitRange`](@ref) and [`OptionallyStaticStepRange`](@ref)
whose start has type `F` and whose stop has type `L`. Use this alias in dispatch that accepts
either unit or stepped optionally static ranges.

# Type Parameters

- `F`: The type of the first value, either `Int` or `StaticInt`.
- `L`: The type of the final value, either `Int` or `StaticInt`.

# Examples

```julia
julia> using Static

julia> r = static(1):static(2):static(5)
static(1):static(2):static(5)

julia> r isa Static.OptionallyStaticRange{StaticInt{1}, StaticInt{5}}
true
```
"""
const OptionallyStaticRange{
    F, L,
} = Union{
    OptionallyStaticUnitRange{F, L},
    OptionallyStaticStepRange{F, <:Any, L},
}

Base.first(x::OptionallyStaticRange{Int}) = getfield(x, :start)
Base.first(::OptionallyStaticRange{StaticInt{F}}) where {F} = F
Base.step(x::OptionallyStaticStepRange{<:Any, Int}) = getfield(x, :step)
Base.step(::OptionallyStaticStepRange{<:Any, StaticInt{S}}) where {S} = S
Base.last(x::OptionallyStaticRange{<:Any, Int}) = getfield(x, :stop)
Base.last(::OptionallyStaticRange{<:Any, StaticInt{L}}) where {L} = L

# FIXME this line causes invalidations
Base.:(:)(L::Integer, ::StaticInt{U}) where {U} = OptionallyStaticUnitRange(L, StaticInt(U))
Base.:(:)(::StaticInt{L}, U::Integer) where {L} = OptionallyStaticUnitRange(StaticInt(L), U)
function Base.:(:)(::StaticInt{L}, ::StaticInt{U}) where {L, U}
    return OptionallyStaticUnitRange(StaticInt(L), StaticInt(U))
end
function Base.:(:)(::StaticInt{F}, ::StaticInt{S}, ::StaticInt{L}) where {F, S, L}
    return OptionallyStaticStepRange(StaticInt(F), StaticInt(S), StaticInt(L))
end
function Base.:(:)(start::Integer, ::StaticInt{S}, ::StaticInt{L}) where {S, L}
    return OptionallyStaticStepRange(start, StaticInt(S), StaticInt(L))
end
function Base.:(:)(::StaticInt{F}, ::StaticInt{S}, stop::Integer) where {F, S}
    return OptionallyStaticStepRange(StaticInt(F), StaticInt(S), stop)
end
function Base.:(:)(::StaticInt{F}, step::Integer, ::StaticInt{L}) where {F, L}
    return OptionallyStaticStepRange(StaticInt(F), step, StaticInt(L))
end
function Base.:(:)(start::Integer, step::Integer, ::StaticInt{L}) where {L}
    return OptionallyStaticStepRange(start, step, StaticInt(L))
end
function Base.:(:)(start::Integer, ::StaticInt{S}, stop::Integer) where {S}
    return OptionallyStaticStepRange(start, StaticInt(S), stop)
end
function Base.:(:)(::StaticInt{F}, step::Integer, stop::Integer) where {F}
    return OptionallyStaticStepRange(StaticInt(F), step, stop)
end
Base.:(:)(start::StaticInt{F}, ::StaticInt{1}, stop::StaticInt{L}) where {F, L} = start:stop
Base.:(:)(start::Integer, ::StaticInt{1}, stop::StaticInt{L}) where {L} = start:stop
Base.:(:)(start::StaticInt{F}, ::StaticInt{1}, stop::Integer) where {F} = start:stop
function Base.:(:)(start::Integer, ::StaticInt{1}, stop::Integer)
    return OptionallyStaticUnitRange(start, stop)
end

Base.isempty(r::OptionallyStaticUnitRange) = first(r) > last(r)
@inline function Base.isempty(x::OptionallyStaticStepRange)
    start = first(x)
    stop = last(x)
    if start === stop
        return false
    else
        s = step(x)
        s > 0 ? start > stop : start < stop
    end
end

function Base.checkindex(
        ::Type{Bool},
        ::SUnitRange{F1, L1},
        ::SUnitRange{F2, L2}
    ) where {F1, L1, F2, L2}
    return (F1::Int <= F2::Int) && (L1::Int >= L2::Int)
end

function Base.getindex(
        r::OptionallyStaticUnitRange,
        i::AbstractUnitRange{<:Integer},
    )
    s = OptionallyStaticUnitRange(i)
    @boundscheck checkbounds(r, s)
    f = getfield(r, :start)
    fnew = f - one(f)
    return (fnew + getfield(s, :start)):(fnew + getfield(s, :stop))
end

function Base.getindex(x::OptionallyStaticUnitRange{StaticInt{1}}, i::Int)
    @boundscheck checkbounds(x, i)
    return i
end
function Base.getindex(x::OptionallyStaticUnitRange, i::Int)
    val = first(x) + (i - 1)
    @boundscheck ((i < 1) || val > last(x)) && throw(BoundsError(x, i))
    return val::Int
end

## length
@inline function Base.length(x::OptionallyStaticUnitRange)
    start = first(x)
    stop = last(x)
    return start > stop ? 0 : stop - start + 1
end
Base.length(r::OptionallyStaticStepRange) = _range_length(first(r), step(r), last(r))
@inline function _range_length(start::Int, s::Int, stop::Int)
    return if s > 0
        stop < start ? 0 : div(stop - start, s) + 1
    else
        stop > start ? 0 : div(start - stop, -s) + 1
    end
end

Base.AbstractUnitRange{Int}(r::OptionallyStaticUnitRange) = r
function Base.AbstractUnitRange{T}(r::OptionallyStaticUnitRange) where {T}
    if isa(getfield(r, :start), StaticInt{1}) && T <: Integer
        return Base.OneTo{T}(T(last(r)))
    else
        return UnitRange{T}(T(first(r)), T(last(r)))
    end
end

Base.isdone(x::OptionallyStaticRange, state::Int) = state === last(x)
function _next(x::OptionallyStaticRange)
    new_state = first(x)
    return (new_state, new_state)
end
@inline function _next(@nospecialize(x::OptionallyStaticUnitRange), state::Int)
    new_state = state + 1
    return (new_state, new_state)
end
@inline function _next(x::OptionallyStaticStepRange, state::Int)
    new_state = state + step(x)
    return (new_state, new_state)
end
@inline Base.iterate(x::OptionallyStaticRange) = isempty(x) ? nothing : _next(x)
@inline function Base.iterate(x::OptionallyStaticRange, s::Int)
    return Base.isdone(x, s) ? nothing : _next(x, s)
end

Base.to_shape(x::OptionallyStaticRange) = length(x)
Base.to_shape(x::Base.Slice{T}) where {T <: OptionallyStaticRange} = length(x)
Base.axes(S::Base.Slice{<:OptionallyStaticUnitRange{StaticInt{1}}}) = (S.indices,)
Base.axes(S::Base.Slice{<:OptionallyStaticRange}) = (Base.IdentityUnitRange(S.indices),)

Base.axes(x::OptionallyStaticRange) = (Base.axes1(x),)
function Base.axes1(x::OptionallyStaticUnitRange)
    return OptionallyStaticUnitRange(StaticInt(1), length(x))
end
Base.axes1(x::OptionallyStaticUnitRange{StaticInt{1}}) = x
function Base.axes1(x::OptionallyStaticUnitRange{StaticInt{F}, StaticInt{L}}) where {F, L}
    return OptionallyStaticUnitRange(StaticInt(1), StaticInt(L - F + 1))
end
function Base.axes1(x::OptionallyStaticStepRange)
    return OptionallyStaticUnitRange(StaticInt(1), length(x))
end
function Base.axes1(
        x::OptionallyStaticStepRange{
            StaticInt{F}, StaticInt{S}, StaticInt{L},
        }
    ) where {
        F,
        S,
        L,
    }
    return OptionallyStaticUnitRange(StaticInt(1), StaticInt(_range_length(F, S, L)))
end
Base.axes1(x::Base.Slice{<:OptionallyStaticUnitRange{One}}) = x.indices
Base.axes1(x::Base.Slice{<:OptionallyStaticRange}) = Base.IdentityUnitRange(x.indices)

function Base.:(-)(r::OptionallyStaticRange)
    s = isa(r, OptionallyStaticStepRange) ? -getfield(r, :step) : -One()
    return (-getfield(r, :start)):s:(-getfield(r, :stop))
end

function Base.reverse(x::OptionallyStaticUnitRange)
    return _OptionallyStaticStepRange(getfield(x, :stop), StaticInt(-1), getfield(x, :start))
end
function Base.reverse(x::OptionallyStaticStepRange)
    return _OptionallyStaticStepRange(getfield(x, :stop), -getfield(x, :step), getfield(x, :start))
end

Base.show(io::IO, @nospecialize(x::OptionallyStaticRange)) = show(io, MIME"text/plain"(), x)
function Base.show(io::IO, ::MIME"text/plain", @nospecialize(r::OptionallyStaticUnitRange))
    return print(io, "$(getfield(r, :start)):$(getfield(r, :stop))")
end
function Base.show(io::IO, ::MIME"text/plain", @nospecialize(r::OptionallyStaticStepRange))
    return print(io, "$(getfield(r, :start)):$(getfield(r, :step)):$(getfield(r, :stop))")
end

# we overload properties because occasionally Base assumes that abstract range types have
# the same exact same set up as native types where `x.start === first(x)`
@inline function Base.getproperty(x::OptionallyStaticRange, s::Symbol)
    if s === :start
        return first(x)
    elseif s === :step
        return step(x)
    elseif s === :stop
        return last(x)
    else
        error("$x has no property $s")
    end
end

# The open argument is restricted to `AbstractUnitRange{<:Integer}` (the only thing a
# broadcast axis ever is) rather than `Any`. A bare `::Any` slot here forms an
# unresolvable diagonal ambiguity with despecialization methods such as
# CommonWorldInvalidations' `axistype(::Any, ::AbstractUnitRange{Float64})`.
function Base.Broadcast.axistype(
        r::OptionallyStaticUnitRange{StaticInt{1}}, ::AbstractUnitRange{<:Integer}
    )
    return Base.OneTo(last(r))
end
function Base.Broadcast.axistype(
        ::AbstractUnitRange{<:Integer}, r::OptionallyStaticUnitRange{StaticInt{1}}
    )
    return Base.OneTo(last(r))
end
function Base.Broadcast.axistype(
        r::OptionallyStaticUnitRange{StaticInt{1}},
        ::OptionallyStaticUnitRange{StaticInt{1}}
    )
    return Base.OneTo(last(r))
end
function Base.similar(
        ::Type{<:Array{T}},
        axes::Tuple{
            OptionallyStaticUnitRange{StaticInt{1}},
            Vararg{
                Union{
                    Base.OneTo,
                    OptionallyStaticUnitRange{StaticInt{1}},
                },
            },
        }
    ) where {
        T,
    }
    return Array{T}(undef, map(last, axes))
end
function Base.similar(
        ::Type{<:Array{T}},
        axes::Tuple{
            Base.OneTo, OptionallyStaticUnitRange{StaticInt{1}},
            Vararg{
                Union{
                    Base.OneTo,
                    OptionallyStaticUnitRange{StaticInt{1}},
                },
            },
        }
    ) where {
        T,
    }
    return Array{T}(undef, map(last, axes))
end

function Base.first(x::OptionallyStaticUnitRange, n::IntType)
    n < 0 && throw(ArgumentError("Number of elements must be nonnegative"))
    start = getfield(x, :start)
    return OptionallyStaticUnitRange(start, min(start - one(start) + n, getfield(x, :stop)))
end
function Base.first(x::OptionallyStaticStepRange, n::IntType)
    n < 0 && throw(ArgumentError("Number of elements must be nonnegative"))
    start = getfield(x, :start)
    s = getfield(x, :step)
    stop = min(((n - one(n)) * s) + start, getfield(x, :stop))
    return OptionallyStaticStepRange(start, s, stop)
end
function Base.last(x::OptionallyStaticUnitRange, n::IntType)
    n < 0 && throw(ArgumentError("Number of elements must be nonnegative"))
    stop = getfield(x, :stop)
    return OptionallyStaticUnitRange(max(stop + one(stop) - n, getfield(x, :start)), stop)
end
function Base.last(x::OptionallyStaticStepRange, n::IntType)
    n < 0 && throw(ArgumentError("Number of elements must be nonnegative"))
    start = getfield(x, :start)
    s = getfield(x, :step)
    stop = getfield(x, :stop)
    return OptionallyStaticStepRange(max(stop + one(stop) - (n * s), start), s, stop)
end
