""
function calc_rmsd(data, mod, chain)
    # ground truth
    gt_g = get_ground_truth_g(data, mod.gm.u⁺)
    gt_θ = get_ground_truth_θ(data)

    # posterior samples
    g_samples = get_posterior_g_samples(mod, chain)
    θ_samples = get_posterior_θ_samples(mod, chain)

    # posterior mean
    g_postmean = mean(g_samples)
    θ_postmean = mean(θ_samples)

    # RMSD
    g_rmsd = rmsd(vec(g_postmean), vec(gt_g))
    θ_rmsd = rmsd(vec(θ_postmean), vec(gt_θ))

    return g_rmsd, θ_rmsd
end

""
function get_ground_truth_g(data, times)
    sol = solve(ODEProblem(data.f, data.x0, data.tspan, data.p), saveat=data.interval)
    func_at_the_time_points = hcat(sol(times).u...)
    # logfunc_at_inducing_points = data.X[:, 1:data.W][:, [any(tick .≈ mod.gm.u) for tick in data.ticks]]
    return func_at_the_time_points' |> Matrix
end

""
function get_ground_truth_θ(data)
    return vcat(collect(data.θ)...)
end

""
function get_posterior_θ_samples(mod::Union{ODEModel,ODECoxProcess}, chain)
    θ_records = [typeof(mod.gm.θ.params)(θvec) for θvec in chain.info.history.state_rec.θ]
    θ₊_records = [get_θ₊(θ, mod.gm.θ.priors) for θ in θ_records]
    posterior_parameter_samples = [vcat([getproperty(θ₊, name) for name in fieldnames(typeof(θ₊))]...) for θ₊ in θ₊_records]
    return posterior_parameter_samples
end

""
function get_posterior_g_samples(mod::Union{ODEModel,ODECoxProcess}, chain)
    @unpack U, U⁺ = mod.gm
    if mod isa ODEModel
        f_samples = [reshape(x, U⁺, mod.data.C)[1:U, :] for x in chain.info.history.state_rec.x]
    elseif mod isa ODECoxProcess
        f_samples = [reshape(exp.(x), U⁺, mod.data.C)[1:U, :] for x in chain.info.history.state_rec.x]
    end
    return f_samples
end

""
function get_posterior_X̂_samples(mod::ODECoxProcess, chain)
    @unpack T⁺ = mod.gm
    X̂_samples = [reshape(y, T⁺, mod.data.C) for y in chain.info.history.state_rec.y]
    return X̂_samples
end

""
function sim_ode_with_posterior_θ_samples(data, mod, chain)
    # posterior θ samples
    θ_samples = LGCPGradientMatching.get_posterior_θ_samples(mod, chain)
    Zs = []

    # simulated ode
    for θ in θ_samples
        prob = ODEProblem(data.f, data.x0, data.tspan, θ)
        sol = solve(prob, saveat=data.interval)
        push!(Zs, hcat(sol.u...))
    end
    Zs = cat(Zs..., dims=3)

    return Zs
end

""
function draw_gt_and_sim_ode(data, Zs; qlow=0.125, qhigh=0.875)
    colors = [
        palette(:seaborn_bright)[1],
        palette(:seaborn_bright)[4],
        palette(:seaborn_bright)[3],
        palette(:seaborn_bright)[2],
        palette(:seaborn_bright)[5]
    ]

    _qlow = mapslices(z -> quantile(z, qlow), Zs, dims=3)
    _median = mapslices(z -> quantile(z, 0.5), Zs, dims=3)
    _qhigh = mapslices(z -> quantile(z, qhigh), Zs, dims=3)

    p = Plots.plot()
    C = length(data.x0)
    len = length(data.ticks)
    for c in 1:C
        _median_c = _median[c, 1:len]
        _qlow_c = _qlow[c, 1:len]
        _qhigh_c = _qhigh[c, 1:len]
        Plots.plot!(data.ticks, _median_c, ribbon=(_median_c - _qlow_c, _qhigh_c - _median_c), c=colors[c], fillalpha=0.3, legend=:none)
    end
    for c in 1:C
        Plots.plot!(data.ticks, data.Λ[c, 1:len], c=colors[c], s=:dash, lw=1)
    end
    return p
end

""
function draw_gt_and_est_ode(data, mod, Zs; qlow=0.125, qhigh=0.875, extrapolation::Bool=true)
    colors = [
        palette(:seaborn_bright)[1],
        palette(:seaborn_bright)[4],
        palette(:seaborn_bright)[3],
        palette(:seaborn_bright)[2],
        palette(:seaborn_bright)[5]
    ]

    _qlow = mapslices(z -> quantile(z, qlow), Zs, dims=3)
    _median = mapslices(z -> quantile(z, 0.5), Zs, dims=3)
    _qhigh = mapslices(z -> quantile(z, qhigh), Zs, dims=3)

    p = Plots.plot()
    C = length(data.x0)
    for c in 1:C
        _median_c = _median[:, c]
        _qlow_c = _qlow[:, c]
        _qhigh_c = _qhigh[:, c]
        Plots.plot!(mod.gm.t⁺, _median_c, ribbon=(_median_c - _qlow_c, _qhigh_c - _median_c), c=colors[c], fillalpha=0.3, legend=:none)
    end
    for c in 1:C
        Plots.plot!(0:data.interval:1.5, data.Λ[c, :], c=colors[c], s=:dash, lw=1)
    end
    if extrapolation
        Plots.vline!([1], ls=:dashdot, c=:black, label=:none, lw=1)
    else
        xlims!(0,1)
    end
    return p
end

""
function eventplot(dat; visible_components::Union{Nothing,Vector{Int}}=nothing, kwargs...)
    colors = [
        palette(:seaborn_bright)[1],
        palette(:seaborn_bright)[4],
        palette(:seaborn_bright)[3],
        palette(:seaborn_bright)[2],
        palette(:seaborn_bright)[5]]
    p = Plots.plot(ticks=:none, axis=false, legend=:none)
    C = length(dat.times)
    for i in 1:C
        if isnothing(visible_components)
            scatter!(dat.times[i], fill(-i, length(dat.times[i])), m=:vline, ms=1.5, c=colors[i]; kwargs...)
        else
            if i in visible_components
                scatter!(dat.times[i], fill(-i, length(dat.times[i])), m=:vline, ms=1.5, c=colors[i]; kwargs...)
            else
                scatter!(dat.times[i], fill(-i, length(dat.times[i])), m=:vline, ms=1.5, c=:white; kwargs...)
            end
        end
    end
    ylims!(-C - 1, 0)
    return p
end

""
function sim_extrapolation_eventcounts(data, mod; n_simulations::Int)
    # prepare ground-truth intensities for extrapolation time 
    obs_times = mod.gm.t⁺
    gt_modulations = LGCPGradientMatching.get_ground_truth_g(data, obs_times)
    gt_intensities = (mod.gm.λ0 / mod.gm.T)' .* gt_modulations
    gt_intensities_for_extrapolation_time = gt_intensities[mod.gm.T+1:mod.gm.T⁺, :]

    # simulate event counts for extrapolation time from ground-truth intensities
    C = mod.data.C
    n_ext_obs_points = mod.gm.T⁺ - mod.gm.T
    M′_samples = []

    for i in 1:n_simulations
        M′ = zeros(n_ext_obs_points, C)
        for c in 1:C
            for t in 1:n_ext_obs_points
                λ = gt_intensities_for_extrapolation_time[t, c]
                M′[t, c] = rand(Poisson(λ))
            end
        end
        push!(M′_samples, M′)
    end
    return M′_samples
end


# function simulate_event(dat)
#     C = length(dat.λ0s)
#     n_extrapolative_windows = dat.T⁺ - dat.T
#     M = Int.(zeros(C, n_extrapolative_windows))
#     for c in 1:C, t in 1:n_extrapolative_windows
#         M[c,t] += rand(Poisson(dat.λ0s[c]/dat.T*dat.Λ[c,t+dat.T]))
#     end
#     # time points
#     times = Vector{Float64}[]
#     ticks = collect(
#         range(1, stop=1+dat.extrapolation_time_length, length=n_extrapolative_windows)
#         )[2:end];
#     for c in 1:C
#         times_c = Float64[]
#         for (i, t) in enumerate(ticks)
#             m = M[c,i]
#             if m > 0
#                 for _ in 1:m
#                     push!(times_c, t)
#                 end
#             end
#         end
#         push!(times, times_c)
#     end
#     return times
# end

# function convert_event_to_count(extrapolative_w_points, dat, events)
#     w = extrapolative_w_points[2] - extrapolative_w_points[1]
#     C = length(dat.x0)
#     n_observations = Matrix{Int}(undef, length(extrapolative_w_points), C)
#     for c in 1:C
#         n_observations[:, c] = Int[
#             sum(i - w / 2 .<= events[c] .< i + w / 2)
#             for i in extrapolative_w_points]
#         n_observations[end, c] += sum(events[c] .== 1+dat.extrapolation_time_length)
#     end
#     return n_observations, extrapolative_w_points
# end

# function eval_nll(λ0s, T, M, Y)
#     Λ = (λ0s ./ T)' .* exp.(Y)
#     T, C = size(Y)
#     nll = 0
#     for t in 1:T, c in 1:C
#         nll -= logpdf(Poisson(Λ[t, c]), M[t, c])
#     end
#     return nll
# end

# function train_plgp(
#     x, y, k, k_priors; n_mcmc, n_burnin, thin, infer_kern=true, infer_mean=false)

#     l = PoisLik()
#     plgp = GaussianProcesses.GP(x, vec(y), MeanZero(), k, l)
#     GaussianProcesses.set_priors!(plgp.kernel, k_priors)
#     samples = GaussianProcesses.mcmc(
#         plgp; nIter=n_mcmc+n_burnin, burn=n_burnin, thin=thin,
#         domean=infer_mean, kern=infer_kern)
#     return plgp, samples
# end

# function get_posterior_lgcpgm_samples(
#     mod::ODECoxProcess, mcp::Dict, xtest::Vector{T}, c::Int
#     ) where T<:Real
#     #Sample predicted values
#     n_i_points = mod.gm.U⁺
#     C = mod.data.C
#     x_samples = [reshape(x, n_i_points, C)[:, c] for x in mcp[:state_rec].x];
#     σ_samples = [σ[c] for σ in mcp[:state_rec].σ]
#     gp = mod.gm.gps[1];
#     kern = gp.ϕ.kernel
#     μ = gp.μ
#     i_points = gp.times_x;
#     f = AbstractGPs.GP(μ, kern)
#     ft = f(i_points, gp.δ^2)
#     fsamples = hcat([rand(posterior(ft, x)(xtest, σ^2))
#             for (x, σ) in zip(x_samples, σ_samples)]...)
#     fmean = mean([exp.(mean(posterior(ft, x)(xtest, σ^2)))
#             for (x, σ) in zip(x_samples, σ_samples)])
#     return fsamples, fmean
# end

# function get_posterior_plgp_samples(
#     plgp::GPA, samples::AbstractMatrix, xtest::Vector{T}
#     ) where T<:Real
#     #Sample predicted values
#     fmean = [];
#     fsamples = Array{Float64}(undef, length(xtest), size(samples,2), );
#     for i in 1:size(samples,2)
#         GaussianProcesses.set_params!(plgp, samples[:, i])
#         GaussianProcesses.update_target!(plgp)
#         push!(fmean, GaussianProcesses.predict_y(plgp, xtest)[1])
#         fsamples[:,i] = rand(plgp, xtest)
#     end
#     return fsamples, mean(fmean)
# end

# get_quantile(fsamples, p) = [quantile(fsamples[i,:], p) for i in 1:size(fsamples, 1)]

# function plot_posterior_function(
#     fsamples::Matrix{T}, xtest::Vector{T}, fmean::Vector{T}; kwargs...
#     ) where T<:Real
#     #Predictive plots
#     q10 = [quantile(fsamples[i,:], 0.1) for i in 1:length(xtest)]
#     q50 = [quantile(fsamples[i,:], 0.5) for i in 1:length(xtest)]
#     q90 = [quantile(fsamples[i,:], 0.9) for i in 1:length(xtest)]
#     p = Plots.plot(
#         xtest, exp.(q50), ribbon=(exp.(q50) .- exp.(q10), exp.(q90) .- exp.(q50)),
#         leg=true, fmt=:png, label="quantiles";
#         kwargs...
#     )
#     plot!(xtest, fmean, label="posterior mean")
#     return p
# end

# function get_competitive_lotka_volterra_θ_samples(mod::ODECoxProcess, mcp::Dict)
#     state_rec = mcp[:state_rec]
#     state_chn = staterec2chain(state_rec)
#     rℝ_est = exp.(Array(state_chn, :θ)[1:1000, 1:3])
#     ηℝ_est = exp.(Array(state_chn, :θ)[1:1000, 4:6])
#     aℝ_est = exp.(Array(state_chn, :θ)[1:1000, 7:12])
#     r_prior, η_prior, a_prior = mod.gm.θ.priors
#     r_est = scaled_logistic.(rℝ_est, r_prior)
#     η_est = scaled_logistic.(ηℝ_est, η_prior)
#     a_est = scaled_logistic.(aℝ_est, a_prior)
#     return r_est, η_est, a_est
# end

# function get_posterior_logA_matrix(
#     chains::TemperedChains{T, I};
#     from::Int,
#     to::Int,
#     n_thinning::Int
#     ) where {T<:Real, I<:Integer}

#     mod = chains.mods[1];
#     @assert typeof(mod.gm.θ.params) == CompetitionParamsFullSupport
#     θ_samples = group(staterec2chain(chains.mcps[1][:state_rec]), :θ)
#     θ_samples = θ_samples.value.data[from:n_thinning:to, :, 1]
#     C = chains.mods[1].data.C
#     a_prior = chains.mods[1].gm.θ.priors.a_prior
#     aℝ_mean = vec(mean(θ_samples[:, 2*C+1:end], dims=1))
#     a_mean = scaled_logistic.(aℝ_mean, a_prior)
#     A_mean = competitive_coef_matrix(a_mean, C)
#     logA_mean = log.(A_mean)
#     return logA_mean
# end