abstract type Properties end

include("filters/filters.jl")
include("drivers/drivers.jl")

Base.cconvert(::Type{API.hid_t}, obj::Properties) = obj
Base.unsafe_convert(::Type{API.hid_t}, obj::Properties) = obj.id

function Base.close(obj::Properties)
    if obj.id != -1
        if isvalid(obj)
            API.h5p_close(obj)
        end
        obj.id = -1
    end
    nothing
end

Base.isvalid(obj::Properties) = obj.id != -1 && API.h5i_is_valid(obj)
Base.copy(obj::P) where {P<:Properties} = P(HDF5.API.h5p_copy(obj.id))

# By default, properties objects are only initialized lazily
function init!(prop::P) where {P<:Properties}
    if !isvalid(prop)
        prop.id = API.h5p_create(classid(P))
    end
    return prop
end

function (::Type{P})(; kwargs...) where {P<:Properties}
    obj = P(API.H5P_DEFAULT)
    for (k, v) in kwargs
        setproperty!(obj, k, v)
    end
    return obj
end
# Properties() do syntax
function (::Type{P})(func::Function; kwargs...) where {P<:Properties}
    p = P(; kwargs...)
    # Eagerly initialize when using do syntax
    # This allows for use low-level API calls
    init!(p)
    try
        func(p)
    finally
        close(p)
    end
end

function Base.getproperty(p::P, name::Symbol) where {P<:Properties}
    name === :id ? getfield(p, :id) : class_getproperty(P, init!(p), name)
end

function Base.setproperty!(p::P, name::Symbol, val) where {P<:Properties}
    if name === :id
        return setfield!(p, :id, API.hid_t(val))
    end
    init!(p)
    class_setproperty!(P, p, name, val)
end

Base.propertynames(p::P) where {P<:Properties} = (all_propertynames(P)..., :id)
all_propertynames(::Type{P}) where {P<:Properties} =
    (class_propertynames(P)..., all_propertynames(superclass(P))...,)

# defaults: refer to super class
class_getproperty(::Type{P}, props, name) where {P<:Properties} =
    class_getproperty(superclass(P), props, name)
class_setproperty!(::Type{P}, p, name, val) where {P<:Properties} =
    class_setproperty!(superclass(P), p, name, val)
class_propertynames(::Type{P}) where {P<:Properties} = ()

"""
    @propertyclass P classid

Define a new subtype of `P <: Properties` corresponding to a HDF5 property list
with class identifier `classid`.

Once defined, the following interfaces can be defined:

    superclass(::Type{P})

This should return the type from which `P` inherits. If not defined, it will
inherit from `GenericProperties`.

    class_propertynames(::Type{P})

This should return a `Tuple` of `Symbol`s, being the names of the properties
associated with `P`.

    class_getproperty(::Type{P}, p::Properties, name::Symbol)

If `name` is an associated property of type `P`, this should return the value of
the property, otherwise call `class_getproperty(superclass(P), p, name)`.

    class_setproperty!(::Type{P}, p::Properties, name::Symbol, val)

If `name` is an associated property of type `P`, this should set the value of
the property, otherwise call `class_setproperty!(superclass(P), p, name, val)`.
"""
macro propertyclass(name, classid)
    expr = quote
        Core.@__doc__ mutable struct $name <: Properties
            id::API.hid_t
            function $name(id::API.hid_t)
                obj = new(id)
                finalizer(API.try_close_finalizer, obj)
                obj
            end
        end
        classid(::Type{$name}) = $classid
    end
    return esc(expr)
end

@propertyclass GenericProperties API.H5P_DEFAULT

superclass(::Type{P}) where {P<:Properties} = GenericProperties

class_getproperty(::Type{GenericProperties}, props, name) =
    error("$(typeof(props)) has no property $name")
class_setproperty!(::Type{GenericProperties}, props, name, val) =
    error("$(typeof(props)) has no property $name")
all_propertynames(::Type{GenericProperties}) = ()

# for initializing multiple Properties from a set of keyword arguments
"""
    setproperties!(props::Properties...; kwargs...)

For each `(key, value)` pair in `kwargs`, set the corresponding properties in
each `Properties` object in `props`. Returns a `Dict` of any pairs which didn't
match properties in `props`.
"""
function setproperties!(props::Properties...; kwargs...)
    filter(kwargs) do (k, v)
        found = false
        for prop in props
            if k in all_propertynames(typeof(prop))
                setproperty!(prop, k, v)
                found = true
            end
        end
        return !found
    end
end

###
### Convenience macros for defining getter/setter functions
###

"""
    @tuple_property(name)
"""
macro tuple_property(property)
    get_property = Symbol(:get_, property)
    set_property! = Symbol(:set_, property, :!)
    api_get_property = :(API.$(Symbol(:h5p_get_, property)))
    api_set_property = :(API.$(Symbol(:h5p_set_, property)))
    quote
        function $(esc(get_property))(p::Properties)
            return $api_get_property(p)
        end
        function $(esc(set_property!))(p::Properties, val::Tuple)
            return $api_set_property(p, val...)
        end
    end
end

"""
    @enum_property(name, sym1 => enumvalue1, sym2 => enumvalue2, ...)

Wrap property getter/setter API functions that use enum values to use symbol instead.
"""
macro enum_property(property, pairs...)
    get_property = Symbol(:get_, property)
    set_property! = Symbol(:set_, property, :!)
    api_get_property = :(API.$(Symbol(:h5p_get_, property)))
    api_set_property = :(API.$(Symbol(:h5p_set_, property)))

    get_expr = :(error("Unknown $property value $enum"))
    set_expr = :(throw(ArgumentError("Invalid $property $val")))

    for pair in reverse(pairs)
        @assert pair isa Expr && pair.head == :call && pair.args[1] == :(=>)
        _, val, enum = pair.args
        get_expr = :(enum == $enum ? $val : $get_expr)
        set_expr = :(val == $val ? $enum : $set_expr)
    end
    quote
        function $(esc(get_property))(p::Properties)
            property = $(QuoteNode(property))
            enum = $api_get_property(p)
            return $get_expr
        end
        function $(esc(set_property!))(p::Properties, val)
            property = $(QuoteNode(property))
            enum = $set_expr
            return $api_set_property(p, enum)
        end
        function $(esc(set_property!))(p::Properties, enum::Integer)
            # deprecate?
            return $api_set_property(p, enum)
        end
    end
end

"""
    @bool_property(name)

Wrap property getter/setter API functions that use `0`/`1` to use `Bool` values
"""
macro bool_property(property)
    get_property = Symbol(:get_, property)
    set_property! = Symbol(:set_, property, :!)
    api_get_property = :(API.$(Symbol(:h5p_get_, property)))
    api_set_property = :(API.$(Symbol(:h5p_set_, property)))
    quote
        function $(esc(get_property))(p::Properties)
            return $api_get_property(p) != 0
        end
        function $(esc(set_property!))(p::Properties, val)
            return $api_set_property(p, val)
        end
    end
end

###
### Define Properties types
###

#! format: off

"""
    ObjectCreateProperties(;kws...)
    ObjectCreateProperties(f::Function; kws...)

Properties used when creating a new object. Available options:

- `obj_track_times :: Bool`: governs the recording of times associated with an
  object. If set to `true`, time data will be recorded. See
  $(h5doc("H5P_SET_OBJ_TRACK_TIMES")).

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass ObjectCreateProperties API.H5P_OBJECT_CREATE

@bool_property(obj_track_times)

class_propertynames(::Type{ObjectCreateProperties}) = (
    :obj_track_times,
    :track_times,
    )
function class_getproperty(::Type{ObjectCreateProperties}, p::Properties, name::Symbol)
    name === :obj_track_times ? get_obj_track_times(p) :
    # deprecated
    name === :track_times ? (depwarn("`track_times` property is deprecated, use `obj_track_times` instead",:track_times); get_obj_track_times(p)) :
    class_getproperty(superclass(ObjectCreateProperties), p, name)
end
function class_setproperty!(::Type{ObjectCreateProperties}, p::Properties, name::Symbol, val)
    name === :obj_track_times ? set_obj_track_times!(p, val) :
    # deprecated
    name === :track_times ? (depwarn("`track_times=$val` keyword option is deprecated, use `obj_track_times=$val` instead",:track_times); set_obj_track_times!(p, val)) :
    class_setproperty!(superclass(ObjectCreateProperties), p, name, val)
end

get_track_order(p::Properties) = API.h5p_get_link_creation_order(p) != 0 && API.h5p_get_attr_creation_order(p) != 0

function set_track_order!(p::Properties, val::Bool)
    crt_order_flags = val ? (API.H5P_CRT_ORDER_TRACKED | API.H5P_CRT_ORDER_INDEXED) : 0
    API.h5p_set_link_creation_order(p, crt_order_flags)
    API.h5p_set_attr_creation_order(p, crt_order_flags)
    nothing
end

"""
    GroupCreateProperties(;kws...)
    GroupCreateProperties(f::Function; kws...)

Properties used when creating a new `Group`. Inherits from
[`ObjectCreateProperties`](@ref), with additional options:

- `local_heap_size_hint :: Integer`: the anticipated maximum local heap size in
  bytes. See $(h5doc("H5P_SET_LOCAL_HEAP_SIZE_HINT")).
- `track_order :: Bool`: tracks the group creation order.

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass GroupCreateProperties API.H5P_GROUP_CREATE
superclass(::Type{GroupCreateProperties}) = ObjectCreateProperties

class_propertynames(::Type{GroupCreateProperties}) = (
    :local_heap_size_hint,
    :track_order,
    )
function class_getproperty(::Type{GroupCreateProperties}, p::Properties, name::Symbol)
    name === :local_heap_size_hint ? API.h5p_get_local_heap_size_hint(p) :
    name === :track_order ? get_track_order(p) :
    class_getproperty(superclass(GroupCreateProperties), p, name)
end
function class_setproperty!(::Type{GroupCreateProperties}, p::Properties, name::Symbol, val)
    name === :local_heap_size_hint ? API.h5p_set_local_heap_size_hint(p, val) :
    name === :track_order ? set_track_order!(p, val) :
    class_setproperty!(superclass(GroupCreateProperties), p, name, val)
end

"""
    FileCreateProperties(;kws...)
    FileCreateProperties(f::Function; kws...)

Properties used when creating a new `File`. Inherits from
[`ObjectCreateProperties`](@ref),  with additional properties:

- `userblock :: Integer`: user block size in bytes. The default user block size
  is 0; it may be set to any power of 2 equal to 512 or greater (512, 1024,
  2048, etc.). See $(h5doc("H5P_SET_USERBLOCK")).
- `track_order :: Bool`: tracks the file creation order.

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass FileCreateProperties API.H5P_FILE_CREATE
superclass(::Type{FileCreateProperties}) = ObjectCreateProperties


class_propertynames(::Type{FileCreateProperties}) = (
    :userblock,
    :track_order,
    :strategy,
    :persist,
    :threshold,
    :file_space_page_size
    )

const FSPACE_STRATEGY_SYMBOLS = Dict(
    :fsm_aggr => API.H5F_FSPACE_STRATEGY_FSM_AGGR,
    :page => API.H5F_FSPACE_STRATEGY_PAGE,
    :aggr => API.H5F_FSPACE_STRATEGY_AGGR,
    :none => API.H5F_FSPACE_STRATEGY_NONE,
    :ntypes => API.H5F_FSPACE_STRATEGY_NTYPES
)

set_strategy!(p::FileCreateProperties, val) = API.h5p_set_file_space_strategy(p, strategy = val)
set_strategy!(p::FileCreateProperties, val::Symbol) = API.h5p_set_file_space_strategy(p, strategy = FSPACE_STRATEGY_SYMBOLS[val])
function get_strategy(p::FileCreateProperties)
    strategy = API.h5p_get_file_space_strategy(p)[:strategy]
    for (k, v) in FSPACE_STRATEGY_SYMBOLS
        if v == strategy
            return k
        end
    end
    return :unknown
end

function class_getproperty(::Type{FileCreateProperties}, p::Properties, name::Symbol)
    name === :userblock   ? API.h5p_get_userblock(p) :
    name === :track_order ? get_track_order(p) :
    name === :strategy    ? get_strategy(p) :
    name === :persist     ? API.h5p_get_file_space_strategy(p)[:persist] :
    name === :threshold   ? API.h5p_get_file_space_strategy(p)[:threshold] :
    name === :file_space_page_size ? API.h5p_get_file_space_page_size(p) :
    class_getproperty(superclass(FileCreateProperties), p, name)
end
function class_setproperty!(::Type{FileCreateProperties}, p::Properties, name::Symbol, val)
    name === :userblock   ? API.h5p_set_userblock(p, val) :
    name === :track_order ? set_track_order!(p, val) :
    name === :strategy ? set_strategy!(p, val) :
    name === :persist ? API.h5p_set_file_space_strategy(p, persist = val) :
    name === :threshold ? API.h5p_set_file_space_strategy(p, threshold = val) :
    name === :file_space_page_size ? API.h5p_set_file_space_page_size(p, val) :
    class_setproperty!(superclass(FileCreateProperties), p, name, val)
end


"""
    DatatypeCreateProperties(;kws...)
    DatatypeCreateProperties(f::Function; kws...)

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass DatatypeCreateProperties API.H5P_DATATYPE_CREATE
superclass(::Type{DatatypeCreateProperties}) = ObjectCreateProperties

"""
    DatasetCreateProperties(;kws...)
    DatasetCreateProperties(f::Function; kws...)

Properties used when creating a new `Dataset`. Inherits from
[`ObjectCreateProperties`](@ref), with additional properties:

- `alloc_time`: the timing for the allocation of storage space for a dataset's
  raw data; one of:
   - `:default`

   - `:early`: allocate all space when the dataset is created

   - `:incremental`: Allocate space incrementally, as data is  written to the
     dataset

   - `:late`: Allocate all space when data is first written to the dataset.

  See $(h5doc("H5P_SET_ALLOC_TIME")).

- `fill_time`: the timing of when the dataset should be filled; one of:
   - `:alloc`: Fill when allocated
   - `:never`: Never fill
   - `:ifset`: Fill if a value is set

- `fill_value`: the fill value for a dataset. See $(h5doc("H5P_SET_FILL_VALUE")).

- `chunk`: a tuple containing the size of the chunks to store each dimension.
  See $(h5doc("H5P_SET_CHUNK")) (note that this uses Julia's column-major
  ordering).

- `external`: A tuple of `(name,offset,size)`, See $(h5doc("H5P_SET_EXTERNAL")).

- `filters` (only valid when `layout=:chunked`): a filter or vector of filters
  that are applied to applied to each chunk of a dataset, see [Filters](@ref).
  When accessed, will return a [`Filters.FilterPipeline`](@ref) object that can
  be modified in-place.

- `layout`: the type of storage used to store the raw data for a dataset. Can be
  one of:

   - `:compact`: Store raw data in the dataset object header in file. This
     should only be used for datasets with small amounts of raw data.

   - `:contiguous`: Store raw data separately from the object header in one
     large chunk in the file.

   - `:chunked`: Store raw data separately from the object header as chunks of
     data in separate locations in the file.

   - `:virtual`:  Draw raw data from multiple datasets in different files. See
     the `virtual` property below.

  See $(h5doc("H5P_SET_LAYOUT")).

- `no_attrs_hint`: Minimize the space for dataset metadata by hinting that no
   attributes will be added if set to `true`. Attributes can still be added but
   may exist elsewhere within the file. See
   $(h5doc("H5P_SET_DSET_NO_ATTRS_HINT")).

- `virtual`: when specified, creates a virtual dataset (VDS). The argument
  should be a "virtuala collection of [`VirtualMapping`](@ref) objects for
  describing the mapping from the dataset to the source datasets. When accessed,
  returns a [`VirtualLayout`](@ref) object.

The following options are shortcuts for the various filters, and are set-only.
They will be appended to the filter pipeline in the order in which they appear

- `blosc = true | level`: set the [`H5Zblosc.BloscFilter`](@ref) compression
  filter; argument can be either `true`, or the compression level.

- `deflate = true | level`: set the [`Filters.Deflate`](@ref) compression
  filter; argument can be either `true`, or the compression level.

- `fletcher32 = true`: set the [`Filters.Fletcher32`](@ref) checksum filter.

- `shuffle = true`: set the [`Filters.Shuffle`](@ref) filter.

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass DatasetCreateProperties API.H5P_DATASET_CREATE
superclass(::Type{DatasetCreateProperties}) = ObjectCreateProperties

@enum_property(alloc_time,
    :default     => API.H5D_ALLOC_TIME_DEFAULT,
    :early       => API.H5D_ALLOC_TIME_EARLY,
    :incremental => API.H5D_ALLOC_TIME_INCR,
    :late        => API.H5D_ALLOC_TIME_LATE)

# reverse indices
function get_chunk(p::Properties)
    dims, N = API.h5p_get_chunk(p)
    ntuple(i -> Int(dims[N-i+1]), N)
end
set_chunk!(p::Properties, dims) = API.h5p_set_chunk(p, length(dims), API.hsize_t[reverse(dims)...])

@enum_property(layout,
    :compact    => API.H5D_COMPACT,
    :contiguous => API.H5D_CONTIGUOUS,
    :chunked    => API.H5D_CHUNKED,
    :virtual    => API.H5D_VIRTUAL)

# See https://portal.hdfgroup.org/display/HDF5/H5P_SET_FILL_TIME
@enum_property(fill_time,
    :alloc => API.H5D_FILL_TIME_ALLOC,
    :never => API.H5D_FILL_TIME_NEVER,
    :ifset => API.H5D_FILL_TIME_IFSET
)

# filters getters/setters
get_filters(p::Properties) = Filters.FilterPipeline(p)
set_filters!(p::Properties, val::Filters.Filter) = push!(empty!(Filters.FilterPipeline(p)), val)
set_filters!(p::Properties, vals::Union{Tuple, AbstractVector}) = append!(empty!(Filters.FilterPipeline(p)), vals)

# convenience
set_deflate!(p::Properties, val::Bool) = val && push!(Filters.FilterPipeline(p), Filters.Deflate())
set_deflate!(p::Properties, level::Integer) = push!(Filters.FilterPipeline(p), Filters.Deflate(level=level))
set_shuffle!(p::Properties, val::Bool) = val && push!(Filters.FilterPipeline(p), Filters.Shuffle())
set_fletcher32!(p::Properties, val::Bool) = val && push!(Filters.FilterPipeline(p), Filters.Fletcher32())
set_blosc!(p::Properties, val) = error("The Blosc filter now requires the H5Zblosc package be loaded")

get_virtual(p::Properties) = VirtualLayout(p)
set_virtual!(p::Properties, vmaps) = append!(VirtualLayout(p), vmaps)


class_propertynames(::Type{DatasetCreateProperties}) = (
    :alloc_time,
    :fill_time,
    :fill_value,
    :chunk,
    :external,
    :filters,
    :layout,
    :no_attrs_hint,
    :virtual,
    # convenience
    :blosc,
    :deflate,
    :fletcher32,
    :shuffle,
    # deprecated
    :compress,
    :filter
    )


function class_getproperty(::Type{DatasetCreateProperties}, p::Properties, name::Symbol)
    name === :alloc_time  ? get_alloc_time(p) :
    name === :fill_time   ? get_fill_time(p) :
    name === :fill_value  ? get_fill_value(p) :
    name === :chunk       ? get_chunk(p) :
    name === :external    ? API.h5p_get_external(p) :
    name === :filters     ? get_filters(p) :
    name === :layout      ? get_layout(p) :
    name === :no_attrs_hint ?
        @static(API.h5_get_libversion() < v"1.10.5" ?
            false :
            API.h5p_get_dset_no_attrs_hint(p)
        ) :
    name === :virtual     ? get_virtual(p) :
    # deprecated
    name === :filter      ? (depwarn("`filter` property name is deprecated, use `filters` instead",:class_getproperty); get_filters(p)) :
    class_getproperty(superclass(DatasetCreateProperties), p, name)
end
function class_setproperty!(::Type{DatasetCreateProperties}, p::Properties, name::Symbol, val)
    name === :alloc_time  ? set_alloc_time!(p, val) :
    name === :fill_time   ? set_fill_time!(p, val) :
    name === :fill_value  ? set_fill_value!(p, val) :
    name === :chunk       ? set_chunk!(p, val) :
    name === :external    ? API.h5p_set_external(p, val...) :
    name === :filters     ? set_filters!(p, val) :
    name === :layout      ? set_layout!(p, val) :
    name === :no_attrs_hint ?
        @static(API.h5_get_libversion() < v"1.10.5" ?
            error("no_attrs_hint is only valid for HDF5 library versions 1.10.5 or greater") :
            API.h5p_set_dset_no_attrs_hint(p, val)
        ) :
    name === :virtual     ? set_virtual!(p, val) :
    # set-only for convenience
    name === :blosc       ? set_blosc!(p, val) :
    name === :deflate     ? set_deflate!(p, val) :
    name === :fletcher32  ? set_fletcher32!(p, val) :
    name === :shuffle     ? set_shuffle!(p, val) :
    # deprecated
    name === :filter      ? (depwarn("`filter=$val` keyword option is deprecated, use `filters=$val` instead",:class_setproperty!); set_filters!(p, val)) :
    name === :compress    ? (depwarn("`compress=$val` keyword option is deprecated, use `deflate=$val` instead",:class_setproperty!); set_deflate!(p, val)) :
    class_setproperty!(superclass(DatasetCreateProperties), p, name, val)
end

"""
    StringCreateProperties(;kws...)
    StringCreateProperties(f::Function; kws...)

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass StringCreateProperties API.H5P_STRING_CREATE

@enum_property(char_encoding,
    :ascii => API.H5T_CSET_ASCII,
    :utf8  => API.H5T_CSET_UTF8)


class_propertynames(::Type{StringCreateProperties}) = (
    :char_encoding,
    )
function class_getproperty(::Type{StringCreateProperties}, p::Properties, name::Symbol)
    name === :char_encoding ? get_char_encoding(p) :
    class_getproperty(superclass(StringCreateProperties), p, name)
end
function class_setproperty!(::Type{StringCreateProperties}, p::Properties, name::Symbol, val)
    name === :char_encoding ? set_char_encoding!(p, val) :
    class_setproperty!(superclass(StringCreateProperties), p, name, val)
end

"""
    LinkCreateProperties(;kws...)
    LinkCreateProperties(f::Function; kws...)

Properties used when creating links.

- `char_encoding`: the character enconding, either `:ascii` or `:utf8`.

- `create_intermediate_group :: Bool`: if `true`, will create missing
  intermediate groups.

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass LinkCreateProperties API.H5P_LINK_CREATE
superclass(::Type{LinkCreateProperties}) = StringCreateProperties

@bool_property(create_intermediate_group)

class_propertynames(::Type{LinkCreateProperties}) = (
    :create_intermediate_group,
    )
function class_getproperty(::Type{LinkCreateProperties}, p::Properties, name::Symbol)
    name === :create_intermediate_group ? get_create_intermediate_group(p) :
    class_getproperty(superclass(LinkCreateProperties), p, name)
end
function class_setproperty!(::Type{LinkCreateProperties}, p::Properties, name::Symbol, val)
    name === :create_intermediate_group ? set_create_intermediate_group!(p, val) :
    class_setproperty!(superclass(LinkCreateProperties), p, name, val)
end

"""
    AttributeCreateProperties(;kws...)
    AttributeCreateProperties(f::Function; kws...)

Properties used when creating attributes.

- `char_encoding`: the character enconding, either `:ascii` or `:utf8`.

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass AttributeCreateProperties API.H5P_ATTRIBUTE_CREATE
superclass(::Type{AttributeCreateProperties}) = StringCreateProperties


"""
    FileAccessProperties(;kws...)
    FileAccessProperties(f::Function; kws...)

Properties used when accessing files.

- `alignment :: Tuple{Integer, Integer}`: a `(threshold, alignment)` pair: any
  file object greater than or equal in size to threshold bytes will be aligned
  on an address which is a multiple of alignment. Default values are 1, implying
  no alignment.

- `driver`: the file driver used to access the file. See [Drivers](@ref).

- `driver_info` (get only)

- `fclose_degree`: file close degree property. One of:

  - `:weak`
  - `:semi`
  - `:strong`
  - `:default`

- `libver_bounds`: a `(low, high)` pair: `low` sets the earliest possible format
  versions that the library will use when creating objects in the file; `high`
  sets the latest format versions that the library will be allowed to use when
  creating objects in the file. Values can be a `VersionNumber` for the hdf5
  library, `:earliest`, or `:latest` . See $(h5doc("H5P_SET_LIBVER_BOUNDS"))

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass FileAccessProperties API.H5P_FILE_ACCESS

# Defaults for FileAccessProperties
function init!(fapl::FileAccessProperties)
    # Call default init! for Properties
    invoke(init!, Tuple{Properties}, fapl)
    # Disable file locking by default for mmap
    @static if API.has_h5p_set_file_locking()
        API.h5p_set_file_locking(fapl, false, true)
    end
    set_fclose_degree!(fapl, :strong)
    return fapl
end

@tuple_property(alignment)

@enum_property(fclose_degree,
               :weak    => API.H5F_CLOSE_WEAK,
               :semi    => API.H5F_CLOSE_SEMI,
               :strong  => API.H5F_CLOSE_STRONG,
               :default => API.H5F_CLOSE_DEFAULT)

# getter/setter for libver_bounds
libver_bound_to_enum(val::Integer) = val
libver_bound_to_enum(val::API.H5F_libver_t) = val
function libver_bound_to_enum(val::VersionNumber)
    val >= v"1.15"   ? API.H5F_LIBVER_V200 :
    val >= v"1.14"   ? API.H5F_LIBVER_V114 :
    val >= v"1.12"   ? API.H5F_LIBVER_V112 :
    val >= v"1.10"   ? API.H5F_LIBVER_V110 :
    val >= v"1.8"    ? API.H5F_LIBVER_V18 :
    throw(ArgumentError("libver_bound must be >= v\"1.8\"."))
end
function libver_bound_to_enum(val::Symbol)
    val == :earliest ? API.H5F_LIBVER_EARLIEST :
    val == :latest   ? API.H5F_LIBVER_LATEST :
    throw(ArgumentError("Invalid libver_bound $val."))
end
function libver_bound_from_enum(enum::API.H5F_libver_t)
    enum == API.H5F_LIBVER_EARLIEST ? :earliest :
    enum == API.H5F_LIBVER_V18      ? v"1.8" :
    enum == API.H5F_LIBVER_V110     ? v"1.10" :
    enum == API.H5F_LIBVER_V112     ? v"1.12" :
    enum == API.H5F_LIBVER_V114     ? v"1.14" :
    enum == API.H5F_LIBVER_V200     ? v"2.0" :
    error("Unknown libver_bound value $enum")
end
libver_bound_from_enum(enum) = libver_bound_from_enum(API.H5F_libver_t(enum))
function get_libver_bounds(p::Properties)
    low, high = API.h5p_get_libver_bounds(p)
    return libver_bound_from_enum(low), libver_bound_from_enum(high)
end
function set_libver_bounds!(p::Properties, (low, high)::Tuple{Any,Any})
    API.h5p_set_libver_bounds(p, libver_bound_to_enum(low), libver_bound_to_enum(high))
end
function set_libver_bounds!(p::Properties, val)
    API.h5p_set_libver_bounds(p, libver_bound_to_enum(val), libver_bound_to_enum(val))
end


class_propertynames(::Type{FileAccessProperties}) = (
    :alignment,
    :driver,
    :driver_info,
    :fapl_mpio,
    :fclose_degree,
    :file_locking,
    :libver_bounds,
    :meta_block_size,
    :file_image,
    )

function class_getproperty(::Type{FileAccessProperties}, p::Properties, name::Symbol)
    name === :alignment     ? get_alignment(p) :
    name === :driver        ? Drivers.get_driver(p) :
    name === :driver_info   ? API.h5p_get_driver_info(p) : # get only
    name === :fclose_degree ? get_fclose_degree(p) :
    name === :file_locking  ? API.h5p_get_file_locking(p) :
    name === :libver_bounds ? get_libver_bounds(p) :
    name === :meta_block_size ? API.h5p_get_meta_block_size(p) :
    name === :file_image      ? API.h5p_get_file_image(p) :
    # deprecated
    name === :fapl_mpio     ? (depwarn("The `fapl_mpio` property is deprecated, use `driver=HDF5.Drivers.MPIO(...)` instead.", :fapl_mpio); drv = get_driver(p, MPIO); (drv.comm, drv.info)) :
    class_getproperty(superclass(FileAccessProperties), p, name)
end
function class_setproperty!(::Type{FileAccessProperties}, p::Properties, name::Symbol, val)
    name === :alignment     ? set_alignment!(p, val) :
    name === :driver        ? Drivers.set_driver!(p, val) :
    name === :fclose_degree ? set_fclose_degree!(p, val) :
    name === :file_locking  ? API.h5p_set_file_locking(p, val...) :
    name === :libver_bounds ? set_libver_bounds!(p, val) :
    name === :meta_block_size ? API.h5p_set_meta_block_size(p, val) :
    name === :file_image      ? API.h5p_set_file_image(p, val) :
    # deprecated
    name === :fapl_mpio     ? (depwarn("The `fapl_mpio` property is deprecated, use `driver=HDF5.Drivers.MPIO(...)` instead.", :fapl_mpio); Drivers.set_driver!(p, Drivers.MPIO(val...))) :
    class_setproperty!(superclass(FileAccessProperties), p, name, val)
end


@propertyclass LinkAccessProperties API.H5P_LINK_ACCESS

"""
    GroupAccessProperties(;kws...)

Properties used when accessing datatypes. None are currently defined.
"""
@propertyclass GroupAccessProperties API.H5P_GROUP_ACCESS
superclass(::Type{GroupAccessProperties}) = LinkAccessProperties

"""
    DatatypeAccessProperties(;kws...)

Properties used when accessing datatypes. None are currently defined.
"""
@propertyclass DatatypeAccessProperties API.H5P_DATATYPE_ACCESS
superclass(::Type{DatatypeAccessProperties}) = LinkAccessProperties

"""
    DatasetAccessProperties(;kws...)
    DatasetAccessProperties(f::Function; kws...)

Properties that control access to data in external, virtual, and chunked datasets.

- `chunk_cache`: Chunk cache parameters as (nslots, nbytes, w0).
  Default: (521, 0x100000, 0.75)
- `efile_prefix`: Path prefix for reading external files.
  The default is the current working directory.
  - `:origin`: alias for `raw"\$ORIGIN"` will make the external file relative to
    the HDF5 file.
- `virtual_prefix`: Path prefix for reading virtual datasets.
- `virtual_printf_gap`: The maximum number of missing source files and/or
   datasets with the printf-style names when getting the extent of an unlimited
   virtual dataset
- `virtual_view`: Influences whether the view of the virtual dataset includes
  or excludes missing mapped elements
  - `:first_missing`: includes all data before the first missing mapped data
  - `:last_available`: includes all available mapped data


A function argument passed via `do` will be given an initialized property list
that will be closed.

See [Dataset Access Properties](https://portal.hdfgroup.org/display/HDF5/Dataset+Access+Properties)
"""
@propertyclass DatasetAccessProperties API.H5P_DATASET_ACCESS
superclass(::Type{DatasetAccessProperties}) = LinkAccessProperties

class_propertynames(::Type{DatasetAccessProperties}) = (
    :chunk_cache,
    :efile_prefix,
    :virtual_prefix,
    :virtual_printf_gap,
    :virtual_view
)

@enum_property(virtual_view,
    :first_missing  => API.H5D_VDS_FIRST_MISSING,
    :last_available => API.H5D_VDS_LAST_AVAILABLE
)

function class_getproperty(::Type{DatasetAccessProperties}, p::Properties, name::Symbol)
    name === :chunk_cache ? API.h5p_get_chunk_cache(p) :
    name === :efile_prefix ? API.h5p_get_efile_prefix(p) :
    name === :virtual_prefix ? API.h5p_get_virtual_prefix(p) :
    name === :virtual_printf_gap ? API.h5p_get_virtual_printf_gap(p) :
    name === :virtual_view ? get_virtual_view(p) :
    class_getproperty(superclass(DatasetAccessProperties), p, name)
end
function class_setproperty!(::Type{DatasetAccessProperties}, p::Properties, name::Symbol, val)
    name === :chunk_cache ? API.h5p_set_chunk_cache(p, val...) :
    name === :efile_prefix ? API.h5p_set_efile_prefix(p, val) :
    name === :virtual_prefix ? API.h5p_set_virtual_prefix(p, val) :
    name === :virtual_printf_gap ? API.h5p_set_virtual_printf_gap(p, val) :
    name === :virtual_view ? set_virtual_view!(p, val) :
    class_setproperty!(superclass(DatasetAccessProperties), p, name, val)
end

@propertyclass AttributeAccessProperties API.H5P_ATTRIBUTE_ACCESS
superclass(::Type{AttributeAccessProperties}) = LinkAccessProperties

"""
    DatasetTransferProperties(;kws...)
    DatasetTransferProperties(f::Function; kws...)

Properties used when transferring data to/from datasets

- `dxpl_mpio`: MPI transfer mode when using [`Drivers.MPIO`](@ref) file driver:
   - `:independent`: use independent I/O access (default),
   - `:collective`: use collective I/O access.

A function argument passed via `do` will be given an initialized property list
that will be closed.
"""
@propertyclass DatasetTransferProperties API.H5P_DATASET_XFER

@enum_property(dxpl_mpio,
               :independent => API.H5FD_MPIO_INDEPENDENT,
               :collective  => API.H5FD_MPIO_COLLECTIVE)

class_propertynames(::Type{DatasetTransferProperties}) = (
    :dxpl_mpio,
    )
function class_getproperty(::Type{DatasetTransferProperties}, p::Properties, name::Symbol)
    name === :dxpl_mpio  ? get_dxpl_mpio(p) :
    class_getproperty(superclass(DatasetTransferProperties), p, name)
end
function class_setproperty!(::Type{DatasetTransferProperties}, p::Properties, name::Symbol, val)
    name === :dxpl_mpio  ? set_dxpl_mpio!(p, val) :
    class_setproperty!(superclass(DatasetTransferProperties), p, name, val)
end

@propertyclass FileMountProperties API.H5P_FILE_MOUNT
@propertyclass ObjectCopyProperties API.H5P_OBJECT_COPY


const DEFAULT_PROPERTIES = GenericProperties()
# These properties are initialized in __init__()
const ASCII_LINK_PROPERTIES = LinkCreateProperties()
const UTF8_LINK_PROPERTIES = LinkCreateProperties()
const ASCII_ATTRIBUTE_PROPERTIES = AttributeCreateProperties()
const UTF8_ATTRIBUTE_PROPERTIES = AttributeCreateProperties()

_link_properties(::AbstractString) = copy(UTF8_LINK_PROPERTIES)
_attr_properties(::AbstractString) = copy(UTF8_ATTRIBUTE_PROPERTIES)

#! format: on
