"""
$(SIGNATURES)

Track the signal `signal` based on the current tracking `state`, the sampling frequency
`sampling_frequency` and PRN `prn`. Optional configurations are:
- Post correlation filter `post_corr_filter` defaults to `get_default_post_corr_filter(...)`
- Intermediate frequency `intermediate_frequency` defaults to 0Hz
- Maximal integration time `max_integration_time` defaults to 1ms. The actual integration
  time can be less in order to find the secondary code or the bit shift. Once this is found
  the integration time is identical to the maximal integration time.
- Minimal integration time `min_integration_time` defaults to 0.75ms. It's the minimal
  integration time, that leads to a valid correlation result. It is only used for the first
  integration period.
- Sample shift of the correlator replica `correlator_sample_shifts` defaults to
  `get_correlator_sample_shifts(...)`
- Bandwidth of the carrier loop `carrier_loop_filter_bandwidth` defaults to 18Hz
- Bandwidth of the code loop `code_loop_filter_bandwidth` defaults to 1Hz
- Velocity aiding `velocity_aiding` defaults to 0Hz
"""
function track(
        signal,
        state::TrackingState{S, C, CALF, COLF, CN, DS, CAR, COR},
        sampling_frequency;
        post_corr_filter = get_default_post_corr_filter(get_correlator(state)),
        intermediate_frequency = 0.0Hz,
        max_integration_time::typeof(1ms) = 1ms,
        min_integration_time::typeof(1.0ms) = 0.75ms,
        correlator_sample_shifts = get_correlator_sample_shifts(get_system(state),
            get_correlator(state), sampling_frequency, 0.5),
        early_late_index_shift = get_early_late_index_shift(get_system(state),
            correlator_sample_shifts, get_correlator(state), sampling_frequency, 0.5),
        carrier_loop_filter_bandwidth = 18Hz,
        code_loop_filter_bandwidth = 1Hz,
        velocity_aiding = 0Hz,
) where {
    S <: AbstractGNSS,
    C <: AbstractCorrelator,
    CALF <: AbstractLoopFilter,
    COLF <: AbstractLoopFilter,
    CN <: AbstractCN0Estimator,
    DS,
    CAR,
    COR
}
    prn = get_prn(state)
    system = get_system(state)
    if get_data_frequency(system) != 0Hz
        @assert rem(1 / get_data_frequency(system), max_integration_time) == 0ms
    end
    correlator = get_correlator(state)
    num_ants = get_num_ants(correlator)
    size(signal, 2) == num_ants || throw(ArgumentError("The second dimension of the signal should be equal to the number of antennas specified by num_ants = NumAnts(N) in the TrackingState."))
    if typeof(system.codes) <: CuMatrix
        typeof(signal) <: StructArray || throw(ArgumentError("Signal is not a StructArray, initialize the signal properly and try again."))
        typeof(signal.re) <: CuArray && typeof(state.system.codes) <: CuArray || throw(ArgumentError("Signal and GNSS codes are not of the same type. Please check if CPU or GPU is used."))
    end
    downconverted_signal_temp = get_downconverted_signal(state)
    downconverted_signal = resize!(downconverted_signal_temp, size(signal, 1), signal)
    carrier_replica = get_carrier(state)
    resize!(choose(carrier_replica, signal), size(signal, 1))
    code_replica = get_code(state)
    resize!(code_replica, size(signal, 1) + correlator_sample_shifts[end]-correlator_sample_shifts[1])
    init_carrier_doppler = get_init_carrier_doppler(state)
    init_code_doppler = get_init_code_doppler(state)
    carrier_doppler = get_carrier_doppler(state)
    code_doppler = get_code_doppler(state)
    carrier_phase = get_carrier_phase(state)
    code_phase = get_code_phase(state)
    sc_bit_detector = get_sc_bit_detector(state)
    carrier_loop_filter = get_carrier_loop_filter(state)
    code_loop_filter = get_code_loop_filter(state)
    prompt_accumulator = get_prompt_accumulator(state)
    integrated_samples = get_integrated_samples(state)
    cn0_estimator = get_cn0_estimator(state)
    signal_start_sample = 1
    bit_buffer = BitBuffer()
    valid_correlator = zero(correlator)
    valid_correlator_carrier_phase = 0.0
    valid_correlator_carrier_frequency = 0.0Hz
    got_correlator = false
    while true
        num_samples_left_to_integrate = get_num_samples_left_to_integrate(
            system,
            max_integration_time,
            sampling_frequency,
            code_doppler,
            code_phase,
            found(sc_bit_detector)
        )
        signal_samples_left = get_num_samples(signal) - signal_start_sample + 1
        num_samples_left = min(num_samples_left_to_integrate, signal_samples_left)
        carrier_frequency = get_current_carrier_frequency(
            intermediate_frequency,
            carrier_doppler
        )
        code_frequency = get_current_code_frequency(system, code_doppler)
        correlator = downconvert_and_correlate!(
            system,
            signal,
            correlator,
            code_replica,
            code_phase,
            carrier_replica,
            carrier_phase,
            downconverted_signal,
            code_frequency,
            correlator_sample_shifts,
            carrier_frequency,
            sampling_frequency,
            signal_start_sample,
            num_samples_left,
            prn
        )
        integrated_samples += num_samples_left
        carrier_phase = update_carrier_phase(
            num_samples_left,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
        )
        prev_code_phase = code_phase
        code_phase = update_code_phase(
            system,
            num_samples_left,
            code_frequency,
            sampling_frequency,
            code_phase,
            found(sc_bit_detector)
        )
        integration_time = integrated_samples / sampling_frequency
        if num_samples_left == num_samples_left_to_integrate &&
                integration_time >= min_integration_time
            got_correlator = true
            correlator = normalize(correlator, integrated_samples)
            valid_correlator = correlator
            valid_correlator_carrier_phase = carrier_phase
            valid_correlator_carrier_frequency = carrier_frequency
            filtered_correlator = filter(post_corr_filter, correlator)
            pll_discriminator = pll_disc(
                system,
                filtered_correlator,
                correlator_sample_shifts
            )
            dll_discriminator = dll_disc(
                system,
                filtered_correlator,
                correlator_sample_shifts,
                early_late_index_shift,
                code_frequency / sampling_frequency
            )
            carrier_freq_update, carrier_loop_filter = filter_loop(
                carrier_loop_filter,
                pll_discriminator,
                integration_time,
                carrier_loop_filter_bandwidth
            )
            code_freq_update, code_loop_filter = filter_loop(
                code_loop_filter,
                dll_discriminator,
                integration_time,
                code_loop_filter_bandwidth
            )
            carrier_doppler, code_doppler = aid_dopplers(
                system,
                init_carrier_doppler,
                init_code_doppler,
                carrier_freq_update,
                code_freq_update,
                velocity_aiding
            )
            cn0_estimator = update(
                cn0_estimator,
                get_prompt(filtered_correlator, correlator_sample_shifts)
            )
            bit_buffer, prompt_accumulator = buffer(
                system,
                bit_buffer,
                prompt_accumulator,
                found(sc_bit_detector),
                prev_code_phase,
                code_phase,
                max_integration_time,
                get_prompt(filtered_correlator, correlator_sample_shifts)
            )
            sc_bit_detector = find(
                system,
                sc_bit_detector,
                get_prompt(filtered_correlator, correlator_sample_shifts)
            )
            correlator = zero(correlator)
            integrated_samples = 0
        end

        num_samples_left == signal_samples_left && break
        signal_start_sample += num_samples_left
    end
    next_state = TrackingState{S, C, CALF, COLF, CN, DS, CAR, COR}(
        prn,
        system,
        init_carrier_doppler,
        init_code_doppler,
        carrier_doppler,
        code_doppler,
        carrier_phase,
        code_phase,
        correlator,
        carrier_loop_filter,
        code_loop_filter,
        sc_bit_detector,
        integrated_samples,
        prompt_accumulator,
        cn0_estimator,
        downconverted_signal,
        carrier_replica,
        code_replica
    )
    estimated_cn0 = estimate_cn0(cn0_estimator, max_integration_time)
    TrackingResults(
        next_state,
        valid_correlator,
        correlator_sample_shifts,
        early_late_index_shift,
        valid_correlator_carrier_frequency,
        valid_correlator_carrier_phase,
        got_correlator,
        bit_buffer,
        estimated_cn0
    )
end

function downconvert_and_correlate!(
    system,
    signal,
    correlator,
    code_replica,
    code_phase,
    carrier_replica,
    carrier_phase,
    downconverted_signal,
    code_frequency,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    signal_start_sample,
    num_samples_left,
    prn
)
    gen_code_replica!(
        code_replica,
        system,
        code_frequency,
        sampling_frequency,
        code_phase,
        signal_start_sample,
        num_samples_left,
        correlator_sample_shifts,
        prn
    )
    gen_carrier_replica!(
        choose(carrier_replica, signal),
        carrier_frequency,
        sampling_frequency,
        carrier_phase,
        signal_start_sample,
        num_samples_left
    )
    downconvert!(
        choose(downconverted_signal, signal),
        signal,
        choose(carrier_replica, signal),
        signal_start_sample,
        num_samples_left
    )
    correlate(
        correlator,
        choose(downconverted_signal, signal),
        code_replica,
        correlator_sample_shifts,
        signal_start_sample,
        num_samples_left
    )
end

# CUDA downconvert_and_correlate for num_ants > 1
function downconvert_and_correlate!(
    system::AbstractGNSS{C},
    signal::AbstractMatrix,
    correlator::T,
    code_replica,
    code_phase,
    carrier_replica,
    carrier_phase,
    downconverted_signal,
    code_frequency,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    signal_start_sample,
    num_samples_left,
    prn
) where {C <: CuMatrix, T <: AbstractCorrelator}
    accumulator_result = downconvert_and_correlate_kernel_wrapper(
        system,
        view(signal, signal_start_sample:signal_start_sample - 1 + num_samples_left,:),
        correlator,
        code_phase,
        carrier_phase,
        code_frequency,
        correlator_sample_shifts,
        carrier_frequency,
        sampling_frequency,
        signal_start_sample,
        num_samples_left,
        prn
    )
    return T(map(+, get_accumulators(correlator), eachcol(Array(accumulator_result[1,:,:]))))
end

# CUDA downconvert_and_correlate for num_ants = 1
function downconvert_and_correlate!(
    system::AbstractGNSS{C},
    signal::AbstractVector,
    correlator::T,
    code_replica,
    code_phase,
    carrier_replica,
    carrier_phase,
    downconverted_signal,
    code_frequency,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    signal_start_sample,
    num_samples_left,
    prn
) where {C <: CuMatrix, T <: AbstractCorrelator}
    accumulator_result = downconvert_and_correlate_kernel_wrapper(
        system,
        view(signal, signal_start_sample:signal_start_sample - 1 + num_samples_left),
        correlator,
        code_phase,
        carrier_phase,
        code_frequency,
        correlator_sample_shifts,
        carrier_frequency,
        sampling_frequency,
        signal_start_sample,
        num_samples_left,
        prn
    )
    addition(a,b) = a + first(b)
    return T(map(addition, get_accumulators(correlator), eachcol(Array(accumulator_result[1,:,:]))))
end

function choose(replica::CarrierReplicaCPU, signal::AbstractArray{Complex{Float64}})
    replica.carrier_f64
end
function choose(replica::CarrierReplicaCPU, signal::AbstractArray{Complex{T}}) where T <: Number
    replica.carrier_f32
end
function choose(replica::DownconvertedSignalCPU, signal::AbstractArray{Complex{Float64}})
    replica.downconverted_signal_f64
end
function choose(replica::DownconvertedSignalCPU, signal::AbstractArray{Complex{T}}) where T <: Number
    replica.downconverted_signal_f32
end
function choose(replica::Nothing, signal::AbstractArray)
    nothing
end

"""
$(SIGNATURES)

Default post corr filter
"""
function get_default_post_corr_filter(correlator::AbstractCorrelator{T}) where T <: SVector
    x -> x[1]
end

function get_default_post_corr_filter(correlator)
    x -> x
end

"""
$(SIGNATURES)

Returns the appropiate integration time. It will be the maximum integration time once the
secondary code or the bit shift has been found.
"""
function get_integration_time(
    system::AbstractGNSS,
    max_integration_time,
    secondary_code_or_bit_found::Bool
)
    ifelse(
        secondary_code_or_bit_found,
        max_integration_time,
        min(
            ceil(typeof(1ms), get_code_length(system) / get_code_frequency(system)),
            max_integration_time
        )
    )
end

"""
$(SIGNATURES)

Calculates the number of chips to integrate.
"""
function get_num_chips_to_integrate(
    system::AbstractGNSS,
    max_integration_time,
    current_code_phase,
    secondary_code_or_bit_found
)
    max_phase = Int(upreferred(get_code_frequency(system) *
        get_integration_time(system, max_integration_time, secondary_code_or_bit_found)))
    current_phase_mod_max_phase = mod(current_code_phase, max_phase)
    return max_phase - current_phase_mod_max_phase
end

"""
$(SIGNATURES)

Calculates the number of samples to integrate.
"""
function get_num_samples_left_to_integrate(
    system::AbstractGNSS,
    max_integration_time,
    sampling_frequency,
    current_code_doppler,
    current_code_phase,
    secondary_code_or_bit_found
)
    phase_to_integrate = get_num_chips_to_integrate(
        system,
        max_integration_time,
        current_code_phase,
        secondary_code_or_bit_found
    )
    code_frequency = get_code_frequency(system) + current_code_doppler
    ceil(Int, phase_to_integrate * sampling_frequency / code_frequency)
end

"""
$(SIGNATURES)

Aid dopplers. That is velocity aiding for the carrier doppler and carrier aiding
for the code doppler.
"""
function aid_dopplers(
    system::AbstractGNSS,
    init_carrier_doppler,
    init_code_doppler,
    carrier_freq_update,
    code_freq_update,
    velocity_aiding
)
    carrier_doppler = carrier_freq_update + velocity_aiding
    code_doppler = code_freq_update + carrier_doppler * get_code_center_frequency_ratio(system)
    init_carrier_doppler + carrier_doppler, init_code_doppler + code_doppler
end

"""
$(SIGNATURES)

Get the number of samples in the signal.
"""
@inline function get_num_samples(signal)
    length(signal)
end

@inline function get_num_samples(signal::AbstractMatrix)
    size(signal, 1)
end

function resize!(ds::DownconvertedSignalCPU, b::Integer, signal::AbstractVector)
    resize!(choose(ds, signal), b)
    ds
end

function resize!(ds::DownconvertedSignalCPU, b::Integer, signal::AbstractMatrix{Complex{Float64}})
    num_ants = size(signal, 2)
    DownconvertedSignalCPU(
        ds.downconverted_signal_f32,
        size(ds.downconverted_signal_f64, 1) == b ?
            ds.downconverted_signal_f64 :
            StructArray{Complex{Float64}}((Matrix{Float64}(undef, b, num_ants), Matrix{Float64}(undef, b, num_ants)))
    )
end

function resize!(ds::DownconvertedSignalCPU, b::Integer, signal::AbstractMatrix{Complex{T}}) where T <: Number
    num_ants = size(signal, 2)
    DownconvertedSignalCPU(
        size(ds.downconverted_signal_f32, 1) == b ?
            ds.downconverted_signal_f32 :
            StructArray{Complex{Float32}}((Matrix{Float32}(undef, b, num_ants), Matrix{Float32}(undef, b, num_ants))),
        ds.downconverted_signal_f64
    )
end

# No need for resizing when dealing with GPU signals
function resize!(ds::Nothing, b::Integer, signal::AbstractArray)
    return ds
end
# No need for resizing the GPU GNSS codes
function resize!(codes::Nothing, b::Integer)
    return codes
end


# This file is a part of Julia. License is MIT: http://julialang.org/license

module FieldValues

using ...VersionWeights
importall .....Base.Operators

export FieldValue, Field, validmax, secondmax

# FieldValue is a numeric type which helps dealing with
# infinities. It holds 5 numbers l0,l1,l2,l3,l4. It can
# be interpreted as a polynomial
#  x = a^4 * l0 + a^3 * l1 + a^2 + l2 + a^1 * l3 + l4
# where a -> Inf
# The levels are used as such:
#  l0 : for hard constraints (dependencies and requirements)
#  l1 : for favoring higher versions of the explicitly required
#       packages
#  l2 : for favoring higher versions of all other packages (and
#       favoring uninstallation of non-needed packages)
#  l3 : for favoring dependants over dependencies
#  l4 : for symmetry-breaking random noise
#
immutable FieldValue
    l0::Int
    l1::VersionWeight
    l2::VersionWeight
    l3::Int
    l4::Int128
end
FieldValue(l0::Int,l1::VersionWeight,l2::VersionWeight,l3::Int) = FieldValue(l0, l1, l2, l3, Int128(0))
FieldValue(l0::Int,l1::VersionWeight,l2::VersionWeight) = FieldValue(l0, l1, l2, 0)
FieldValue(l0::Int,l1::VersionWeight) = FieldValue(l0, l1, zero(VersionWeight))
FieldValue(l0::Int) = FieldValue(l0, zero(VersionWeight))
FieldValue() = FieldValue(0)

typealias Field Vector{FieldValue}

Base.zero(::Type{FieldValue}) = FieldValue()

Base.typemin(::Type{FieldValue}) = (x=typemin(Int); y=typemin(VersionWeight); FieldValue(x,y,y,x,typemin(Int128)))

Base.:-(a::FieldValue, b::FieldValue) = FieldValue(a.l0-b.l0, a.l1-b.l1, a.l2-b.l2, a.l3-b.l3, a.l4-b.l4)
Base.:+(a::FieldValue, b::FieldValue) = FieldValue(a.l0+b.l0, a.l1+b.l1, a.l2+b.l2, a.l3+b.l3, a.l4+b.l4)

function Base.isless(a::FieldValue, b::FieldValue)
    a.l0 < b.l0 && return true
    a.l0 > b.l0 && return false
    c = cmp(a.l1, b.l1)
    c < 0 && return true
    c > 0 && return false
    c = cmp(a.l2, b.l2)
    c < 0 && return true
    c > 0 && return false
    a.l3 < b.l3 && return true
    a.l3 > b.l3 && return false
    a.l4 < b.l4 && return true
    return false
end

==(a::FieldValue, b::FieldValue) =
    a.l0 == b.l0 && a.l1 == b.l1 && a.l2 == b.l2 && a.l3 == b.l3 && a.l4 == b.l4

Base.abs(a::FieldValue) = FieldValue(abs(a.l0), abs(a.l1), abs(a.l2), abs(a.l3), abs(a.l4))

# if the maximum field has l0 < 0, it means that
# some hard constraint is being violated
validmax(a::FieldValue) = a.l0 >= 0

# like usual indmax, but favors the highest indices
# in case of a tie
function Base.indmax(f::Field)
    m = typemin(FieldValue)
    mi = 0
    for j = length(f):-1:1
        if f[j] > m
            m = f[j]
            mi = j
        end
    end
    @assert mi != 0
    return mi
end

# secondmax returns the (normalized) value of the second maximum in a
# field. It's used to determine the most polarized field.
function secondmax(f::Field)
    m = typemin(FieldValue)
    m2 = typemin(FieldValue)
    for i = 1:length(f)
        a = f[i]
        if a > m
            m2 = m
            m = a
        elseif a > m2
            m2 = a
        end
    end
    return m2 - m
end

end


using Statistics
using DataAPI

abstract type FitResult end
"""
A measured set of correlated k-ratios.
"""
struct BasicFitResult <: FitResult
    label::Label
    kratios::UncertainValues
end

function findlabel(ffr::FitResult, cxr::CharXRay)
    lbls = labels(ffr.kratios)
    fa = findall(lbl -> (lbl isa CharXRayLabel) && (cxr in lbl.xrays), lbls)
    @assert length(fa) ≠ 0 "No label found for $cxr."
    @assert length(fa) == 1 "Multiple labels found for $cxr."
    return lbls[fa[1]]
end

Base.show(io::IO, ffr::FitResult) = print(io, "$(ffr.label)")
NeXLUncertainties.NeXLUncertainties.value(ffr::FitResult, label::ReferenceLabel) =
    NeXLUncertainties.value(ffr.kratios, label)
NeXLUncertainties.σ(ffr::FitResult, label::ReferenceLabel) = σ(ffr.kratios, label)
NeXLUncertainties.uncertainty(ffr::FitResult, label::ReferenceLabel) =
    uncertainty(ffr.kratios, label)
NeXLUncertainties.getindex(ffr::FitResult, label::ReferenceLabel) =
    getindex(ffr.kratios, label)
Base.keys(ffr::FitResult) = keys(ffr.kratios)
NeXLUncertainties.labels(ffr::FitResult) = labels(ffr.kratios)
function kratio(cxr::CharXRay, ffr::FitResult)
    lbl = findlabel(ffr, cxr)
    return uv(NeXLUncertainties.value(ffr, lbl), σ(ffr, lbl))
end
unknown(ffr::FitResult)::Label = ffr.label
Base.values(ffrs::Vector{<:FitResult}, lbl::ReferenceLabel) =
    [NeXLUncertainties.value(ffr.kratios, lbl, 0.0) for ffr in ffrs]
σs(ffrs::Vector{<:FitResult}, lbl::ReferenceLabel) =
    [σ(ffr.kratios, lbl, 0.0) for ffr in ffrs]

"""
    kratios(ffr::FitResult)::Vector{KRatio}

The k-ratios associated with each `CharXRayLabel` as a vector 'KRatio' objects.
"""
function kratios(ffr::FitResult)::Vector{KRatio}
    lbls = filter(labels(ffr.kratios)) do l
        (l isa CharXRayLabel) && haskey(properties(l)[:Composition], element(l))
    end
    return map(lbls) do lbl
        props = properties(lbl)
        KRatio(lbl.xrays, properties(unknown(ffr)), props, props[:Composition], ffr.kratios[lbl])
    end
end

"""
    kratio(ffr::FitResult, cxr::CharXRay)::KRatio

Extract the k-ratio associated with the specified CharXRay (zero if not measured)
"""
function kratio(ffr::FitResult, cxr::CharXRay)
    lbls = labels(ffr.kratios)
    i = findfirst(lbl->(lbl isa CharXRayLabel) && (cxr in lbl.xrays), lbls)
    @assert !isnothing(i) "There is no k-ratio associated with $cxr."
    lbl, props = lbls[i], properties(lbls[i])
    KRatio(lbl.xrays, unknown(ffr).spectrum.properties, props, props[:Composition], ffr.kratios[lbl])
  end

"""
    extractStandards(ffr::FitResult, elm::Element, mat::Material)::Vector{KRatio}

Extract a `Vector{KRatio}` for `elm::Element` from a `ffr::FilterFitResult` measured from `mat::Material`.
"""
function extractStandards(ffr::FitResult, elm::Element, mat::Material)::Vector{KRatio}
    @assert haskey(mat, elm) "The element $elm is not contained within $mat."
    lbls = filter(labels(ffr.kratios)) do l
        (l isa CharXRayLabel) && (elm == element(l))
    end
    krs = map(lbls) do lbl
        props = properties(lbl)
        kr = KRatio(lbl.xrays, properties(unknown(ffr)), props, props[:Composition], ffr.kratios[lbl])
        kr.unkProps[:Composition] = mat
        kr
    end
    length(krs) == 0 && @info  "There are no k-ratios associated with the $elm in $ffr."
    return krs
end

"""
    extractStandards(ffr::FitResult, cxrs::AbstractVector{CharXRay}, mat::Material)::Vector{KRatio}

Extract a `KRatio` for the `CharXRay` from a `FilterFitResult` associated with
the `Material`.
"""
function extractStandards(ffr::FitResult, cxrs::AbstractVector{CharXRay}, mat::Material)::Vector{KRatio}
    lbls = filter(labels(ffr.kratios)) do lbl
        (lbl isa CharXRayLabel) && any(cxr->cxr in xrays(lbl), cxrs)
    end
    krs = map(lbls) do lbl
        props = properties(lbl)
        kr = KRatio(lbl.xrays, properties(unknown(ffr)), props, props[:Composition], ffr.kratios[lbl])
        kr.unkProps[:Composition] = mat
        kr
    end
    length(krs) == 0 && @info "There are no k-ratios associated with any of $cxrs in $ffr."
    return krs
end

"""
    heterogeneity(ffrs::Vector{FilterFitResult}, lbl::ReferenceLabel)

Computes the ratio of the standard deviation of the measured values over the mean calculated uncertainty
from the fit.  A value near 1 means the sample appears homogeneous and a value greater than 1 means the sample
appears heterogeneous.
"""
heterogeneity(ffrs::Vector{<:FitResult}, lbl::ReferenceLabel) =
    std(values(ffrs, lbl)) / mean(σs(ffrs, lbl))

"""
    NeXLUncertainties.asa(::Type{DataFrame}, ffrs::AbstractVector{<:FitResult}; charOnly = true, withUnc = false, pivot = false)

Return generic `FitResult` as a DataFrame.
"""
function NeXLUncertainties.asa(
    ::Type{DataFrame},
    ffrs::AbstractVector{<:FitResult};
    charOnly = true,
    withUnc = false,
    pivot = false,
)::DataFrame
    lbls = sort(collect(Set(mapreduce(labels, append!, ffrs))))
    lbls = charOnly ? filter(lbl -> lbl isa CharXRayLabel, lbls) : lbls
    if pivot
        res = DataFrame(ROI = lbls)
        for ffr in ffrs
            vals = [value(ffr.kratios, lbl, missing) for lbl in lbls]
            insertcols!(res, ncol(res) + 1, Symbol(repr(ffr.label)) => vals)
            if withUnc
                unc = [σ(ffr.kratios, lbl, missing) for lbl in lbls]
                insertcols!(res, ncol(res) + 1, Symbol('Δ' * repr(ffr.label)) => unc)
            end
        end
    else
        rowLbls = [repr(ffr.label) for ffr in ffrs]
        res = DataFrame(Symbol("Spectra") => rowLbls)
        for lbl in lbls
            vals = [value(ffr.kratios, lbl, missing) for ffr in ffrs]
            insertcols!(res, ncol(res) + 1, Symbol(repr(lbl)) => vals)
            if withUnc
                unc = [σ(ffr.kratios, lbl, missing) for ffr in ffrs]
                insertcols!(res, ncol(res) + 1, Symbol('Δ' * repr(lbl)) => unc)
            end
        end
    end
    return res
end

function NeXLUncertainties.asa(::Type{DataFrame}, fr::FitResult; withUnc = false)
    rois = labels(fr)
    res =  DataFrame(
        ROI = rois,
        k = map(l -> value(fr.kratios, l), rois),
    )
    if withUnc
        res[:, "σ(k)"] = map(l -> σ(fr.kratios, l), rois)
    end
    return res
end

function DataAPI.describe(ffrs::Vector{<:FitResult})::DataFrame
    df = asa(DataFrame, ffrs)[:, 2:end]
    desc = describe(df, :mean, :std, :min, :q25, :median, :q75, :max)
    lbls = filter(
        lbl -> lbl isa CharXRayLabel,
        sort(collect(Set(mapreduce(labels, append!, ffrs)))),
    )
    insertcols!(desc, 4, :hetero => heterogeneity.(Ref(ffrs), lbls))
    return desc
end

"""
    FilterFitResult

Represents the result of fitting a FilteredUnknownW to a FilteredUnknown.

Struct elements

    label::UnknownLabel  # Identifies the unknown
    kratios::UncertainValues # Labeled with ReferenceLabel objects
    roi::UnitRange{Int} # Range of channels fit
    raw::Vector{Float64} # Raw spectrum data
    residual::Vector{Float64} # Residual spectrum
"""
struct FilterFitResult <: FitResult
    label::UnknownLabel
    kratios::UncertainValues
    roi::UnitRange{Int}
    raw::Vector{Float64}
    residual::Vector{Float64}
    peakback::Dict{ReferenceLabel,NTuple{2,Float64}}
end

"""
    residual(ffr::FilterFitResult)::Spectrum

A Spectrum containing the histogram representing the unknown spectrum
minus the fitted characteristic peaks shapes times the best fit coefficient.
"""
function residual(ffr::FilterFitResult)::Spectrum
    props = copy(properties(ffr.label.spectrum))
    props[:Name] = "Residual[$(props[:Name])]"
    return Spectrum(ffr.label.spectrum.energy, ffr.residual, props)
end

"""
    spectrum(ffr::FilterFitResult)::Spectrum

Returns the original unknown spectrum.
"""
spectrum(ffr::FilterFitResult)::Spectrum = ffr.label.spectrum

"""
    characteristiccounts(ffr::FiterFitResult, strip)

Number of spectrum counts that were accounted for by the fitted elements with the `strip` Element(s) removed.
"""
characteristiccounts(ffr::FilterFitResult, strip) =
    sum(element(ref) in strip ? 0.0 : v[1] - v[2] for (ref, v) in ffr.peakback) # sum(ffr.raw[ffr.roi]-ffr.residual[ffr.roi])

"""
    peaktobackground(ffr::FilterFitResult, backwidth::Float64=10.0)::Float64

The peak-to-background ratio as determined from the raw and residual spectra integrated over
the fit region-of-interest and scaled to `backwidth` eV of continuum (nominally 10 eV).
"""
function peaktobackground(
    ffr::FilterFitResult,
    klabel::ReferenceLabel,
    backwidth::Float64 = 10.0,
)::Float64
    unk = spectrum(unknown(ffr))
    peak, back = ffr.peakback[klabel]
    return (peak * (energy(klabel.roi.stop, unk) - energy(klabel.roi.start, unk))) /
           (backwidth * back)
end

"""
    NeXLUncertainties.asa(::Type{DataFrame}, ffr::FilterFitResult; charOnly::Bool=true, material=nothing)::DataFrame

Tabulate details about each region-of-interest in the 'FilterFitResult' in a 'DataFrame'.
  * If charOnly then only display characteristic X-ray data (not escapes etc.)
  * If `material` is a Material then the computed k-ratio will also be tabulated along with kmeas/kcalc.
"""
function NeXLUncertainties.asa(
    ::Type{DataFrame},
    ffr::FilterFitResult;
    charOnly::Bool = true,
    material = nothing,
)::DataFrame
    lbls, klbl, std, kr, dkr, roi1, roi2, peak, back, ptob = UnknownLabel[],
    ReferenceLabel[],
    String[],
    Float64[],
    Float64[],
    Int[],
    Int[],
    Float64[],
    Float64[],
    Float64[]
    kcalc, ratio = Float64[], Float64[]
    for lbl in NeXLUncertainties.sortedlabels(ffr.kratios)
        if (!charOnly) || (lbl isa CharXRayLabel)
            stdspec, unkspec = properties(lbl), ffr.label.spectrum
            push!(lbls, ffr.label)
            push!(std, stdspec[:Name])
            push!(klbl, lbl)
            push!(roi1, lbl.roi.start)
            push!(roi2, lbl.roi.stop)
            push!(kr, NeXLUncertainties.value(ffr.kratios, lbl))
            push!(dkr, σ(ffr.kratios, lbl))
            pb = ffr.peakback[lbl]
            push!(peak, pb[1])
            push!(back, pb[2])
            push!(ptob, peaktobackground(ffr, lbl))
            if material isa Material
                kc = NaN64
                if lbl isa CharXRayLabel
                    try
                        zcu = zafcorrection(
                            XPP,
                            ReedFluorescence,
                            NullCoating,
                            material,
                            lbl.xrays,
                            unkspec[:BeamEnergy],
                        )
                        zcs = zafcorrection(
                            XPP,
                            ReedFluorescence,
                            NullCoating,
                            stdspec[:Composition],
                            lbl.xrays,
                            stdspec[:BeamEnergy],
                        )
                        kc = k(zcu, zcs, unkspec[:TakeOffAngle], stdspec[:TakeOffAngle])
                    catch
                        kc = NaN64
                    end
                end
                push!(kcalc, kc)
                push!(ratio, NeXLUncertainties.value(ffr.kratios, lbl) / kc)
            end
        end
    end
    return material isa Material ?
           DataFrame(
        Spectrum = lbls,
        Feature = klbl,
        Reference = std,
        Start = roi1,
        Stop = roi2,
        K = kr,
        dK = dkr,
        Peak = peak,
        Back = back,
        PtoB = ptob,
        Kcalc = kcalc,
        Ratio = ratio,
    ) :
           DataFrame(
        Spectrum = lbls,
        Feature = klbl,
        Reference = std,
        Start = roi1,
        Stop = roi2,
        K = kr,
        dK = dkr,
        Peak = peak,
        Back = back,
        PtoB = ptob,
    )
end

"""
    filteredresidual(fit::FilterFitResult, unk::FilteredUnknown, ffs::AbstractVector{FilteredReference})::Vector{Float64}

Computes the difference between the best fit and the unknown filtered spectral data.
"""
function filteredresidual(
    fit::FilterFitResult,
    unk::FilteredUnknown,
    ffs::AbstractVector{FilteredReference},
)::Vector{Float64}
    return unk.filtered - mapreduce(+, ffs) do ff
        (value(fit, ff.label) * (ff.scale / unk.scale)) * extract(ff, unk.ffroi)
    end
end

"""
    NeXLMatrixCorrection.estimatecoating(fr::FitResult, substrate::Material, coating::Material, cxr::CharXRay, mc::Type{<:MatrixCorrection}=XPP)::Film

Estimate the mass-thickness of `coating` on bulk `substrate` using the X-ray `cxr` for which there is KRatio in `fr`.  The result is
assigned to the properties of `fr` so that it can be account for in the matrix correction process.

This method is intended for use on standards where the substrate composition is known a priori.
"""
function NeXLMatrixCorrection.estimatecoating(fr::FitResult, substrate::Material, coating::Material, cxr::CharXRay, mc::Type{<:MatrixCorrection}=XPP)::Film
    coating[:Density] = get(coating, :Density, 1.0)
    properties(fr.label.spectrum)[:Coating] = estimatecoating(substrate, coating, kratio(fr, cxr), mc)
end


#gnum-constants.jl
#setting gnum values to unum constants.

function nan!{ESS,FSS}(x::Gnum{ESS,FSS})
  x.scratchpad.flags |= GNUM_NAN_MASK
end

@generated function inf!{ESS,FSS,side}(x::Gnum{ESS,FSS}, flags::UInt16, ::Type{Val{side}})
  :(clear_gflags!(x.$side); x.$side.flags = flags | GNUM_INF_MASK; nothing)
end
@generated function mmr!{ESS,FSS,side}(x::Gnum{ESS,FSS}, flags::UInt16, ::Type{Val{side}})
  :(clear_gflags!(x.$side); x.$side.flags = flags | GNUM_MMR_MASK; nothing)
end
@generated function sss!{ESS,FSS,side}(x::Gnum{ESS,FSS}, flags::UInt16, ::Type{Val{side}})
  :(clear_gflags!(x.$side); x.$side.flags = flags | GNUM_SSS_MASK; nothing)
end
@generated function zero!{ESS,FSS,side}(x::Gnum{ESS,FSS}, ::Type{Val{side}})
  :(clear_gflags!(x.$side); x.$side.flags = GNUM_ZERO_MASK; nothing)
end

#testing gnum values for unum constants
function is_nan{ESS,FSS}(x::Gnum{ESS,FSS})
  x.scratchpad.flags & GNUM_NAN_MASK != 0
end

set_g_mmr!{ESS,FSS}(x::Unum{ESS,FSS}) = (x.flags |= GNUM_MMR_MASK)

is_g_inf{ESS,FSS}(x::Unum{ESS,FSS}) = x.flags & GNUM_INF_MASK != 0
@generated function is_inf{ESS,FSS,side}(x::Gnum{ESS,FSS}, ::Type{Val{side}})
 :(is_g_inf(x.$side))
end

is_g_mmr{ESS,FSS}(x::Unum{ESS,FSS}) = x.flags & GNUM_MMR_MASK != 0
@generated function is_mmr{ESS,FSS,side}(x::Gnum{ESS,FSS}, ::Type{Val{side}})
  :(is_g_mmr(x.$side))
end

is_g_sss{ESS,FSS}(x::Unum{ESS,FSS}) = x.flags & GNUM_SSS_MASK != 0
@generated function is_sss{ESS,FSS,side}(x::Gnum{ESS,FSS}, ::Type{Val{side}})
  :(is_g_sss(x.$side))
end

is_g_zero{ESS,FSS}(x::Unum{ESS,FSS}) = x.flags & GNUM_ZERO_MASK != 0
@generated function is_zero{ESS,FSS,side}(x::Gnum{ESS,FSS}, ::Type{Val{side}})
  :(is_g_zero(x.$side))
end

@generated function force_from_flags!{ESS,FSS,side}(src::Gnum{ESS,FSS}, dest::Unum{ESS,FSS}, ::Type{Val{side}})
  quote
    (src.$side.flags & GNUM_INF_MASK != 0) && (inf!(dest, src.$side.flags & UNUM_SIGN_MASK); return true)
    (src.$side.flags & GNUM_MMR_MASK != 0) && (mmr!(dest, src.$side.flags & UNUM_SIGN_MASK); return true)
    (src.$side.flags & GNUM_SSS_MASK != 0) && (sss!(dest, src.$side.flags & UNUM_SIGN_MASK); return true)
    (src.$side.flags & GNUM_ZERO_MASK != 0) && (zero!(dest); return true)
    false
  end
end


import TimeZones: astimezone

"""
    AnchoredInterval{P, T}(anchor::T, [inclusivity::Inclusivity]) where {P, T} -> AnchoredInterval{P, T}
    AnchoredInterval{P, T}(anchor::T, [closed_left::Bool, closed_right::Bool]) where {P, T} -> AnchoredInterval{P, T}

`AnchoredInterval` is a subtype of `AbstractInterval` that represents a non-iterable range
or span of values defined not by two endpoints but instead by a single `anchor` point and
the value type `P` which represents the size of the range. When `P` is positive, the
`anchor` represents the lesser endpoint (the beginning of the range); when `P` is negative,
the `anchor` represents the greater endpoint (the end of the range).

The interval represented by an `AnchoredInterval` value may be closed (both endpoints are
included in the interval), open (neither endpoint is included), or half-open. This openness
is defined by an `Inclusivity` value, which defaults to half-open (with the lesser endpoint
included for positive values of `P` and the greater endpoint included for negative values).

### Why?

`AnchoredIntervals` are most useful in cases where a single value is used to stand in for a
range of values. This happens most often with dates and times, where "HE15" is often used as
shorthand for (14:00..15:00].

To this end, `HourEnding` is a type alias for `AnchoredInterval{Hour(-1)}`. Similarly,
`HourBeginning` is a type alias for `AnchoredInterval{Hour(1)}`.

### Rounding

While the user may expect an `HourEnding` or `HourBeginning` value to be anchored to a
specific hour, the constructor makes no guarantees that the anchor provided is rounded:

```julia
julia> HourEnding(DateTime(2016, 8, 11, 2, 30))
HourEnding{DateTime}(2016-08-11T02:30:00, Inclusivity(false, true))
```

The `HE` and `HB` pseudoconstructors round the input up or down to the nearest hour, as
appropriate:

```julia
julia> HE(DateTime(2016, 8, 11, 2, 30))
HourEnding{DateTime}(2016-08-11T03:00:00, Inclusivity(false, true))

julia> HB(DateTime(2016, 8, 11, 2, 30))
HourBeginning{DateTime}(2016-08-11T02:00:00, Inclusivity(true, false))
```

### Example

```julia
julia> AnchoredInterval{Hour(-1)}(DateTime(2016, 8, 11, 12))
HourEnding{DateTime}(2016-08-11T12:00:00, Inclusivity(false, true))

julia> AnchoredInterval{Day(1)}(DateTime(2016, 8, 11))
AnchoredInterval{1 day, DateTime}(2016-08-11T00:00:00, Inclusivity(true, false))

julia> AnchoredInterval{Minute(5)}(DateTime(2016, 8, 11, 12, 30), true, true)
AnchoredInterval{5 minutes, DateTime}(2016-08-11T12:30:00, Inclusivity(true, true))
```

See also: [`Interval`](@ref), [`Inclusivity`](@ref), [`HE`](@ref), [`HB`](@ref)
"""
struct AnchoredInterval{P, T} <: AbstractInterval{T}
    anchor::T
    inclusivity::Inclusivity
end

function AnchoredInterval{P, T}(i::T, x::Bool, y::Bool) where {P, T}
    return AnchoredInterval{P, T}(i, Inclusivity(x, y))
end

# When an interval is anchored to the lesser endpoint, default to Inclusivity(false, true)
# When an interval is anchored to the greater endpoint, default to Inclusivity(true, false)
function AnchoredInterval{P, T}(i::T) where {P, T}
    return AnchoredInterval{P, T}(i::T, Inclusivity(P ≥ zero(P), P ≤ zero(P)))
end

function AnchoredInterval{P, T}(i, inc...) where {P, T}
    return AnchoredInterval{P, T}(convert(T, i), inc...)
end

AnchoredInterval{P}(i::T, inc...) where {P, T} = AnchoredInterval{P, T}(i, inc...)

"""
    HourEnding{T<:TimeType} <: AbstractInterval{T}

A type alias for `AnchoredInterval{Hour(-1), T}` which is used to denote a 1-hour period of
time which ends at a time instant (of type `T`).
"""
const HourEnding{T} = AnchoredInterval{Hour(-1), T} where T <: TimeType
HourEnding(a::T, args...) where T = HourEnding{T}(a, args...)

"""
    HourBeginning{T<:TimeType} <: AbstractInterval{T}

A type alias for `AnchoredInterval{Hour(1), T}` which is used to denote a 1-hour period of
time which begins at a time instant (of type `T`).
"""
const HourBeginning{T} = AnchoredInterval{Hour(1), T} where T <: TimeType
HourBeginning(a::T, args...) where T = HourBeginning{T}(a, args...)

"""
    HE(anchor, args...) -> HourEnding

`HE` is a pseudoconstructor for [`HourEnding`](@ref) that rounds the anchor provided up to the
nearest hour.
"""
HE(a, args...) = HourEnding(ceil(a, Hour), args...)

"""
    HB(anchor, args...) -> HourBeginning

`HB` is a pseudoconstructor for [`HourBeginning`](@ref) that rounds the anchor provided down to the
nearest hour.
"""
HB(a, args...) = HourBeginning(floor(a, Hour), args...)

function Base.copy(x::AnchoredInterval{P, T}) where {P, T}
    return AnchoredInterval{P, T}(anchor(x), inclusivity(x))
end

##### ACCESSORS #####

# We would typically compute `first` and `last` using `min` and `max` respectively, but we
# can get unexpected behaviour if adding the span to the anchor endpoint produces a value
# that is no longer comparable (e.g., `NaN`).

function Base.first(interval::AnchoredInterval{P}) where P
    P < zero(P) ? (interval.anchor + P) : (interval.anchor)
end

function Base.last(interval::AnchoredInterval{P}) where P
    P < zero(P) ? (interval.anchor) : (interval.anchor + P)
end

anchor(interval::AnchoredInterval) = interval.anchor
span(interval::AnchoredInterval{P}) where P = abs(P)

##### CONVERSION #####

function Base.convert(::Type{Interval}, interval::AnchoredInterval{P, T}) where {P, T}
    return Interval{T}(first(interval), last(interval), inclusivity(interval))
end

function Base.convert(::Type{Interval{T}}, interval::AnchoredInterval{P, T}) where {P, T}
    return Interval{T}(first(interval), last(interval), inclusivity(interval))
end

# Conversion methods which currently aren't needed but could prove useful. Commented out
# since these are untested.

#=
function Base.convert(::Type{AnchoredInterval{P, T}}, interval::Interval{T}) where {P, T}
    @assert abs(P) == span(interval)
    anchor = P < zero(P) ? last(interval) : first(interval)
    AnchoredInterval{P, T}(last(interval), inclusivity(interval))
end

function Base.convert(::Type{AnchoredInterval{P}}, interval::Interval{T}) where {P, T}
    @assert abs(P) == span(interval)
    anchor = P < zero(P) ? last(interval) : first(interval)
    AnchoredInterval{P, T}(anchor, inclusivity(interval))
end
=#

function Base.convert(::Type{AnchoredInterval{Ending}}, interval::Interval{T}) where {T}
    AnchoredInterval{-span(interval), T}(last(interval), inclusivity(interval))
end

function Base.convert(::Type{AnchoredInterval{Beginning}}, interval::Interval{T}) where {T}
    AnchoredInterval{span(interval), T}(first(interval), inclusivity(interval))
end

Base.convert(::Type{T}, interval::AnchoredInterval{P, T}) where {P, T} = anchor(interval)

# Date/DateTime attempt to convert to Int64 instead of falling back to convert(T, ...)
Dates.Date(interval::AnchoredInterval{P, Date}) where P = convert(Date, interval)
Dates.DateTime(interval::AnchoredInterval{P, DateTime}) where P = convert(DateTime, interval)

##### DISPLAY #####

function Base.show(io::IO, interval::T) where T <: AnchoredInterval
    if get(io, :compact, false)
        print(io, interval)
    else
        print(io, "$T(")
        show(io, anchor(interval))
        print(io, ", ")
        show(io, inclusivity(interval))
        print(io, ")")
    end
end

function Base.print(io::IO, interval::AnchoredInterval{P, T}) where {P, T <: Union{Date, AbstractDateTime}}
    # Print to io in order to keep properties like :limit and :compact
    if get(io, :compact, false)
        io = IOContext(io, :limit=>true)
    end

    print(io, description(interval))
end

##### ARITHMETIC #####

Base.:+(a::T, b) where {T <: AnchoredInterval} = T(anchor(a) + b, inclusivity(a))

Base.:+(a, b::AnchoredInterval) = b + a
Base.:-(a::AnchoredInterval, b) = a + -b

# Required for StepRange{<:AnchoredInterval}
Base.:-(a::AnchoredInterval, b::AnchoredInterval) = anchor(a) - anchor(b)

Base.:-(a::T, b::AnchoredInterval{P, T}) where {P, T <: Number} = a + -b

function Base.:-(a::AnchoredInterval{P, T}) where {P, T <: Number}
    inc = inclusivity(a)
    AnchoredInterval{-P, T}(-anchor(a), Inclusivity(last(inc), first(inc)))
end

##### EQUALITY #####

function Base.:(==)(a::AnchoredInterval{P, T}, b::AnchoredInterval{P, T}) where {P, T}
    return anchor(a) == anchor(b) && inclusivity(a) == inclusivity(b)
end

# Required for min/max of AnchoredInterval{LaxZonedDateTime} when the anchor is AMB or DNE
function Base.isless(a::AnchoredInterval{P, T}, b::AnchoredInterval{P, T}) where {P, T}
    return (
        anchor(a) < anchor(b) ||
        (anchor(a) == anchor(b) && first(inclusivity(a)) && !first(inclusivity(b)))
    )
end

##### RANGE #####

function Base.steprange_last(start::T, step, stop::T) where {T <: AnchoredInterval}
    T(Base.steprange_last(anchor(start), step, anchor(stop)), inclusivity(start))
end

# Infer step for two-argument StepRange{<:AnchoredInterval}
if VERSION < v"0.7.0-DEV.4003"
    function Base.colon(start::AnchoredInterval{P, T}, stop::AnchoredInterval{P, T}) where {P,T}
        return colon(start, abs(P), stop)
    end
else
    # https://github.com/JuliaLang/julia/pull/26074
    function Base.:(:)(start::AnchoredInterval{P, T}, stop::AnchoredInterval{P, T}) where {P,T}
        return (:)(start, abs(P), stop)
    end
end

function Base.length(r::StepRange{<:AnchoredInterval})
    return length(anchor(r.start):r.step:anchor(r.stop))
end

##### SET OPERATIONS #####

function Base.isempty(interval::AnchoredInterval{P, T}) where {P, T}
    return P == zero(P) && !isclosed(interval)
end

function Base.intersect(a::AnchoredInterval{P, T}, b::AnchoredInterval{Q, T}) where {P,Q,T}
    interval = invoke(intersect, Tuple{AbstractInterval{T}, AbstractInterval{T}}, a, b)

    sp = isa(P, Period) ? canonicalize(typeof(P), span(interval)) : span(interval)
    if P ≤ zero(P)
        anchor = last(interval)
        new_P = -sp
    else
        anchor = first(interval)
        new_P = sp
    end

    return AnchoredInterval{new_P, T}(anchor, inclusivity(interval))
end

##### UTILITIES #####

function canonicalize(target_type::Type{<:Period}, p::P) where P <: Period
    Q, max_val = coarserperiod(P)
    if (value(p) % max_val == 0) && (max_val > 1)
        p = canonicalize(target_type, convert(Q, p))
    end
    return p
end

canonicalize(target_type::Type{P}, p::P) where P <: Period = p

##### TIME ZONES #####

function astimezone(i::AnchoredInterval{P, ZonedDateTime}, tz::TimeZone) where P
    return AnchoredInterval{P, ZonedDateTime}(astimezone(anchor(i), tz), inclusivity(i))
end


# Juliaを使った株価の取得と通知
using JSON, DataFrames, Dates, CSV

# 株式名or番号，取得期間，取得間隔を指定してURLを生成
function yahoofinurl(stock, period, interval)
    prefix = "https://query1.finance.yahoo.com/v7/finance/chart/"
    url = prefix * stock * "?range=" * period * "&interval=" * interval * "&indicators=quote&includeTimestamps=true"
    println(url)
    return url
end

# Yahoo!FinanceのJSONからDataFrameに変換
function stockjson2df(json; dropnan=true)
    # 時刻の変換
    unixtime = json["chart"]["result"][1]["timestamp"]
    dt = Dates.unix2datetime.(unixtime) + Dates.Hour(9) # 時刻は日本時間としている．米国時間の場合"+Dates.Hour(9)"を削る

    # DataFrameに代入
    df = DataFrame()
    df."timestamp" = dt
    df."open" = json["chart"]["result"][1]["indicators"]["quote"][1]["open"]
    df."low" = json["chart"]["result"][1]["indicators"]["quote"][1]["low"]
    df."high" = json["chart"]["result"][1]["indicators"]["quote"][1]["high"]
    df."close" = json["chart"]["result"][1]["indicators"]["quote"][1]["close"]
    df."volume" = json["chart"]["result"][1]["indicators"]["quote"][1]["volume"]

    # nothingを削除あるいはNaNに変換
    idx = (df."open" .== nothing) # nothingのある行
    if dropnan
        # 1557.TなどNothingが含まれる物がある．DataFrameからnothingを含む行を削除する．
        df = df[.!idx,:]
    else
        # nothingのある行をNaNにする
        df[idx,2:end] = ones(sum(idx), 5) .* NaN
    end

    # 数値をFloat64にする
    df2 = hcat(df."timestamp", convert.(Float64, df[:,2:end]))
    rename!(df2, "x1" => "timestamp")

    return df2
end

# 株価をYahoo!Financeから取得する．
# stock: 株価ティッカー
# period: 株価取得期間．"1d"なら1日，"1y"なら1年
# interval: 株価間隔．"1m"なら1分
function getstockprice(stock, period="1d", interval="1m")
    # URLの作成
    url = yahoofinurl(stock, period, interval)

    # データ取得・JSON->DataFrameへ変換
    str = read(download(url), String) # urlからjson文字列の取得
    json = JSON.parse(str) # json文字列をパースしてDict型に変換
    df = stockjson2df(json) # JSONからDataFrameに変換
end

# 過去の1日単位のS&P500関連の株価を最大期間(30年分)取得する
function getstockpricepast()
    # 取得対象の銘柄の指定．東証の株価は".T"をつける．
    stocks = ["1557.T" "SPY" "SPXL" "SPXS"]
    periods = ["10y" "30y" "30y" "30y"]    # 取得期間 1y, 2y, 1d, 2dという指定方法．1557は10yまでしか無い模様．
    savestockprice.(stocks, periods)
end

# 1日足データ，過去最大期間のデータを取得し，CSVで保存する．
function savestockprice(stock, period)
    interval = "1d" # 取得データの間隔
    df = getstockprice(stock, period, interval)

    # ファイル名用の時刻文字列
    now = Dates.format(df."timestamp"[end], "_yyyymmdd_HHMMSS")

    # CSV書き込み前に時刻のフォーマットを変更しておく
    df."timestamp" = Dates.format.(df."timestamp", "yyyy/mm/dd HH:MM:SS")

    # CSVへの書き込み
    println("write stock price: " * stock * now * ".csv")
    CSV.write(stock * now * ".csv", df)
end

# 現在のS&P500関連の株価を取得する
function getstockpricenow(stock)
    price = getstockprice(stock, "10m", "1m")."close"[end]
end


import MathOptInterface
const MOI = MathOptInterface
const MOIU = MathOptInterface.Utilities

using .ClpCInterface

# Helper functions
_bounds(s::MOI.GreaterThan{Float64}) = (s.lower, Inf)
_bounds(s::MOI.LessThan{Float64}) = (-Inf, s.upper)
_bounds(s::MOI.EqualTo{Float64}) = (s.value, s.value)
_bounds(s::MOI.Interval{Float64}) = (s.lower, s.upper)

# Supported scalar sets
const SCALAR_SETS = Union{
    MOI.GreaterThan{Float64},
    MOI.LessThan{Float64},
    MOI.EqualTo{Float64},
    MOI.Interval{Float64}
}

# Maps Clp's parameters to getter/setter function
const CLP_OPTION_MAP = Dict(
    :PrimalTolerance => (Clp.primal_tolerance, Clp.set_primal_tolerance),
    :DualTolerance => (Clp.dual_tolerance, Clp.set_dual_tolerance),
    :DualObjectiveLimit => (Clp.dual_objective_limit, Clp.set_dual_objective_limit),
    :MaximumIterations => (Clp.maximum_iterations, Clp.set_maximum_iterations),
    :MaximumSeconds => (Clp.maximum_seconds, Clp.set_maximum_seconds),
    :LogLevel => (Clp.log_level, Clp.set_log_level),
    :Scaling => (Clp.scaling_flag, Clp.scaling),
    :Perturbation => (Clp.perturbation, Clp.set_perturbation),
    :Algorithm => (Clp.algorithm, Clp.set_algorithm)
)

const SOLVE_OPTION_MAP = Dict(
   :PresolveType => (Clp.get_presolve_type, Clp.set_presolve_type),
   :SolveType => (Clp.get_solve_type, Clp.set_solve_type),
   :InfeasibleReturn => (Clp.infeasible_return, Clp.set_infeasible_return)
)

mutable struct Optimizer <: MOI.AbstractOptimizer
    # Inner Clp object
    inner::Clp.ClpModel

    # Clp solve options
    solver_options::Clp.ClpSolve
    options_set::Set{Symbol}
    # To handle MOI.OPTIMIZE_NOT_CALLED status
    optimize_called::Bool

    function Optimizer(;kwargs...)
        inner_model = Clp.ClpModel()
        solver_options = Clp.ClpSolve()
        model = new(inner_model, solver_options, Set{Symbol}(), false)

        for (key, value) in kwargs
            MOI.set(model, MOI.RawParameter(String(key)), value)
        end

        return model
    end
end

# ====================
#   empty functions
# ====================
function MOI.is_empty(model::Optimizer)
    # A problem is empty if it has no variable and no linear constraint
    return (Clp.get_num_rows(model.inner) == 0) && (Clp.get_num_cols(model.inner) == 0)
end

function MOI.empty!(model::Optimizer)
    old_model = model.inner
    model.inner = Clp.ClpModel()
    # Copy parameters from old model into new model
    for key in model.options_set
        getter, setter = CLP_OPTION_MAP[key]
        setter(model.inner, getter(old_model))
    end
    model.optimize_called = false
    return
end

MOI.get(::Optimizer, ::MOI.SolverName) = "Clp"

# TODO: improve type-stability of `MOI.RawParameter`-related methods.
MOI.supports(::Optimizer, param::MOI.RawParameter) = true

function MOI.set(model::Optimizer, param::MOI.RawParameter, value)
    key = Symbol(param.name)
    if haskey(CLP_OPTION_MAP, key)
        push!(model.options_set, key)
        CLP_OPTION_MAP[key][2](model.inner, value)
    elseif haskey(SOLVE_OPTION_MAP, key)
        SOLVE_OPTION_MAP[key][2](model.solver_options, value)
    else
        throw(MOI.UnsupportedAttribute(param))
    end
    return
end

function MOI.get(model::Optimizer, param::MOI.RawParameter)
    key = Symbol(param.name)
    if haskey(CLP_OPTION_MAP, key)
        return CLP_OPTION_MAP[key][1](model.inner)
    elseif haskey(SOLVE_OPTION_MAP, key)
        return SOLVE_OPTION_MAP[key][1](model.solver_options)
    else
        throw(MOI.UnsupportedAttribute(param))
    end
end

MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.set(model::Optimizer, ::MOI.Silent, value::Bool)
    push!(model.options_set, :LogLevel)
    if value
        Clp.set_log_level(model.inner, 0)
    else
        Clp.set_log_level(model.inner, 1)
    end
    return nothing
end

function MOI.get(model::Optimizer, ::MOI.Silent)
    return Clp.log_level(model.inner) == 0
end

MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true

function MOI.set(model::Optimizer, ::MOI.TimeLimitSec, value)
    push!(model.options_set, :MaximumSeconds)
    if value === nothing
        # De-activate time limit
        Clp.set_maximum_seconds(model.inner, -1.0)
    else
        Clp.set_maximum_seconds(model.inner, value)
    end
    return nothing
end

# Clp behaves weird here
MOI.get(model::Optimizer, ::MOI.TimeLimitSec) = Clp.maximum_seconds(model.inner)

MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = false

# ========================================
#   Supported constraints and objectives
# ========================================
MOI.supports_constraint(::Optimizer,
    ::Type{MOI.ScalarAffineFunction{Float64}}, ::Type{<:SCALAR_SETS}
) = true

MOI.supports_constraint(::Optimizer,
    ::Type{MOI.SingleVariable}, ::Type{<:SCALAR_SETS}
) = true

MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true

MOI.supports(::Optimizer,
    ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}
) = true

# =======================
#   `copy_to` function
# =======================

function MOI.copy_to(
    dest::Optimizer,
    src::MOI.ModelLike;
    copy_names=false
)
    # Check that all constraints and objectives
    for (F, S) in MOI.get(src, MOI.ListOfConstraints())
        MOI.supports_constraint(dest, F, S) || throw(MOI.UnsupportedConstraint{F, S}(
            "Clp.Optimizer does not support constraints of type $F-in-$S."
        ))
    end
    fobj_type = MOI.get(src, MOI.ObjectiveFunctionType())
    MOI.supports(dest, MOI.ObjectiveFunction{fobj_type}()) || throw(
        MOI.UnsupportedAttribute(MOI.ObjectiveFunction(fobj))
    )

    # Empty dest
    MOI.empty!(dest)

    mapping = MOIU.IndexMap()

    # First create variables (including bounds)

    # Create variables
    # TODO: it may be more efficient to create the problem all at once
    x_src = MOI.get(src, MOI.ListOfVariableIndices())
    for (j, x) in enumerate(x_src)
        # Variable j corresponds to j-th variable of x_src
        mapping.varmap[x] = MOI.VariableIndex(j)

        # Possible bound combinations:
        #=
            * No bound (default case)
            * Interval
            * EqualTo
            * LessThan
            * GreaterThan
            * LessThan && GreaterThan
        =#
        lb, ub = -Inf, Inf  # Default case: free variable

        idx_RG = MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}}(x.value)
        idx_ET = MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}}(x.value)
        idx_LT = MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}}(x.value)
        idx_GT = MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}}(x.value)
        if MOI.is_valid(src, idx_LT)
            # Get the (upper) bound
            s = MOI.get(src, MOI.ConstraintSet(), idx_LT)
            ub = s.upper

            # Track index of upper-bound constraint
            mapping.conmap[idx_LT] = MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}}(j)
        end
        if MOI.is_valid(src, idx_GT)
            # Get the (lower) bound
            s = MOI.get(src, MOI.ConstraintSet(), idx_GT)
            lb = s.lower

            # Track index of upper-bound constraint
            mapping.conmap[idx_GT] = MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}}(j)
        end
        if MOI.is_valid(src, idx_RG)
            # Get upper and lower bound
            s = MOI.get(src, MOI.ConstraintSet(), idx_RG)
            lb, ub = s.lower, s.upper

            # Track index of upper-bound constraint
            mapping.conmap[idx_RG] = MOI.ConstraintIndex{MOI.SingleVariable, MOI.Interval{Float64}}(j)
        end
        if MOI.is_valid(src, idx_ET)
            # Get the bounds
            s = MOI.get(src, MOI.ConstraintSet(), idx_ET)
            lb, ub = s.value, s.value

            # Track index of upper-bound constraint
            mapping.conmap[idx_ET] = MOI.ConstraintIndex{MOI.SingleVariable, MOI.EqualTo{Float64}}(j)
        end

        Clp.add_column(dest.inner, Cint(0), Int32[], Float64[], lb, ub, 0.0)
    end

    # Create linear constraints
    nrows = 0  # number of rows in the problem
    for (F, S) in MOI.get(src, MOI.ListOfConstraints())
        # Check if constraint is a linear constraint
        (F == MOI.ScalarAffineFunction{Float64}) || continue

        for index in MOI.get(src, MOI.ListOfConstraintIndices{F, S}())
            # Retrieve problem data
            # TODO: ensure that `(f, s)` are in canonical form
            f = MOIU.canonical(MOI.get(src, MOI.ConstraintFunction(), index))
            s = MOI.get(src, MOI.ConstraintSet(), index)

            # Extract bounds
            # Constraint writes a'x + a0 - in - set
            # So we offset the bounds by -a0
            lb, ub = _bounds(s) .- f.constant

            # Identify constraint terms
            coeffs = [t.coefficient for t in f.terms]
            cols::Vector{Int} = [
                mapping.varmap[t.variable_index].value
                for t in f.terms
            ]

            # Add constraint
            Clp.add_row(dest.inner, Cint(length(f.terms)), Cint.(cols .- 1), coeffs, lb, ub)

            # Track index
            mapping.conmap[index] = MOI.ConstraintIndex{F, S}(nrows + 1)
            nrows += 1
        end
    end

    # Objective function
    sense = MOI.get(src, MOI.ObjectiveSense())
    if sense == MOI.MIN_SENSE
        Clp.set_obj_sense(dest.inner, 1)   # minimize
    elseif sense == MOI.MAX_SENSE
        Clp.set_obj_sense(dest.inner, -1)  # maximize
    elseif sense == MOI.FEASIBILITY_SENSE
        Clp.set_obj_sense(dest.inner, 0)   # feasibility
    else
        # Should not be reached, but just in case
        error("Unexpected optimization sense $sense")
    end

    fobj = MOI.get(src, MOI.ObjectiveFunction{fobj_type}())
    obj_coeffs = zeros(length(x_src))
    obj_offset = 0.0

    if fobj_type == MOI.ScalarAffineFunction{Float64}
        # Record objective coeffs
        for term in fobj.terms
            x = mapping.varmap[term.variable_index]
            # Here we do += instead of = to accumulate dupplicates
            obj_coeffs[x.value] += term.coefficient
        end
        obj_offset = fobj.constant
    else
        # Should not be reached here, but just in case
        error("Unexpected objective $fobj")
    end

    # Set objective coefficients
    Clp.chg_obj_coefficients(dest.inner, obj_coeffs)
    # Set objective offset
    # Clp seems to negates the objective offset
    Clp.set_objective_offset(dest.inner, -obj_offset)

    return mapping
end

# ===============================
#   Optimize and post-optimize
# ===============================

function MOI.optimize!(model::Optimizer)
    Clp.initial_solve_with_options(model.inner, model.solver_options)
    model.optimize_called = true
    return nothing
end

MOI.get(model::Optimizer, ::MOI.NumberOfVariables) = Clp.get_num_cols(model.inner)

function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue)
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    return Clp.get_obj_value(model.inner)
end

function MOI.get(model::Optimizer, ::MOI.TerminationStatus)

    model.optimize_called || return MOI.OPTIMIZE_NOT_CALLED

    st = Clp.status(model.inner)

    if st == 0
        return MOI.OPTIMAL
    elseif st == 1
        return MOI.INFEASIBLE
    elseif st == 2
        return MOI.DUAL_INFEASIBLE
    elseif st == 3
        # No more granular information that "some limit is reached"
        return MOI.OTHER_LIMIT
    else
        return MOI.OTHER_ERROR
    end
end

function MOI.get(model::Optimizer, ::MOI.RawStatusString)
    model.optimize_called || return "MOI.OPTIMIZE_NOT_CALLED"

    st = Clp.status(model.inner)
    if st == 0
        return "0 - optimal"
    elseif st == 1
        return "1 - primal infeasible"
    elseif st == 2
        return "2 - dual infeasible"
    elseif st == 3
        return "3 - stopped on iterations etc"
    elseif st == 4
        return "4 - stopped due to errors"
    else
        error("Expected integer in [0, 4] but got $st")
    end
end

function MOI.get(model::Optimizer, ::MOI.ResultCount)
    st = MOI.get(model, MOI.TerminationStatus())

    # TODO: check that this is correct
    if st == MOI.OPTIMIZE_NOT_CALLED || st == MOI.OTHER_ERROR
        return 0
    end
    return 1
end

# Solution status
function MOI.get(model::Optimizer, attr::MOI.PrimalStatus)
    # Check result index
    attr.N == 1 || return MOI.NO_SOLUTION

    if Clp.is_proven_dual_infeasible(model.inner)
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif Clp.primal_feasible(model.inner)
        return MOI.FEASIBLE_POINT
    else
        return MOI.UNKNOWN_RESULT_STATUS
    end
end

function MOI.get(model::Optimizer, attr::MOI.DualStatus)
    # Check result index
    attr.N == 1 || return MOI.NO_SOLUTION

    if Clp.is_proven_primal_infeasible(model.inner)
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif Clp.dual_feasible(model.inner)
        return MOI.FEASIBLE_POINT
    else
        return MOI.UNKNOWN_RESULT_STATUS
    end
end

# ===================
#   Primal solution
# ===================
function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, x::MOI.VariableIndex)
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    # We assume the variable index is valid,
    # since Clp should be accessed via a CachingOptimizer
    primal_status = MOI.get(model, MOI.PrimalStatus())
    if primal_status == MOI.INFEASIBILITY_CERTIFICATE
        sol = Clp.unbounded_ray(model.inner)
        return sol[x.value]
    elseif primal_status == MOI.FEASIBLE_POINT
        sol = Clp.get_col_solution(model.inner)
        return sol[x.value]
    else
        error("Primal solution not available")
    end
end

function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, xs::Vector{MOI.VariableIndex})
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    col_indices = [idx.value for idx in xs]

    # We assume all variable indices is valid,
    # since Clp should be accessed via a CachingOptimizer
    primal_status = MOI.get(model, MOI.PrimalStatus())
    if primal_status == MOI.INFEASIBILITY_CERTIFICATE
        sol = Clp.unbounded_ray(model.inner)
        return sol[col_indices]
    elseif primal_status == MOI.FEASIBLE_POINT
        sol = Clp.get_col_solution(model.inner)
        return sol[col_indices]
    else
        error("Primal solution not available")
    end
end

function MOI.get(model::Optimizer, attr::MOI.ConstraintPrimal,
    c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}
) where{S <:SCALAR_SETS}
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    # We assume that the constraint index is valid,
    # since Clp should be accessed via a CachingOptimizer
    # TODO: What happens if model is unbounded / infeasible?

    # Get primal row solution
    sol = Clp.get_row_activity(model.inner)
    return sol[c.value]
end

function MOI.get(model::Optimizer, attr::MOI.ConstraintPrimal,
    c::MOI.ConstraintIndex{MOI.SingleVariable, S}
) where{S <: SCALAR_SETS}
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    # TODO: What happens if model is unbounded / infeasible?
    return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value))
end

# =================
#   Dual solution
# =================
# If sense is maximize, we negate all the duals to follow MOI conventions
# Feasibility problems are treated as a minimization
function MOI.get(model::Optimizer, attr::MOI.ConstraintDual,
    c::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64}, S}
) where{S <: SCALAR_SETS}
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    sense = (Clp.get_obj_sense(model.inner) == -1) ? -1 : 1

    dual_status = MOI.get(model, MOI.DualStatus())
    if dual_status == MOI.FEASIBLE_POINT
        # Return dual solution
        dsol = Clp.get_row_price(model.inner)
        return sense * dsol[c.value]
    elseif dual_status == MOI.INFEASIBILITY_CERTIFICATE
        # Return infeasibility ray
        dsol = Clp.infeasibility_ray(model.inner)
        return sense * dsol[c.value]
    else
        # Throw error if dual solution is not available
        error("Dual solution not available")
    end
end

# Reduced costs
# TODO: what happens if problem is unbounded / infeasible?
function MOI.get(model::Optimizer, attr::MOI.ConstraintDual,
    c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.LessThan{Float64}}
)
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    rc = Clp.get_reduced_cost(model.inner)[c.value]
    sense = (Clp.get_obj_sense(model.inner) == -1) ? -1 : 1

    # Dual should be non-positive
    if sense == 1 && rc <= 0.0
        return rc
    elseif sense == -1 && rc >= 0.0
        return -rc
    else
        return 0.0
    end
end

function MOI.get(model::Optimizer, attr::MOI.ConstraintDual,
    c::MOI.ConstraintIndex{MOI.SingleVariable, MOI.GreaterThan{Float64}}
)
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    rc = Clp.get_reduced_cost(model.inner)[c.value]
    sense = (Clp.get_obj_sense(model.inner) == -1) ? -1 : 1

    # Dual should be non-negative
    if sense == 1 && rc >= 0.0
        return rc
    elseif sense == -1 && rc <= 0.0
        return -rc
    else
        return 0.0
    end
end

function MOI.get(model::Optimizer, attr::MOI.ConstraintDual,
    c::MOI.ConstraintIndex{MOI.SingleVariable, S}
) where{S <: Union{MOI.Interval{Float64}, MOI.EqualTo{Float64}}}
    # Check result index
    MOI.check_result_index_bounds(model, attr)

    sense = (Clp.get_obj_sense(model.inner) == -1) ? -1 : 1
    return sense * Clp.get_reduced_cost(model.inner)[c.value]
end


using st16SseqJuliaTools
using Test
using BioTools.BLAST
using BioSequences
using IntervalSets
@testset "Blast results manipulation" begin
    playxml = "test/playdata/chimrem/blast_results.xml"
    data = split_by_query(readblastXML(read(playxml, String)))
    @test [10..16, 17..19] == add_new_interval([10..12,15..16,17..19],12..15)
    @test [10..12, 13..14, 15..16, 17..19] == add_new_interval([10..12,15..16,17..19],13..14)
    @test covered_length([10..16,17..19]) == 10
    @test 0.0 == unique_length_fraction([10..12, 13..14, 15..16, 17..19],12..13)
    @test 0.5 == unique_length_fraction([10..12, 13..14, 15..16, 17..19],19..20)
    @test [("gi|1384345818|gb|CP029032.1|", 556866, 556195), ("gi|1384345818|gb|CP029032.1|", 1997488, 1997716)] == map(x -> (x.hitid,x.hitfrom,x.hitto),get_coverage_maximising_hits("1_1828;size=1",data))
end


using Random, Colors, Gtk, Cairo

#=========== Channel and flag (IPC) section ===================#

""" channel, communicates player's mouse choice of card or color to game logic """
const channel = Channel{Any}(100)

""" flush the channel from mouse choice to game logic """
flushchannel() = while !isempty(channel) take!(channel); end

""" challenge flag: true if challenge taken by player """
const challenge = [false]

#============ Game play section ==================#

""" The Uno card type. The first field is color, second field is number or command. """
UnoCard = Pair{String, String}
color(c::UnoCard) = first(c)
type(c::UnoCard) = last(c)

""" Each Uno player has a name, a score, may be a bot, and has a hand of UnoCards. """
mutable struct UnoCardGamePlayer
    name::String
    score::Int
    isabot::Bool
    hand::Vector{UnoCard}
end

"""
    mutable struct UnoCardGameState
Encapsulates a board state of the gane, including players, cards, color being played,
order of play, current player, and whether the card showing has had its command used
"""
mutable struct UnoCardGameState
    drawpile::Vector{UnoCard}
    discardpile::Vector{UnoCard}
    players::Vector{UnoCardGamePlayer}
    pnow::Int
    colornow::String
    lastcolor::String
    clockwise::Bool
    commandsvalid::Bool
end

""" classifications of colors and types for card faces """
const colors = ["Red", "Yellow", "Green", "Blue"]  # suit colors
const types = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "Skip", "Draw Two", "Reverse"]
const numtypes = types[begin:end-3]
const wildtypes = ["Wild", "Draw Four"]
const cmdtypes = vcat(types[end-2:end], wildtypes)
const alltypes = vcat(types, wildtypes)
const unopreferred = ["Skip", "Draw Two", "Reverse", "Draw Four"]
const ttypes = sort!(vcat(types, types))
const typeordering = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "Wild", "Skip",
    "Reverse", "Draw Two", "Draw Four"]
popfirst!(ttypes) # only 1 "0" card per color

""" The Uno card game deck, unshuffled. """
const originaldeck = [vec([UnoCard(c, v) for v in ttypes, c in colors]);
      fill(UnoCard("Wild", "Wild"), 4); fill(UnoCard("Wild", "Draw Four"), 4)]

""" Set the next player to play to game.pnow (clockwise or counterclockwise) """
function nextplayer!(game, idx)
   game.pnow = mod1(game.clockwise ? idx + 1 : idx - 1, length(game.players))
end
nextplayer!(game) = nextplayer!(game, game.pnow)

"""
    nextsaiduno(game)
Returns true if the next player to play has said Uno, which means they have only one card left.
If so, it is best for the current player if they play to make them draw or lose a turn.
"""
function nextsaiduno(game)
    idx = game.pnow
    nextp = mod1(game.pnow + (game.clockwise ? 1 : -1), length(game.players))
    return length(game.players[nextp].hand) == 1
end

cardscore(c) = (t = type(c); t == "Draw Four" ? 50 : t in cmdtypes ? 20 : parse(Int, t))
handscore(hand) = isempty(hand) ? 0 : sum(cardscore, hand)

"""
    UnoCardGameState(playernames = ["Player", "Bot1", "Bot2", "Bot3"])
Construct and initialize Uno game. Includes dealing hands and picking who is to start.
"""
function UnoCardGameState(playernames = ["Player", "Bot1", "Bot2", "Bot3"])
    deck = shuffle(originaldeck)
    discardpile, drawpile = UnoCard[], UnoCard[]
    while true  # cannot start with a Draw Four on discard pile top
        discardpile, drawpile = [deck[29]], deck[30:end]
        last(last(discardpile)) != "Draw Four" && break
        deck[29:end] .= shuffle(deck[29:end])
    end
    hands = [deck[i:i+6] for i in 1:7:27]
    game = UnoCardGameState(drawpile, discardpile, [UnoCardGamePlayer(playernames[i], 0,
        startswith(playernames[i], "Bot"), hands[i])
        for i in 1:length(playernames)], 1, "Wild", "Wild", true, true)
    dealer = rand(1:length(playernames))
    logline("Player $(playernames[dealer]) is dealer.")
    # handle an initial Reverse card
    if type(last(discardpile)) == "Reverse"
            game.clockwise = false
            logline("First card Reverse, so starting counterclockwise with dealer.")
            game.commandsvalid = false
            game.pnow = dealer
    else
        nextplayer!(game, dealer)
    end
    logline("Player $(playernames[game.pnow]) starts play.")
    if color(last(discardpile)) == "Wild"
        choosecolor!(game)
        game.commandsvalid = false
    else
        game.colornow = color(last(discardpile))
    end
    return game
end

function nextgame(game::UnoCardGameState)
    newgame = UnoCardGameState()
    for i in eachindex(newgame.players)
        newgame.players[i].score = game.players[i].score
    end
    return newgame
end

cardvalue(c::UnoCard) = something(findfirst(x -> x == type(c), typeordering), 0)
countcolor(cards, clr) = count(c -> color(c) == clr, cards)
colorcounts(cards) = sort!([(countcolor(cards, clr), clr) for clr in colors])

""" Preferred color is the one that is most counted in the hand. """
preferredcolor(hand) = last(last(colorcounts(hand)))

"""
    playableindices(game)
Return a vector of indices of cards in hand that are legal to discard
"""
function playableindices(game)
    hand = game.players[game.pnow].hand
    mcolor, mtype = game.colornow, type(game.discardpile[end])
    return [i for (i, c) in enumerate(hand) if
        color(c) == mcolor || type(c) == mtype || color(c) == "Wild"]
end

""" Current player to draw n cards from the draw pile. """
function drawcardsfromdeck!(game, n=1)
    n in [2, 4] && (game.commandsvalid = false)
    if n == 4  # draw four
        # bot will challenge half the time, player must challenge in 5 seconds.
        if game.players[game.pnow].isabot && rand() < 0.5  ||
            (!game.players[game.pnow].isabot && challenge[begin])
            challenge[begin] = false
            logline("$(game.players[game.pnow].name) challenged Draw Four!")
            challenger, savecolor = game.pnow, game.colornow
            nextplayer!(game); nextplayer!(game); nextplayer!(game); # prior player
            game.colornow = game.lastcolor
            hand = game.players[game.pnow].hand
            if any(c -> color(c) == game.colornow, hand)
                logline("Challenge sustained! Challenged Draw Four player draws 4.")
                drawcardsfromdeck!(game, 2); drawcardsfromdeck!(game, 2)
                game.pnow, game.colornow = challenger, savecolor
                return
            else
                logline("Challenge fails. Challenging player now draws 6.")
                n = 6
            end
            game.pnow, game.colornow = challenger, savecolor
        end
        challenge[begin] = false
    end
    logline("Player $(game.players[game.pnow].name) draws $n card$(n == 1 ? "" : "s").")
    for _ in 1:n
        push!(game.players[game.pnow].hand, pop!(game.drawpile))
        if isempty(game.drawpile)  # out of draw pile
            game.drawpile = shuffle(game.discardpile[begin:end-1])
            game.discardpile = [game.discardpile[end]]
        end
    end
end

"""
    discard!(game, idx = -1)
Current player to discard card at index idx in hand (last card in hand as default).
Handle wild card discard by having current player choose the new game.colornow.
"""
function discard!(game, idx = -1)
    hand = game.players[game.pnow].hand
    if idx != -1
        hand[idx], hand[end] = hand[end], hand[idx]
    end
    push!(game.discardpile, pop!(hand))
    lastdiscard = last(game.discardpile)
    logline("Player $(game.players[game.pnow].name) discarded $lastdiscard")
    game.lastcolor = game.colornow
    if color(lastdiscard) == "Wild"  # wild card discard, so choose a color to be colornow
        choosecolor!(game)
        logline("New color chosen: $(game.colornow)")
    else
        game.colornow = color(lastdiscard)
    end
    game.commandsvalid = true
end

"""
    turn!(game)
Execute a single turn of the game.  Command cards are followed only the first turn after played.
"""
function turn!(game)
    name, hand = game.players[game.pnow].name, game.players[game.pnow].hand
    lastdiscard, indices = game.discardpile[end], playableindices(game)
    mcolor, mtype = game.colornow, type(lastdiscard)
    isempty(hand) && error("Empty hand held by $name")
    if mtype in cmdtypes && game.commandsvalid && mtype != "Wild"
        game.commandsvalid = false
        if mtype == "Draw Four"
            drawcardsfromdeck!(game, 4)
        elseif mtype == "Draw Two"
            drawcardsfromdeck!(game, 2)
        elseif mtype == "Skip"    # skip, no uno check
            logline("$name skips a turn.")
        elseif mtype == "Reverse"
            game.clockwise = !game.clockwise
            logline("Reverse: now going $(game.clockwise ? "clockwise." : "counter-clockwise.")")
            nextplayer!(game)
        end
        nextplayer!(game)
        return
    else  # num card, or command card is already used
        if isempty(indices)
            drawcardsfromdeck!(game, 1)  # draw, then discard if drawn card is a match
            indices  = playableindices(game)
            if !isempty(indices)
                logline("Drawn card was discardable.")
                discard!(game, first(indices))
            end
        elseif !startswith(name, "Bot")  # not bot, so player moves
            logline("Click on a card to play.")
            flushchannel()
            while true
                item = take!(channel)
                if item isa Int && item in indices
                    discard!(game, item)
                    break
                end
                logline("That card is not playable.")
            end
        elseif nextsaiduno(game)  # bot might need to stop next player win
            sort!(hand, lt = (x, y) -> cardvalue(x) < cardvalue(y))
            indices = playableindices(game)
            discard!(game, last(indices))
        else # bot play any playable in hand
            discard!(game, rand(indices))
        end
    end
    length(hand) == 1 && logline("$name says UNO!")
    nextplayer!(game)
end

"""
    choosecolor!(game)
Choose a new game.colornow, automatically if a bot, via player choice if not a bot.
"""
function choosecolor!(game)
    logline("Player $(game.players[game.pnow].name) choosing color")
    hand = game.players[game.pnow].hand
    isempty(hand) && return rand(colors)
    if game.players[game.pnow].isabot
        game.colornow = preferredcolor(hand)
    else
        flushchannel()
        while true
            item = take!(channel)
            if item isa String && item in colors
                game.colornow = item
                break
            end
        end
    end
    logline("Current color is now $(game.colornow).")
end

#================  required documentation section ======================#

const unodocshtml = """
Official Rules For Uno Card Game
The aim of the game is to be the first player to score 500 points, achieved (usually over several rounds of play) by being the first to play all of one's own cards and scoring points for the cards still held by the other players.
The deck consists of 108 cards: four each of "Wild" and "Wild Draw Four", and 25 each of four colors (red, yellow, green, blue). Each color consists of one zero, two each of 1 through 9, and two each of "Skip", "Draw Two", and "Reverse". These last three types are known as "action cards".
To start a hand, seven cards are dealt to each player, and the top card of the remaining deck is flipped over and set aside to begin the discard pile. The player to the dealer's left plays first unless the first card on the discard pile is an action or Wild card (see below). On a player's turn, they must do one of the following:
*    play one card matching the discard in color, number, or symbol
*    play a Wild card, or a playable Wild Draw Four card (see restriction below)
*    draw the top card from the deck, then play it if possible
Cards are played by laying them face-up on top of the discard pile. Play proceeds clockwise around the table.
Action or Wild cards have the following effects:
===============================================================================================================================================================
Card            Effect when played from hand                                                                                Effect as first discard
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Skip            Next player in sequence misses a turn                                                                                Player to dealer's left misses a turn
Reverse         Order of play switches directions (clockwise to counterclockwise, or vice versa)            Dealer plays first; play proceeds counterclockwise
Draw Two        Next player in sequence draws two cards and misses a turn                                             Player to dealer's left draws two cards and misses a turn
Wild            Player declares the next color to be matched ; current color may be chosen                       Player to dealer's left declares the first color to be matched and plays a card in it
Wild Draw Four  Player declares the next color to be matched; next player in sequence draws four   Return card to the deck, shuffle, flip top card to start discard pile
A player who draws from the deck must either play or keep that card and may play no other card from their hand on that turn.
A player may play a Wild card at any time, even if that player has other playable cards.
A player may play a Wild Draw Four card only if that player has no cards matching the current color. The player may have cards of a different color matching the current number or symbol or a Wild card and still play the Wild Draw Four card.[5] A player who plays a Wild Draw Four may be challenged by the next player in sequence (see Penalties) to prove that their hand meets this condition.
If the entire deck is used during play, the top discard is set aside and the rest of the pile is shuffled to create a new deck. Play then proceeds normally.
It is illegal to trade cards of any sort with another player.
A player who plays their next-to-last-card must call "uno" as a warning to the other players.[6]
The first player to get rid of their last card ("going out") wins the hand and scores points for the cards held by the other players. Number cards count their face value, all action cards count 20, and Wild and Wild Draw Four cards count 50. If a Draw Two or Wild Draw Four card is played to go out, the next player in the sequence must draw the appropriate number of cards before the score is tallied.
The first player to score 500 points wins the game.
Penalties
=========
If a player does not call "uno" after laying down their next-to-last card and is caught before the next player in sequence takes a turn (i.e., plays a card from their hand, draws from the deck, or touches the discard pile), they must draw two cards as a penalty. If the player is not caught in time (subject to interpretation) or remembers to call "uno" before being caught, they suffer no penalty.
If a player plays a Wild Draw Four card, the following player can challenge its use. The player who used the Wild Draw Four must privately show their hand to the challenging player, in order to demonstrate that they had no matching colored cards. If the challenge is correct, then the challenged player draws four cards instead. If the challenge is wrong, then the challenger must draw six cards; the four cards they were already required to draw plus two more cards.
"""

#============ GUI interface section =======================#

const logwindow = GtkScrolledWindow()
const logtxt = GtkTextBuffer()
logtxt.text[String] = "Started a game of Uno."
const tview = GtkTextView(logtxt)
push!(logwindow, tview)

""" Lines are logged by extending logtxt at its start. """
function logline(txt)
    set_gtk_property!(logtxt, :text, txt * "\n" * get_gtk_property(logtxt, :text, String))
end

const cairocolor = Dict("Red" => colorant"red", "Yellow" => colorant"gold",
    "Green" => colorant"green", "Blue" => colorant"blue", "Wild" => colorant"black")

""" CSS style button for bold colored text """
function colorbutton(txt::String, clr::String)
   button = GtkButton(txt)
   sc = Gtk.GAccessor.style_context(button)
   pr = Gtk.CssProviderLeaf(data="button {color:$(clr); font-weight: bolder}")
   push!(sc, Gtk.StyleProvider(pr), 600)
   return button
end

""" Draw a UnoCard as a rectangle with rounded corners. """
function cairocard(ctx, card, x0, y0, width, height, bcolor=colorant"white")
    fcolor = cairocolor[color(card)]
    set_source(ctx, fcolor)
    radius = (width + height) / 4
    set_line_width(ctx, radius / 5)
    x1 = x0 + width
    y1 = y0 + height
    if width / 2 < radius
        if height / 2 < radius
            move_to(ctx, x0, (y0 + y1) / 2)
            curve_to(ctx, x0 ,y0, x0, y0, (x0 + x1) / 2, y0)
            curve_to(ctx, x1, y0, x1, y0, x1, (y0 + y1) / 2)
            curve_to(ctx, x1, y1, x1, y1, (x1 + x0) / 2, y1)
            curve_to(ctx, x0, y1, x0, y1, x0, (y0 + y1) / 2)
        else
            move_to(ctx, x0, y0 + radius)
            curve_to(ctx, x0 ,y0, x0, y0, (x0 + x1) / 2, y0)
            curve_to(ctx, x1, y0, x1, y0, x1, y0 + radius)
            line_to(ctx, x1 , y1 - radius)
            curve_to(ctx, x1, y1, x1, y1, (x1 + x0) / 2, y1)
            curve_to(ctx, x0, y1, x0, y1, x0, y1 - radius)
        end
    else
        if rect_height / 2 < radius
            move_to(ctx, x0, (y0 + y1)  /2)
            curve_to(ctx, x0 , y0, x0 , y0, x0 + radius, y0)
            line_to(ctx, x1 - radius, y0)
            curve_to(ctx, x1, y0, x1, y0, x1, (y0 + y1) / 2)
            curve_to(ctx, x1, y1, x1, y1, x1 - radius, y1)
            line_to(ctx, x0 + radius, y1)
            curve_to(ctx, x0, y1, x0, y1, x0, (y0 + y1) / 2)
        else
            move_to(ctx, x0, y0 + radius)
            curve_to(ctx, x0 , y0, x0 , y0, x0 + radius, y0)
            line_to(ctx, x1 - radius, y0)
            curve_to(ctx, x1, y0, x1, y0, x1, y0 + radius)
            line_to(ctx, x1 , y1 - radius)
            curve_to(ctx, x1, y1, x1, y1, x1 - radius, y1)
            line_to(ctx, x0 + radius, y1)
            curve_to(ctx, x0, y1, x0, y1, x0, y1- radius)
        end
    end
    close_path(ctx)
    set_source(ctx, bcolor)
    fill_preserve(ctx)
    set_source(ctx, fcolor)
    stroke(ctx)
    move_to(ctx, x0 + width / 3, y0 + height / 3)
    txt = uppercase(type(card))
    if first(txt) in ['R', 'S', 'W']
        txt = string(first(txt))
    elseif first(txt) == 'D'
        txt = "D" * (txt[end] == 'O' ? "2" : "4")
    end
    show_text(ctx, txt)
    stroke(ctx)
end

""" Face down Uno cards are displayed as blank black rectangles with rounded corners. """
function cairodrawfacedowncard(ctx, x0, y0, width, height, bcolor=colorant"darkgray")
    cairocard(ctx, UnoCard("Wild", " "), x0, y0, width, height, bcolor)
end

"""
    UnoCardGameApp(w = 800, hcan = 600, hlog = 100)
Uno card game Gtk app. Draws game on a canvas, logs play on box below canvas.
"""
function UnoCardGameApp(w = 1120, wcan = 810, h = 700)
    win = GtkWindow("Uno Card Game", w, h) |> (GtkFrame() |> (hbox = GtkBox(:h)))
    vbox = GtkBox(:v)
    can = GtkCanvas(wcan, h)
    push!(hbox, can)
    push!(vbox, logwindow)  # from log section
    set_gtk_property!(logwindow, :expand, true)
    b, g = colorbutton("Blue", "blue"), colorbutton("Green", "green")
    r, y = colorbutton("Red", "red"), colorbutton("Yellow", "gold")
    chal = GtkButton("Challenge")
    signal_connect(w -> push!(channel, "Blue"), b, "clicked")
    signal_connect(w -> push!(channel, "Green"), g, "clicked")
    signal_connect(w -> push!(channel, "Red"), r, "clicked")
    signal_connect(w -> push!(channel, "Yellow"), y, "clicked")
    signal_connect(w -> (challenge[begin] = true), chal, "clicked")
    push!(vbox, b, g, r, y, chal)
    push!(hbox, vbox)
    fontpointsize = w / 50
    cardpositions = Dict{Int, Vector{Int}}()

    # announce the rules and penalties per task description
    info_dialog(unodocshtml, win)

    # create a game instance to start play
    game = UnoCardGameState()

    """ Draw the game board on the canvas including player's hand """
    @guarded Gtk.draw(can) do widget
        ctx = Gtk.getgc(can)
        height, width = Gtk.height(ctx), Gtk.width(ctx)
        select_font_face(ctx, "Courier", Cairo.FONT_SLANT_NORMAL, Cairo.FONT_WEIGHT_BOLD)
        set_font_size(ctx, fontpointsize)
        boardcolor = colorant"lightyellow"
        set_source(ctx, boardcolor)
        rectangle(ctx, 0, 0, width, height)
        fill(ctx)
        color = colorant"navy"
        set_source(ctx, color)
        move_to(ctx, 360, 420)
        show_text(ctx, game.players[1].name)
        stroke(ctx)
        move_to(ctx, 60, 300)
        show_text(ctx, game.players[2].name)
        stroke(ctx)
        move_to(ctx, 370, 60)
        show_text(ctx, game.players[3].name)
        stroke(ctx)
        move_to(ctx, 680, 300)
        show_text(ctx, game.players[4].name)
        stroke(ctx)
        cairocard(ctx, last(game.discardpile), 350, 240, 40, 80)
        cairodrawfacedowncard(ctx, 410, 240, 40, 80)
        hand = first(game.players).hand
        isempty(hand) && return
        nrow = (length(hand) + 15) ÷ 15
        for row in 1:nrow
            cards = hand[(row - 1) * 15 + 1 : min(length(hand), row * 15 - 1)]
            startx, starty = 40 + (15 - length(cards)) * 20, 500 + 85 * (row - 1)
            for (i, card) in enumerate(cards)
                idx, x0 = (row - 1) * 15 + i, startx + 50 * (i - 1)
                cardpositions[idx] = [x0, starty, x0 + 40, starty + 80]
                cairocard(ctx, card, x0, starty, 40, 80)
            end
        end
    end

    """ Gtk mouse callback: translates valid mouse clicks to a channel item """
    signal_connect(can, "button-press-event") do b, evt
        for p in cardpositions
            x0, y0, x1, y1 = last(p)
            if x0 < evt.x < x1 && y0 < evt.y < y1
                push!(channel, first(p))
                return
            end
        end
    end

    # do the turns of play in the game, keeping score totals from the rounds
    for n in 1:1000
        draw(can)
        Gtk.showall(win)
        while !any(i -> isempty(game.players[i].hand), 1:4)
            turn!(game)
            if startswith(game.players[game.pnow].name, "Play") &&
                type(game.discardpile[end]) == "Draw Four" && game.commandsvalid
                draw(can)
                Gtk.showall(win)
                info_dialog("Click Challenge within 5 seconds to challenge Draw Four")
                sleep(3)
            end
            sleep(2)
            draw(can)
            Gtk.showall(win)
        end
        winner = findfirst(i -> isempty(game.players[i].hand), 1:length(game.players))
        if type(game.discardpile[end]) == "Draw Two"
            nextplayer!(game)  # next player might have to draw before scoring done
            drawcardsfromdeck!(game, 2)
        elseif type(game.discardpile[end]) == "Draw Four"
            nextplayer!(game)
            drawcardsfromdeck!(game, 2)
            drawcardsfromdeck!(game, 2)   # D2 twice because not to be contested as a D4
        end
        roundpoints = sum(x -> handscore(x.hand), game.players)
        game.players[winner].score += roundpoints

        logline("Player $(game.players[winner].name) wins round $(n)!")
        info_dialog("The winner of round $n is $(game.players[winner].name).\n" *
            "Winner gains $roundpoints points.", win)
        logline("Scores: $([x.score for x in game.players])")
        if any(x -> x.score >= 500, game.players)
            s = "Game over. Scores:\n\n"
            for p in game.players
                s *= "   $(p.name):  $(p.score)  $(p.score >= 500 ? "WINNER!" : "")\n"
            end
            info_dialog(s)
            break
        end
        logline("-------------------------\nNew round!\n-----------------------")
        game = nextgame(game)
        draw(can)
        Gtk.showall(win)
    end
end

UnoCardGameApp()


using Test

using FileIO: Stream, File, @format_str, load, save
using OpticalFlowUtils

@testset "io" begin
    data = UInt32[0x48454950, 0x00000002, 0x00000001,
        0x00000000, 0x3f800000, 0x7f7fffff, 0x00000000]

    file, io = mktemp()
    write(io, data)
    close(io)

    x = load(file)
    @test size(x) == (2, 1, 2)
    @test all(x[:, 1, 1] .== (1., 0))
    @test all(ismissing, x[:, 1, 2])

    save(File{format"FLO"}(file), x)
    saveddata = reinterpret(eltype(data), read(file))
    # last entry will be written as being missing
    @test saveddata[1:end-1] == data[1:end-1]
    @test saveddata[end] == 0x7f7fffff
end