const DenseStringView = StringView{<:Union{DenseVector{UInt8}, <:FastContiguousSubArray{UInt8, 1, <:DenseVector{UInt8}}}}
const StringAndSub = Union{String, SubString{String}}
const StringViewAndSub = Union{StringView, SubString{<:StringView}}
const DenseStringViewAndSub = Union{DenseStringView, SubString{<:DenseStringView}}
const DenseUTF8String = Union{DenseStringViewAndSub, StringAndSub}
const UTF8String = Union{StringAndSub, StringViewAndSub}

StringView(v::AbstractVector{UInt8}) = StringView{typeof(v)}(v)
Vector{UInt8}(s::StringViewAndSub) = Vector{UInt8}(codeunits(s))
Array{UInt8}(s::StringViewAndSub) = Vector{UInt8}(s)
String(s::StringViewAndSub) = String(copyto!(StringVector(ncodeunits(s)), codeunits(s)))
copy(s::StringView) = StringView(copy(s.data))

function Symbol(s::DenseStringViewAndSub)
    return ccall(:jl_symbol_n, Ref{Symbol}, (Ptr{UInt8}, Int), s, ncodeunits(s))
end

pointer(s::DenseStringView) = pointer(s.data)
pointer(s::DenseStringView, i::Integer) = pointer(s.data, i)
pointer(x::SubString{<:DenseStringView}) = pointer(x.string) + x.offset
pointer(x::SubString{<:DenseStringView}, i::Integer) = pointer(x.string) + x.offset + (i - 1)

unsafe_convert(::Type{Ptr{UInt8}}, s::DenseStringViewAndSub) = pointer(s)
unsafe_convert(::Type{Ptr{Int8}}, s::DenseStringViewAndSub) = convert(Ptr{Int8}, pointer(s))

cconvert(::Type{Ptr{UInt8}}, s::DenseStringViewAndSub) = s
cconvert(::Type{Ptr{Int8}}, s::DenseStringViewAndSub) = s

"""
    reverse(s::AbstractString)::AbstractString

Reverses a string. Technically, this function reverses the codepoints in a string and its
main utility is for reversed-order string processing, especially for reversed
regular-expression searches. See also [`reverseind`](@ref) to convert indices in `s` to
indices in `reverse(s)` and vice-versa, and `graphemes` from module `Unicode` to
operate on user-visible "characters" (graphemes) rather than codepoints.
See also [`Iterators.reverse`](@ref) for
reverse-order iteration without making a copy. Custom string types must implement the
`reverse` function themselves and should typically return a string with the same normalization
and encoding to ensure that `reverseind` works; if they return a string with a different encoding,
they must also override `reverseind` for that string type to satisfy `s[reverseind(s,i)] == reverse(s)[i]`.

# Examples
```jldoctest
julia> reverse("JuliaLang")
"gnaLailuJ"
```

!!! note
    The examples below may be rendered differently on different systems.
    The comments indicate how they're supposed to be rendered

Combining characters can lead to surprising results:

```jldoctest
julia> reverse("ax̂e") # hat is above x in the input, above e in the output
"êxa"

julia> using Unicode

julia> join(reverse(collect(graphemes("ax̂e")))) # reverses graphemes; hat is above x in both in- and output
"ex̂a"
```
"""
function reverse(s::UTF8String)::String
    # Read characters forwards from `s` and write backwards to `out`
    out = _string_n(sizeof(s))
    offs = sizeof(s) + 1
    for c in s
        offs -= ncodeunits(c)
        if s isa StringViewAndSub
            # Since StringView is generic over the wrapped array, we could invoke UB
            # if we don't validate the array behaves as expected.
            offs < 1 && error("Invalid implementation of vector length")
        end
        __unsafe_string!(out, c, offs)
    end
    # note that for StringViewAndSub, we cannot return the same type of StringView
    # anyway since the data type may not be mutable, so we just return String
    return out
end

sizeof(s::StringView) = length(s.data)
ncodeunits(s::StringView) = length(s.data)
codeunit(::StringView) = UInt8
@propagate_inbounds codeunit(s::StringView, i::Integer) = s.data[i]
codeunits(s::StringView) = s.data
codeunits(s::SubString{<:StringView}) = @view s.string.data[(1 + s.offset):(s.offset + s.ncodeunits)]

# For UTF8 encoded strings, we can operate on codeunits directly.
# For non-UTF8 strings, we use the AbstractString fallback
cmp(a::UTF8String, b::UTF8String) = cmp(codeunits(a), codeunits(b))
==(a::UTF8String, b::UTF8String) = codeunits(a) == codeunits(b)

# Typemin and one is the empty string (multiplicative identity)
typemin(::Type{StringView{CodeUnits{UInt8, String}}}) = StringView(CodeUnits(""))
typemin(::Type{StringView{V}}) where V = StringView(V())
typemin(::T) where {T <: StringView} = typemin(T)
one(::Union{T, Type{T}}) where {T <: StringView} = typemin(T)
oneunit(::Union{T, Type{T}}) where {T <: StringView} = typemin(T)

# Forward to optimised isascii(::AbstractVector{UInt8})
isascii(s::StringViewAndSub) = isascii(codeunits(s))

# For dense string views, pointer-based hashing is faster than array based.
function hash(s::DenseStringViewAndSub, h::UInt)
    GC.@preserve s hash_bytes(pointer(s), ncodeunits(s), UInt64(h), HASH_SECRET) % UInt
end

# The canonical binary representation of strings is simply their byte content.
write(io::IO, s::StringViewAndSub) = write(io, codeunits(s))::Int
print(io::IO, s::StringViewAndSub) = (write(io, s); nothing)

@propagate_inbounds thisind(s::StringViewAndSub, i::Integer) = _thisind_str(s, Int(i)::Int)
@propagate_inbounds thisind(s::StringViewAndSub, i::Int) = _thisind_str(s, i)

@propagate_inbounds nextind(s::StringViewAndSub, i::Integer) = _nextind_str(s, Int(i)::Int)
@propagate_inbounds nextind(s::StringViewAndSub, i::Int) = _nextind_str(s, i)

isvalid(s::StringViewAndSub, i::Int) = checkbounds(Bool, s, i) && thisind(s, i) == i

# This is different from the String implementation, because when r is empty,
# we cannot just return the constant "".
@inline function getindex(s::StringView, r::UnitRange{Int})
    cu = codeunits(s)
    isempty(r) && return StringView(cu[1:0])
    i, j = first(r), last(r)
    @boundscheck begin
        checkbounds(cu, r)
        @inbounds isvalid(s, i) || string_index_err(s, i)
        @inbounds isvalid(s, j) || string_index_err(s, j)
    end
    j = nextind(s, j) - 1
    return StringView(cu[i:j])
end

function chomp(s::StringViewAndSub)
    cu = codeunits(s)
    ncu = length(cu)
    len = if iszero(ncu)
        0
    else
        has_lf = cu[ncu] == 0x0a
        two_bytes = ncu > 1
        has_cr = has_lf & two_bytes & (cu[ncu - two_bytes] == 0x0d)
        ncu - (has_lf + has_cr)
    end
    @inbounds raw_substring(s, 1, len)
end

function replace(io::IO, s::DenseStringViewAndSub, pat_f::Pair...; count = typemax(Int))
    return _replace_(io, s, pat_f, Int(count))
end
