#=
The functions here are intended to generate AdmittanceModels.Circuit objects
from ANSYS Q3D plain text output files containing RLGC parameters.

Here's a small example of the expected Q3D file format, valid for ANSYS Electronics Desktop 2016:

################################################################################################################
DesignVariation:\$DummyParam1='1' \$DummyParam2='1mm' \$dummy_param3='100e19pF' \$dummy_param4='1.2eUnits'
Setup1:LastAdaptive
Problem Type:C
C Units:farad, G Units:sie
Reduce Matrix:Original
Frequency: 4E+09 Hz

Capacitance Matrix
	net_name_1	net_name_2
net_name_1	1.1924E-13	-1.3563E-16
net_name_2	-1.3563E-16	3.7607E-13

Conductance Matrix
	net_name_1	net_name_2
net_name_1	2.4489E-09	-2.3722E-12
net_name_2	-2.3722E-12	7.7123E-09

Capacitance Matrix Coupling Coefficient
	net_name_1	net_name_2
net_name_1	1	0.00064047	0.00018631
net_name_2	0.00064047	1	0.00010129

Conductance Matrix Coupling Coefficient
	net_name_1	net_name_2
net_name_1	1	0.00054586	0.00017696
net_name_2	0.00054586	1	9.2987E-05
################################################################################################################

Each data block like \"Capacitance Matrix\" must start with a line enumerating
the N net names. These are the column labels for the given matrix. The matrix
rows are specified directly below this line. Each of the N row lines start with
a net name to label the row, followed by N space-separated float values for the
matrix elements. We expect these to be strings `parse(Float64, _)` can handle.
=#

using LinearAlgebra: diagind

"""
    parse_value_and_unit(s::AbstractString)

Parse strings like "9.9e-9mm" => (9.9e-9, "mm").
"""
function parse_value_and_unit(s::AbstractString)
    # Three capture groups in the regex intended to match
    # (±X.X)(e-X)(unit)
    m = match(r"(\-?[\d|\.]+)(e-?[\d]+)?(\D*)", s)
    exp_str = m[2] == nothing ? "" : m[2]
    num_str = m[1] * exp_str
    num = parse(Float64, num_str)
    unit_str = String(m[3])
    (num, unit_str)
end

const matrix_types = Dict(
    :capacitance => "Capacitance Matrix",
    :conductance => "Conductance Matrix",
    # :dc_inductance => "DC Inductance Matrix",
    # :dc_resistance => "DC Resistance Matrix",
    # :ac_inductance => "AC Inductance Matrix",
    # :ac_resistance => "AC Resistance Matrix",
    # :dc_plus_ac_resistance => "DCPlusAC Resistance Matrix",
)

const unit_names = Dict(
    :capacitance => "C Units",
    :conductance => "G Units"
)

const units_to_multipliers = Dict(
    "fF" => 1e-15,
    "pF" => 1e-12,
    "nF" => 1e-9,
    "uF" => 1e-6,
    "mF" => 1e-3,
    "farad" => 1.0,
    "mSie" => 1e-3,
    "sie" => 1.0
)

"""
    parse_q3d_txt(filepath, matrix_type::Symbol)

Parses a plain text file generated by ANSYS Q3D containing RLGC simulation output.

# Args
- `q3d_file`: the path to the text file.
- `matrix_type`: A `Symbol` indicating the type of data to read. Currently,
  symbols in $(collect(keys(matrix_types))) are the only available choices,
  and only the units in $(collect(keys(units_to_multipliers))) are handled.

# Returns
- A tuple `(design_variation, net_names, matrix)`, where:
    - `design_variation` is a `Dict` of design variables
    - `net_names` is a `Vector` of net names (strings)
    - `matrix` is the requested matrix
"""
function parse_q3d_txt(q3d_file, matrix_type::Symbol)
    !haskey(matrix_types, matrix_type) && error("matrix type not implemented.")

    matrix_name = matrix_types[matrix_type]
    unit_name = unit_names[matrix_type]

    # Windows uses \r\n for newlines, where Linux just uses \n.
    # We work around that difference here by deleting all carriage return (\r)
    # characters from the file.
    linesep = "\n"
    file_text = replace(read(q3d_file, String), "\r" => "")
    file_chunks = split(file_text, linesep * linesep)
    @assert length(file_chunks) > 1 "expected more than one block in Q3D plain text file."

    is_chunk(header) = chunk -> startswith(chunk, header)
    function get_chunk(header)
        # the chunk may be missing, guard against errors
        idx = findfirst(is_chunk(header), file_chunks)
        !isnothing(idx) && return file_chunks[idx]
        return nothing
    end
    get_chunk_line(chunk, line_num) = split(chunk, linesep)[line_num]

    # The following parses lines like:
    # DesignVariation:var1='12' var2='1e-05' var3='123um'
    # into Dict("var1" => (12, ""),
    #           "var2" => (1e-5, ""),
    #           "var3" => (123, "um"))
    design_variation_chunk = get_chunk("DesignVariation")
    design_variation = if !isnothing(design_variation_chunk)
        _, design_variation_data_str = split(get_chunk_line(design_variation_chunk, 1), ":")
        design_strings = split(design_variation_data_str)
        parse_design_kv((k, v)) = String(k) => parse_value_and_unit(strip(v, ['\'']))
        Dict(parse_design_kv.(split.(design_strings, ['='])))
    else
        Dict{String, Tuple{Float64, String}}()
    end

    local unit_multiplier
    for l in split(file_chunks[1], linesep)
        if occursin("Units", l)
            unit = Dict(split.(split(l, ", "), ':'))[unit_name]
            !haskey(units_to_multipliers, unit) && error("unit $unit not implemented.")
            unit_multiplier = units_to_multipliers[unit]
            break
        end
    end
    (@isdefined unit_multiplier) || error("units not given in Q3D plain text file.")

    # Don't forget to include the line separator here.
    # We typically have matrix_name = "Capacitance Matrix"
    # But, these files can have a heading like "Capacitance Matrix Coupling Coefficient"
    matrix_chunk = get_chunk(matrix_name * linesep)
    net_names = String.(split(get_chunk_line(matrix_chunk, 2)))

    get_matrix_row(row_idx) = parse.(Float64,
        split(get_chunk_line(matrix_chunk, 2 + row_idx))[2:end]) .* unit_multiplier
    matrix = reduce(hcat, get_matrix_row.(1:length(net_names)))

    (design_variation, net_names, matrix)
end

"""
    Circuit(q3d_file; matrix_types = [:capacitance])

Return an `AdmittanceModels.Circuit` from a plain text file generated by
ANSYS Q3D containing RLGC simulation output.

# Args
- `q3d_file`: the path to the text file.
- `matrix_type`: A list of symbols indicating which type of matrix data to read.
  Currently, symbols in $(collect(keys(matrix_types))) are the only available
  choices, and only the units in $(collect(keys(units_to_multipliers))) are handled.
"""
function Circuit(q3d_file; matrix_types = [:capacitance])
    parse_dict = Dict(t => parse_q3d_txt(q3d_file, t) for t ∈ matrix_types)

    # Check that all matrix types are defined over the same set of nets
    # `take_only` asserts the iterator it's passed has only one element
    nets = take_only(collect(Set(map(t -> t[2], values(parse_dict)))))
    matrix_dict = Dict(t => parse_dict[t][3] for t ∈ matrix_types)

    zero_mat() = zeros(length(nets), length(nets))

    # The C and G matrices expected in AdmittanceModels.Circuit are the same
    # as those parsed from Q3D, except with all diagonal entries set to 0 and
    # all values made positive.
    function prep_matrix(matrix_type)
        x = get(matrix_dict, matrix_type, zero_mat())
        x[diagind(x)] .= 0.
        abs.(x)
    end

    k = zero_mat()
    g = prep_matrix(:conductance)
    c = prep_matrix(:capacitance)
    return Circuit(k, g, c, nets)
end

function take_only(xs)
    @assert length(xs) == 1 "Expected $xs to have one element."
    return xs[1]
end
