# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
    Arrow.ArrowVector

An abstract type that subtypes `AbstractVector`. Each specific arrow array type
subtypes `ArrowVector`. See [`BoolVector`](@ref), [`Primitive`](@ref), [`List`](@ref),
[`Map`](@ref), [`FixedSizeList`](@ref), [`Struct`](@ref), [`DenseUnion`](@ref),
[`SparseUnion`](@ref), and [`DictEncoded`](@ref) for more details.
"""
abstract type ArrowVector{T} <: AbstractVector{T} end

Base.IndexStyle(::Type{A}) where {A<:ArrowVector} = Base.IndexLinear()
Base.similar(::Type{A}, dims::Dims) where {T,A<:ArrowVector{T}} = Vector{T}(undef, dims)
validitybitmap(x::ArrowVector) = x.validity
nullcount(x::ArrowVector) = validitybitmap(x).nc
getmetadata(x::ArrowVector) = x.metadata
Base.deleteat!(x::T, inds) where {T<:ArrowVector} = throw(
    ArgumentError("`$T` does not support `deleteat!`; arrow data is by nature immutable"),
)

function toarrowvector(
    x,
    i=1,
    de=Dict{Int64,Any}(),
    ded=DictEncoding[],
    meta=getmetadata(x);
    compression::Union{Nothing,Symbol,LZ4FrameCompressor,ZstdCompressor}=nothing,
    kw...,
)
    @debug "converting top-level column to arrow format: col = $(typeof(x)), compression = $compression, kw = $(values(kw))"
    @debug x
    A = arrowvector(x, i, 0, 0, de, ded, meta; compression=compression, kw...)
    if compression isa LZ4FrameCompressor
        A = compress(Meta.CompressionType.LZ4_FRAME, compression, A)
    elseif compression isa ZstdCompressor
        A = compress(Meta.CompressionType.ZSTD, compression, A)
    elseif compression isa Symbol && compression == :lz4
        comp = lz4_frame_compressor()
        A = Base.@lock comp begin
            compress(Meta.CompressionType.LZ4_FRAME, comp[], A)
        end
    elseif compression isa Symbol && compression == :zstd
        comp = zstd_compressor()
        A = Base.@lock comp begin
            compress(Meta.CompressionType.ZSTD, comp[], A)
        end
    end
    @debug "converted top-level column to arrow format: $(typeof(A))"
    @debug A
    return A
end

function arrowvector(
    x,
    i,
    nl,
    fi,
    de,
    ded,
    meta;
    dictencoding::Bool=false,
    dictencode::Bool=false,
    maxdepth::Int=DEFAULT_MAX_DEPTH,
    kw...,
)
    if nl > maxdepth
        error(
            "reached nested serialization level ($nl) deeper than provided max depth argument ($(maxdepth)); to increase allowed nesting level, pass `maxdepth=X`",
        )
    end
    T = maybemissing(eltype(x))
    if !(x isa DictEncode) && !dictencoding && (dictencode || DataAPI.refarray(x) !== x)
        x = DictEncode(x, dictencodeid(i, nl, fi))
    elseif x isa DictEncoded
        return arrowvector(
            DictEncodeType,
            x,
            i,
            nl,
            fi,
            de,
            ded,
            meta;
            dictencode=dictencode,
            kw...,
        )
    elseif !(x isa DictEncode)
        x = ToArrow(x)
    end
    S = maybemissing(eltype(x))
    if ArrowTypes.hasarrowname(T)
        meta = _arrowtypemeta(
            _normalizemeta(meta),
            String(ArrowTypes.arrowname(T)),
            String(ArrowTypes.arrowmetadata(T)),
        )
    end
    return arrowvector(
        S,
        x,
        i,
        nl,
        fi,
        de,
        ded,
        meta;
        dictencode=dictencode,
        maxdepth=maxdepth,
        kw...,
    )
end

_normalizemeta(::Nothing) = nothing
_normalizemeta(meta) = toidict(String(k) => String(v) for (k, v) in meta)

_normalizecolmeta(::Nothing) = nothing
_normalizecolmeta(colmeta) = toidict(
    Symbol(k) => toidict(String(v1) => String(v2) for (v1, v2) in v) for (k, v) in colmeta
)

function _arrowtypemeta(::Nothing, n, m)
    return toidict(("ARROW:extension:name" => n, "ARROW:extension:metadata" => m))
end

function _arrowtypemeta(meta, n, m)
    dict = Dict(meta)
    dict["ARROW:extension:name"] = n
    dict["ARROW:extension:metadata"] = m
    return toidict(dict)
end

# now we check for ArrowType converions and dispatch on ArrowKind
function arrowvector(::Type{S}, x, i, nl, fi, de, ded, meta; kw...) where {S}
    meta = _normalizemeta(meta)
    return arrowvector(ArrowKind(S), x, i, nl, fi, de, ded, meta; kw...)
end

struct NullVector{T} <: ArrowVector{T}
    data::MissingVector
    metadata::Union{Nothing,Base.ImmutableDict{String,String}}
end
Base.size(v::NullVector) = (length(v.data),)
Base.getindex(v::NullVector{T}, i::Int) where {T} =
    ArrowTypes.fromarrow(T, getindex(v.data, i))

arrowvector(::NullKind, x, i, nl, fi, de, ded, meta; kw...) = NullVector{eltype(x)}(
    MissingVector(length(x)),
    isnothing(meta) ? nothing : toidict(meta),
)
compress(Z::Meta.CompressionType.T, comp, v::NullVector) =
    Compressed{Z,NullVector}(v, CompressedBuffer[], length(v), length(v), Compressed[])

function makenodesbuffers!(
    col::NullVector,
    fieldnodes,
    fieldbuffers,
    bufferoffset,
    alignment,
)
    push!(fieldnodes, FieldNode(length(col), length(col)))
    @debug "made field node: nodeidx = $(length(fieldnodes)), col = $(typeof(col)), len = $(fieldnodes[end].length), nc = $(fieldnodes[end].null_count)"
    return bufferoffset
end

function writebuffer(io, col::NullVector, alignment)
    return
end

"""
    Arrow.ValidityBitmap

A bit-packed array type where each bit corresponds to an element in an
[`ArrowVector`](@ref), indicating whether that element is "valid" (bit == 1),
or not (bit == 0). Used to indicate element missingness (whether it's null).

If the null count of an array is zero, the `ValidityBitmap` will be "empty"
and all elements are treated as "valid"/non-null.
"""
struct ValidityBitmap <: ArrowVector{Bool}
    bytes::Vector{UInt8} # arrow memory blob
    pos::Int # starting byte of validity bitmap
    ℓ::Int # # of _elements_ (not bytes!) in bitmap (because bitpacking)
    nc::Int # null count
end

Base.size(p::ValidityBitmap) = (p.ℓ,)
nullcount(x::ValidityBitmap) = x.nc

function ValidityBitmap(x)
    T = eltype(x)
    if !(T >: Missing)
        return ValidityBitmap(UInt8[], 1, length(x), 0)
    end
    len = length(x)
    blen = cld(len, 8)
    bytes = Vector{UInt8}(undef, blen)
    st = iterate(x)
    nc = 0
    b = 0xff
    j = k = 1
    for y in x
        if y === missing
            nc += 1
            b = setbit(b, false, j)
        end
        j += 1
        if j == 9
            @inbounds bytes[k] = b
            b = 0xff
            j = 1
            k += 1
        end
    end
    if j > 1
        bytes[k] = b
    end
    return ValidityBitmap(nc == 0 ? UInt8[] : bytes, 1, nc == 0 ? 0 : len, nc)
end

@propagate_inbounds function Base.getindex(p::ValidityBitmap, i::Integer)
    # no boundscheck because parent array should do it
    # if a validity bitmap is empty, it either means:
    #   1) the parent array null_count is 0, so all elements are valid
    #   2) parent array is also empty, so "all" elements are valid
    p.nc == 0 && return true
    # translate element index to bitpacked byte index
    a, b = divrem(i - 1, 8) .+ (1, 1)
    @inbounds byte = p.bytes[p.pos + a - 1]
    # check individual bit of byte
    return getbit(byte, b)
end

@propagate_inbounds function Base.setindex!(p::ValidityBitmap, v, i::Integer)
    x = convert(Bool, v)
    p.ℓ == 0 && !x && throw(BoundsError(p, i))
    a, b = fldmod1(i, 8)
    @inbounds byte = p.bytes[p.pos + a - 1]
    @inbounds p.bytes[p.pos + a - 1] = setbit(byte, x, b)
    return v
end

function writebitmap(io, col::ArrowVector, alignment)
    v = col.validity
    @debug "writing validity bitmap: nc = $(v.nc), n = $(cld(v.ℓ, 8))"
    v.nc == 0 && return 0
    n = Base.write(io, view(v.bytes, (v.pos):(v.pos + cld(v.ℓ, 8) - 1)))
    return n + writezeros(io, paddinglength(n, alignment))
end

include("compressed.jl")
include("primitive.jl")
include("bool.jl")
include("list.jl")
include("fixedsizelist.jl")
include("map.jl")
include("struct.jl")
include("unions.jl")
include("dictencoding.jl")
include("views.jl")
