"""
    bytes_to_human_readable(bytes)

Return a human readable string representation of the given number.

```jldoctest
julia> using MetagraphOptimization

julia> bytes_to_human_readable(4096)
"4.0 KiB"
```
"""
function bytes_to_human_readable(bytes)
    units = ["B", "KiB", "MiB", "GiB", "TiB"]
    unit_index = 1
    while bytes >= 1024 && unit_index < length(units)
        bytes /= 1024
        unit_index += 1
    end
    return string(round(bytes, sigdigits = 4), " ", units[unit_index])
end

"""
    lt_nodes(n1::Node, n2::Node)

Less-Than comparison between nodes. Uses the nodes' ids to sort.
"""
function lt_nodes(n1::Node, n2::Node)
    return n1.id < n2.id
end

"""
    sort_node!(node::Node)

Sort the nodes' parents and children vectors. The vectors are mostly very short so sorting does not take a lot of time.
Sorted nodes are required to make the finding of [`NodeReduction`](@ref)s a lot faster using the [`NodeTrie`](@ref) data structure.
"""
function sort_node!(node::Node)
    sort!(children(node), lt = lt_nodes)
    return sort!(parents(node), lt = lt_nodes)
end

"""
    mem(graph::DAG)

Return the memory footprint of the graph in Byte. Should be the same result as `Base.summarysize(graph)` but a lot faster.
"""
function mem(graph::DAG)
    size = 0
    size += Base.summarysize(graph.nodes, exclude = Union{Node})
    for n in graph.nodes
        size += mem(n)
    end

    size += sizeof(graph.appliedOperations)
    size += sizeof(graph.operationsToApply)

    size += sizeof(graph.possibleOperations)
    for op in graph.possibleOperations.nodeFusions
        size += mem(op)
    end
    for op in graph.possibleOperations.nodeReductions
        size += mem(op)
    end
    for op in graph.possibleOperations.nodeSplits
        size += mem(op)
    end

    size += Base.summarysize(graph.dirtyNodes, exclude = Union{Node})
    return size += sizeof(diff)
end

"""
    mem(op::Operation)

Return the memory footprint of the operation in Byte. Used in [`mem(graph::DAG)`](@ref). Unlike `Base.summarysize()` this doesn't follow all references which would yield (almost) the size of the entire graph.
"""
function mem(op::Operation)
    return Base.summarysize(op, exclude = Union{Node})
end

"""
    mem(op::Operation)

Return the memory footprint of the node in Byte. Used in [`mem(graph::DAG)`](@ref). Unlike `Base.summarysize()` this doesn't follow all references which would yield (almost) the size of the entire graph.
"""
function mem(node::Node)
    return Base.summarysize(node, exclude = Union{Node, Operation})
end

"""
    unroll_symbol_vector(vec::Vector{Symbol})

Return the given vector as single String without quotation marks or brackets.
"""
function unroll_symbol_vector(vec::Vector)
    result = ""
    for s in vec
        if (result != "")
            result *= ", "
        end
        result *= "$s"
    end
    return result
end
