text:
"""
    wkt(geometry; [srid])

Return a string containing the well-known text for a geometry.
"""
function wkt(geometry; srid::String = "")
    io = IOBuffer()
    if srid != ""
        write(io, "SRID=")
        write(io, srid)
        write(io, "; ")
    end
    write_wkt(io, geometry)
    return String(take!(io))
end

# Filenamed-based I/O uses "load" and "save" terminology

"""
    load_wkt(filename; [srid])

Open a file to read well-known text.

Optionally, may read a SRID string from eWKT into the *reference* provided. E.g.

```
srid_ref = Ref("")
geometry = load_wkt("file.wkt"; srid=srid_ref)
srid = srid_ref[]
```
"""
function load_wkt(filename::AbstractString; srid::RefValue{String} = Ref(""))
    open(io -> read_wkt(io; srid = srid), filename)
end

"""
    save_wkt(filename, geometry; [srid])

Save a geometry to a file in well-known text format. Optionally, provide a `srid` string to
write eWKT.
"""
function save_wkt(filename::AbstractString, geometry; srid::String = "")
    open(filename, "w") do (io)
        if srid != ""
            write(io, "SRID=")
            write(io, srid)
            write(io, "; ")
        end
        write_wkt(io, geometry)
    end
    return nothing
end

# IO stream based I/O uses "read" and "write" terminology

"""
    write_wkt(io, geometry)

Writes well-known text to the IO stream `io` for the provided `geometry`.
"""
function write_wkt(io::IO, point::StaticVector{2})
    bytes = 0 # write functions in Julia tend to return number of written bytes
    bytes += write(io, "POINT (")
    bytes += write(io, string(point[1]))
    bytes += write(io, " ")
    bytes += write(io, string(point[2]))
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, point::StaticVector{3})
    bytes = 0
    bytes += write(io, "POINT Z (")
    bytes += write(io, string(point[1]))
    bytes += write(io, " ")
    bytes += write(io, string(point[2]))
    bytes += write(io, " ")
    bytes += write(io, string(point[3]))
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, l::Line{2})
    bytes = 0
    bytes += write(io, "LINESTRING (") # There is no LINE type in WKT
    bytes += write(io, string(l.p1[1]))
    bytes += write(io, " ")
    bytes += write(io, string(l.p1[2]))
    bytes += write(io, ", ")
    bytes += write(io, string(l.p2[1]))
    bytes += write(io, " ")
    bytes += write(io, string(l.p2[2]))
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, l::Line{3})
    bytes = 0
    bytes += write(io, "LINESTRING Z (") # There is no LINE type in WKT
    bytes += write(io, string(l.p1[1]))
    bytes += write(io, " ")
    bytes += write(io, string(l.p1[2]))
    bytes += write(io, " ")
    bytes += write(io, string(l.p1[3]))
    bytes += write(io, ", ")
    bytes += write(io, string(l.p2[1]))
    bytes += write(io, " ")
    bytes += write(io, string(l.p2[2]))
    bytes += write(io, " ")
    bytes += write(io, string(l.p2[3]))
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, ls::LineString{2})
    if length(ls.points) < 2
        return write(io, "LINESTRING EMPTY")
    end

    bytes = 0
    bytes += write(io, "LINESTRING (")
    @inbounds for i = 1:length(ls.points)
        p = ls.points[i]
        bytes += write(io, string(p[1]))
        bytes += write(io, " ")
        bytes += write(io, string(p[2]))
        if i < length(ls.points)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, ls::LineString{3})
    if length(ls.points) < 2
        return write(io, "LINESTRING Z EMPTY")
    end

    bytes = 0
    bytes += write(io, "LINESTRING Z (")
    @inbounds for i = 1:length(ls.points)
        p = ls.points[i]
        bytes += write(io, string(p[1]))
        bytes += write(io, " ")
        bytes += write(io, string(p[2]))
        bytes += write(io, " ")
        bytes += write(io, string(p[3]))
        if i < length(ls.points)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, polygon::Polygon{2})
    if length(polygon.exterior.points) < 4
        return write(io, "POLYGON EMPTY")
    end

    bytes = 0
    bytes += write(io, "POLYGON ((")
    @inbounds for i = 1:length(polygon.exterior.points)
        p = polygon.exterior.points[i]
        bytes += write(io, string(p[1]))
        bytes += write(io, " ")
        bytes += write(io, string(p[2]))
        if i < length(polygon.exterior.points)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")

    for ls in polygon.interiors
        bytes += write(io, ", (")
        @inbounds for i = 1:length(ls.points)
            p = ls.points[i]
            bytes += write(io, string(p[1]))
            bytes += write(io, " ")
            bytes += write(io, string(p[2]))
            if i < length(ls.points)
                bytes += write(io, ", ")
            end
        end
        bytes += write(io, ")")
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, polygon::Polygon{3})
    if length(polygon.exterior.points) < 4
        return write(io, "POLYGON Z EMPTY")
    end

    bytes = 0
    bytes += write(io, "POLYGON Z ((")
    @inbounds for i = 1:length(polygon.exterior.points)
        p = polygon.exterior.points[i]
        bytes += write(io, string(p[1]))
        bytes += write(io, " ")
        bytes += write(io, string(p[2]))
        bytes += write(io, " ")
        bytes += write(io, string(p[3]))
        if i < length(polygon.exterior.points)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")

    for ls in polygon.interiors
        bytes += write(io, ", (")
        @inbounds for i = 1:length(ls.points)
            p = ls.points[i]
            bytes += write(io, string(p[1]))
            bytes += write(io, " ")
            bytes += write(io, string(p[2]))
            bytes += write(io, " ")
            bytes += write(io, string(p[3]))
            if i < length(ls.points)
                bytes += write(io, ", ")
            end
        end
        bytes += write(io, ")")
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, points::AbstractVector{<:StaticVector{2, <:Real}})
    if isempty(points)
        return write(io, "MULTIPOINT EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTIPOINT (")
    @inbounds for i in 1:length(points)
        p = points[i]
        bytes += write(io, "(")
        bytes += write(io, string(p[1]))
        bytes += write(io, " ")
        bytes += write(io, string(p[2]))
        bytes += write(io, ")")
        if i < length(points)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, points::AbstractVector{<:StaticVector{3, <:Real}})
    if isempty(points)
        return write(io, "MULTIPOINT Z EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTIPOINT Z (")
    @inbounds for i in 1:length(points)
        p = points[i]
        bytes += write(io, "(")
        bytes += write(io, string(p[1]))
        bytes += write(io, " ")
        bytes += write(io, string(p[2]))
        bytes += write(io, " ")
        bytes += write(io, string(p[3]))
        bytes += write(io, ")")
        if i < length(points)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, lines::AbstractVector{<:Line{2}})
    if isempty(lines)
        return write(io, "MULTILINESTRING EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTILINESTRING (") # There is no LINE type in WKT
    @inbounds for i in 1:length(lines)
        line = lines[i]
        bytes += write(io, "(")
        bytes += write(io, string(line.p1[1]))
        bytes += write(io, " ")
        bytes += write(io, string(line.p1[2]))
        bytes += write(io, ", ")
        bytes += write(io, string(line.p2[1]))
        bytes += write(io, " ")
        bytes += write(io, string(line.p2[2]))
        bytes += write(io, ")")

        if i < length(lines)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, lines::AbstractVector{<:Line{3}})
    if isempty(lines)
        return write(io, "MULTILINESTRING Z EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTILINESTRING Z (") # There is no LINE type in WKT
    @inbounds for i in 1:length(lines)
        line = lines[i]
        bytes += write(io, "(")
        bytes += write(io, string(line.p1[1]))
        bytes += write(io, " ")
        bytes += write(io, string(line.p1[2]))
        bytes += write(io, " ")
        bytes += write(io, string(line.p1[3]))
        bytes += write(io, ", ")
        bytes += write(io, string(line.p2[1]))
        bytes += write(io, " ")
        bytes += write(io, string(line.p2[2]))
        bytes += write(io, " ")
        bytes += write(io, string(line.p2[3]))
        bytes += write(io, ")")

        if i < length(lines)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, linestrings::AbstractVector{<:LineString{2}})
    if isempty(linestrings)
        return write(io, "MULTILINESTRING EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTILINESTRING (")
    @inbounds for i in 1:length(linestrings)
        ls = linestrings[i]
        if length(ls.points) < 2
            bytes += write(io, "EMPTY")
        else
            bytes += write(io, "(")
            for j in 1:length(ls.points)
                p = ls.points[j]
                bytes += write(io, string(p[1]))
                bytes += write(io, " ")
                bytes += write(io, string(p[2]))
                if j < length(ls.points)
                    bytes += write(io, ", ")
                end
            end
            bytes += write(io, ")")
        end
        if i < length(linestrings)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, linestrings::AbstractVector{<:LineString{3}})
    if isempty(linestrings)
        return write(io, "MULTILINESTRING Z EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTILINESTRING Z (")
    @inbounds for i in 1:length(linestrings)
        ls = linestrings[i]
        if length(ls.points) < 2
            bytes += write(io, "EMPTY")
        else
            bytes += write(io, "(")
            for j in 1:length(ls.points)
                p = ls.points[j]
                bytes += write(io, string(p[1]))
                bytes += write(io, " ")
                bytes += write(io, string(p[2]))
                bytes += write(io, " ")
                bytes += write(io, string(p[3]))
                if j < length(ls.points)
                    bytes += write(io, ", ")
                end
            end
            bytes += write(io, ")")
        end
        if i < length(linestrings)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, polygons::AbstractVector{<:Polygon{2}})
    if isempty(polygons)
        return write(io, "MULTIPOLYGON EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTIPOLYGON (")
    @inbounds for i in 1:length(polygons)
        polygon = polygons[i]
        if length(polygon.exterior.points) < 4
            bytes += write(io, "EMPTY")
        else
            bytes += write(io, "((")
            for j in 1:length(polygon.exterior.points)
                p = polygon.exterior.points[j]
                bytes += write(io, string(p[1]))
                bytes += write(io, " ")
                bytes += write(io, string(p[2]))
                if j < length(polygon.exterior.points)
                    bytes += write(io, ", ")
                end
            end
            bytes += write(io, ")")
            for ls in polygon.interiors
                bytes += write(io, ", (")
                for j in 1:length(ls.points)
                    p = ls.points[j]
                    bytes += write(io, string(p[1]))
                    bytes += write(io, " ")
                    bytes += write(io, string(p[2]))
                    if j < length(ls.points)
                        bytes += write(io, ", ")
                    end
                end
                bytes += write(io, ")")
            end
            bytes += write(io, ")")
        end
        if i < length(polygons)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, polygons::AbstractVector{<:Polygon{3}})
    if isempty(polygons)
        return write(io, "MULTIPOLYGON Z EMPTY")
    end

    bytes = 0
    bytes += write(io, "MULTIPOLYGON Z (")
    @inbounds for i in 1:length(polygons)
        polygon = polygons[i]
        if length(polygon.exterior.points) < 4
            bytes += write(io, "EMPTY")
        else
            bytes += write(io, "((")
            for j in 1:length(polygon.exterior.points)
                p = polygon.exterior.points[j]
                bytes += write(io, string(p[1]))
                bytes += write(io, " ")
                bytes += write(io, string(p[2]))
                bytes += write(io, " ")
                bytes += write(io, string(p[3]))
                if j < length(polygon.exterior.points)
                    bytes += write(io, ", ")
                end
            end
            bytes += write(io, ")")
            for ls in polygon.interiors
                bytes += write(io, ", (")
                for j in 1:length(ls.points)
                    p = ls.points[j]
                    bytes += write(io, string(p[1]))
                    bytes += write(io, " ")
                    bytes += write(io, string(p[2]))
                    bytes += write(io, " ")
                    bytes += write(io, string(p[3]))
                    if j < length(ls.points)
                        bytes += write(io, ", ")
                    end
                end
                bytes += write(io, ")")
            end
            bytes += write(io, ")")
        end
        if i < length(polygons)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

function write_wkt(io::IO, geometries::AbstractVector)
    if isempty(geometries)
        return write(io, "GEOMETRYCOLLECTION EMPTY")
    end

    bytes = 0
    bytes += write(io, "GEOMETRYCOLLECTION (")
    for i in 1:length(geometries)
        bytes += write_wkt(io, geometries[i])
        if i < length(geometries)
            bytes += write(io, ", ")
        end
    end
    bytes += write(io, ")")
    return bytes
end

# Reading functions
const WKT_WHITESPACE = [' ' #= space =#, '\t' #= tab =#, '\n' #= newline =#, '\r' #= for Windows carriage returns =#]
const WKT_SPECIAL_CHARS = ['(', ')', ',', ';', '=']

"""
    tokenize_wkt(io)

Stream `io` and break it into potential WKT tokens, such as "POINT", "3.0", "(", ")", or ",".
Any whitespace is discarded.
"""
function tokenize_wkt(io::IO)
    out = String[]
    str = ""
    while !eof(io)
        c = read(io, Char) # UTF-8
        if isempty(str)
            if c ∉ WKT_WHITESPACE
                if c ∈ WKT_SPECIAL_CHARS
                    push!(out, string(c))
                else
                    str = string(c)
                end
            end
        else
            if c ∈ WKT_WHITESPACE
                push!(out, str)
                str = ""
            elseif c ∈ WKT_SPECIAL_CHARS
                push!(out, str)
                push!(out, string(c))
                str = ""
            else
                str = string(str, c)
            end
        end
    end
    if str != ""
        push!(out, str)
    end
    return out
end

struct WKTParsingError <: Exception
    str::String
end

"""
    read_wkt(string::String)

Parse a well-known text `String` and return a geometry.
"""
function read_wkt(str::String; srid::RefValue{String} = Ref(""))
    read_wkt(IOBuffer(str); srid=srid)
end

"""
    read_wkt(io::IO)

Parse a well-known text from IO stream `io` and return a geometry.
"""
function read_wkt(io::IO; srid::RefValue{String} = Ref(""))
    tokens = tokenize_wkt(io)

    if isempty(tokens)
        throw(WKTParsingError("WKT is empty"))
    end

    i = 1
    if uppercase(tokens[1]) == "SRID"
        # Search for ";" and save SRID information
        srid_string = ""
        while i < length(tokens)
            i += 1
            if i == 2 && tokens[i] != "="
                throw(WKTParsingError("SRID field does not have ="))
            end
            if tokens[i] == ";"
                i += 1
                break
            elseif i > 2
                srid_string = string(srid_string, tokens[i])
            end
        end
        if isempty(srid_string)
            throw(WKTParsingError("SRID field is empty"))
        end
        srid[] = srid_string # Note that this has lost any whitespace...
        if i >= length(tokens)
            throw(WKTParsingError("SRID field does not terminate"))
        end
    end
    (geom, i) = parse_wkt(tokens, i)

    if i != length(tokens) + 1
        throw(WKTParsingError("WKT does not terminate when expected"))
    end
    return geom
end

function parse_wkt(tokens::AbstractVector{<:AbstractString}, i::Integer)
    if uppercase(tokens[i]) == "POINT"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(SVector{2, Float64}, tokens, i + 1)
        elseif length(tokens) > i+1 && uppercase(tokens[i+1]) == "Z" && (tokens[i+2] == "(" || uppercase(tokens[i+2]) == "EMPTY")
            (geom, i) = parse_wkt(SVector{3, Float64}, tokens, i + 2)
        elseif length(tokens) > i && (uppercase(tokens[i+1]) == "M" || tokens[i+1] == "ZM")
            throw(WKTParsingError("Error parsing POINT $(uppercase(tokens[i+1])) (Measures are not supported)"))
        else
            throw(WKTParsingError("Error parsing POINT"))
        end
    elseif uppercase(tokens[i]) == "LINESTRING"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(LineString{2, Float64}, tokens, i + 1)
        elseif length(tokens) > i+1 && uppercase(tokens[i+1]) == "Z" && (tokens[i+2] == "(" || uppercase(tokens[i+2]) == "EMPTY")
            (geom, i) = parse_wkt(LineString{3, Float64}, tokens, i + 2)
        elseif length(tokens) > i && (uppercase(tokens[i+1]) == "M" || tokens[i+1] == "ZM")
            throw(WKTParsingError("Error parsing LINESTRING $(uppercase(tokens[i+1])) (Measures are not supported)"))
        else
            throw(WKTParsingError("Error parsing LINESTRING"))
        end
    elseif uppercase(tokens[i]) == "POLYGON"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(Polygon{2, Float64}, tokens, i + 1)
        elseif length(tokens) > i+1 && uppercase(tokens[i+1]) == "Z" && (tokens[i+2] == "(" || uppercase(tokens[i+2]) == "EMPTY")
            (geom, i) = parse_wkt(Polygon{3, Float64}, tokens, i + 2)
        elseif length(tokens) > i && (uppercase(tokens[i+1]) == "M" || tokens[i+1] == "ZM")
            throw(WKTParsingError("Error parsing POLYGON $(uppercase(tokens[i+1])) (Measures are not supported)"))
        else
            throw(WKTParsingError("Error parsing POLYGON"))
        end
    elseif uppercase(tokens[i]) == "MULTIPOINT"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{SVector{2, Float64}}, tokens, i + 1)
        elseif length(tokens) > i+1 && uppercase(tokens[i+1]) == "Z" && (tokens[i+2] == "(" || uppercase(tokens[i+2]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{SVector{3, Float64}}, tokens, i + 2)
        elseif length(tokens) > i && (uppercase(tokens[i+1]) == "M" || tokens[i+1] == "ZM")
            throw(WKTParsingError("Error parsing MULTIPOINT $(uppercase(tokens[i+1])) (Measures are not supported)"))
        else
            throw(WKTParsingError("Error parsing MULTIPOINT"))
        end
    elseif uppercase(tokens[i]) == "MULTILINESTRING"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{LineString{2, Float64}}, tokens, i + 1)
        elseif length(tokens) > i+1 && uppercase(tokens[i+1]) == "Z" && (tokens[i+2] == "(" || uppercase(tokens[i+2]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{LineString{3, Float64}}, tokens, i + 2)
        elseif length(tokens) > i && (uppercase(tokens[i+1]) == "M" || tokens[i+1] == "ZM")
            throw(WKTParsingError("Error parsing MULTILINESTRING $(uppercase(tokens[i+1])) (Measures are not supported)"))
        else
            throw(WKTParsingError("Error parsing MULTILINESTRING"))
        end
    elseif uppercase(tokens[i]) == "MULTIPOLYGON"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{Polygon{2, Float64}}, tokens, i + 1)
        elseif length(tokens) > i+1 && uppercase(tokens[i+1]) == "Z" && (tokens[i+2] == "(" || uppercase(tokens[i+2]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{Polygon{3, Float64}}, tokens, i + 2)
        elseif length(tokens) > i && (uppercase(tokens[i+1]) == "M" || tokens[i+1] == "ZM")
            throw(WKTParsingError("Error parsing MULTIPOLYGON $(uppercase(tokens[i+1])) (Measures are not supported)"))
        else
            throw(WKTParsingError("Error parsing MULTIPOLYGON"))
        end
    elseif uppercase(tokens[i]) == "GEOMETRYCOLLECTION"
        if length(tokens) > i && (tokens[i+1] == "(" || uppercase(tokens[i+1]) == "EMPTY")
            (geom, i) = parse_wkt(Vector{Any}, tokens, i + 1)
        else
            throw(WKTParsingError("Error parsing GEOMETRYCOLLECTION"))
        end
    else
        throw(WKTParsingError("Found unknown geometry type $(tokens[i])"))
    end

    return (geom, i)
end

function parse_wkt(::Type{SVector{2,Float64}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    # Note that in many contexts, it is optional to surround point coordinates in brackets
    if tokens[i] == "("
        if length(tokens) < i+3 || tokens[i+3] != ")"
            throw(WKTParsingError("Error parsing POINT"))
        end

        x = parse(Float64, tokens[i+1])
        y = parse(Float64, tokens[i+2])
        return (SVector(x, y), i+4)
    elseif tokens[i] == "EMPTY"
        # Not sure what this should be? `SVector(NaN, NaN)`? Or `missing`?
        throw(WKTParsingError("POINT EMPTY is not supported"))
    else
        if length(tokens) < i+1
            throw(WKTParsingError("Error parsing POINT"))
        end

        x = parse(Float64, tokens[i])
        y = parse(Float64, tokens[i+1])
        return (SVector(x, y), i+2)
    end
end

function parse_wkt(::Type{SVector{3,Float64}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    # Note that in many contexts, it is optional to surround point coordinates in brackets
    if tokens[i] == "("
        if length(tokens) < i+4 || tokens[i+4] != ")"
            throw(WKTParsingError("Error parsing POINT Z"))
        end

        x = parse(Float64, tokens[i+1])
        y = parse(Float64, tokens[i+2])
        z = parse(Float64, tokens[i+3])
        return (SVector(x, y,z), i+5)
    elseif tokens[i] == "EMPTY"
        # Not sure what this should be? `SVector(NaN, NaN, Nan)`? Or `missing`?
        throw(WKTParsingError("POINT Z EMPTY is not supported"))
    else
        if length(tokens) < i+2
            throw(WKTParsingError("Error parsing POINT Z"))
        end

        x = parse(Float64, tokens[i])
        y = parse(Float64, tokens[i+1])
        z = parse(Float64, tokens[i+2])
        return (SVector(x, y), i+3)
    end
end

function parse_wkt(::Type{LineString{2,Float64}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (LineString(SVector{2,Float64}[]), i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing LINESTRING"))
    end
    points = Vector{SVector{2, Float64}}()
    i += 1
    while length(tokens) >= i+2
        x = parse(Float64, tokens[i])
        y = parse(Float64, tokens[i+1])
        push!(points, SVector(x, y))

        if tokens[i+2] == ")"
            break
        elseif tokens[i+2] == ","
            i += 3
        else
            throw(WKTParsingError("Error parsing LINESTRING"))
        end
    end

    if tokens[i+2] != ")" || length(points) < 2
        throw(WKTParsingError("Error parsing LINESTRING"))
    end

    return (LineString(points), i+3)
end

function parse_wkt(::Type{LineString{3, Float64}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (LineString(SVector{3,Float64}[]), i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing LINESTRING"))
    end
    points = Vector{SVector{3, Float64}}()
    i += 1
    while length(tokens) > i+2
        x = parse(Float64, tokens[i])
        y = parse(Float64, tokens[i+1])
        z = parse(Float64, tokens[i+2])
        push!(points, SVector(x, y, z))

        if tokens[i+3] == ")"
            break
        elseif tokens[i+3] == ","
            i += 4
        else
            throw(WKTParsingError("Error parsing LINESTRING"))
        end
    end

    if tokens[i+3] != ")" || length(points) < 2
        throw(WKTParsingError("Error parsing LINESTRING"))
    end

    return (LineString(points), i+4)
end

function parse_wkt(::Type{Polygon{2, Float64}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (Polygon(SVector{2,Float64}[]), i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing POLYGON"))
    end
    interiors = Vector{LineString{2, Float64, Vector{SVector{2, Float64}}}}()
    n_linestrings = 0
    i += 1
    while i <= length(tokens)
        (ls, i) = parse_wkt(LineString{2,Float64}, tokens, i)
        if n_linestrings == 0
            exterior = ls
            n_linestrings += 1
        else
            push!(interiors, ls)
            n_linestrings += 1
        end

        if length(tokens) < i
            throw(WKTParsingError("Error parsing POLYGON"))
        end
        if tokens[i] == ")"
            return (Polygon(exterior, interiors), i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing POLYGON"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing POLYGON"))
end

function parse_wkt(::Type{Polygon{3, Float64}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (Polygon(SVector{3,Float64}[]), i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing POLYGON Z"))
    end
    interiors = Vector{LineString{3, Float64, Vector{SVector{3, Float64}}}}()
    n_linestrings = 0
    i += 1
    while i <= length(tokens)
        (ls, i) = parse_wkt(LineString{3,Float64}, tokens, i)
        if n_linestrings == 0
            exterior = ls
            n_linestrings += 1
        else
            push!(interiors, ls)
            n_linestrings += 1
        end

        if length(tokens) < i
            throw(WKTParsingError("Error parsing POLYGON Z"))
        end
        if tokens[i] == ")"
            return (Polygon(exterior, interiors), i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing POLYGON Z"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing POLYGON Z"))
end

function parse_wkt(::Type{Vector{SVector{2, Float64}}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (SVector{2,Float64}[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing MULTIPOINT"))
    end
    points = Vector{SVector{2, Float64}}()
    i += 1
    while i <= length(tokens)
        (point, i) = parse_wkt(SVector{2,Float64}, tokens, i)
        push!(points, point)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing MULTIPOINT"))
        end
        if tokens[i] == ")"
            return (points, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing MULTIPOINT"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing MULTIPOINT"))
end

function parse_wkt(::Type{Vector{SVector{3, Float64}}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (SVector{3,Float64}[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing MULTIPOINT Z"))
    end
    points = Vector{SVector{3, Float64}}()
    i += 1
    while i <= length(tokens)
        (point, i) = parse_wkt(SVector{3,Float64}, tokens, i)
        push!(points, point)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing MULTIPOINT Z"))
        end
        if tokens[i] == ")"
            return (points, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing MULTIPOINT Z"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing MULTIPOINT Z"))
end

function parse_wkt(::Type{Vector{LineString{2, Float64}}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (LineString{2,Float64,Vector{SVector{2,Float64}}}[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing MULTILINESTRING"))
    end
    linestrings = Vector{LineString{2, Float64, Vector{SVector{2, Float64}}}}()
    i += 1
    while i <= length(tokens)
        (ls, i) = parse_wkt(LineString{2,Float64}, tokens, i)
        push!(linestrings, ls)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing MULTILINESTRING"))
        end
        if tokens[i] == ")"
            return (linestrings, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing MULTILINESTRING"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing MULTILINESTRING"))
end

function parse_wkt(::Type{Vector{LineString{3, Float64}}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (LineString{3,Float64,Vector{SVector{3,Float64}}}[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing MULTILINESTRING Z"))
    end
    linestrings = Vector{LineString{3, Float64, Vector{SVector{3, Float64}}}}()
    i += 1
    while i <= length(tokens)
        (ls, i) = parse_wkt(LineString{3,Float64}, tokens, i)
        push!(linestrings, ls)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing MULTILINESTRING Z"))
        end
        if tokens[i] == ")"
            return (linestrings, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing MULTILINESTRING Z"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing MULTILINESTRING Z"))
end

function parse_wkt(::Type{Vector{Polygon{2, Float64}}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (Polygon{2,Float64,LineString{2,Float64,Vector{SVector{2,Float64}}},Vector{LineString{2,Float64,Vector{SVector{2,Float64}}}}}[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing MULTIPOLYGON"))
    end
    polygons = Vector{Polygon{2,Float64,LineString{2,Float64,Vector{SVector{2,Float64}}},Vector{LineString{2,Float64,Vector{SVector{2,Float64}}}}}}()
    i += 1
    while i <= length(tokens)
        (polygon, i) = parse_wkt(Polygon{2,Float64}, tokens, i)
        push!(polygons, polygon)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing MULTIPOLYGON"))
        end
        if tokens[i] == ")"
            return (polygons, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing MULTIPOLYGON"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing MULTIPOLYGON"))
end

function parse_wkt(::Type{Vector{Polygon{3, Float64}}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (Polygon{3,Float64,LineString{3,Float64,Vector{SVector{3,Float64}}},Vector{LineString{3,Float64,Vector{SVector{3,Float64}}}}}[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing MULTIPOLYGON Z"))
    end
    polygons = Vector{Polygon{3,Float64,LineString{3,Float64,Vector{SVector{3,Float64}}},Vector{LineString{3,Float64,Vector{SVector{3,Float64}}}}}}()
    i += 1
    while i <= length(tokens)
        (polygon, i) = parse_wkt(Polygon{3,Float64}, tokens, i)
        push!(polygons, polygon)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing MULTIPOLYGON Z"))
        end
        if tokens[i] == ")"
            return (polygons, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing MULTIPOLYGON Z"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing MULTIPOLYGON Z"))
end

function parse_wkt(::Type{Vector{Any}}, tokens::AbstractVector{<:AbstractString}, i::Integer)
    if tokens[i] == "EMPTY"
        return (Any[], i+1)
    end
    if tokens[i] != "("
        throw(WKTParsingError("Error parsing GEOMETRYCOLLECTION"))
    end
    geometries = Vector{Any}()
    i += 1
    while i <= length(tokens)
        (geometry, i) = parse_wkt(tokens, i)
        push!(geometries, geometry)

        if length(tokens) < i
            throw(WKTParsingError("Error parsing GEOMETRYCOLLECTION"))
        end
        if tokens[i] == ")"
            return (geometries, i+1)
        end
        if tokens[i] != ","
           throw(WKTParsingError("Error parsing GEOMETRYCOLLECTION"))
        end
        i += 1
    end
    throw(WKTParsingError("Error parsing GEOMETRYCOLLECTION"))
end


text:
using Lehmann
using FastGaussQuadrature, Printf

function kernelMatrix(ktype, x, ω, β) # both x and \omege are vectors
    if ktype == 0
        ker = zeros(Float64, (length(x), length(ω)))
    else
        ker = zeros(ComplexF64, (length(x), length(ω)))
    end
    for (ei, e) in enumerate(x)
        for (wi, w) in enumerate(ω)
            ker[ei, wi] = kernel(ktype, e, w, β)
        end
    end
    return ker
end

function kernel(ktype, x, ω, β)
    if ktype == 0
        ker = Spectral.kernelT(type, x, ω, β)
    elseif ktype == 1
        ker = Spectral.kernelΩ(type, x, ω, β)
    else
        ωn = (2x + 1) * π / β
        ker = 1im / (1im * ωn - ω)^2
    end
    return ker
end

function SemiCircle(type, Grid, β, Euv, ktype)
    # calculate Green's function defined by the spectral density
    # S(ω) = sqrt(1 - (ω / Euv)^2) / Euv # semicircle -1<ω<1

    ##### Panels endpoints for composite quadrature rule ###
    npo = Int(ceil(log(β * Euv) / log(2.0)))
    pbp = zeros(Float64, 2npo + 1)
    pbp[npo + 1] = 0.0
    for i in 1:npo
        pbp[npo + i + 1] = 1.0 / 2^(npo - i)
    end
    pbp[1:npo] = -pbp[2npo + 1:-1:npo + 2]

    function Green(n, ktype)
        # n: polynomial order
        xl, wl = gausslegendre(n)
        xj, wj = gaussjacobi(n, 1 / 2, 0.0)
        if ktype == 0
            G = zeros(Float64, length(Grid))
        elseif ktype == 1 || ktype == 2
            G = zeros(ComplexF64, length(Grid))
        end
        err = zeros(Float64, length(Grid))
        for (τi, τ) in enumerate(Grid)
            for ii in 2:2npo - 1
                a, b = pbp[ii], pbp[ii + 1]
                for jj in 1:n
                    x = (a + b) / 2 + (b - a) / 2 * xl[jj]
                    if type == :corr && x < 0.0
                        # spectral density is defined for positivie frequency only for correlation functions
                        continue
                    end
                    ker = kernel(ktype, τ, x * Euv, β)
                    G[τi] += (b - a) / 2 * wl[jj] * ker * sqrt(1 - x^2)
                end
            end

            a, b = 1.0 / 2, 1.0
            for jj in 1:n
                x = (a + b) / 2 + (b - a) / 2 * xj[jj]
                ker = kernel(ktype, τ, x * Euv, β)
                # ker = IsMatFreq ? Spectral.kernelΩ(type, τ, Euv * x, β) : Spectral.kernelT(type, τ, Euv * x, β)
                G[τi] += ((b - a) / 2)^1.5 * wj[jj] * ker * sqrt(1 + x)
            end

            if type != :corr
                # spectral density is defined for positivie frequency only for correlation functions
                a, b = -1.0, -1.0 / 2
                for jj in 1:n
                    x = (a + b) / 2 + (b - a) / 2 * (-xj[n - jj + 1])
                    ker = kernel(ktype, τ, x * Euv, β)
                    # ker = IsMatFreq ? Spectral.kernelΩ(type, τ, Euv * x, β) : Spectral.kernelT(type, τ, Euv * x, β)
                    G[τi] += ((b - a) / 2)^1.5 * wj[n - jj + 1] * ker * sqrt(1 - x)
                end
            end
        end
        return G
    end

    g1 = Green(24, ktype)
    g2 = Green(48, ktype)
    err = abs.(g1 - g2)

    println("Semi-circle case integration error = ", maximum(err))
    return g2, err
end

function dlr2matfreqD(dlrcoeff, dlrGrid::DLR.DLRGrid, nGrid; axis=1)
    @assert length(size(dlrcoeff)) >= axis "dimension of the dlr coefficients should be larger than axis!"
    ωGrid = dlrGrid.ω

    kernel = kernelMatrix(2, nGrid, ωGrid, dlrGrid.β)

    coeff, partialsize = DLR._tensor2matrix(dlrcoeff, axis)

    G = kernel * coeff # tensor dot product: \sum_i kernel[..., i]*coeff[i, ...]

    return DLR._matrix2tensor(G, partialsize, axis)
end

rtol(x, y) = maximum(abs.(x - y)) / maximum(abs.(x))

β = 10.0
Euv = 1.0
eps = 1.0e-8
type = :fermi

dlr = DLR.DLRGrid(type, 1Euv, β, eps) # construct dlr basis

Gt0 = SemiCircle(type, dlr.τ, β, Euv, 0)[1] # Gτ in dlr τ grid

coeff = DLR.tau2dlr(type, Gt0, dlr)

Gtfitted = DLR.dlr2tau(type, coeff, dlr, dlr.τ)

println("τ fit error: ", rtol(Gt0, Gtfitted))

ngrid = collect(-10:10)
Gwn = SemiCircle(type, ngrid, β, Euv, 1)[1]  # G(wn) in user-defined wn grid

Gwnfitted = DLR.dlr2matfreq(type, coeff, dlr, ngrid)
println("ωn fit error: ", rtol(Gwn, Gwnfitted))

GwnD = SemiCircle(type, ngrid, β, Euv, 2)[1]

GwnDfitted = dlr2matfreqD(coeff, dlr, ngrid)
println("derivative in ωn fit error: ", rtol(GwnD, GwnDfitted))

for (wi, w) in enumerate(ngrid)
    @printf("%3i   %10.6f + %10.6fim   %10.6f + %10.6fim   %12.6f\n", w, real(GwnD[wi]), imag(GwnD[wi]), real(GwnDfitted[wi]), imag(GwnDfitted[wi]), abs(GwnDfitted[wi] - GwnD[wi]))
end

ext: jl


text:
@test mPulseAPI.default_SOASTAEndpoint == "https://mpulse.soasta.com/concerto"

@test isconst(mPulseAPI, :default_SOASTAEndpoint)

if !isempty(endpoint)
    @test mPulseAPI.ObjectEndpoint == "$(endpoint)/services/rest/RepositoryService/v1/Objects"
    @test mPulseAPI.mPulseEndpoint == "$(endpoint)/mpulse/api/v2/"
else
    @test mPulseAPI.ObjectEndpoint == "$(mPulseAPI.default_SOASTAEndpoint)/services/rest/RepositoryService/v1/Objects"
    @test mPulseAPI.mPulseEndpoint == "$(mPulseAPI.default_SOASTAEndpoint)/mpulse/api/v2/"
end

new_endpoint = "https://foo.bar.com"

@test mPulseAPI.setEndpoints(new_endpoint) == ("$new_endpoint/mpulse/api/v2/", "$new_endpoint/services/rest/RepositoryService/v1")

@test mPulseAPI.ObjectEndpoint == "$(new_endpoint)/services/rest/RepositoryService/v1/Objects"
@test mPulseAPI.mPulseEndpoint == "$(new_endpoint)/mpulse/api/v2/"

@test mPulseAPI.setEndpoints() == ("$(mPulseAPI.default_SOASTAEndpoint)/mpulse/api/v2/", "$(mPulseAPI.default_SOASTAEndpoint)/services/rest/RepositoryService/v1")

@test mPulseAPI.ObjectEndpoint == "$(mPulseAPI.default_SOASTAEndpoint)/services/rest/RepositoryService/v1/Objects"
@test mPulseAPI.mPulseEndpoint == "$(mPulseAPI.default_SOASTAEndpoint)/mpulse/api/v2/"


text:
# [
#  [ MPIStateArray Name, Field Name, Maximum, Minimum, Mean, Standard Deviation ],
#  [         :                :          :        :      :          :           ],
# ]

#! format: off

parr = [
 ["state",     "ρ", 12, 12, 12, 12],
 ["state", "ρu[1]",  0,  0,  0,  0],
 ["state", "ρu[2]",  0,  0,  0,  0],
 ["state", "ρu[3]", 12,  8, 12, 12],
 ["state",    "ρθ", 12, 10, 12, 12],
]

second_order_flat = (
[
 [ "state",     "ρ",   9.95252314022507689195e-01,  9.99992856011554298590e-01,  9.98330419819817738158e-01,  1.48639562654353791886e-03 ],
 [ "state", "ρu[1]",  -6.36093867411581375298e-15,  8.28629359748938561747e-15,  5.16470747993988398929e-16,  1.17216775586980107502e-15 ],
 [ "state", "ρu[2]",  -3.70797682276813316617e-15,  9.42818503691220985643e-15,  6.07164354178963622023e-16,  1.23143801722553508699e-15 ],
 [ "state", "ρu[3]",  -1.65133743521589989450e-03,  5.29367075491514133403e-09, -8.40309050919211576736e-04,  4.66618546037470796790e-04 ],
 [ "state",    "ρθ",  -9.95249493245247940365e+00,  1.99973806108046075331e-05, -4.98740538889376860965e+00,  2.91965149708573168397e+00 ],
],
 parr,
)

second_order = (
[
 [ "state",     "ρ",   9.95252314022507689195e-01,  9.99992856011554298590e-01,  9.98330419819817738158e-01,  1.48639562654353791886e-03 ],
 [ "state", "ρu[1]",  -6.36142448250610767485e-15,  8.28656656189152269018e-15,  5.17120118507278235699e-16,  1.17302082244252352324e-15 ],
 [ "state", "ρu[2]",  -3.70772154095122499196e-15,  9.42925797553789671672e-15,  6.06575884435408245721e-16,  1.23026363135339100359e-15 ],
 [ "state", "ρu[3]",  -1.65133743521588883564e-03,  5.29367075398690075732e-09, -8.40309050919211468315e-04,  4.66618546037470417320e-04 ],
 [ "state",    "ρθ",  -9.95249493245247940365e+00,  1.99973806108054952236e-05, -4.98740538889376860965e+00,  2.91965149708573168397e+00 ],
],
 parr,
)

fourth_order_flat = (
[
 [ "state",     "ρ",   9.95377495534709000324e-01,  9.99992951378667060958e-01,  9.98321272635789513927e-01,  1.50722816639464090097e-03 ],
 [ "state", "ρu[1]",  -1.22866254290737312252e-14,  1.95946879007561421956e-14,  1.73741725325975513295e-15,  4.23083086870931785383e-15 ],
 [ "state", "ρu[2]",  -1.63058197687820133665e-14,  2.71134092821328864807e-14,  1.75302916450504673999e-15,  4.33921909208334743316e-15 ],
 [ "state", "ρu[3]",  -1.66252523985498286418e-03,  5.55021884647818839491e-08, -8.13842490777055664608e-04,  4.76414940515918383830e-04 ],
 [ "state",    "ρθ",  -9.95373884733410818626e+00, -4.05672848247591079102e-07, -4.98722877855243940104e+00,  2.97859288054288384728e+00 ],
],
 parr,
)

fourth_order = (
[
 [ "state",     "ρ",   9.95377495534709000324e-01,  9.99992951378667060958e-01,  9.98321272635789513927e-01,  1.50722816639464068413e-03 ],
 [ "state", "ρu[1]",  -1.22778750186973996879e-14,  1.95893093780676565332e-14,  1.73744809621522011733e-15,  4.23044883848200063451e-15 ],
 [ "state", "ρu[2]",  -1.62568523313266733927e-14,  2.71071963864711276060e-14,  1.75189805678757112174e-15,  4.33763908149213610479e-15 ],
 [ "state", "ρu[3]",  -1.66252523985503642377e-03,  5.55021884639224168229e-08, -8.13842490777055447768e-04,  4.76414940515918817511e-04 ],
 [ "state",    "ρθ",  -9.95373884733410818626e+00, -4.05672848142820462126e-07, -4.98722877855243940104e+00,  2.97859288054288384728e+00 ],
],
 parr,
)

#! format: on

refVals = (; second_order, fourth_order, second_order_flat, fourth_order_flat)


text:
#################################################################
#                                                               #
# This file provides functions for interfacing with .pwf files  #
#                                                               #
#################################################################

# This parser was develop using ANAREDE v09 user manual

const _fban_1_dtypes = [("FROM BUS", Int64, 1:5), ("OPERATION", Int64, 7),
    ("TO BUS", Int64, 9:13), ("CIRCUIT", Int64, 15:16), ("CONTROL MODE", Char, 18),
    ("MINIMUM VOLTAGE", Float64, 20:23, 20), ("MAXIMUM VOLTAGE", Float64, 25:28, 25),
    ("CONTROLLED BUS", Int64, 30:34), ("INITIAL REACTIVE INJECTION", Float64, 36:41),
    ("CONTROL TYPE", Char, 43), ("ERASE DBAR", Char, 45), ("EXTREMITY", Int64, 47:51)]

const _fban_2_dtypes = [("GROUP", Int64, 1:2), ("OPERATION", Char, 5), ("STATUS", Char, 7),
    ("UNITIES", Int64, 9:11), ("OPERATING UNITIES", Int64, 13:15),
    ("REACTANCE", Float64, 17:22)]

const _dbsh_dtypes = Dict("first half" => _fban_1_dtypes, "first name" => "BUS AND VOLTAGE CONTROL",
    "second half" => _fban_2_dtypes, "second name" => "REACTORS AND CAPACITORS BANKS",
    "separator" => "FBAN", "subgroup" => "REACTANCE GROUPS")

const _fagr_1_dtypes = [("NUMBER", Int64, 1:3), ("DESCRIPTION", String, 5:40)]

const _fagr_2_dtypes = [("NUMBER", Int64, 1:3), ("OPERATION", Char, 5), ("DESCRIPTION", String, 7:42)]

const _dagr_dtypes = Dict("first half" => _fagr_1_dtypes, "first name" => "AGGREGATOR IDENTIFICATION",
    "second half" => _fagr_2_dtypes, "second name" => "AGGREGATOR OCCURENCES",
    "separator" => "FAGR", "subgroup" => "OCCURENCES")

const _divided_sections = Dict("DBSH" => _dbsh_dtypes,
                               "DAGR" => _dagr_dtypes)

"""
A list of data file sections in the order that they appear in a PWF file
"""
const _dbar_dtypes = [("NUMBER", Int64, 1:5), ("OPERATION", Int64, 6),
    ("STATUS", Char, 7), ("TYPE", Int64, 8), ("BASE VOLTAGE GROUP", String, 9:10),
    ("NAME", String, 11:22), ("VOLTAGE LIMIT GROUP", String, 23:24),
    ("VOLTAGE", Float64, 25:28, 25), ("ANGLE", Float64, 29:32),
    ("ACTIVE GENERATION", Float64, 33:37), ("REACTIVE GENERATION", Float64, 38:42),
    ("MINIMUM REACTIVE GENERATION", Float64, 43:47),
    ("MAXIMUM REACTIVE GENERATION",Float64, 48:52), ("CONTROLLED BUS", Int64, 53:58),
    ("ACTIVE CHARGE", Float64, 59:63), ("REACTIVE CHARGE", Float64, 64:68),
    ("TOTAL REACTIVE POWER", Float64, 69:73), ("AREA", Int64, 74:76),
    ("CHARGE DEFINITION VOLTAGE", Float64, 77:80, 77), ("VISUALIZATION", Int64, 81),
    ("AGGREGATOR 1", Int64, 82:84), ("AGGREGATOR 2", Int64, 85:87),
    ("AGGREGATOR 3", Int64, 88:90), ("AGGREGATOR 4", Int64, 91:93),
    ("AGGREGATOR 5", Int64, 94:96), ("AGGREGATOR 6", Int64, 97:99),
    ("AGGREGATOR 7", Int64, 100:102), ("AGGREGATOR 8", Int64, 103:105),
    ("AGGREGATOR 9", Int64, 106:108), ("AGGREGATOR 10", Int64, 109:111)]

const _dlin_dtypes = [("FROM BUS", Int64, 1:5), ("OPENING FROM BUS", Char, 6),
    ("OPERATION", Int64, 8), ("OPENING TO BUS", Char, 10), ("TO BUS", Int64, 11:15),
    ("CIRCUIT", Int64, 16:17), ("STATUS", Char, 18), ("OWNER", Char, 19),
    ("RESISTANCE", Float64, 21:26, 24), ("REACTANCE", Float64, 27:32, 30),
    ("SHUNT SUSCEPTANCE", Float64, 33:38, 35), ("TAP", Float64, 39:43, 40),
    ("MINIMUM TAP", Float64, 44:48, 45), ("MAXIMUM TAP", Float64, 49:53, 50),
    ("LAG", Float64, 54:58, 56), ("CONTROLLED BUS", Int64, 59:64),
    ("NORMAL CAPACITY", Float64, 65:68), ("EMERGENCY CAPACITY", Float64, 69:72),
    ("NUMBER OF TAPS", Int64, 73:74), ("EQUIPAMENT CAPACITY", Float64, 75:78),
    ("AGGREGATOR 1", Int64, 79:81), ("AGGREGATOR 2", Int64, 82:84),
    ("AGGREGATOR 3", Int64, 85:87), ("AGGREGATOR 4", Int64, 88:90),
    ("AGGREGATOR 5", Int64, 91:93), ("AGGREGATOR 6", Int64, 94:96),
    ("AGGREGATOR 7", Int64, 97:99), ("AGGREGATOR 8", Int64, 100:102),
    ("AGGREGATOR 9", Int64, 103:105), ("AGGREGATOR 10", Int64, 106:108)]

const _dgbt_dtypes = [("GROUP", String, 1:2), ("VOLTAGE", Float64, 4:8)]

const _dglt_dtypes = [("GROUP", String, 1:2), ("LOWER BOUND", Float64, 4:8),
    ("UPPER BOUND", Float64, 10:14), ("LOWER EMERGENCY BOUND", Float64, 16:20),
    ("UPPER EMERGENCY BOUND", Float64, 22:26)]

const _dger_dtypes = [("NUMBER", Int, 1:5), ("OPERATION", Char, 7),
    ("MINIMUM ACTIVE GENERATION", Float64, 9:14),
    ("MAXIMUM ACTIVE GENERATION", Float64, 16:21),
    ("PARTICIPATION FACTOR", Float64, 23:27),
    ("REMOTE CONTROL PARTICIPATION FACTOR", Float64, 29:33),
    ("NOMINAL POWER FACTOR", Float64, 35:39), ("ARMATURE SERVICE FACTOR", Float64, 41:44),
    ("ROTOR SERVICE FACTOR", Float64, 46:49), ("CHARGE ANGLE", Float64, 51:54),
    ("MACHINE REACTANCE", Float64, 56:60), ("NOMINAL APPARENT POWER", Float64, 62:66)]

const _dshl_dtypes = [("FROM BUS", Int64, 1:5), ("OPERATION", Int64, 7),
    ("TO BUS", Int64, 10:14), ("CIRCUIT", Int64, 15:16), ("SHUNT FROM", Float64, 18:23),
    ("SHUNT TO", Float64, 24:29), ("STATUS FROM", String, 31:32), ("STATUS TO", String, 34:35)]

const _dcba_dtypes = [("NUMBER", Int64, 1:4), ("OPERATION", Int64, 6), ("TYPE", Int64, 8),
    ("POLARITY", Char, 9), ("NAME", String, 10:21), ("VOLTAGE LIMIT GROUP", String, 22:23),
    ("VOLTAGE", Float64, 24:28), ("GROUND ELECTRODE", Float64, 67:71), ("DC LINK", Int64, 72:75)]

const _dcli_dtypes = [("FROM BUS", Int64, 1:4), ("OPERATION", Int64, 6), ("TO BUS", Int64, 9:12),
    ("CIRCUIT", Int64, 13:14), ("OWNER", Char, 16), ("RESISTANCE", Float64, 18:23),
    ("INDUCTANCE", Float64, 24:29), ("CAPACITY", Float64, 61:64)]

const _dcnv_dtypes = [("NUMBER", Int64, 1:4), ("OPERATION", Int64, 6), ("AC BUS", Int64, 8:12),
    ("DC BUS", Int64, 14:17), ("NEUTRAL BUS", Int64, 19:22), ("OPERATION MODE", Char, 24),
    ("BRIDGES", Int64, 26), ("CURRENT", Float64, 28:32), ("COMMUTATION REACTANCE", Float64, 34:38),
    ("SECONDARY VOLTAGE", Float64, 40:44), ("TRANSFORMER POWER", Float64, 46:50),
    ("REACTOR RESISTANCE", Float64, 52:56), ("REACTOR INDUCTANCE", Float64, 58:62),
    ("CAPACITANCE", Float64, 64:68), ("FREQUENCY", Float64, 70:71)]

const _dccv_dtypes = [("NUMBER", Int64, 1:4), ("OPERATION", Int64, 6), ("LOOSENESS", Char, 8),
    ("INVERTER CONTROL MODE", Char, 9), ("CONVERTER CONTROL TYPE", Char, 10),
    ("SPECIFIED VALUE", Float64, 12:16), ("CURRENT MARGIN", Float64,18:22),
    ("MAXIMUM OVERCURRENT", Float64, 24:28), ("CONVERTER ANGLE", Float64, 30:34),
    ("MINIMUM CONVERTER ANGLE", Float64, 36:40), ("MAXIMUM CONVERTER ANGLE", Float64, 42:46),
    ("MINIMUM TRANSFORMER TAP", Float64, 48:52), ("MAXIMUM TRANSFORMER TAP", Float64, 54:58),
    ("TRANSFORMER TAP NUMBER OF STEPS", Int64, 60:61),
    ("MINIMUM DC VOLTAGE FOR POWER CONTROL", Float64, 63:66, 63),
    ("TAP HI MVAR MODE", Float64, 68:72), ("TAP REDUCED VOLTAGE MODE", Float64, 74:78)]

const _delo_dtypes = [("NUMBER", Int64, 1:4), ("OPERATION", Int64, 6), ("VOLTAGE", Float64, 8:12),
    ("BASE", Float64, 14:18), ("NAME", String, 20:39), ("HI MVAR MODE", Char, 41), ("STATUS", Char, 43)]

const _dcer_dtypes = [("BUS", Int, 1:5), ("OPERATION", Char, 7), ("GROUP", Int64, 9:10),
    ("UNITIES", Int64, 12:13), ("CONTROLLED BUS", Int64, 15:19), ("INCLINATION", Float64, 21:26),
    ("REACTIVE GENERATION", Float64, 28:32), ("MINIMUM REACTIVE GENERATION", Float64, 33:37),
    ("MAXIMUM REACTIVE GENERATION", Float64, 38:42), ("CONTROL MODE", Char, 44), ("STATUS", Char, 46)]

const _dcsc_dtypes = [("FROM BUS", Int64, 1:5), ("OPERATION", Char, 7), ("TO BUS", Int64, 10:14),
    ("CIRCUIT", Int64, 15:16), ("STATUS", Char, 17), ("OWNER", Char, 18), ("BYPASS", Char, 19),
    ("MINIMUM VALUE", Float64, 26:31), ("MAXIMUM VALUE", Float64, 32:37), ("INITIAL VALUE", Float64, 38:43),
    ("CONTROL MODE", Char, 44), ("SPECIFIED VALUE", Float64, 46:51), ("MEASUREMENT EXTREMITY", Int64, 53:57),
    ("NUMBER OF STAGES", Int64, 58:60), ("NORMAL CAPACITY", Float64, 61:64),
    ("EMERGENCY CAPACITY", Float64, 65:68), ("EQUIPAMENT CAPACITY", Float64, 69:72), ("AGGREGATOR 1", Int64, 73:75),
    ("AGGREGATOR 2", Int64, 76:78), ("AGGREGATOR 3", Int64, 79:81), ("AGGREGATOR 4", Int64, 82:84),
    ("AGGREGATOR 5", Int64, 85:87), ("AGGREGATOR 6", Int64, 88:90), ("AGGREGATOR 7", Int64, 91:93),
    ("AGGREGATOR 8", Int64, 94:96), ("AGGREGATOR 9", Int64, 97:99), ("AGGREGATOR 10", Int64, 100:102)]

const _dcar_dtypes = [("ELEMENT 1 TYPE", String, 1:4), ("ELEMENT 1 IDENTIFICATION", Int64, 6:10),
    ("CONDITION 1", Char, 12), ("ELEMENT 2 TYPE", String, 14:17), ("ELEMENT 2 IDENTIFICATION", Int64, 19:23),
    ("MAIN CONDITION", Char, 25), ("ELEMENT 3 TYPE", String, 27:30), ("ELEMENT 3 IDENTIFICATION", Int64, 32:36),
    ("CONDITION 2", Char, 38), ("ELEMENT 4 TYPE", String, 40:43), ("ELEMENT 4 IDENTIFICATION", Int64, 45:49),
    ("OPERATION", Char, 51), ("PARAMETER A", Float64, 53:55), ("PARAMETER B", Float64, 57:59),
    ("PARAMETER C", Float64, 61:63), ("PARAMETER D", Float64, 65:67), ("VOLTAGE", Float64, 69:73)]

const _dctr_dtypes = [("FROM BUS", Int64, 1:5), ("OPERATION", Char, 7), ("TO BUS", Int64, 9:13),
    ("CIRCUIT", Int64, 15:16), ("MINIMUM VOLTAGE", Float64, 18:21), ("MAXIMUM VOLTAGE", Float64, 23:26),
    ("BOUNDS CONTROL TYPE", Char, 28), ("CONTROL MODE", Char, 30), ("MINIMUM PHASE", Float64, 32:37),
    ("MAXIMUM PHASE", Float64, 39:44), ("CONTROL TYPE", Char, 46), ("SPECIFIED VALUE", Float64, 48:53),
    ("MEASUREMENT EXTREMITY", Int64, 55:59)]

const _dare_dtypes = [("NUMBER", Int64, 1:3), ("NET INTERCHANGE", Float64, 8:13),
    ("NAME", String, 19:54), ("MINIMUM INTERCHANGE", Float64, 56:61),
    ("MAXIMUM INTERCHANGE", Float64, 63:68)]

const _dtpf_circ_dtypes = [("FROM BUS 1", Int64, 1:5), ("TO BUS 1", Int64, 7:11),
    ("CIRCUIT 1", Int64, 13:14), ("FROM BUS 2", Int64, 16:20), ("TO BUS 2", Int64, 22:26),
    ("CIRCUIT 2", Int64, 28:29), ("FROM BUS 3", Int64, 31:35), ("TO BUS 3", Int64, 37:41),
    ("CIRCUIT 3", Int64, 43:44), ("FROM BUS 4", Int64, 46:50), ("TO BUS 4", Int64, 52:56),
    ("CIRCUIT 4", Int64, 58:59), ("FROM BUS 5", Int64, 61:65), ("TO BUS 5", Int64, 67:71),
    ("CIRCUIT 5", Int64, 73:74), ("OPERATION", Char, 76)]

const _dmte_dtypes = [("ELEMENT 1 TYPE", String, 1:4), ("ELEMENT 1 IDENTIFICATION", Int64, 6:10),
    ("CONDITION 1", Char, 12), ("ELEMENT 2 TYPE", String, 14:17), ("ELEMENT 2 IDENTIFICATION", Int64, 19:23),
    ("MAIN CONDITION", Char, 25), ("ELEMENT 3 TYPE", String, 27:30), ("ELEMENT 3 IDENTIFICATION", Int64, 32:36),
    ("CONDITION 2", Char, 38), ("ELEMENT 4 TYPE", String, 40:43), ("ELEMENT 4 IDENTIFICATION", Int64, 45:49),
    ("OPERATION", Char, 51), ("BOUNDARIES", Char, 53)]

const _dmfl_circ_dtypes = [("FROM BUS 1", Int64, 1:5), ("TO BUS 1", Int64, 7:11),
    ("CIRCUIT 1", Int64, 13:14), ("FROM BUS 2", Int64, 16:20), ("TO BUS 2", Int64, 22:26),
    ("CIRCUIT 2", Int64, 28:29), ("FROM BUS 3", Int64, 31:35), ("TO BUS 3", Int64, 37:41),
    ("CIRCUIT 3", Int64, 43:44), ("FROM BUS 4", Int64, 46:50), ("TO BUS 4", Int64, 52:56),
    ("CIRCUIT 4", Int64, 58:59), ("FROM BUS 5", Int64, 61:65), ("TO BUS 5", Int64, 67:71),
    ("CIRCUIT 5", Int64, 73:74), ("OPERATION", Char, 76)]

const _dcai_dtypes = [("BUS", Int64, 1:5), ("OPERATION", Char, 7), ("GROUP", Int64, 10:11),
    ("STATUS", Char, 13), ("UNITIES", Int64, 15:17), ("OPERATING UNITIES", Int64, 19:21),
    ("ACTIVE CHARGE", Float64, 23:27), ("REACTIVE CHARGE", Float64, 29:33),
    ("PARAMETER A", Float64, 35:37), ("PARAMETER B", Float64, 39:41),
    ("PARAMETER C", Float64, 43:45), ("PARAMETER D", Float64, 47:49), ("VOLTAGE", Float64, 51:55),
    ("CHARGE DEFINITION VOLTAGE", Float64, 57:60)]

const _dgei_dtypes = [("BUS", Int64, 1:5), ("OPERATION", Char, 7), ("AUTOMATIC MODE", Char, 8),
    ("GROUP", Int64, 10:11), ("STATUS", Char, 13), ("UNITIES", Int64, 14:16),
    ("OPERATING UNITIES", Int64, 17:19), ("MINIMUM OPERATING UNITIES", Int64, 20:22),
    ("ACTIVE GENERATION", Float64, 23:27), ("REACTIVE GENERATION", Float64, 28:32),
    ("MINIMUM REACTIVE GENERATION", Float64, 33:37), ("MAXIMUM REACTIVE GENERATION", Float64, 38:42),
    ("ELEVATOR TRANSFORMER REACTANCE", Float64, 43:48), ("XD", Float64, 50:54, 53),
    ("XQ", Float64, 55:59, 58), ("XL", Float64, 60:64, 63), ("POWER FACTOR", Float64, 66:69, 67),
    ("APPARENT POWER", Float64, 70:74, 72), ("MECHANICAL LIMIT", Float64, 75:79, 77)]

const _dmot_dtypes = [("BUS", Int64, 1:5), ("OPERATION", Char, 7), ("STATUS", Char, 8),
    ("GROUP", Int64, 10:11), ("SIGN", Char, 12), ("LOADING FACTOR", Float64, 13:15),
    ("UNITIES", Int64, 17:19), ("STATOR RESISTANCE", Float64, 21:25), ("STATOR REACTANCE", Float64, 27:31),
    ("MAGNETAZING REACTANCE", Float64, 33:37), ("ROTOR RESISTANCE", Float64, 39:43),
    ("ROTOR REACTANCE", Float64, 45:49), ("BASE POWER", Float64, 51:55),
    ("ENGINE TYPE", Int64, 57:59), ("ACTIVE CHARGE PORTION", Float64, 60:63),
    ("BASE POWER DEFINITION PERCENTAGE", Float64, 65:67)]

const _dcmt_dtypes = [("COMMENTS", String, 1:80)]

const _dinj_dtypes = [("NUMBER", Int64, 1:5), ("OPERATION", Char, 7),
    ("EQUIVALENT ACTIVE INJECTION", Float64, 9:15), ("EQUIVALENT REACTIVE INJECTION", Float64, 16:22),
    ("EQUIVALENT SHUNT", Float64, 23:29), ("EQUIVALENT PARTICIPATION FACTOR", Float64, 30:36)]

const _pwf_dtypes = Dict("DBAR" => _dbar_dtypes, "DLIN" => _dlin_dtypes, "DGBT" => _dgbt_dtypes,
    "DGLT" => _dglt_dtypes, "DGER" => _dger_dtypes, "DSHL" => _dshl_dtypes, "DCBA" => _dcba_dtypes,
    "DCLI" => _dcli_dtypes, "DCNV" => _dcnv_dtypes, "DCCV" => _dccv_dtypes, "DELO" => _delo_dtypes,
    "DCER" => _dcer_dtypes, "BUS AND VOLTAGE CONTROL" => _fban_1_dtypes, "REACTORS AND CAPACITORS BANKS" => _fban_2_dtypes,
    "DCSC" => _dcsc_dtypes, "DCAR" => _dcar_dtypes, "DCTR" => _dctr_dtypes, "DARE" => _dare_dtypes,
    "DTPF CIRC" => _dtpf_circ_dtypes, "DMTE" => _dmte_dtypes, "DMFL CIRC" => _dmfl_circ_dtypes,
    "AGGREGATOR IDENTIFICATION" => _fagr_1_dtypes, "AGGREGATOR OCCURENCES" => _fagr_2_dtypes,
    "DCAI" => _dcai_dtypes, "DGEI" => _dgei_dtypes, "DMOT" => _dmot_dtypes, "DCMT" => _dcmt_dtypes,
    "DINJ" => _dinj_dtypes)

const _mnemonic_dopc = (filter(x -> x[1]%7 == 1, [i:i+3 for i in 1:66]),
                        filter(x -> x%7 == 6, 1:69), Char)

const _mnemonic_dcte = (filter(x -> x[1]%12 == 1, [i:i+3 for i in 1:68]),
                        filter(x -> x[1]%12 == 6, [i:i+5 for i in 1:66]), Float64)

const _mnemonic_dbre = (filter(x -> x[1]%4 == 2, [i:i+1 for i in 1:78]),
                        filter(x -> x[1]%4 == 2, [i:i+1 for i in 1:78]), Int64)

"""
Sections which contains pairs that set values to some contants (DCTE)
and specify some execution control options (DOPC).
"""
const _mnemonic_pairs = Dict("DOPC" =>  _mnemonic_dopc,
    "DCTE" => _mnemonic_dcte, "DBRE" => _mnemonic_dbre, "DOPC IMPR" => _mnemonic_dopc)

const _default_dbar = Dict("NUMBER" => nothing, "OPERATION" => 'A', "STATUS" => 'L',
    "TYPE" => 0, "BASE VOLTAGE GROUP" => " 0", "NAME" => nothing, "VOLTAGE LIMIT GROUP" => " 0",
    "VOLTAGE" => 1.0, "ANGLE" => 0.0, "ACTIVE GENERATION" => 0.0,
    "REACTIVE GENERATION" => 0.0, "MINIMUM REACTIVE GENERATION" => 0.0,
    "MAXIMUM REACTIVE GENERATION" => 0.0, "CONTROLLED BUS" => nothing,
    "ACTIVE CHARGE" => 0.0, "REACTIVE CHARGE" => 0.0, "TOTAL REACTIVE POWER" => 0.0,
    "AREA" => 1, "CHARGE DEFINITION VOLTAGE" => 1.0, "VISUALIZATION" => 0,
    "AGGREGATOR 1" => nothing, "AGGREGATOR 2" => nothing, "AGGREGATOR 3" => nothing,
    "AGGREGATOR 4" => nothing, "AGGREGATOR 5" => nothing, "AGGREGATOR 6" => nothing,
    "AGGREGATOR 7" => nothing, "AGGREGATOR 8" => nothing, "AGGREGATOR 9" => nothing,
    "AGGREGATOR 10" => nothing)

const _default_dlin = Dict("FROM BUS" => nothing, "OPENING FROM BUS" => 'L',
    "OPERATION" => 'A', "OPENING TO BUS" => 'L', "TO BUS" => nothing, "CIRCUIT" => 0,
    "STATUS" => 'L', "OWNER" => 'F', "RESISTANCE" => 0.0, "REACTANCE" => nothing,
    "SHUNT SUSCEPTANCE" => 0.0, "TAP" => 1.0, "MINIMUM TAP" => nothing,
    "MAXIMUM TAP" => nothing, "LAG" => 0.0, "CONTROLLED BUS" => nothing,
    "NORMAL CAPACITY" => Inf, "EMERGENCY CAPACITY" => nothing, "NUMBER OF TAPS" => 33,
    "EQUIPAMENT CAPACITY" => nothing, "AGGREGATOR 1" => nothing, "AGGREGATOR 2" => nothing,
    "AGGREGATOR 3" => nothing, "AGGREGATOR 4" => nothing, "AGGREGATOR 5" => nothing,
    "AGGREGATOR 6" => nothing, "AGGREGATOR 7" => nothing, "AGGREGATOR 8" => nothing,
    "AGGREGATOR 9" => nothing, "AGGREGATOR 10" => nothing)

const _default_dopc = Dict()

const _default_dcte = Dict("TEPA" => 0.1, "TEPR" => 0.1, "TLPR" => 0.1, "TLVC" => .5,
    "TLTC" => 0.01, "TETP" => 5.0, "TBPA" => 5.0, "TSFR" => 0.01, "TUDC" => 0.001,
    "TADC" => 0.01, "BASE" => 100.0, "DASE" => 100.0, "ZMAX" => 500.0, "ACIT" => 30,
    "LPIT" => 50, "LFLP" => 10, "LFIT" => 10, "DCIT" => 10, "VSIT" => 10, "LCRT" => 23,
    "LPRT" => 60, "LFCV" => 1, "TPST" => 0.2, "QLST" => 0.2, "EXST" => 0.4, "TLPP" => 1.0,
    "TSBZ" => 0.01, "TSBA" => 5.0, "PGER" => 30.0, "VDVN" => 40.0, "VDVM" => 200.0,
    "ASTP" => 0.05, "VSTP" => 5.0, "CSTP" => 5.0, "VFLD" => 70, "HIST" => 0, "ZMIN" => 0.001,
    "PDIT" => 10, "ICIT" => 50, "FDIV" => 2.0, "DMAX" => 5, "ICMN" => 0.05, "VART" => 5.0,
    "TSTP" => 33, "TSDC" => 0.02, "ASDC" => 1, "ICMV" => 0.5, "APAS" => 90, "CPAR" => 70,
    "VAVT" => 2.0, "VAVF" => 5.0, "VMVF" => 15.0, "VPVT" => 2.0, "VPVF" => 5.0,
    "VPMF" => 10.0, "VSVF" => 20.0, "VINF" => 1.0, "VSUP" => 1.0, "TLSI" => 0.0)

const _default_dger = Dict("NUMBER" => nothing, "OPERATION" => 'A',
    "MINIMUM ACTIVE GENERATION" => 0.0, "MAXIMUM ACTIVE GENERATION" => 99999.0,
    "PARTICIPATION FACTOR" => 0.0, "REMOTE CONTROL PARTICIPATION FACTOR" => 100.0,
    "NOMINAL POWER FACTOR" => nothing, "ARMATURE SERVICE FACTOR" => nothing,
    "ROTOR SERVICE FACTOR" => nothing, "CHARGE ANGLE" => nothing,
    "MACHINE REACTANCE" => nothing, "NOMINAL APPARENT POWER" => nothing)

const _default_dgbt = Dict("GROUP" => 0, "VOLTAGE" => 1.0)

const _default_dglt = Dict("GROUP" => nothing,  "LOWER BOUND" => 0.8, "UPPER BOUND" => 1.2,
    "LOWER EMERGENCY BOUND" => 0.8, "UPPER EMERGENCY BOUND" => 1.2)

const _default_dshl = Dict("FROM BUS" => nothing, "OPERATION" => 'A', "TO BUS" => nothing,
    "CIRCUIT" => nothing, "SHUNT FROM" => nothing, "SHUNT TO" => nothing,
    "STATUS FROM" => " L", "STATUS TO" => " L")

const _default_dcba = Dict("NUMBER" => nothing, "OPERATION" => 'A', "TYPE" => 0,
    "POLARITY" => nothing, "NAME" => nothing, "VOLTAGE LIMIT GROUP" => " 0",
    "VOLTAGE" => 0, "GROUND ELECTRODE" => 0.0, "DC LINK" => 1)

const _default_dcli = Dict("FROM BUS" => nothing, "OPERATION" => 'A', "TO BUS" => nothing,
    "CIRCUIT" => 0, "OWNER" => nothing, "RESISTANCE" => nothing, "INDUCTANCE" => 0.0,
    "CAPACITY" => Inf)

const _default_dcnv = Dict("NUMBER" => nothing, "OPERATION" => 'A', "AC BUS" => nothing,
    "DC BUS" => nothing, "NEUTRAL BUS" => nothing, "OPERATION MODE" => nothing,
    "BRIDGES" => nothing, "CURRENT" => nothing, "COMMUTATION REACTANCE" => nothing,
    "SECONDARY VOLTAGE" => nothing, "TRANSFORMER POWER" => nothing, "REACTOR RESISTANCE" => 0.0,
    "REACTOR INDUCTANCE" => 0.0, "CAPACITANCE" => Inf, "FREQUENCY" => 60.0)

const _default_dccv = Dict("NUMBER" => nothing, "OPERATION" => 'A', "LOOSENESS" => 'N',
    "INVERTER CONTROL MODE" => nothing, "CONVERTER CONTROL TYPE" => nothing,
    "SPECIFIED VALUE" => nothing, "CURRENT MARGIN" => 10.0, "MAXIMUM OVERCURRENT" => 9999,
    "CONVERTER ANGLE" => 0.0, "MINIMUM CONVERTER ANGLE" => 0.0,
    "MAXIMUM CONVERTER ANGLE" => 0.0, "MINIMUM TRANSFORMER TAP" => nothing,
    "MAXIMUM TRANSFORMER TAP" => nothing, "TRANSFORMER TAP NUMBER OF STEPS" => Inf,
    "MINIMUM DC VOLTAGE FOR POWER CONTROL" => 0.0, "TAP HI MVAR MODE" => nothing,
    "TAP REDUCED VOLTAGE MODE" => 1.0)

const _default_delo = Dict("NUMBER" => nothing, "OPERATION" => 'A', "VOLTAGE" => nothing,
    "BASE" => nothing, "NAME" => nothing, "HI MVAR MODE" => 'N', "STATUS" => 'L')

const _default_dcer = Dict("BUS" => nothing, "OPERATION" => 'A', "GROUP" => nothing,
    "UNITIES" => 1, "CONTROLLED BUS" => nothing, "INCLINATION" => nothing,
    "REACTIVE GENERATION" => nothing, "MINIMUM REACTIVE GENERATION" => nothing,
    "MAXIMUM REACTIVE GENERATION" => nothing, "CONTROL MODE" => 'I', "STATUS" => 'L')

const _default_fban_2 = Dict("GROUP" => nothing, "OPERATION" => 'A', "STATUS" => 'L',
    "UNITIES" => 1, "OPERATING UNITIES" => nothing, "REACTANCE" => nothing)

const _default_fban_1 = Dict("FROM BUS" => nothing, "OPERATION" => 'A', "TO BUS" => nothing,
    "CIRCUIT" => 1, "CONTROL MODE" => 'C', "MINIMUM VOLTAGE" => nothing,
    "MAXIMUM VOLTAGE" => nothing, "CONTROLLED BUS" => nothing,
    "INITIAL REACTIVE INJECTION" => 0.0, "CONTROL TYPE" => 'C', "ERASE DBAR" => 'N',
    "EXTREMITY" => nothing, "REACTANCE GROUPS" => _default_fban_2)

const _default_fagr_2 = Dict("NUMBER" => nothing, "OPERATION" => 'A', "DESCRIPTION" => nothing)

const _default_fagr_1 = Dict("NUMBER" => nothing, "DESCRIPTION" => nothing, "OCCURENCES" => _default_fagr_2)

const _default_dcsc = Dict("FROM BUS" => nothing, "OPERATION" => 'A', "TO BUS" => nothing,
    "CIRCUIT" => 0, "STATUS" => 'L', "OWNER" => 'F', "BYPASS" => 'D',
    "MINIMUM VALUE" => -9999.0, "MAXIMUM VALUE" => 9999.0, "INITIAL VALUE" => nothing,
    "CONTROL MODE" => 'X', "SPECIFIED VALUE" => nothing, "MEASUREMENT EXTREMITY" => nothing,
    "NUMBER OF STAGES" => nothing, "NORMAL CAPACITY" => Inf, "EMERGENCY CAPACITY" => Inf,
    "EQUIPAMENT CAPACITY" => Inf, "AGGREGATOR 1" => nothing, "AGGREGATOR 2" => nothing,
    "AGGREGATOR 3" => nothing, "AGGREGATOR 4" => nothing, "AGGREGATOR 5" => nothing,
    "AGGREGATOR 6" => nothing, "AGGREGATOR 8" => nothing, "AGGREGATOR 7" => nothing,
    "AGGREGATOR 9" => nothing, "AGGREGATOR 10" => nothing)

const _default_dcar = Dict("ELEMENT 1 TYPE" => nothing, "ELEMENT 1 IDENTIFICATION" => nothing,
    "CONDITION 1" => nothing, "ELEMENT 2 TYPE" => nothing, "ELEMENT 2 IDENTIFICATION" => nothing,
    "MAIN CONDITION" => nothing, "ELEMENT 3 TYPE" => nothing, "ELEMENT 3 IDENTIFICATION" => nothing,
    "CONDITION 2" => nothing, "ELEMENT 4 TYPE" => nothing, "ELEMENT 4 IDENTIFICATION" => nothing,
    "OPERATION" => 'A', "PARAMETER A" => nothing, "PARAMETER B" => nothing, "PARAMETER C" => nothing,
    "PARAMETER D" => nothing, "VOLTAGE" => nothing)

const _default_dctr = Dict("FROM BUS" => nothing, "OPERATION" => 'A', "TO BUS" => nothing,
    "CIRCUIT" => nothing, "MINIMUM VOLTAGE" => nothing, "MAXIMUM VOLTAGE" => nothing,
    "BOUNDS CONTROL TYPE" => 'C', "CONTROL MODE" => nothing, "MINIMUM PHASE" => nothing,
    "MAXIMUM PHASE" => nothing, "CONTROL TYPE" => 'F', "SPECIFIED VALUE" => nothing,
    "MEASUREMENT EXTREMITY" => nothing)

const _default_dare = Dict("NUMBER" => nothing, "NET INTERCHANGE" => 0.0, "NAME" => nothing,
    "MINIMUM INTERCHANGE" => 0.0, "MAXIMUM INTERCHANGE" => 0.0)

const _default_dtpf_circ = Dict("FROM BUS 1" => nothing, "TO BUS 1" => nothing, "CIRCUIT 1" => 0,
    "FROM BUS 2" => nothing, "TO BUS 2" => nothing, "CIRCUIT 2" => 0,
    "FROM BUS 3" => nothing, "TO BUS 3" => nothing, "CIRCUIT 3" => 0,
    "FROM BUS 4" => nothing, "TO BUS 4" => nothing, "CIRCUIT 4" => 0,
    "FROM BUS 5" => nothing, "TO BUS 5" => nothing, "CIRCUIT 5" => 0, "OPERATION" => 'A')

const _default_dmte = Dict("ELEMENT 1 TYPE" => nothing, "ELEMENT 1 IDENTIFICATION" => nothing,
    "CONDITION 1" => nothing, "ELEMENT 2 TYPE" => nothing, "ELEMENT 2 IDENTIFICATION" => nothing,
    "MAIN CONDITION" => nothing, "ELEMENT 3 TYPE" => nothing, "ELEMENT 3 IDENTIFICATION" => nothing,
    "CONDITION 2" => nothing, "ELEMENT 4 TYPE" => nothing, "ELEMENT 4 IDENTIFICATION" => nothing,
    "OPERATION" => 'A', "BOUNDARIES" => 'T')

const _default_dmfl_circ = Dict("FROM BUS 1" => nothing, "TO BUS 1" => nothing, "CIRCUIT 1" => 0,
    "FROM BUS 2" => nothing, "TO BUS 2" => nothing, "CIRCUIT 2" => 0,
    "FROM BUS 3" => nothing, "TO BUS 3" => nothing, "CIRCUIT 3" => 0,
    "FROM BUS 4" => nothing, "TO BUS 4" => nothing, "CIRCUIT 4" => 0,
    "FROM BUS 5" => nothing, "TO BUS 5" => nothing, "CIRCUIT 5" => 0, "OPERATION" => 'A')

const _default_dbre = Dict()

const _default_dcai = Dict("BUS" => nothing, "OPERATION" => 'A', "GROUP" => nothing,
    "STATUS" => 'L', "UNITIES" => 1, "OPERATING UNITIES" => 1, "ACTIVE CHARGE" => 0.0,
    "REACTIVE CHARGE" => 0.0, "PARAMETER A" => nothing, "PARAMETER B" => nothing,
    "PARAMETER C" => nothing, "PARAMETER D" => nothing, "VOLTAGE" => 0.7, "CHARGE DEFINITION VOLTAGE" => 1.0)

const _default_dgei = Dict("BUS" => nothing, "OPERATION" => 'A', "AUTOMATIC MODE" => 'N',
    "GROUP" => nothing, "STATUS" => 'L', "UNITIES" => 1, "OPERATING UNITIES" => 1,
    "MINIMUM OPERATING UNITIES" => 1, "ACTIVE GENERATION" => 0.0, "REACTIVE GENERATION" => 0.0,
    "MINIMUM REACTIVE GENERATION" => -9999.0, "MAXIMUM REACTIVE GENERATION" => 99999.0,
    "ELEVATOR TRANSFORMER REACTANCE" => nothing, "XD" => 0.0, "XQ" => 0.0, "XL" => 0.0,
    "POWER FACTOR" => 1.0, "APPARENT POWER" => 99999.0, "MECHANICAL LIMIT" => 99999.0)

const _default_dmot = Dict("BUS" => nothing, "OPERATION" => 'A', "STATUS" => 'L',
    "GROUP" => nothing, "SIGN" => '+', "LOADING FACTOR" => 100.0, "UNITIES" => 1,
    "STATOR RESISTANCE" => nothing, "STATOR REACTANCE" => nothing, "MAGNETAZING REACTANCE" => nothing,
    "ROTOR RESISTANCE" => nothing, "ROTOR REACTANCE" => nothing, "BASE POWER" => nothing,
    "ENGINE TYPE" => nothing, "ACTIVE CHARGE PORTION" => nothing,
    "BASE POWER DEFINITION PERCENTAGE" => nothing)

const _default_dcmt = Dict("COMMENTS" => nothing)

const _default_dinj = Dict("NUMBER" => nothing, "OPERATION" => 'A',
    "EQUIVALENT ACTIVE INJECTION" => 0.0, "EQUIVALENT REACTIVE INJECTION" => 0.0,
    "EQUIVALENT SHUNT" => 0.0, "EQUIVALENT PARTICIPATION FACTOR" => 0.0)

const _default_titu = ""

const _default_name = ""

const _pwf_defaults = Dict("DBAR" => _default_dbar, "DLIN" => _default_dlin, "DCTE" => _default_dcte,
    "DOPC" => _default_dopc, "TITU" => _default_titu, "name" => _default_name, "DGER" => _default_dger,
    "DGBT" => _default_dgbt, "DGLT" => _default_dglt, "DSHL" => _default_dshl, "DCER" => _default_dcer,
    "DBSH" => _default_fban_1, "REACTANCE GROUPS" => _default_fban_2, "DCBA" => _default_dcba,
    "DCLI" => _default_dcli, "DCNV" => _default_dcnv, "DCCV" => _default_dccv, "DELO" => _default_delo,
    "DCSC" => _default_dcsc, "DCAR" => _default_dcar, "DCTR" => _default_dctr, "DARE" => _default_dare,
    "DTPF CIRC" => _default_dtpf_circ, "DMTE" => _default_dmte, "DMFL CIRC" => _default_dmfl_circ,
    "DBRE" => _default_dbre, "DOPC IMPR" => _default_dopc, "DAGR" => _default_fagr_1,
    "OCCURENCES" => _default_fagr_2, "DCAI" => _default_dcai, "DGEI" => _default_dgei,
    "DMOT" => _default_dmot, "DCMT" => _default_dcmt, "DINJ" => _default_dinj)

const title_identifier = "TITU"
const end_section_identifier = "99999"

function _remove_titles_from_file_lines(file_lines::Vector{String}, section_titles_idx::Vector{Int64})
    remove_titles_idx = vcat(section_titles_idx, section_titles_idx .+ 1)
    file_lines_without_titles_idx = setdiff(1:length(file_lines), remove_titles_idx)
    file_lines = file_lines[file_lines_without_titles_idx]
    return file_lines
end

"""
    _split_sections(io)

Internal function. Parses a pwf file into an array where each
element corresponds to a section, divided by the delimiter 99999.
"""
function _split_sections(io::IO)
    file_lines = readlines(io)
    filter!(x -> x != "" && x[1] != '(', file_lines) # Ignore commented and empty lines
    file_lines = replace.(file_lines, repeat([Char(65533) => ' '], length(file_lines)))
    sections = Vector{String}[]

    section_titles_idx = findall(line -> line == title_identifier, file_lines)
    if !isempty(section_titles_idx)
        last_section_title_idx = section_titles_idx[end]:section_titles_idx[end] + 1
        push!(sections, file_lines[last_section_title_idx])
    end

    file_lines = _remove_titles_from_file_lines(
        file_lines, section_titles_idx
    )

    section_delim = vcat(
        0,
        findall(x -> x == end_section_identifier, file_lines)
    )

    num_sections = length(section_delim) - 1

    for i in 1:num_sections
        section_begin_idx = section_delim[i] + 1
        section_end_idx   = section_delim[i + 1] - 1

        # Account for multiple sections in the same pwf
        section_i = findall(x -> x[1] == file_lines[section_begin_idx], sections)
        @assert length(section_i) < 2
        if length(section_i) == 0
            push!(sections, file_lines[section_begin_idx:section_end_idx])
        else
            section_i = section_i[1]
            sections[section_i] = vcat(sections[section_i], file_lines[section_begin_idx + 1:section_end_idx])
        end
    end

    return sections
end

function _handle_implicit_decimal_point!(
    data::Dict, pwf_section::Vector, field::String, dtype, cols, element::String)

    field_idx     = findfirst(x -> x[1:3] == (field, dtype, cols), pwf_section)
    decimal_point = length(pwf_section[field_idx]) == 4 ? cols[end] - pwf_section[field_idx][4] : 0
    data[field]   = parse(dtype, element) / 10^decimal_point
end

"""
    _parse_line_element!(data, line, section)

Internal function. Parses a single line of data elements from a PWF file
and saves it into `data::Dict`.
"""
function _parse_line_element!(data::Dict{String, Any}, line::String, section::AbstractString)

    line_length = _pwf_dtypes[section][end][3][end]
    if length(line) < line_length
        extra_characters_needed = line_length - length(line)
        line = line * repeat(" ", extra_characters_needed)
    end

    for (field, dtype, cols) in _pwf_dtypes[section]
        element = line[cols]

        try
            if dtype != String && dtype != Char
                if dtype == Float64 && !('.' in element) # Implicit decimal point
                    _handle_implicit_decimal_point!(data, _pwf_dtypes[section], field, dtype, cols, element)
                else
                    data[field] = parse(dtype, element)
                end
            else
                data[field] = element
            end
        catch
            if !_needs_default(element)
                @warn "Could not parse $element to $dtype inside $section section, setting it as a String"
            end
            data[field] = element
        end

    end

end

function _parse_line_element!(data::Dict{String, Any}, lines::Vector{String}, section::AbstractString)

    mn_keys, mn_values, mn_type = _mnemonic_pairs[section]

    for line in lines
        for i in 1:length(mn_keys)
            k, v = mn_keys[i], mn_values[i]
            if v[end] <= length(line)

                if mn_type != String && mn_type != Char
                    try
                        data[line[k]] = parse(mn_type, line[v])
                    catch
                        if !_needs_default(line[v])
                            @warn "Could not parse $(line[v]) to $mn_type, setting it as a String"
                        end
                        !_needs_default(line[k]) ? data[line[k]] = line[v] : nothing
                    end
                else
                    !_needs_default(line[k]) ? data[line[k]] = line[v] : nothing
                end

            end
        end
    end
end

"""
    _parse_section_element!(data, section_lines, section)
Internal function. Parses a section containing a system component.
Returns a Vector of Dict, where each entry corresponds to a single element.
"""
function _parse_section_element!(data::Dict{String, Any}, section_lines::Vector{String}, section::AbstractString, idx::Int64=1)

    if section == "DBAR"
        for line in section_lines[2:end]

            line_data = Dict{String, Any}()
            _parse_line_element!(line_data, line, section)

            bus_i = line_data["NUMBER"]
            data["$bus_i"] = line_data
        end

    else
        for line in section_lines[2:end]

            line_data = Dict{String, Any}()
            _parse_line_element!(line_data, line, section)

            data["$idx"] = line_data
            idx += 1
        end
    end
end

function _parse_divided_section!(data::Dict{String, Any}, section_lines::Vector{String}, section::String)

    separator = _divided_sections[section]["separator"]
    sub_titles_idx = vcat(1, findall(x -> x == separator, section_lines))
    for (i, idx) in enumerate(sub_titles_idx)

        if idx != sub_titles_idx[end]
            next_idx = sub_titles_idx[i + 1]
            _parse_section_element!(data, section_lines[idx:idx + 1], _divided_sections[section]["first name"], i)

            rc = Dict{String, Any}()
            _parse_section_element!(rc, section_lines[idx + 1:next_idx - 1], _divided_sections[section]["second name"], i)

            group = _divided_sections[section]["subgroup"]
            data["$i"][group] = rc
        end

    end
end

"""
    _parse_section(data, section_lines)

Internal function. Receives an array of lines corresponding to a PWF section,
transforms it into a Dict and saves it into `data::Dict`.
"""
function _parse_section!(data::Dict{String, Any}, section_lines::Vector{String})
    section = section_lines[1]
    section_data = Dict{String, Any}()

    if section == title_identifier
        section_data = section_lines[end]

    elseif section in keys(_mnemonic_pairs)
        _parse_line_element!(section_data, section_lines[2:end], section)

    elseif section in keys(_pwf_dtypes)
        _parse_section_element!(section_data, section_lines, section)

    elseif section in keys(_divided_sections)
        _parse_divided_section!(section_data, section_lines, section)

    else
        @warn "Currently there is no support for $section parsing"
        section_data = nothing
    end
    data[section] = section_data
end

_needs_default(str::String) = unique(str) == [' ']
_needs_default(ch::Char) = ch == ' '

function _populate_defaults!(pwf_data::Dict{String, Any})

    @warn "Populating defaults"

    for (section, section_data) in pwf_data
        if !haskey(_pwf_defaults, section)
            @warn "Parser doesn't have default values for section $(section)."
        else
            if section in keys(_pwf_dtypes)
                _populate_section_defaults!(pwf_data, section, section_data)
            elseif section in keys(_mnemonic_pairs)
                _populate_mnemonic_defaults!(pwf_data, section, section_data)
            elseif section in keys(_divided_sections)
                _populate_section_defaults!(pwf_data, section, section_data)
            end
        end
    end

end

function _populate_section_defaults!(pwf_data::Dict{String, Any}, section::String, section_data::Dict{String, Any})
    component_defaults = _pwf_defaults[section]

    for (i, element) in section_data
        for (component, default) in component_defaults
            if haskey(element, component)
                component_value = element[component]
                if isa(component_value, String) || isa(component_value, Char)
                    if _needs_default(component_value)
                        pwf_data[section][i][component] = default
                        _handle_special_defaults!(pwf_data, section, i, component)
                    end
                elseif isa(component_value, Dict) || isa(component_value, Vector{Dict{String,Any}})
                    sub_data = pwf_data[section][i]
                    _populate_section_defaults!(sub_data, component, component_value)
                    pwf_data[section][i] = sub_data
                end
            else
                pwf_data[section][i][component] = default
                _handle_special_defaults!(pwf_data, section, i, component)
            end
        end
        _handle_transformer_default!(pwf_data, section, i)
    end
end

function _populate_mnemonic_defaults!(pwf_data::Dict{String, Any}, section::String, section_data::Dict{String, Any})
    component_defaults = _pwf_defaults[section]

    for (component, default) in component_defaults
        if haskey(section_data, component)
            component_value = section_data[component]
            if isa(component_value, String) || isa(component_value, Char)
                if _needs_default(component_value)
                    pwf_data[section][component] = default
                end
            end
        else
            pwf_data[section][component] = default
        end
    end
end

function _handle_special_defaults!(pwf_data::Dict{String, Any}, section::String, i::String, component::String)

    if section == "DBAR" && component == "MINIMUM REACTIVE GENERATION"
        bus_type = pwf_data[section][i]["TYPE"]
        if bus_type == 2
            pwf_data[section][i][component] = -9999.0
        end
    end
    if section == "DBAR" && component == "MAXIMUM REACTIVE GENERATION"
        bus_type = pwf_data[section][i]["TYPE"]
        if bus_type == 2
            pwf_data[section][i][component] = 99999.0
        end
    end

    if section == "DLIN" && component in ["TAP", "MINIMUM TAP", "MAXIMUM TAP"]
        # Count how many defaults were needed i.e. if there is any tap information in the PWF file
        pwf_data[section][i]["TRANSFORMER"] = get(pwf_data[section][i], "TRANSFORMER", 0) + 1
    end

    if section == "DBAR" && component == "CONTROLLED BUS"
        pwf_data[section][i][component] = pwf_data[section][i]["NUMBER"] # Default: the bus itself
    end
    if section == "DLIN" && component == "CONTROLLED BUS"
        pwf_data[section][i][component] = pwf_data[section][i]["FROM BUS"] # Default: the bus itself
    end

    if section == "DLIN" && component in ["EMERGENCY CAPACITY", "EQUIPAMENT CAPACITY"]
        normal_capacity = pwf_data[section][i]["NORMAL CAPACITY"]
        normal_capacity = isa(normal_capacity, AbstractString) && _needs_default(normal_capacity) ? _pwf_defaults[section]["NORMAL CAPACITY"] : normal_capacity
        pwf_data[section][i][component] = normal_capacity
    end

    if section == "DBSH" && component == "MINIMUM VOLTAGE"
        ctrl_bus = pwf_data[section][i]["CONTROLLED BUS"]
        ctrl_bus = !isa(ctrl_bus, Int64) ? pwf_data[section][i]["FROM BUS"] : ctrl_bus
        group = pwf_data["DBAR"]["$ctrl_bus"]["VOLTAGE LIMIT GROUP"]
        group = isa(group, AbstractString) && _needs_default(group) ? " 0" : group
        group_idx = findfirst(x -> x["GROUP"] == group, pwf_data["DGLT"])
        lower_bound = pwf_data["DGLT"][group_idx]["LOWER BOUND"]
        lower_bound = isa(lower_bound, AbstractString) && _needs_default(lower_bound) ? 0.8 : lower_bound
        pwf_data[section][i][component] = lower_bound
    end
    if section == "DBSH" && component == "MAXIMUM VOLTAGE"
        ctrl_bus = pwf_data[section][i]["CONTROLLED BUS"]
        ctrl_bus = !isa(ctrl_bus, Int64) ? pwf_data[section][i]["FROM BUS"] : ctrl_bus
        group = pwf_data["DBAR"]["$ctrl_bus"]["VOLTAGE LIMIT GROUP"]
        group = isa(group, AbstractString) && _needs_default(group) ? " 0" : group
        group_idx = findfirst(x -> x["GROUP"] == group, pwf_data["DGLT"])
        upper_bound = pwf_data["DGLT"][group_idx]["UPPER BOUND"]
        upper_bound = isa(upper_bound, AbstractString) && _needs_default(upper_bound) ? 1.2 : upper_bound
        pwf_data[section][i][component] = upper_bound
    end
    if section == "DBSH" && component == "CONTROLLED BUS"
        pwf_data[section][i][component] = pwf_data[section][i]["FROM BUS"]
    end

    if section == "DCTR" && component in ["MINIMUM VOLTAGE", "MAXIMUM VOLTAGE"]
        pwf_data[section][i]["VOLTAGE CONTROL"] = false
    end
    if section == "DCTR" && component in ["MINIMUM PHASE", "MAXIMUM PHASE"]
        pwf_data[section][i]["PHASE CONTROL"] = false
    end

    if section == "DCTR" && component == "MEASUREMENT EXTREMITY"
        pwf_data[section][i][component] = pwf_data[section][i]["FROM BUS"]
    end

    if section == "DGLT" && component == "LOWER EMERGENCY BOUND"
        pwf_data[section][i][component] = pwf_data[section][i]["LOWER BOUND"]
    end
    if section == "DGLT" && component == "UPPER EMERGENCY BOUND"
        pwf_data[section][i][component] = pwf_data[section][i]["UPPER BOUND"]
    end

    if section == "DCBA" && component == "VOLTAGE"
        if pwf_data[section][i]["POLARITY"] == '0'
            pwf_data[section][i][component] = 0.0
        else
            pwf_data[section][i][component] = 1.0
        end
    end

    if section == "DCCV" && component == "TAP HI MVAR MODE"
        step = (pwf_data[section][i]["MAXIMUM TRANSFORMER TAP"] - pwf_data[section][i]["MINIMUM TRANSFORMER TAP"]) / pwf_data[section][i]["TRANSFORMER TAP NUMBER OF STEPS"]
        pwf_data[section][i][component] = pwf_data[section][i]["MAXIMUM TRANSFORMER TAP"] - step
    end

    if section == "DELO" && component == "BASE" && haskey(pwf_data, "DCTE")
        pwf_data[section][i][component] = pwf_data["DCTE"]["DASE"]
    end

    if section == "DCER" && component == "CONTROLLED BUS"
        pwf_data[section][i][component] = pwf_data[section][i]["BUS"]
    end

    if section == "DCSC" && component == "INITIAL VALUE"
        pwf_data[section][i][component] = pwf_data[section][i]["MAXIMUM VALUE"]
    end
    if section == "DCSC" && component == "MEASUREMENT EXTREMITY"
        pwf_data[section][i][component] = pwf_data[section][i]["FROM BUS"]
    end

    if section == "DCAR" && component == "VOLTAGE" && haskey(pwf_data, "DCTE")
        pwf_data[section][i][component] = pwf_data["DCTE"]["VFLD"]
    end

    if section == "DCAI" && component == "OPERATING UNITIES"
        pwf_data[section][i][component] = pwf_data[section][i]["UNITIES"]
    end
    if section == "DCAI" && component == "VOLTAGE" && haskey(pwf_data, "DCTE")
        vfld = pwf_data["DCTE"]["VFLD"]
        vfld = isa(vfld, AbstractString) && _needs_default(vfld) ? 70 : vfld
        pwf_data[section][i][component] = vfld
    end
    if section == "DGEI" && component == "OPERATING UNITIES"
        pwf_data[section][i][component] = pwf_data[section][i]["UNITIES"]
    end
    if section == "REACTANCE GROUPS" && component == "OPERATING UNITIES"
        unities = pwf_data[section][i]["UNITIES"]
        unities = isa(unities, AbstractString) && _needs_default(unities) ? 1 : unities
        pwf_data[section][i][component] = unities
    end
end

function _handle_transformer_default!(pwf_data::Dict{String, Any}, section::String, i::String)
    if section == "DLIN"
        if haskey(pwf_data[section][i], "TRANSFORMER") && pwf_data[section][i]["TRANSFORMER"] == 3
            pwf_data[section][i]["TRANSFORMER"] = false
        else
            pwf_data[section][i]["TRANSFORMER"] = true
        end
    end
end

"""
    _parse_pwf_data(data_io)

Internal function. Receives a pwf file as an IOStream and parses into a Dict.
"""
function _parse_pwf_data(data_io::IO)

    sections = _split_sections(data_io)
    pwf_data = Dict{String, Any}()
    pwf_data["name"] = match(r"^\<file\s[\/\\]*(?:.*[\/\\])*(.*)\.pwf\>$", lowercase(data_io.name)).captures[1]
    for section in sections
        _parse_section!(pwf_data, section)
    end
    _populate_defaults!(pwf_data)

    return pwf_data
end

function parse_pwf(filename::String)::Dict
    pwf_data = open(filename) do f
        parse_pwf(f)
    end

    return pwf_data
end

"""
    parse_pwf(io)

Reads PWF data in `io::IO`, returning a `Dict` of the data parsed into the
proper types.
"""
function parse_pwf(io::IO)
    # Open file, read it and parse to a Dict
    pwf_data = _parse_pwf_data(io)
    return pwf_data
end


text:
abstract type AbstractMetric end

abstract type SingleRecommendMetric <: AbstractMetric end

abstract type TopKMetric <: SingleRecommendMetric end

struct Recall <: TopKMetric
    k::Int
    name::AbstractString

    Recall(k::Int) = new(k, "recall")
end

function (recall::Recall)(recommend_list, ground_truth)
    if recommend_list === nothing || length(recommend_list) == 0
        return 0.0
    end

    k = min(recall.k, length(recommend_list))
    truncated_recom = recommend_list[1:k]
    num_hits = length(intersect(truncated_recom, ground_truth))
    return num_hits / length(ground_truth)
end

struct Precision <: TopKMetric
    k::Int
    name::AbstractString

    Precision(k::Int) = new(k, "precision")
end

function (precision::Precision)(recommend_list, ground_truth)
    if recommend_list === nothing || length(recommend_list) == 0
        return 0.0
    end

    k = min(precision.k, length(recommend_list))
    truncated_recom = recommend_list[1:k]
    num_hits = length(intersect(truncated_recom, ground_truth))
    return num_hits / length(truncated_recom)
end

function _dcg(relevances)
    result = 0.0
    for i in eachindex(relevances)
        result += (2^relevances[i] - 1) / log2(1.0 + i)
    end
    return result
end

struct DCG <: TopKMetric
    k::Int
    name::AbstractString

    DCG(k::Int) = new(k, "dcg")
end

function (dcg::DCG)(recommend_list, ground_truth, true_relevance = nothing)
    if recommend_list === nothing || length(recommend_list) == 0
        return 0.0
    end

    k = min(dcg.k, length(recommend_list))
    truncated_recom = recommend_list[1:k]

    if true_relevance === nothing
        true_relevance = ones(length(ground_truth))
    end

    relevances = zeros(k)
    for i = 1:k
        j = findfirst(truncated_recom[i] .== ground_truth)
        if !(j === nothing)
            relevances[i] = true_relevance[j]
        end
    end
    return _dcg(relevances)
end

struct NDCG <: TopKMetric
    k::Int
    name::AbstractString

    NDCG(k::Int) = new(k, "ndcg")
end

function (ndcg::NDCG)(recommend_list, ground_truth, true_relevance = nothing)
    if recommend_list === nothing || length(recommend_list) == 0
        return 0.0
    end

    k = min(ndcg.k, length(recommend_list))
    truncated_recom = recommend_list[1:k]

    if true_relevance === nothing
        true_relevance = ones(length(ground_truth))
    end

    relevances = zeros(k)
    for i = 1:k
        j = findfirst(truncated_recom[i] .== ground_truth)
        if !(j === nothing)
            relevances[i] = true_relevance[j]
        end
    end
    dcg = _dcg(relevances)

    # Note: Table 3 of [Dacrema, Cremonesi and Jannach, 2019]("Are we really...")
    # probably adopts n = length(true_relevance) for IDCG.
    # I believe it is not suitable and use instead the definition below
    # This formulation is seen, for instance, in
    # [Rendle 2021]("Item Recommendation from Implicit Feedback")
    n = min(length(ground_truth), k)
    idcg = _dcg(sort(true_relevance, rev = true)[1:n])

    return dcg / idcg
end

#only for implicit recommendation
struct MeanMetric{T<:SingleRecommendMetric} <: AbstractMetric
    base_metric::T
end

function (metric::MeanMetric)(recommend_lists, ground_truth_list)
    result = 0.0
    for (recom, gt) in zip(recommend_lists, ground_truth_list)
        result += metric.base_metric(recom, gt)
    end
    return result / length(recommend_lists)
end

@doc raw"""
    MeanRecall(k)

Create callbale struct to compute Recall@k averaged over all predictions. Recall@k is defined by
```math
\mathrm{Recall@k} = \frac{|(\text{ground truth}) \cap (\text{top k prediction})|}{|(\text{ground truth})|}
```

# Example

```julia
recall10 = MeanRecall(10)
recall10(predictions, ground_truth)
```
"""
MeanRecall(k) = MeanMetric(Recall(k))

@doc raw"""
    MeanPrecision(k)

Create callbale struct to compute Precision@k averaged over all predictions. Precision@k is defined by
```math
\mathrm{Precision@k} = \frac{|(\text{ground truth}) \cap (\text{top k prediction})|}{k}
```

# Example

```julia
prec10 = MeanPrecision(10)
prec10(predictions, ground_truth)
```
"""
MeanPrecision(k) = MeanMetric(Precision(k))

@doc raw"""
    MeanDCG(k)

Create callbale struct to compute DCG@k averaged over all predictions. DCG@k is defined by
```math
\mathrm{DCG@k} = \sum_{i=1}^{\mathrm{min}(k, \mathrm{length}(\text{prediction}))} \frac{2^{r_i}-1}{\log(i+1)}\,,
```
where ``r_i`` is the true relevance for the i-th predicted item (binary for implicit feedback).

# Example

```julia
dcg10 = MeanDCG(10)
dcg10(predictions, ground_truth)
```
"""
MeanDCG(k) = MeanMetric(DCG(k))

@doc raw"""
    MeanNDCG(k)

Create callbale struct to compute NDCG@k averaged over all predictions. NDCG@k is defined by
```math
\mathrm{NDCG@k} = \frac{\mathrm{DCG}@k}{\mathrm{IDCG}@k}
```
where IDCG is the ideal DCG, prediction sorted by true relevance. Note that if the number of ground truth items is smaller than ``k``, the predicted item list is truncated to that length.

# Example

```julia
ndcg10 = MeanNDCG(10)
ndcg10(predictions, ground_truth)
```
"""
MeanNDCG(k) = MeanMetric(NDCG(k))

name(metric::MeanMetric) = Symbol(metric.base_metric.name, metric.base_metric.k)


text:
"""
    macro stack(into, structs...)

Stack the fields of multiple structs and create a new one.
The first argument is the name of the new struct followed by the
ones to be stacked. Parametric types are not supported and
the fieldnames needs to be unique.

Example:

    @stack Baz Foo Bar

Creates `Baz` with the concatenated fields of `Foo` and `Bar`

"""
macro stack(into, structs...)
    fields = []
    for _struct in structs
        names = fieldnames(getfield(__module__, _struct))
        types = [fieldtype(getfield(__module__, _struct), field) for field in names]
        for (n, t) in zip(names, types)
            push!(fields, :($n::$t))
        end
    end

    esc(
        quote
            struct $into
                $(fields...)
            end
        end
    )
end


text: sum_values(x, y) = x + y


text:
module StarWarsArrays

export StarWarsArray, OriginalOrder, MacheteOrder

# Orders
abstract type StarWarsOrder end
struct OriginalOrder <: StarWarsOrder end
struct MacheteOrder <: StarWarsOrder end

# Exception
struct StarWarsError <: Exception
    i::Any
    order::Any
end

function Base.showerror(io::IO, err::StarWarsError)
    print(io, "StarWarsError: there is no episode $(err.i) in $(err.order)")
end

# The main struct
struct StarWarsArray{T,N,P<:AbstractArray,O<:StarWarsOrder} <: AbstractArray{T,N}
    parent::P
end
function StarWarsArray(p::P, order::Type{<:StarWarsOrder}=OriginalOrder) where {T,N,P<:AbstractArray{T,N}}
    if order === OriginalOrder
        minimum(size(p)) < 6 &&
            throw(ArgumentError("A StarWarsArray with OriginalOrder requires at least 6 elements in each dimension.  Input array has size $(size(p))"))
    end
    return StarWarsArray{T,N,P,order}(p)
end

machete_view_index(i) = range(1, stop=i)
function StarWarsArray(p::P, order::Type{MacheteOrder}) where {T,N,P<:AbstractArray{T,N}}
    minimum(size(p)) < 5 &&
        throw(ArgumentError("A StarWarsArray with MacheteOrder requires at least 5 elements in each dimension.  Input array has size $(size(p))"))
    return StarWarsArray{T,N,P,order}(view(p, machete_view_index.(size(p) .- 1)...))
end

order(::StarWarsArray{T,N,P,O}) where {T,N,P,O} = O

# Indexing
function index(i::Int, ::Int, ::Type{OriginalOrder})
    if 4 <= i <= 6
        return i - 3
    elseif 1 <= i <= 3
        return i + 3
    else
        return i
    end
end
function index(i::Int, size::Int, order::Type{MacheteOrder})
    if 4 <= i <= 5
        return i - 3
    elseif 2 <= i <= 3
        return i + 1
    elseif  6 <= i <= size + 1
        return i - 1
    elseif i == 1
        throw(StarWarsError(i,order))
    else
        return i
    end
end

# Get the parent
Base.parent(A::StarWarsArray) = A.parent

# Get the size
Base.size(A::StarWarsArray{T,N,P,O}) where {T,N,P,O} = size(parent(A))

# Get the elements
Base.getindex(A::StarWarsArray, i::Int) =
    getindex(parent(A), index(i, length(parent(A)), order(A)))
Base.getindex(A::StarWarsArray{T,N}, i::Vararg{Int,N}) where {T,N} =
    getindex(parent(A), index.(i, size(parent(A)), order(A))...)
Base.setindex!(A::StarWarsArray, v, i::Int) =
    setindex!(parent(A), v, index(i, length(parent(A)), order(A)))
Base.setindex!(A::StarWarsArray{T,N}, v, i::Vararg{Int,N}) where {T,N} =
    setindex!(parent(A), v, index.(i, size(parent(A)), order(A))...)

# Axes
Base.axes(A::StarWarsArray{T,N,P,OriginalOrder}) where {T,N,P} =
    map(i->index.(i, length(A), order(A)), machete_view_index.(size(A)))

Base.axes(A::StarWarsArray{T,N,P,MacheteOrder}) where {T,N,P} =
    map(i->index.(i .+ 1, length(A), MacheteOrder), machete_view_index.(size(A)))

# Iteration
function iterate_revindex(i::Int, ::Type{OriginalOrder})
    if 1 <= i <= 3
        return i + 3
    elseif 4 <= i <= 6
        return i - 3
    end
    return i
end
function iterate_revindex(i::Int, ::Type{MacheteOrder})
    if 1 <= i <= 2
        return i + 3
    elseif 3 <= i <= 4
        return i - 1
    end
    return i + 1
end

function Base.iterate(A::StarWarsArray, i=1)
    i in 1:length(A) || return nothing
    return A[iterate_revindex(i, order(A))], i + 1
end

# Showing.  Note: this is awful, but it does what I want
Base.show(io::IO, m::MIME"text/plain", A::StarWarsArray{T,N}) where {T,N} =
    show(io, m, view(parent(A), axes(A)...))

end # module

max_stars_repo_name: giordano/StarWarsArrays

max_issues_repo_name: giordano/StarWarsArrays

max_forks_repo_name: giordano/StarWarsArrays