
"Exception type for error thrown by the NetCDF library"
mutable struct NetCDFError <: Exception
    code::Cint
    msg::String
end

"Construct a NetCDFError from the error code"
NetCDFError(code::Cint) = NetCDFError(code, unsafe_string(nc_strerror(code)))

function Base.showerror(io::IO, err::NetCDFError)
    print(io, "NetCDF error code $(err.code):\n\t$(err.msg)")
end

"Check the NetCDF error code, raising an error if nonzero"
function check(code::Cint)
    # zero means success, return
    if code == Cint(0)
        return
    # otherwise throw an error message
    else
        throw(NetCDFError(code))
    end
end

const funext = [
    (Float64, "double", "float64a"),
    (Float32, "float", "float32a"),
    (Int32, "int", "int32a"),
    (UInt32, "uint", "uint32a"),
    (UInt8, "uchar", "uint8a"),
    (Int8, "schar", "int8a"),
    (Int16, "short", "int16a"),
    (UInt16, "ushort", "uint16a"),
    (Int64, "longlong", "int64a"),
    (UInt64, "ulonglong", "uint64a"),
]

const ida = zeros(Int32, 1)
const namea = zeros(UInt8, NC_MAX_NAME + 1)
const lengtha = zeros(Csize_t, 1)
const dimida = zeros(Int32, NC_MAX_VAR_DIMS)
const chunk_sizea = zeros(Csize_t, NC_MAX_VAR_DIMS)
const storagep = zeros(Int32, 1)
const ndima = zeros(Int32, 1)
const nvara = zeros(Int32, 1)
const ngatta = zeros(Int32, 1)
const nunlimdimida = zeros(Int32, 1)
const typea = zeros(Int32, 1)
const natta = zeros(Int32, 1)
const nvals = zeros(Csize_t, 1)
const dima = zeros(Int32, 1)
const varida = zeros(Int32, 1)
const vara = zeros(Int32, 1)
const dumids = zeros(Int32, NC_MAX_DIMS)
const gstart = zeros(UInt, NC_MAX_DIMS)
const gcount = zeros(UInt, NC_MAX_DIMS)

for (t, ending, aname) in funext
    @eval const $(Symbol(aname)) = zeros($t, 1)
end

"""
    nc_char2string(x::Matrix{ASCIIChar})

Convert a `ASCIIChar` array read from a NetCDF variable of type `NC_CHAR` to a Julia array of Strings
"""
function nc_char2string(x::Array{ASCIIChar})
    mapslices(x, dims = 1) do r
        i = findfirst(isequal(ASCIIChar(0)), r)
        if i === nothing
            String(r)
        else
            String(r[1:i-1])
        end
    end |> vec
end

"""
    nc_string2char(x::Array{String})

Convert a Julia `String` array to an `Array{ASCIIChar}` so that it can be written to a
NetCDF variable of type `NC_CHAR`.
"""
function nc_string2char(s::Array{String})
    maxlen = maximum(length, s)
    c = fill(ASCIIChar(0), maxlen + 1, size(s)...)
    for ii in eachindex(s)
        for j = 1:length(s[ii])
            cp = s[ii][j]
            !isascii(cp) && error("Only ascii string can be written to NC_CHAR arrays.")
            cp2 = ASCIIChar(UInt8(cp))
            c[j, ii] = cp2
        end
    end
    c
end

function nc_open(fname::AbstractString, omode::UInt16)
    # Function to open file fname, returns a NetCDF file ID
    # the library doesn't handle this case
    isempty(fname) && throw(ArgumentError("Cannot open empty filename."))
    nc_open(fname, omode, ida)
    return ida[1]
end

function nc_create(name, mode)
    # the library doesn't handle this case
    isempty(name) && throw(ArgumentError("Cannot create empty filename."))
    nc_create(name, mode, ida)
    return ida[1]
end

function nc_inq_dim(id::Integer, idim::Integer)
    # File to inquire dimension idimm returns dimension name and length
    nc_inq_dim(id, idim, namea, lengtha)
    namea[end] = 0
    name = unsafe_string(pointer(namea))
    return (name, lengtha[1])
end

function nc_inq_dimid(id::Integer, name::AbstractString)
    # Function to read dimension id for a given function name
    NetCDF.nc_inq_dimid(id, name, dimida)
    return dimida[1]
end

function nc_redef(nc::NcFile)
    if (!nc.in_def_mode)
        nc_redef(nc.ncid)
        nc.in_def_mode = true
    end
end

function nc_enddef(nc::NcFile)
    if nc.in_def_mode
        nc_enddef(nc.ncid)
        nc.in_def_mode = false
    end
end


function nc_inq(id::Integer)
    # Inquire NetCDF file, return number of dims, number of variables, number of global attributes and number of unlimited dimensions
    nc_inq(id, ndima, nvara, ngatta, nunlimdimida)
    return (ndima[1], nvara[1], ngatta[1], nunlimdimida[1])
end


function nc_inq_attname(ncid::Integer, varid::Integer, attnum::Integer)
    # Get attribute name from attribute number
    nc_inq_attname(ncid, varid, attnum, namea)
    namea[end] = 0
    name = unsafe_string(pointer(namea))
    return name
end

function nc_get_att(ncid::Integer, varid::Integer, att::Union{Integer,AbstractString})
    #Reads attribute name, type and number of values
    name = typeof(att) <: Integer ? nc_inq_attname(ncid, varid, att) : string(att)
    nc_inq_att(ncid, varid, name, typea, nvals)
    text = nc_get_att(ncid, varid, string(name), typea[1], nvals[1])
    return name, text
end

#Define methods for writing attributes
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Int8}) =
    nc_put_att_schar(ncid, varid, name, NC_BYTE, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{UInt8}) =
    nc_put_att_ubyte(ncid, varid, name, NC_UBYTE, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Int16}) =
    nc_put_att_short(ncid, varid, name, NC_SHORT, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{UInt16}) =
    nc_put_att_ushort(ncid, varid, name, NC_USHORT, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Int32}) =
    nc_put_att_int(ncid, varid, name, NC_INT, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{UInt32}) =
    nc_put_att_uint(ncid, varid, name, NC_UINT, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Int64}) =
    nc_put_att_longlong(ncid, varid, name, NC_INT64, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{UInt64}) =
    nc_put_att_ulonglong(ncid, varid, name, NC_UINT64, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Float32}) =
    nc_put_att_float(ncid, varid, name, NC_FLOAT, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Float64}) =
    nc_put_att_double(ncid, varid, name, NC_DOUBLE, length(val), val)
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Array{Any}) =
    error("Writing attribute array of type Any is not possible")

nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Int8) = begin
    int8a[1] = val
    nc_put_att(ncid, varid, name, int8a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::UInt8) = begin
    uint8a[1] = val
    nc_put_att(ncid, varid, name, uint8a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Int16) = begin
    int16a[1] = val
    nc_put_att(ncid, varid, name, int16a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::UInt16) = begin
    uint16a[1] = val
    nc_put_att(ncid, varid, name, uint16a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Int32) = begin
    int32a[1] = val
    nc_put_att(ncid, varid, name, int32a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::UInt32) = begin
    uint32a[1] = val
    nc_put_att(ncid, varid, name, uint32a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Int64) = begin
    int64a[1] = val
    nc_put_att(ncid, varid, name, int64a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::UInt64) = begin
    uint64a[1] = val
    nc_put_att(ncid, varid, name, uint64a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Float32) = begin
    float32a[1] = val
    nc_put_att(ncid, varid, name, float32a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val::Float64) = begin
    float64a[1] = val
    nc_put_att(ncid, varid, name, float64a)
end
nc_put_att(ncid::Integer, varid::Integer, name::AbstractString, val) =
    error("Writing attribute of type $(typeof(val)) is currently not possible.")

function nc_put_att(
    ncid::Integer,
    varid::Integer,
    name::AbstractString,
    val::AbstractString,
)
    val = string(val)
    len = sizeof(val)
    nc_put_att_text(ncid, varid, name, len, val)
end

function nc_put_att(
    ncid::Integer,
    varid::Integer,
    name::AbstractString,
    val::AbstractArray{String},
)
    vals_p = map(x -> pointer(x), val)
    nc_put_att_string(ncid, varid, name, length(val), vals_p)
end

function nc_get_att(
    ncid::Integer,
    varid::Integer,
    name::AbstractString,
    attype::Integer,
    attlen::Integer,
)
    if attype == NC_CHAR
        valsa = zeros(ASCIIChar, attlen + 1)
        nc_get_att_text(ncid, varid, name, valsa)
        valsa[end] = ASCIIChar(0)
        return String(valsa[1:findfirst(isequal(ASCIIChar(0)), valsa)-1])
    else
        valsa = Array{nctype2jltype[attype]}(undef, attlen)
        return nc_get_att!(ncid, varid, name, valsa)
    end
end

nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{UInt8}) =
    begin
        nc_get_att_ubyte(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{Int8}) = begin
    nc_get_att_schar(ncid, varid, name, valsa)
    valsa
end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{Int16}) =
    begin
        nc_get_att_short(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{UInt16}) =
    begin
        nc_get_att_ushort(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{Int32}) =
    begin
        nc_get_att_int(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{UInt32}) =
    begin
        nc_get_att_uint(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{Int64}) =
    begin
        nc_get_att_longlong(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{UInt64}) =
    begin
        nc_get_att_ulonglong(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{Float32}) =
    begin
        nc_get_att_float(ncid, varid, name, valsa)
        valsa
    end
nc_get_att!(ncid::Integer, varid::Integer, name::AbstractString, valsa::Array{Float64}) =
    begin
        nc_get_att_double(ncid, varid, name, valsa)
        valsa
    end

function nc_get_att!(
    ncid::Integer,
    varid::Integer,
    name::AbstractString,
    valsa::Array{T,N},
) where {T<:AbstractString,N}
  #Assert that length of the attribute matches length of output
    nc_inq_attlen(ncid, varid, name, lengtha)
    @assert lengtha[1] == length(valsa)
    valsa_c = Array{Ptr{UInt8}}(undef, length(valsa))
    nc_get_att_string(ncid, varid, name, valsa_c)
    for i = 1:length(valsa)
        valsa[i] = unsafe_string(valsa_c[i])
    end
    nc_free_string(length(valsa_c), valsa_c)
    valsa
end

function nc_inq_var(nc::NcFile, varid::Integer)
    # Inquire variables in the file
    nc_inq_var(nc.ncid, varid, namea, typea, ndima, dimida, natta)
    dimids = ndima[1] > 0 ? dimida[1:ndima[1]] : Int32[]
    namea[end] = 0
    name = unsafe_string(pointer(namea))
    #Find out chunks
    nc_inq_var_chunking(nc.ncid, varid, storagep, chunk_sizea)
    chunksize = storagep[1] == NC_CONTIGUOUS ? ntuple(i -> 0, ndima[1]) :
                ntuple(i -> chunk_sizea[i], ndima[1])
    return name, typea[1], dimids, natta[1], ndima[1], isdimvar(nc, name), chunksize
end

#Test if a variable name is also a dimension name
isdimvar(v::NcVar) = v.name == v.dim[1].name ? true : false

function isdimvar(nc::NcFile, name::AbstractString)
    for n in nc.dim
        if n[2].name == name
            return true
        end
    end
    return false
end


function getdimnamebyid(nc::NcFile, dimid::Integer)
    da = ""
    for d in nc.dim
        da = d[2].dimid == dimid ? d[2].name : da
    end
    return da
end


function getatts_all(ncid::Integer, varid::Integer, natts::Integer)
    atts = Dict{Any,Any}()
    for attnum = 0:natts-1
        attname, attval = nc_get_att(ncid, varid, attnum)
        if ((length(attval) == 1) && !(typeof(attval) <: AbstractString))
            attval = attval[1]
        end
        atts[attname] = attval
    end
    NC_VERBOSE && println(atts)
    return atts
end

function getatts(
    ncid::Integer,
    varid::Integer,
    attnames::Union{AbstractString,AbstractArray},
)
    atts = Dict{Any,Any}()
    for aattname in (typeof(attnames) <: AbstractArray ? attnames : [attnames])
        attname, attval = nc_get_att(ncid, varid, aattname)
        if ((length(attval) == 1) && !(typeof(attval) <: AbstractString))
            attval = attval[1]
        end
        atts[attname] = attval
    end
    NC_VERBOSE && println(atts)
    return atts
end

function readdimvars(nc::NcFile)
    for v in nc.vars
        if isdimvar(v[2])
            v[2].dim[1].vals = readvar(nc, v[1])
            d[2].atts = v[2].atts
        end
    end
end

function checkboundsNC(v)
    nd = ndims(v)
    for i = 1:nd
        ci = nd + 1 - i
        gstart[ci] < 0 && error("Start index must not be smaller than 1")
        if gstart[ci] + gcount[ci] > v.dim[i].dimlen
            if v.dim[i].unlim
                #Reset length of unlimited dimension
                v.dim[i].dimlen = gstart[ci] + gcount[ci]
            else
                error("Start + Count exceeds dimension length in dimension $(v.dim[i].name)")
            end
        end
        nothing
    end
end

function preparestartcount(start, count, v::NcVar)

    length(start) == v.ndim || error("Length of start ($(length(start))) must equal the number of variable dimensions ($(v.ndim))")
    length(count) == v.ndim || error("Length of start ($(length(count))) must equal the number of variable dimensions ($(v.ndim))")

    p = one(eltype(gcount))
    nd = length(start)

    for i = 1:nd
        ci = nd + 1 - i
        gstart[ci] = start[i] - 1
        gcount[ci] = count[i] < 0 ? v.dim[i].dimlen - gstart[ci] : count[i]

        p *= gcount[ci]
    end

    checkboundsNC(v)

    return p
end

function _readdimvars(nc::NcFile)
    for d in nc.dim
        for v in nc.vars
            if d[2].name == v[2].name
                NC_VERBOSE && println(d[2].name, " ", v[2].name)
                d[2].vals = readvar(nc, v[2].name)
                d[2].atts = v[2].atts
            end
        end
    end
end

ischunked(v::NcVar) = v.chunksize[1] > 0

defaultstart(v::NcVar) = ones(Int, v.ndim)
defaultcount(v::NcVar) = Int[i for i in size(v)]


function parsedimargs(dim)
    isempty(dim) && return NcDim[]
    idim = 0
    dimlen = nothing
    dimvals = nothing
    dimatts = nothing
    name = nothing
    d = NcDim[]
    for a in dim
        NC_VERBOSE && println(a, idim)
        if isa(a, AbstractString)
            #Assume a name is given
            #first create an NcDim object from the last dim
            if (name != nothing)
                push!(d, finalizedim(dimlen, dimvals, dimatts, name))
                idim = idim + 1
                dimlen = nothing
                dimvals = nothing
                dimatts = nothing
                name = nothing
            end
            name = a
        elseif isa(a, Integer)
            #Assume a dimension length is given
            if a > 0
                dimlen = a
            else
                #Assume unlimited dimension
                dimlen = 0
            end
        elseif isa(a, AbstractFloat) && isinf(a)
            #Generate unlimited dimension
            dimlen = 0
        elseif isa(a, AbstractArray)
            #Assume dimension values are given
            if dimvals == nothing
                dimvals = a
                dimlen = length(dimvals)
            else
                error("Dimension values of $name defined more than once")
            end
        elseif isa(a, Dict)
            #Assume attributes are given
            dimatts = dimatts == nothing ? a :
                      error("Dimension attributes of $name defined more than once")
        else
            error("Could not parse argument $a in nccreate")
        end
    end
    push!(d, finalizedim(dimlen, dimvals, dimatts, name))
    return d
end

function finalizedim(dimlen, dimvals, dimatts, name)
    if (dimlen == nothing) && (dimvals == nothing)
        dimlen = 1
    end
    if (dimlen != nothing) && (dimvals == nothing)
        dimvals = Array{Float64}(undef, 0)
    end
    if dimatts == nothing
        dimatts = Dict("missval" => -9999)
    end
    isunlimited = dimlen == 0 ? true : false
    return NcDim(name, dimlen, atts = dimatts, values = dimvals, unlimited = isunlimited)
end
