"""
Parse QAQC JATHub slave log.

See [`parse_slavelog_file`](@ref) for the main function.
"""
module SlaveLogParser

using StaticArrays
using Printf
using AutoHashEquals

const HEADER_QSPIP_START = "=============== Test QAPIp Start ==============="
const HEADER_POWER_START = "=============== Test Power Start ==============="
const HEADER_ASDTP_START = "=============== Test ASDTP Start ==============="
const HEADER_RECOV_START = "=============== Test Recov Start ==============="
HEADER_STARTS =
    SVector(HEADER_QSPIP_START, HEADER_POWER_START, HEADER_ASDTP_START, HEADER_RECOV_START)

"""
Indicate parser state.
Default is `MODE_NONE`.
In `MODE_NONE`, each line is fed into parser to detect the start of each section.
"""
@enum SlaveLogSection begin
    MODE_NONE
    MODE_QSPIP
    MODE_POWER
    MODE_ASDTP
    MODE_RECOV
end

struct SlaveLogResult end

"""
    get_psbid_runid_from_filename(filename::AbstractString)::Tuple{Int64,Int64,Bool}

Extract info from slave log filename.

Returns a tuple of `psbid`, `runid`, `islongrun`.
"""
function get_psbid_runid_from_filename(filename::AbstractString)::Tuple{Int64, Int64, Bool}
    main, _ext = splitext(filename)
    parts = split(main, '_')
    psbid = parse(Int64, parts[1])
    runid = parse(Int64, parts[2])
    islongrun = if length(parts) == 3
        true
    else
        false
    end

    (psbid, runid, islongrun)
end

function is_valid_slavelog(filename::AbstractString)::Bool
    error("not yet implemented")
end

"""
    detect_mode_start(line::AbstractString)

Detect [`SlaveLogSection`](@ref) from section starting header line.
If the line doesn't match any section, returns `nothing`.
"""
function detect_mode_start(line::AbstractString)
    if line == HEADER_QSPIP_START
        MODE_QSPIP
    elseif line == HEADER_POWER_START
        MODE_POWER
    elseif line == HEADER_ASDTP_START
        MODE_ASDTP
    elseif line == HEADER_RECOV_START
        MODE_RECOV
    else
        nothing
    end
end

"""
    detect_mode_start!(mode::SlaveLogSection, line::AbstractString)

Detect mode from the `line` and update `mode`.
"""
function detect_mode_start(mode::SlaveLogSection, line::AbstractString)
    newmode = detect_mode_start(line)
    if !isnothing(newmode)
        mode = newmode
    end
    mode
end

"""
    parse_qspip_section(lines::Base.Iterators.Stateful)

Parse QSPIp section of given stateful iterator of log.

# Args
- `lines`: Stateful iterator of slave log file lines
"""
function parse_qspip_section!(lines::Base.Iterators.Stateful)
    # TODO
    nothing
end

# ==================================
# Power

"""
Results from power test

# Fields
- result_3v3d::Float64
- result_3v3a::Float64
- result_n3va::Float64
- fpga_temp::Float64
- channelvals::SVector{16, @NamedTuple{dac::Int64, adc::Int64}}

- result::Bool
"""
struct PowerResult
    result_3v3d::Float64
    result_3v3a::Float64
    result_n3va::Float64
    fpga_temp::Float64
    channelvals::SVector{16, @NamedTuple{dac::Int64, adc::Int64}}

    result::Bool
end

"""
    parse_power_section!(lines::Base.Iterators.Stateful)::PowerResult

Parse Power section of given stateful iterator of log.

# Args
- `lines`: Stateful iterator of slave log file lines
"""
function parse_power_section!(lines::Base.Iterators.Stateful)::PowerResult
    line = popfirst!(lines)
    result_3v3d = let
        m = match(r"^3V3D \[V\] = ([\d|\.]+)$", line)
        parse(Float64, m[1])
    end
    line = popfirst!(lines)
    result_3v3a = let
        m = match(r"^3V3A \[V\] = ([\d|\.]+)$", line)
        parse(Float64, m[1])
    end
    line = popfirst!(lines)
    result_n3va = let
        m = match(r"^-3VA \[V\] = ([-|\d|\.]+)$", line)
        parse(Float64, m[1])
    end
    line = popfirst!(lines)
    fpga_temp = let
        # can be minus (see 127_172.txt)
        m = match(r"^FPGA Temprature \[C\] = (-?[\d|\.]+)$", line)
        parse(Float64, m[1])
    end
    line = popfirst!(lines)
    line = popfirst!(lines)
    channelvals =
        Iterators.map(1:16) do ch
            ch_s = @sprintf "%x" (ch - 1)
            re = Regex("channel $(ch_s): DAC \\[mV\\] = (\\d+), ADC \\[mV\\] = (\\d+)\$")
            line = popfirst!(lines) # I'm not sure this mutating operation is called in sequence
            m = match(re, line)
            (dac = parse(Int64, m[1]), adc = parse(Int64, m[2]))
        end |>
        Tuple |>
        SVector{16, @NamedTuple{dac::Int64, adc::Int64}}
    line = popfirst!(lines)
    result = let
        m = match(r"^Test Power Reseult = (\d)$", line)
        if m[1] == "1"
            true
        else
            false
        end
    end

    PowerResult(result_3v3d, result_3v3a, result_n3va, fpga_temp, channelvals, result)
end

# ==================================
# Asdtp

"""
Measurement result for asic in asdtp test.
"""
@auto_hash_equals struct AsdtpMeasurement
    before::Float64
    current::Float64
    next::Float64
end

AsdtpMeasurement(x::NTuple{3, <:Real}) = AsdtpMeasurement(x...)

function Base.parse(::Type{AsdtpMeasurement}, s::AbstractString)
    v = split(s, ':')
    @assert length(v) == 3
    AsdtpMeasurement(parse(Float64, v[1]), parse(Float64, v[2]), parse(Float64, v[3]))
end

@auto_hash_equals struct AsdtpResult
    reconfig_done::Int64
    always_hit_flag::Int64
    autoreconfig::Union{Bool, Missing}
    asdtp_main::Union{Vector{Vector{AsdtpMeasurement}}, Missing}
    asdtp_reset::Int64
    asdtp_total::Int64
    reconfig_done_2::Int64
    always_hit_flag_2::Int64
    si_done::UInt64
    lolb_in::UInt64
    ppconfig_done::UInt64
    ppconfig_error::UInt64
    pllld_fail_counter::UInt64
    ppconfig_fail_counter::UInt64
    pp_pllds::MVector{8, UInt32}
end

"""
    parse_asdtp_section!(lines::Base.Iterators.Stateful)::AsdtpResult

Parse ASDTP section of given stateful iterator of log.

# Args
- `lines`: Stateful iterator of slave log file lines
"""
function parse_asdtp_section!(lines::Base.Iterators.Stateful)::AsdtpResult
    line = popfirst!(lines)
    result_reconfig_done = let
        m = match(r"^reconfig_done = (\d+)$", line)
        parse(Int64, m[1])
    end
    line = popfirst!(lines)
    result_always_hit_flag = let
        m = match(r"^always_hit_flag = (\d+)$", line)
        parse(Int64, m[1])
    end
    line = popfirst!(lines)
    result_autoreconfig = if line == "Autoreconfig done"
        true
    elseif line == "Autoreconfig fail"
        false
    else
        missing
    end

    result_asdtp_main = if !ismissing(result_autoreconfig) && result_autoreconfig
        line_count = 0
        line_count += 1
        results = map(_ -> AsdtpMeasurement[], 1:8)
        for asic_id in 1:8
            header_line = "----PP$(asic_id)----"
            while line != header_line
                line = popfirst!(lines)
                line_count += 1
            end
            for _ in 1:32
                line = popfirst!(lines)
                line_count += 1
                mes = parse(AsdtpMeasurement, line)
                push!(results[asic_id], mes)
            end
        end
        @assert length(results[1]) == 32 "unexpected length: $(length(results[1]))"

        line = popfirst!(lines)
        @assert line == "100"
        results
    else
        missing
    end

    line = popfirst!(lines)
    result_asdtp_reset, result_asdtp_total = let
        m = match(r"^ASDTP : (\d+) times reset : result = (\d+)$", line)
        parse(Int64, m[1]), parse(Int64, m[2])
    end
    line = popfirst!(lines)
    result_reconfig_done_2 = let
        m = match(r"^reconfig_done = (\d+)$", line)
        parse(Int64, m[1])
    end
    line = popfirst!(lines)
    result_always_hit_flag_2 = let
        m = match(r"^always_hit_flag = (\d+)$", line)
        parse(Int64, m[1])
    end
    line = popfirst!(lines)
    @assert line == ""

    line = popfirst!(lines)
    @assert line == "------- Done check -------" "actual line: $line"
    line = popfirst!(lines)
    result_si_done = let
        m = match(r"^Si_done = (0x[[:xdigit:]]+)$", line)
        parse(UInt64, m[1])
    end
    line = popfirst!(lines)
    result_lolb_in = let
        m = match(r"^LOLB_in = (0x[[:xdigit:]]+)$", line)
        parse(UInt64, m[1])
    end
    line = popfirst!(lines)
    result_ppconfig_done = let
        m = match(r"^PPconfig_done = (0x[[:xdigit:]]+)$", line)
        parse(UInt64, m[1])
    end
    line = popfirst!(lines)
    result_ppconfig_error = let
        m = match(r"^PPconfig_error = (0x[[:xdigit:]]+)$", line)
        parse(UInt64, m[1])
    end
    line = popfirst!(lines)
    result_pllld_fail_counter = let
        m = match(r"^PLLLD_fail_counter = (0x[[:xdigit:]]+)$", line)
        parse(UInt64, m[1])
    end
    line = popfirst!(lines)
    result_ppconfig_fail_counter = let
        m = match(r"^PPconfig_fail_counter = (0x[[:xdigit:]]+)$", line)
        parse(UInt64, m[1])
    end
    result_pp_pllds = MVector{8, UInt32}(undef)
    for ppid in 1:8
        line = popfirst!(lines)
        m = match(Regex("^PP$(ppid)_PLLLD = (0x[[:xdigit:]]+)\$"), line)
        result_pp_pllds[ppid] = parse(UInt32, m[1])
    end

    AsdtpResult(
        result_reconfig_done,
        result_always_hit_flag,
        result_autoreconfig,
        result_asdtp_main,
        result_asdtp_reset,
        result_asdtp_total,
        result_reconfig_done_2,
        result_always_hit_flag_2,
        result_si_done,
        result_lolb_in,
        result_ppconfig_done,
        result_ppconfig_error,
        result_pllld_fail_counter,
        result_ppconfig_fail_counter,
        result_pp_pllds,
    )
end

# ==================================
# Recov

"""
    parse_recov_section!(lines::Base.Iterators.Stateful)

Parse Recov section of given stateful iterator of log.

# Args
- `lines`: Stateful iterator of slave log file lines

# Return
- `missing`: if failed to parse
- `true`: if succeeded
- `false`
"""
function parse_recov_section!(lines::Base.Iterators.Stateful)::Union{Bool, Missing}
    line = popfirst!(lines)
    if startswith("====")(line)
        line = popfirst!(lines)
    end
    m = match(r"Test Recov Result = (\d+)", line)
    if isnothing(m)
        return missing
    else
        return m[1] == "1"
    end
end

# ==================================
# Main

"""
    parse_slavelog_file(filename::AbstractString)

Main function to parse slave log file.
Returns `NamedTuple` with keys `adtp`, `power`, `recov` and their values are `Vector` of each results.
For a single run result, these `Vector`s have only one element and for a hundred(extra) runs, the lengths are usually 100.
For details on results for each section, see documents on dedicated types.
"""
function parse_slavelog_file(filename::AbstractString)
    lines_iter = Iterators.Stateful(eachline(filename))

    asdtp_results = AsdtpResult[]
    power_results = PowerResult[]
    recov_results = Union{Bool, Missing}[]

    mode::SlaveLogSection = MODE_NONE
    # main loop
    while !isempty(lines_iter)
        # @info "section" mode
        # each sections
        if mode == MODE_NONE
            line = popfirst!(lines_iter)
            mode = detect_mode_start(mode, line)
        elseif mode == MODE_QSPIP
            parse_qspip_section!(lines_iter)
            mode = MODE_NONE
        elseif mode == MODE_POWER
            result = parse_power_section!(lines_iter)
            push!(power_results, result)
            mode = MODE_NONE
        elseif mode == MODE_ASDTP
            result = parse_asdtp_section!(lines_iter)
            push!(asdtp_results, result)
            mode = MODE_NONE
        elseif mode == MODE_RECOV
            result = parse_recov_section!(lines_iter)
            push!(recov_results, result)
            mode = MODE_NONE
        end
    end

    return (asdtp = asdtp_results, power = power_results, recov = recov_results)
end

function eff99_count_map(asdtp_results)
    # try(100) × channel(8) × channel(32)
    @assert length(asdtp_results) == 100
    @assert length(asdtp_results[begin]) == 8
    @assert length(asdtp_results[begin][begin]) == 32
    map(1:8) do i_asic
        map(1:32) do i_channel
            sum(1:100) do i_try
                asdtp_results[i_try][i_asic][i_channel] != AsdtpMeasurement(0, 1, 0)
            end
        end
    end
end

end # module SlaveLogParser
