# Copyright (C) 2023 Richard Stiskalek
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

from os.path import join

import matplotlib.pyplot as plt
import numpy
import scienceplots  # noqa
from cache_to_disk import cache_to_disk, delete_disk_caches_for_function
from scipy.stats import norm
from sklearn.metrics import r2_score
from tqdm import tqdm, trange
from astropy import units as u
from astropy.coordinates import SkyCoord

from colossus.cosmology import cosmology
from colossus.halo import concentration

import csiborgtools
import plt_utils

MASS_KINDS = {"csiborg": "fof_totpartmass",
              "quijote": "group_mass",
              }


def open_cat(nsim, simname):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)

    if simname == "csiborg":
        bounds = {"dist": (0, 155)}
        cat = csiborgtools.read.CSiBORGHaloCatalogue(
            nsim, paths, bounds=bounds)
    elif simname == "quijote":
        cat = csiborgtools.read.QuijoteHaloCatalogue(
            nsim, paths, nsnap=4, load_fitted=True, load_initial=True,
            with_lagpatch=False)
    else:
        raise ValueError(f"Unknown simulation name: {simname}.")

    return cat


def open_cats(nsims, simname):
    catxs = [None] * len(nsims)

    for i, nsim in enumerate(tqdm(nsims, desc="Opening catalogues")):
        catxs[i] = open_cat(nsim, simname)

    return catxs


@cache_to_disk(120)
def get_overlap_summary(nsim0, simname, min_logmass, smoothed):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)
    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)
    mass0 = reader.cat0(MASS_KINDS[simname])
    mask = mass0 > 10**min_logmass

    return {"mass0": mass0[mask],
            "hid0": reader.cat0("index")[mask],
            "summed_overlap": reader.summed_overlap(smoothed)[mask],
            "max_overlap": reader.max_overlap(0, smoothed)[mask],
            "prob_nomatch": reader.prob_nomatch(smoothed)[mask],
            }


# --------------------------------------------------------------------------- #
###############################################################################
#                   Total DM halo mass vs pair overlaps                       #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_mtot_vs_all_pairoverlap(nsim0, simname, mass_kind, min_logmass,
                                smoothed, nbins):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(simname, nsim0, paths,
                                                 min_logmass,
                                                 smoothed=smoothed)
    nsimxs = nsimxs

    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)

    x = [None] * len(catxs)
    y = [None] * len(catxs)
    for i in trange(len(catxs), desc="Stacking catalogues"):
        x[i] = numpy.log10(
            numpy.concatenate(reader[i].copy_per_match(mass_kind)))
        y[i] = numpy.concatenate(reader[i].overlap(smoothed))

    x = numpy.concatenate(x)
    y = numpy.concatenate(y)

    xbins = numpy.linspace(min(x), max(x), nbins)

    return x, y, xbins


def mtot_vs_all_pairoverlap(nsim0, simname, min_logmass, smoothed, nbins,
                            ext="png"):
    mass_kind = MASS_KINDS[simname]
    x, y, xbins = get_mtot_vs_all_pairoverlap(nsim0, simname, mass_kind,
                                              min_logmass, smoothed, nbins)
    sigma = 1

    with plt.style.context(plt_utils.mplstyle):
        plt.figure()
        hb = plt.hexbin(x, y, mincnt=1, gridsize=50, bins="log")

        y_median, yerr = plt_utils.compute_error_bars(x, y, xbins, sigma=sigma)
        plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]), y_median, yerr=yerr,
                     color='red', ls='dashed', capsize=3,
                     label="CSiBORG" if simname == "csiborg" else None,
                     fmt="o", ms=3)

        if simname == "csiborg":
            x_quijote, y_quijote, xbins_quijote = get_mtot_vs_all_pairoverlap(
                0, "quijote", "group_mass", min_logmass, smoothed, nbins)
            y_median_quijote, yerr_quijote = plt_utils.compute_error_bars(
                x_quijote, y_quijote, xbins_quijote, sigma=sigma)
            plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]) + 0.01,
                         y_median_quijote, yerr=yerr_quijote,
                         color='sandybrown', ls='dashed', capsize=3,
                         label="Quijote", fmt="o", ms=3)
            plt.legend(loc="upper left", ncols=2, columnspacing=1.0)

        plt.colorbar(hb, label="Counts in bins", pad=0)
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$\mathcal{O}_{a b}$")
        plt.xlim(numpy.min(x))
        plt.ylim(0., 1.)

        plt.tight_layout()
        fout = join(plt_utils.fout,
                    f"mass_vs_pair_overlap_{simname}_{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                  Total DM halo mass vs maximum pair overlaps                #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_mtot_vs_maxpairoverlap(nsim0, simname, mass_kind, min_logmass,
                               smoothed, nbins, concatenate=True):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)
    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    def get_max(y_):
        if len(y_) == 0:
            return 0
        return numpy.nanmax(y_)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)

    x = [None] * len(catxs)
    y = [None] * len(catxs)
    for i in trange(len(catxs), desc="Stacking catalogues"):
        x[i] = numpy.log10(cat0[mass_kind])
        y[i] = numpy.array([get_max(y_) for y_ in reader[i].overlap(smoothed)])

        mask = x[i] > min_logmass
        x[i] = x[i][mask]
        y[i] = y[i][mask]

    xbins = numpy.linspace(numpy.min(x), numpy.max(x), nbins)
    if concatenate:
        x = numpy.concatenate(x)
        y = numpy.concatenate(y)

    return x, y, xbins


def mtot_vs_maxpairoverlap(nsim0, simname, mass_kind, min_logmass, smoothed,
                           nbins, ext="png"):
    x, y, xbins = get_mtot_vs_maxpairoverlap(nsim0, simname, mass_kind,
                                             min_logmass, smoothed, nbins)

    with plt.style.context(plt_utils.mplstyle):
        plt.figure()
        plt.hexbin(x, y, mincnt=1, gridsize=50, bins="log")

        y_median, yerr = plt_utils.compute_error_bars(x, y, xbins, sigma=1)
        plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]), y_median, yerr=yerr,
                     color='red', ls='dashed', capsize=3,
                     label="CSiBORG" if simname == "csiborg" else None,
                     fmt="o", ms=3)

        if simname == "csiborg":
            x_quijote, y_quijote, xbins_quijote = get_mtot_vs_all_pairoverlap(
                0, "quijote", "group_mass", min_logmass, smoothed, nbins)
            y_median_quijote, yerr_quijote = plt_utils.compute_error_bars(
                x_quijote, y_quijote, xbins_quijote, sigma=1)
            plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]) + 0.01,
                         y_median_quijote, yerr=yerr_quijote,
                         color='sandybrown', ls='dashed', capsize=3,
                         label="Quijote", fmt="o", ms=3)

        plt.colorbar(label="Counts in bins", pad=0)
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")
        plt.ylim(-0.02, 1.)
        plt.xlim(numpy.min(x) - 0.05)

        plt.tight_layout()
        fout = join(plt_utils.fout, f"mass_vs_max_pair_overlap{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


def mtot_vs_maxpairoverlap_statistic(nsim0, simname, mass_kind, min_logmass,
                                     smoothed, nbins, ext="png"):
    x, y, xbins = get_mtot_vs_maxpairoverlap(nsim0, simname, mass_kind,
                                             min_logmass, smoothed, nbins)
    with plt.style.context(plt_utils.mplstyle):
        plt.figure()
        plt.hexbin(x, y, mincnt=1, gridsize=50, bins="log")

        y_median, yerr = plt_utils.compute_error_bars(x, y, xbins, sigma=2)
        plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]), y_median, yerr=yerr,
                     color='red', ls='dashed', capsize=3,
                     label="CSiBORG" if simname == "csiborg" else None,
                     fmt="o", ms=3)

        if simname == "csiborg":
            x_quijote, y_quijote, xbins_quijote = get_mtot_vs_all_pairoverlap(
                0, "quijote", "group_mass", min_logmass, smoothed, nbins)
            y_median_quijote, yerr_quijote = plt_utils.compute_error_bars(
                x_quijote, y_quijote, xbins_quijote, sigma=2)
            plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]) + 0.01,
                         y_median_quijote, yerr=yerr_quijote,
                         color='blue', ls='dashed', capsize=3,
                         label="Quijote", fmt="o", ms=3)
            plt.legend(ncol=2, fontsize="small")

        # plt.colorbar(label="Counts in bins", pad=0)
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")
        plt.ylim(-0.02, 1.)
        plt.xlim(numpy.min(x) - 0.05)

        plt.tight_layout()
        fout = join(plt_utils.fout, f"mass_vs_max_pair_overlap{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


def mtot_vs_maxpairoverlap_fraction(min_logmass, smoothed, nbins, ext="png"):

    csiborg_nsims = [7444 + 24 * n for n in range(10)]
    quijote_nsims = [n for n in range(10)]

    @cache_to_disk(120)
    def get_xy_maxoverlap_fraction(n):
        x_csiborg, y_csiborg, __ = get_mtot_vs_maxpairoverlap(
            csiborg_nsims[n], "csiborg", MASS_KINDS["csiborg"], min_logmass,
            smoothed, nbins, concatenate=False)
        x_quijote, y_quijote, __ = get_mtot_vs_maxpairoverlap(
            quijote_nsims[n], "quijote", MASS_KINDS["quijote"], min_logmass,
            smoothed, nbins, concatenate=False)

        x_csiborg = x_csiborg[0]
        x_quijote = x_quijote[0]

        y_csiborg = numpy.asanyarray(y_csiborg)
        y_quijote = numpy.asanyarray(y_quijote)
        y_csiborg = numpy.median(y_csiborg, axis=0)
        y_quijote = numpy.median(y_quijote, axis=0)

        xbins = numpy.arange(min_logmass, 15.61, 0.2)
        x = 0.5 * (xbins[1:] + xbins[:-1])
        y = numpy.full((len(x), 3), numpy.nan)
        percentiles = norm.cdf(x=[1, 2, 3]) * 100

        for i in range(len(xbins)-1):
            mask_csiborg = (x_csiborg >= xbins[i]) & (x_csiborg < xbins[i+1])
            mask_quijote = (x_quijote >= xbins[i]) & (x_quijote < xbins[i+1])

            current_y_csiborg = y_csiborg[mask_csiborg]
            current_y_quijote = y_quijote[mask_quijote]
            current_tot_csiborg = len(current_y_csiborg)

            for j, q in enumerate(percentiles):
                threshold = numpy.percentile(current_y_quijote, q)
                y[i, j] = (current_y_csiborg > threshold).sum()
                y[i, j] /= current_tot_csiborg
        return x, y

    ys = [None] * 10
    for n in range(10):
        x, ys[n] = get_xy_maxoverlap_fraction(n)
    ys = numpy.asanyarray(ys)

    ymean = numpy.nanmean(ys, axis=0)
    ystd = numpy.nanstd(ys, axis=0)

    with plt.style.context(plt_utils.mplstyle):
        plt.figure()
        for i in range(3):
            plt.plot(x, ymean[:, i], label=r"${}\sigma$".format(i+1))
            plt.fill_between(x, ymean[:, i] - ystd[:, i],
                             ymean[:, i] + ystd[:, i], alpha=0.2)

        plt.legend()
        plt.ylim(0.0, 1.025)

        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$f_{\rm significant}$")

        plt.xlim(numpy.nanmin(x), numpy.nanmax(x))

        plt.tight_layout()
        fout = join(plt_utils.fout, f"mass_vs_max_pair_overlap_fraction.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


def summed_to_max_overlap(min_logmass, smoothed, nbins, ext="png"):
    x_csiborg = get_overlap_summary(7444, "csiborg", min_logmass, smoothed)

    with plt.style.context(plt_utils.mplstyle):
        plt.figure()
        x = numpy.mean(x_csiborg["summed_overlap"], axis=1)
        y = numpy.mean(x_csiborg["max_overlap"], axis=1)

        plt.hexbin(x, y, mincnt=0, gridsize=40,
                   C=numpy.log10(x_csiborg["mass0"]),
                   reduce_C_function=numpy.median)
        plt.colorbar(label=r"$\log M_{\rm tot} ~ [M_\odot / h]$", pad=0)

        plt.axline((0, 0), slope=1, color='red', linestyle='--',
                   label=r"$1-1$")

        plt.legend()

        plt.xlabel(r"$\langle \sum_{b \in \mathcal{B}} \mathcal{O}_{a b}\rangle_{\mathcal{B}}$") # noqa
        plt.ylabel(r"$\langle \max_{b \in \mathcal{B}} \mathcal{O}_{a b}\rangle_{\mathcal{B}}$") # noqa

        print(x.min(), x.max())
        print(y.min(), y.max())

        plt.tight_layout()
        ext = "pdf"
        fout = join(plt_utils.fout, f"summed_to_max_overlap.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                   Total DM halo mass vs pair overlaps                       #
###############################################################################
# --------------------------------------------------------------------------- #

@cache_to_disk(120)
def get_max_overlap_agreement(nsim0, simname, min_logmass, smoothed):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)
    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    return csiborgtools.summary.max_overlap_agreements(cat0, catxs, 13.25,
                                                       155.5, paths)


def maximum_overlap_agreement(nsim0, simname, min_logmass, smoothed):

    agreements = get_max_overlap_agreement(nsim0, simname, min_logmass,
                                           smoothed)

    x, y, mass_bins = get_mtot_vs_maxpairoverlap(
        nsim0, simname, MASS_KINDS[simname], min_logmass, smoothed, 10,
        concatenate=False)
    x = x[0]
    y = numpy.asanyarray(y)
    mean_max_overlap = numpy.mean(y, axis=0)

    cat0 = open_cat(nsim0, simname)
    totpartmass = numpy.log10(cat0[MASS_KINDS[simname]])

    mask = totpartmass > min_logmass
    agreements = agreements[:, mask]
    totpartmass = totpartmass[mask]

    mask = numpy.any(numpy.isfinite(agreements), axis=0)
    y = numpy.sum(agreements == 1., axis=0)

    with plt.style.context(plt_utils.mplstyle):
        plt.figure(figsize=(3.5, 2.625))

        plt.scatter(totpartmass[mask], y[mask], s=5, c=mean_max_overlap,
                    rasterized=True)
        plt.colorbar(label=r"$\langle \max_{b \in \mathcal{B}} \mathcal{O}_{a b}\rangle_{\mathcal{B}}$", pad=0) # noqa

        ymed, yerr = plt_utils.compute_error_bars(totpartmass[mask], y[mask],
                                                  mass_bins, sigma=1)
        plt.errorbar(0.5 * (mass_bins[1:] + mass_bins[:-1]), ymed, yerr=yerr,
                     capsize=3, c="red", ls="--", lw=0.5, fmt="o", ms=3)

        plt.xlim(numpy.nanmin(totpartmass[mask]))
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$f_{\rm sym}$")
        plt.tight_layout()
        fout = join(plt_utils.fout,
                    f"maximum_overlap_agreement{simname}_{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()

    with plt.style.context(plt_utils.mplstyle):
        plt.figure(figsize=(3.5, 2.625))

        plt.scatter(mean_max_overlap[mask], y[mask], s=5, c=totpartmass,
                    rasterized=True)
        plt.colorbar(label=r"$\log M_{\rm tot} ~ [M_\odot / h]$", pad=0)
        bins = numpy.arange(0, 0.7, 0.05)
        ymed, yerr = plt_utils.compute_error_bars(
            mean_max_overlap[mask], y[mask], bins, sigma=1)

        plt.errorbar(0.5 * (bins[1:] + bins[:-1]), ymed, yerr=yerr,
                     capsize=3, c="red", ls="--", lw=0.5, fmt="o", ms=3)
        plt.xlabel(r"$\langle \max_{b \in \mathcal{B}} \mathcal{O}_{a b}\rangle_{\mathcal{B}}$")  # noqa
        plt.ylabel(r"$f_{\rm sym}$")
        plt.xlim(numpy.nanmin(mean_max_overlap[mask]))

        # plt.xscale("log")
        plt.tight_layout()
        fout = join(plt_utils.fout, f"maximum_overlap_agreement_mean_overlap{simname}_{nsim0}.{ext}")  # noqa
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                  Total DM halo mass vs summed pair overlaps                 #
###############################################################################
# --------------------------------------------------------------------------- #

def mtot_vs_summedpairoverlap(nsim0, simname, min_logmass, smoothed, nbins,
                              ext="png"):
    x = get_overlap_summary(nsim0, simname, min_logmass, smoothed)

    mass0 = numpy.log10(x["mass0"])
    mean_overlap = numpy.nanmean(x["summed_overlap"], axis=1)
    std_overlap = numpy.nanstd(x["summed_overlap"], axis=1)

    xbins = numpy.linspace(numpy.nanmin(mass0), numpy.nanmax(mass0), nbins)

    with plt.style.context(plt_utils.mplstyle):
        plt.figure(figsize=(3.5, 2.625))
        plt.hexbin(mass0, mean_overlap, mincnt=1, bins="log", gridsize=30)

        y_median, yerr = plt_utils.compute_error_bars(
            mass0, mean_overlap, xbins, sigma=1)
        plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]), y_median, yerr=yerr,
                     color='red', ls='dashed', capsize=3, label="CSiBORG",
                     fmt="o", ms=3)

        if simname == "csiborg":
            x_quijote = get_overlap_summary(0, "quijote", min_logmass,
                                            smoothed)
            mass0_quijote = numpy.log10(x_quijote["mass0"])
            mean_overlap_quijote = numpy.nanmean(x_quijote["summed_overlap"],
                                                 axis=1)
            xbins_quijote = numpy.linspace(numpy.nanmin(mass0),
                                           numpy.nanmax(mass0), nbins)

            y_median_quijote, yerr_quijote = plt_utils.compute_error_bars(
                mass0_quijote, mean_overlap_quijote, xbins_quijote, sigma=1)
            plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]) + 0.01,
                         y_median_quijote, yerr=yerr_quijote,
                         color='sandybrown', ls='dashed', capsize=3,
                         label="Quijote", fmt="o", ms=3)
            plt.legend()

        plt.xlim(numpy.min(mass0))
        plt.xlim(numpy.min(mass0))
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$\langle \sum_{b \in \mathcal{B}} \mathcal{O}_{a b}\rangle_{\mathcal{B}}$") # noqa
        plt.colorbar(label="Counts in bins", pad=0)

        plt.tight_layout()
        fout = join(plt_utils.fout, f"prob_match_mean_{simname}_{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()

    with plt.style.context(plt_utils.mplstyle):
        plt.figure(figsize=(3.5, 2.625))
        plt.hexbin(mass0, std_overlap, mincnt=1, bins="log", gridsize=30)

        y_median, yerr = plt_utils.compute_error_bars(
            mass0, std_overlap, xbins, sigma=2)
        plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]), y_median, yerr=yerr,
                     color='red', ls='dashed', capsize=3, fmt="o", ms=3)

        if simname == "csiborg":
            x_quijote = get_overlap_summary(0, "quijote", min_logmass,
                                            smoothed)
            mass0_quijote = numpy.log10(x_quijote["mass0"])
            mean_overlap_quijote = numpy.nanmean(x_quijote["summed_overlap"],
                                                 axis=1)
            std_overlap_quijote = numpy.nanstd(x_quijote["summed_overlap"],
                                               axis=1)
            xbins_quijote = numpy.linspace(numpy.nanmin(mass0),
                                           numpy.nanmax(mass0), nbins)

            y_median_quijote, yerr_quijote = plt_utils.compute_error_bars(
                mass0_quijote, std_overlap_quijote, xbins_quijote, sigma=2)
            plt.errorbar(0.5 * (xbins[1:] + xbins[:-1]) + 0.01,
                         y_median_quijote, yerr=yerr_quijote,
                         color='sandybrown', ls='dashed', capsize=3,
                         fmt="o", ms=3)

        plt.colorbar(label=r"Counts in bins", pad=0)
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$\sigma\left(\sum_{b \in \mathcal{B}} \mathcal{O}_{a b}\right)_{\mathcal{B}}$")  # noqa

        plt.tight_layout()
        fout = join(plt_utils.fout, f"prob_match_std_{simname}_{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                  Total DM halo mass vs mean separation                      #
###############################################################################
# --------------------------------------------------------------------------- #

@cache_to_disk(120)
def get_mass_vs_separation(nsim0, simname, min_logmass, boxsize,
                           smoothed):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)
    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(
        cat0, catxs, paths, min_logmass)

    mass = numpy.log10(reader.cat0(MASS_KINDS[simname]))
    mus = numpy.zeros((len(catxs), len(mass)))
    for i in trange(len(catxs), desc="Stacking catalogues"):
        dist = reader[i].dist(in_initial=False, boxsize=boxsize,
                              norm_kind=None)
        overlap = reader[i].overlap(smoothed)
        mus[i], __ = csiborgtools.summary.weighted_stats(dist, overlap)

    return mass, mus


def mass_vs_separation(nsim0, simname, min_logmass, nbins, smoothed,
                       boxsize):
    mass, dist = get_mass_vs_separation(
        nsim0, simname, min_logmass, boxsize, smoothed)

    dist = numpy.nanmean(dist, axis=0)
    mask = numpy.isfinite(mass) & numpy.isfinite(dist)

    mass = mass[mask]
    dist = dist[mask]

    xbins = numpy.linspace(numpy.nanmin(mass), numpy.nanmax(mass), nbins)

    with plt.style.context(plt_utils.mplstyle):
        fig, ax = plt.subplots()
        plt.rcParams["axes.grid"] = False

        cx = ax.hexbin(mass, dist, mincnt=0, bins="log", gridsize=50)
        y_median, yerr = plt_utils.compute_error_bars(mass, dist, xbins,
                                                      sigma=1)
        ax.errorbar(0.5 * (xbins[1:] + xbins[:-1]), y_median, yerr=yerr,
                    color='red', ls='dashed', capsize=3,
                    label="CSiBORG" if simname == "csiborg" else None,
                    fmt="o", ms=3)
        ax.set_xlim(numpy.nanmin(mass))

        if simname == "csiborg":
            mass_quijote, dist_quijote = get_mass_vs_separation(
                0, "quijote", min_logmass, boxsize, smoothed)
            dist_quijote = numpy.nanmean(dist_quijote, axis=0)
            mask = numpy.isfinite(mass_quijote) & numpy.isfinite(dist_quijote)
            mass_quijote = mass_quijote[mask]
            dist_quijote = dist_quijote[mask]

            # dist_quijote = numpy.log10(dist_quijote)
            xbins_quijote = numpy.linspace(numpy.nanmin(mass_quijote),
                                           numpy.nanmax(mass_quijote), nbins)
            xbins_quijote = xbins_quijote[:-1]
            y_median_quijote, yerr_quijote = plt_utils.compute_error_bars(
                mass_quijote, dist_quijote, xbins_quijote, sigma=1)
            ax.errorbar(0.5 * (xbins_quijote[1:] + xbins_quijote[:-1]),
                        y_median_quijote, yerr=yerr_quijote,
                        color='sandybrown', ls='dashed', capsize=3,
                        label="Quijote", fmt="o", ms=3)
            ax.legend(ncols=2)

        fig.colorbar(cx, label="Bin counts", pad=0)
        ax.set_xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        ax.set_ylabel(r"$\langle \Delta R / R_{\rm 200c}\rangle_{\mathcal{B}}$")  # noqa
        ax.set_ylabel(r"$\langle \Delta R \rangle_{\mathcal{B}} ~ [\mathrm{Mpc} / h]$")  # noqa

        fig.tight_layout()
        fout = join(plt_utils.fout, f"mass_vs_sep_{simname}_{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        fig.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                Total DM halo mass vs expected matched mass                  #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_expected_mass(nsim0, simname, min_logmass, smoothed):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(simname, nsim0, paths,
                                                 min_logmass, smoothed=True)

    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)
    mass0 = reader.cat0(MASS_KINDS[simname])
    mask = mass0 > 10**min_logmass

    mean_expected, std_expected = reader.expected_property(
        MASS_KINDS[simname], smoothed, min_logmass)

    return {"mass0": mass0[mask],
            "mu": mean_expected[mask],
            "std": std_expected[mask],
            "prob_match": reader.summed_overlap(smoothed)[mask],
            }


def mtot_vs_expected_mass(nsim0, simname, min_logmass, smoothed, ext="png"):
    x = get_expected_mass(nsim0, simname, min_logmass, smoothed)

    mass = x["mass0"]
    mu = x["mu"]
    std = x["std"]
    prob_match = x["prob_match"]

    mass = numpy.log10(mass)
    prob_match = numpy.nanmean(prob_match, axis=1)
    mask = numpy.isfinite(mass) & numpy.isfinite(mu) & numpy.isfinite(std)

    with plt.style.context(plt_utils.mplstyle):
        fig, axs = plt.subplots(ncols=3, figsize=(3.5 * 2, 2.625))

        im0 = axs[0].hexbin(mass[mask], mu[mask], mincnt=1, bins="log",
                            gridsize=30,)
        im1 = axs[1].hexbin(mass[mask], std[mask], mincnt=1, bins="log",
                            gridsize=30)
        im2 = axs[2].hexbin(prob_match[mask], mu[mask] - mass[mask],
                            gridsize=30, C=mass[mask],
                            reduce_C_function=numpy.nanmedian)

        axs[2].axhline(0, color="red", linestyle="--", alpha=0.5)
        axs[0].set_xlabel(r"$\log M_{\rm tot, ref} ~ [M_\odot / h]$")
        axs[0].set_ylabel(r"$\log M_{\rm tot, exp} ~ [M_\odot / h]$")
        axs[1].set_xlabel(r"$\log M_{\rm tot, ref} ~ [M_\odot / h]$")
        axs[1].set_ylabel(r"$\sigma_{\log M_{\rm tot, exp}}$")
        axs[2].set_xlabel(r"$\langle \sum_{b \in \mathcal{B}} \mathcal{O}_{a b}\rangle_{\mathcal{B}}$") # noqa
        axs[2].set_ylabel(r"$\log (M_{\rm tot, exp} / M_{\rm tot, ref})$")

        z = numpy.nanmean(mass[mask])
        axs[0].axline((z, z), slope=1, color='red', linestyle='--',
                      label=r"$1-1$")
        axs[0].legend()

        ims = [im0, im1, im2]
        labels = ["Bin counts", "Bin counts",
                  r"$\log M_{\rm tot} ~ [M_\odot / h]$"]
        for i in range(3):
            axins = axs[i].inset_axes([0.0, 1.0, 1.0, 0.05])
            fig.colorbar(ims[i], cax=axins, orientation="horizontal",
                         label=labels[i])
            axins.xaxis.tick_top()
            axins.xaxis.set_tick_params(labeltop=True)
            axins.xaxis.set_label_position("top")

        fig.tight_layout()
        fout = join(plt_utils.fout, f"mass_vs_expmass_{nsim0}_{simname}.{ext}")
        print(f"Saving to `{fout}`.")
        fig.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#               Total DM halo mass vs maximum overlap halo property           #
###############################################################################
# --------------------------------------------------------------------------- #

@cache_to_disk(120)
def get_expected_key(nsim0, simname, min_logmass, key, smoothed):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(simname, nsim0, paths,
                                                 min_logmass, smoothed=True)

    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)
    mass0 = reader.cat0(MASS_KINDS[simname])
    mask = mass0 > 10**min_logmass

    in_log = False if key == "conc" else True
    mean_expected, std_expected = reader.expected_property(
        key, smoothed, min_logmass, in_log=in_log)

    log_mass0 = numpy.log10(mass0)
    control = numpy.full(len(log_mass0), numpy.nan)
    # for i in trange(len(log_mass0), desc="Control"):
    #     if not mask[i]:
    #         continue

    #     control_ = [None] * len(catxs)
    #     for j in range(len(catxs)):
    #         log_massx = numpy.log10(reader[j].catx(MASS_KINDS[simname]))
    #         ks = numpy.argsort(numpy.abs(log_massx - log_mass0[i]))[:15]
    #         control_[j] = reader[j].catx(key)[ks]

    #     control[i] = numpy.nanmean(numpy.concatenate(control_))

    return {"mass0": mass0[mask],
            "prop0": reader.cat0(key)[mask],
            "mu": mean_expected[mask],
            "std": std_expected[mask],
            "control": control[mask],
            "prob_match": reader.summed_overlap(smoothed)[mask],
            }


def mtot_vs_expected_key(nsim0, simname, min_logmass, key, smoothed,
                         min_logmass_run=None):
    mass_kind = MASS_KINDS[simname]
    assert key != mass_kind

    x = get_expected_key(nsim0, simname, min_logmass, key, smoothed)
    mass0 = numpy.log10(x["mass0"])
    prop0 = x["prop0"]
    mu = x["mu"]
    std = x["std"]
    prob_match = numpy.nanmean(x["prob_match"], axis=1)
    control = x["control"]

    print("prop0 ", prop0)
    print("mu    ", mu)
    print("std   ", std)
    print("control", control)

    mask = numpy.isfinite(prop0) & numpy.isfinite(mu) & numpy.isfinite(std)
    if min_logmass_run is not None:
        mask &= mass0 > 15
    mask &= prop0 > 0.4

    mass0 = mass0[mask]
    prop0 = prop0[mask]
    mu = mu[mask]
    std = std[mask]
    control = control[mask]
    prob_match = prob_match[mask]

    def rmse(x, y, sample_weight=None):
        return numpy.sqrt(numpy.average((x - y)**2, weights=sample_weight))

    print("Unweigted R2         = ", r2_score(prop0, mu))
    print("Err Weighted R2      = ", r2_score(prop0, mu, sample_weight=1 / std**2))  # noqa
    print("Pmatch R2            = ", r2_score(prop0, mu, sample_weight=prob_match))  # noqa

    print()

    print("Unweigted RMSE       = ", rmse(prop0, mu))
    print("Err Weighted RMSE    = ", rmse(prop0, mu, 1 / std**2))
    print("Pmatch RMSE          = ", rmse(prop0, mu, prob_match))

    with plt.style.context(plt_utils.mplstyle):
        fig, ax = plt.subplots(figsize=(3.5, 2.625))

        ax.errorbar(prop0, mu, yerr=std, capsize=3, ls="none", marker="o")

        z = numpy.nanmean(prop0)
        ax.axline((z, z), slope=1, color='red', linestyle='--', label=r"$1-1$")

        ax.legend()

        # if key == "lambda200c":
        #     ax.axhline(numpy.median(control), color="red", ls="--", zorder=0)

        if key == "lambda200c":
            ax.set_xlabel(r"$\log \lambda_{\rm 200c, ref}$")
            ax.set_ylabel(r"$\log \lambda_{\rm 200c, exp}$")
        elif key == "conc":
            ax.set_xlabel(r"$c_{\rm 200c, ref}$")
            ax.set_ylabel(r"$c_{\rm 200c, exp}$")

        fig.tight_layout()
        fout = join(plt_utils.fout, f"max_{key}_{simname}_{nsim0}.{ext}")
        print(f"Saving to `{fout}`.")
        fig.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                  Total mass of a single halo expectation                    #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_expected_single(k, nsim0, simname, min_logmass, key, smoothed,
                        in_log):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)

    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)

    x0 = reader.cat0(key)

    if "maxmass" in k:
        k0 = int(k.split("__")[1])
        k = numpy.argsort(reader.cat0(MASS_KINDS[simname]))[::-1][k0]
        print(f"Doing the {k0}th maximum hass halo with index {k}.")

    if k == "max":
        k = numpy.nanargmax(x0)

    xcross, overlaps = reader.expected_property_single(k, key, smoothed,
                                                       in_log)

    control = [None] * len(catxs)
    log_mass0 = numpy.log10(reader.cat0(MASS_KINDS[simname])[k])
    for j in range(len(catxs)):
        log_massx = numpy.log10(reader[j].catx(MASS_KINDS[simname]))
        ks = numpy.argsort(numpy.abs(log_massx - log_mass0))[:15]
        control[j] = reader[j].catx(key)[ks]

    xcross = numpy.asanyarray(xcross)
    overlaps = numpy.asanyarray(overlaps)

    return x0[k], xcross, overlaps, control


def mtot_vs_expected_single(k, nsim0, simname, min_logmass, key, smoothed):
    x0, xcross, overlaps, control = get_expected_single(
        k, nsim0, simname, min_logmass, key, smoothed, False)

    control = numpy.concatenate(control)

    if key == "lambda200c" or key == "totpartmass":
        xcross = numpy.log10(xcross)
        control = numpy.log10(control)
        x0 = numpy.log10(x0)

    m = numpy.isfinite(xcross) & numpy.isfinite(overlaps)
    with plt.style.context("science"):
        cols = plt.rcParams["axes.prop_cycle"].by_key()["color"]
        plt.figure()
        plt.hist(xcross[m], weights=overlaps[m], bins=30, histtype="step",
                 density=1, label="Matched", color=cols[0])

        peak = csiborgtools.summary.find_peak(xcross[m], overlaps[m])
        plt.axvline(peak, color="mediumblue", ls="--")
        plt.axvline(x0, color="red", ls="--")

        if key != "totpartmass":
            m = numpy.isfinite(control)
            plt.hist(control[m], bins=30, histtype="step", density=1,
                     label="Control", color=cols[1])

            m = numpy.isfinite(control)
            xmin, xmax = numpy.percentile(control[m], [16, 84])
            plt.axvspan(xmin, xmax, color=cols[1], alpha=0.1)

        if key == "totpartmass" or key == "fof_totpartmass":
            plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        elif key == "lambda200c":
            plt.xlabel(r"$\log \lambda_{\rm 200c}$")
        elif key == "conc":
            plt.xlabel(r"$c$")

        plt.ylabel("Normalized counts")
        plt.legend()

        plt.tight_layout()
        fout = join(
            plt_utils.fout,
            f"expected_single_{k}_{key}_{nsim0}_{simname}_{min_logmass}.pdf")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


def mtot_vs_expected_single_panel(k, nsim0, simname, min_logmass, smoothed):
    print("Plotting..")

    def local_data(key):
        x0, xcross, overlaps, control = get_expected_single(
            k, nsim0, simname, min_logmass, key, smoothed, False)

        control = numpy.concatenate(control)
        if key == "lambda200c" or key == "totpartmass":
            xcross = numpy.log10(xcross)
            control = numpy.log10(control)
            x0 = numpy.log10(x0)

        m = numpy.isfinite(xcross) & numpy.isfinite(overlaps)
        return x0, xcross[m], overlaps[m], control

    with plt.style.context("science"):
        cols = plt.rcParams["axes.prop_cycle"].by_key()["color"]
        fig, axs = plt.subplots(ncols=3, figsize=(2 * 3.5, 2.625))
        fig.subplots_adjust(wspace=0.0)
        nbins = 25

        # TOTAL MASS
        x0, x, weights, __ = local_data("totpartmass")
        axs[0].hist(x, weights=weights, bins=nbins, histtype="step", density=1,
                    label="Matched")
        axs[0].legend()
        peak = csiborgtools.summary.find_peak(x, weights)
        axs[0].axvline(peak, color="mediumblue", ls="--")
        axs[0].axvline(x0, color="red", ls="--")
        std = numpy.average((x - peak)**2, weights=weights)**0.5
        xmin, xmax = peak - std, peak + std
        axs[0].axvspan(xmin, xmax, color=cols[0], alpha=0.2)

        # SPIN
        x0, x, weights, control = local_data("lambda200c")
        axs[1].hist(x, weights=weights, bins=nbins, histtype="step", density=1,
                    color=cols[0])
        axs[1].hist(control, bins=nbins, histtype="step", density=1,
                    color=cols[1], label="Control")
        axs[1].legend()
        xmin, xmax = numpy.percentile(control, [16, 84])
        axs[1].axvspan(xmin, xmax, color=cols[1], alpha=0.1)
        peak = csiborgtools.summary.find_peak(x, weights)
        axs[1].axvline(peak, color="mediumblue", ls="--")
        axs[1].axvline(x0, color="red", ls="--")
        axs[1].set_yticklabels([])
        m = numpy.isfinite(control)
        xmin, mu, xmax = numpy.percentile(control[m], [16, 50, 84])
        axs[1].axvspan(xmin, xmax, color=cols[1], alpha=0.2)
        axs[1].axvline(mu, color=cols[1], ls="--")
        std = numpy.average((x - peak)**2, weights=weights)**0.5
        xmin, xmax = peak - std, peak + std
        axs[1].axvspan(xmin, xmax, color=cols[0], alpha=0.2)

        # CONCENTRATION
        x0, x, weights, control = local_data("conc")
        axs[2].hist(x, weights=weights, bins=nbins, histtype="step", density=1,
                    color=cols[0])
        axs[2].hist(control, bins=nbins, histtype="step", density=1,
                    color=cols[1])
        xmin, xmax = numpy.percentile(control, [16, 84])
        axs[2].axvspan(xmin, xmax, color=cols[1], alpha=0.1)
        peak = csiborgtools.summary.find_peak(x, weights)
        axs[2].axvline(peak, color="mediumblue", ls="--")
        axs[2].axvline(x0, color="red", ls="--")
        axs[2].set_yticklabels([])
        m = numpy.isfinite(control)
        xmin, mu, xmax = numpy.percentile(control[m], [16, 50, 84])
        axs[2].axvspan(xmin, xmax, color=cols[1], alpha=0.2)
        axs[2].axvline(mu, color=cols[1], ls="--")
        std = numpy.average((x - peak)**2, weights=weights)**0.5
        xmin, xmax = peak - std, peak + std
        axs[2].axvspan(xmin, xmax, color=cols[0], alpha=0.2)

        axs[0].set_xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        axs[1].set_xlabel(r"$\log \lambda_{\rm 200c}$")
        axs[2].set_xlabel(r"$c$")

        axs[0].set_ylabel("Normalized counts")

        fig.tight_layout(h_pad=0, w_pad=0)
        fout = join(
            plt_utils.fout,
            f"expected_single_{k}_panel_{nsim0}_{simname}_{min_logmass}.pdf")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                        Concentration reconstruction                         #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_expected_many_topmass(kmax, nsim0, simname, min_logmass, key, smoothed,
                              in_log):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)

    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)

    log_mass0 = numpy.log10(reader.cat0(MASS_KINDS[simname]))
    x0 = reader.cat0(key)

    out_mass = numpy.full((kmax), numpy.nan)
    out_true = numpy.full((kmax), numpy.nan)
    out_matched = numpy.full((kmax, len(nsimxs), 2), numpy.nan)
    out_control = numpy.full((kmax, len(nsimxs)), numpy.nan)

    for k0 in trange(kmax, desc="Looping over top masses"):
        k = numpy.argsort(log_mass0)[::-1][k0]
        xcross, overlaps = reader.expected_property_single(k, key, smoothed,
                                                           in_log)
        out_matched[k0, :, 0] = xcross
        out_matched[k0, :, 1] = overlaps

        out_mass[k0] = log_mass0[k]
        out_true[k0] = x0[k]

        for j in range(len(catxs)):
            log_massx = numpy.log10(reader[j].catx(MASS_KINDS[simname]))
            ks = numpy.argsort(numpy.abs(log_massx - log_mass0[k]))[:15]
            out_control[k0, j] = numpy.nanmean(reader[j].catx(key)[ks])

    return out_mass, out_true, out_matched, out_control


def expected_in_tolerance(kmax, nsim0, simname, min_logmass, key, smoothed):
    out_mass, out_true, out_matched, out_control = get_expected_many_topmass(
        kmax, nsim0, simname, min_logmass, key, smoothed, False)

    if key == "lambda200c" or key == "totpartmass":
        out_true = numpy.log10(out_true)
        out_matched[..., 0] = numpy.log10(out_matched[..., 0])
        out_control = numpy.log10(out_control)

    tolerances = [0.1, 0.3, 0.5]

    in_tolerance = numpy.full((kmax, len(tolerances)), numpy.nan)
    for k in range(kmax):
        frac_diff = (out_matched[k, :, 0] - out_true[k]) / out_true[k]
        frac_diff = numpy.abs(frac_diff)
        for j, tol in enumerate(tolerances):
            in_tolerance[k, j] = numpy.sum(frac_diff < tol)

    left_edges = numpy.arange(numpy.nanmin(out_mass), numpy.nanmax(out_mass),
                              0.01)
    bw = 0.2
    x = left_edges + 0.5 * bw
    nsimx = out_matched.shape[1]

    with plt.style.context("science"):
        plt.figure()

        for j, tol in enumerate(tolerances):

            med = csiborgtools.binned_statistic(out_mass, in_tolerance[:, j],
                                                left_edges, bw, numpy.median)
            lower = csiborgtools.binned_statistic(
                out_mass, in_tolerance[:, j], left_edges, bw,
                lambda x: numpy.percentile(x, 16))
            upper = csiborgtools.binned_statistic(
                out_mass, in_tolerance[:, j], left_edges, bw,
                lambda x: numpy.percentile(x, 84))
            lower /= nsimx
            upper /= nsimx
            med /= nsimx

            plt.plot(x, med, label=r"${} \%$".format(int(tol * 100)),)
            plt.fill_between(x, lower, upper, alpha=0.2)

        plt.xlim(x.min(), x.max())
        plt.ylim(0, 1)
        plt.xlabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        plt.ylabel(r"$f_{\rm in~tolerance}$")
        plt.legend(ncols=3)

        plt.tight_layout()
        fout = join(
            plt_utils.fout,
            f"expected_tolerance_{key}_{nsim0}_{simname}_{min_logmass}.png")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                        Concentration reconstruction                         #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_max_overlap_velocity(kmax, nsim0, simname, min_logmass, smoothed):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsimxs = csiborgtools.summary.get_cross_sims(
        simname, nsim0, paths, min_logmass, smoothed=smoothed)

    cat0 = open_cat(nsim0, simname)
    catxs = open_cats(nsimxs, simname)

    reader = csiborgtools.summary.NPairsOverlap(cat0, catxs, paths,
                                                min_logmass)

    log_mass0 = numpy.log10(reader.cat0(MASS_KINDS[simname]))
    vx, vy, vz = reader.cat0("vx"), reader.cat0("vy"), reader.cat0("vz")

    out_mass = numpy.full((kmax), numpy.nan)
    out_true = numpy.full((kmax, 3), numpy.nan)
    out_matched = numpy.full((kmax, len(nsimxs), 4), numpy.nan)

    for k0 in trange(kmax, desc="Looping over top masses"):
        k = numpy.argsort(log_mass0)[::-1][k0]
        for i, key in enumerate(["vx", "vy", "vz"]):
            vel, overlaps = reader.expected_property_single(k, key, smoothed,
                                                            in_log=False)
            out_matched[k0, :, i] = vel
        out_matched[k0, :, -1] = overlaps

        out_mass[k0] = log_mass0[k]
        out_true[k0, 0] = vx[k]
        out_true[k0, 1] = vy[k]
        out_true[k0, 2] = vz[k]

    return out_mass, out_true, out_matched


def expected_in_velocity(kmax, nsim0, simname, min_logmass, smoothed):
    cat0 = open_cat(nsim0, simname)
    pos = cat0.position(cartesian=False)

    if kmax == -1:
        kmax = numpy.sum(numpy.log10(cat0[MASS_KINDS[simname]]) > min_logmass) - 1  # noqa
        print(f"Selected kmax = -1, so doing {kmax}.")

    out_mass, out_true, out_matched = get_max_overlap_velocity(
        kmax, nsim0, simname, min_logmass, smoothed)
    __, out_true_conc, out_matched_conc, out_control_conc = get_expected_many_topmass(kmax, nsim0, simname, min_logmass, "conc", smoothed, False)  # noqa
    __, out_true_mass, out_matched_mass, out_control_mass = get_expected_many_topmass(kmax, nsim0, simname, min_logmass, "totpartmass", smoothed, False)  # noqa

    sep_mass, sep = get_mass_vs_separation(nsim0, simname, min_logmass, 677.7,
                                           smoothed)
    m = numpy.argsort(sep_mass)[::-1]
    sep_mass = sep_mass[m][:kmax]
    sep = sep[:, m][:, :kmax]

    clusters = {
        'Virgo': {'l': 283.8, 'b': 74.5, 'distance_mpc': 17},
        'Fornax': {'l': 236.8, 'b': -53.6, 'distance_mpc': 19},
        'Coma': {'l': 58.1, 'b': 87.9, 'distance_mpc': 98},
        'Perseus': {'l': 150.6, 'b': -13.3, 'distance_mpc': 73},
        'Hydra': {'l': 269.1, 'b': 26.5, 'distance_mpc': 49},
        'Centaurus': {'l': 302.4, 'b': 21.6, 'distance_mpc': 52}
        }

    nsimx = out_matched.shape[1]
    # Calculate the cosine similarity between the true and expected velocity
    # and the difference in velocity.
    yalign = numpy.full((kmax, nsimx), numpy.nan)
    ydiff = numpy.full((kmax, nsimx), numpy.nan)
    ymag = numpy.full((kmax, nsimx), numpy.nan)
    w = numpy.full((kmax, nsimx), numpy.nan)
    for k in range(kmax):
        yalign[k] = csiborgtools.utils.cosine_similarity(out_true[k],
                                                         out_matched[k, :, :3])
        ydiff[k] = numpy.sum(
            (out_true[k] - out_matched[k, :, :3])**2, axis=1)**0.5
        ymag[k] = numpy.linalg.norm(out_matched[k, :, :3], axis=1)
        w[k] = out_matched[k, :, -1]

    for k in range(0):
        k0 = numpy.argsort(cat0["totpartmass"])[::-1][k]
        with plt.style.context("science"):
            fig, axs = plt.subplots(nrows=4, ncols=3,
                                    figsize=(2 * 3.5, 2.5 * 2.625))
            fig.subplots_adjust(wspace=0., hspace=0.0)

            # Maximum overlap histogram
            axs[0, 0].hist(w[k], bins="auto", histtype="step", density=1)
            axs[0, 0].set_xlabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            axs[0, 0].set_ylabel(r"Normalized counts")
            axs[0, 0].set_xlim(0.01, 1)
            # Cosine similarity alignment
            axs[0, 1].hist(yalign[k], weights=w[k], bins=20, histtype="step",
                           density=1)
            axs[0, 1].set_xlabel(r"$\cos \theta$")
            axs[0, 1].set_xlim(-1, 1)
            axs[0, 1].set_ylabel(r"Normalized counts")
            # Normalized Absolute difference in velocity
            axs[0, 2].hist(ydiff[k] / numpy.linalg.norm(out_true[k]),
                           weights=w[k], bins=20, histtype="step",
                           density=1)
            axs[0, 2].set_xlabel(r"$|\Delta \textbf{v}| / |\textbf{v}_{\rm ref}|$")  # noqa
            axs[0, 2].set_xlim(0)
            axs[0, 2].set_ylabel(r"Normalized counts")
            # Max overlap vs mass
            axs[1, 0].scatter(w[k], numpy.log10(out_matched_mass[k, :, 0]),
                              s=3)
            axs[1, 0].axhline(out_mass[k], color="red", ls="--")
            axs[1, 0].axhline(
                numpy.average(numpy.log10(out_matched_mass[k, :, 0]),
                              weights=w[k]),
                color="blue", ls="--")
            axs[1, 0].set_xlabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            axs[1, 0].set_ylabel(r"$\log M_{\rm tot} ~ [M_\odot / h]$")
            # Max overlap vs concentration
            axs[1, 1].scatter(w[k], out_matched_conc[k, :, 0], s=3)
            axs[1, 1].axhline(out_true_conc[k], color="red", ls="--")
            axs[1, 1].set_xlabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            axs[1, 1].set_ylabel(r"$c$")
            # Alignment vs difference in velocity
            axs[1, 2].scatter(yalign[k], ydiff[k], s=3)
            axs[1, 2].set_xlabel(r"$\cos \theta$")
            axs[1, 2].set_ylabel(r"$|\Delta \textbf{v}|~[\mathrm{km} / \mathrm{s}]$")  # noqa
            axs[1, 2].set_ylim(0.01)
            # Sky positions
            c = SkyCoord(ra=pos[k0, 1] * u.degree, dec=pos[k0, 2] * u.degree,
                         frame='icrs')
            axs[2, 0].scatter(c.galactic.l, c.galactic.b, s=3, zorder=1,
                              c="red")
            dist = str(round(pos[k0, 0], 0))
            axs[2, 0].text(c.galactic.l.to_value(), c.galactic.b.to_value(),
                           dist, fontsize=6, ha='left',
                           va='bottom', zorder=0)
            for cluster in clusters.keys():
                l, b = clusters[cluster]['l'], clusters[cluster]['b']
                axs[2, 0].scatter(l, b, c="blue", s=2, zorder=0)
                dist = round(clusters[cluster]["distance_mpc"], 0)
                key = f"{cluster} ({dist})"
                axs[2, 0].text(l, b, key, fontsize=6, ha='right',
                               va='bottom', zorder=0)
                axs[2, 0].axhspan(-10, 10, alpha=0.2)
            axs[2, 0].set_xlabel(r"$l$")
            axs[2, 0].set_ylabel(r"$b$")
            # Max overlap vs alignment
            axs[2, 1].scatter(w[k], yalign[k], s=3)
            axs[2, 1].set_xlabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            axs[2, 1].set_ylabel(r"$\cos \theta$")
            # Max overlap vs velocity difference
            axs[2, 2].scatter(w[k], ydiff[k], s=3)
            axs[2, 2].set_xlabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            axs[2, 2].set_ylabel(r"$|\Delta \textbf{v}|~[\mathrm{km} / \mathrm{s}]$")  # noqa
            # Alignment vs separation
            axs[3, 0].scatter(yalign[k], sep[:, k], s=3)
            axs[3, 0].set_xlabel(r"$\cos \theta$")
            axs[3, 0].set_ylabel(r"$\Delta R ~ [Mpc / h]$")
            # Separation vs max overlap
            paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
            axs[3, 1].scatter(sep[:, k], w[k], s=3)
            axs[3, 1].set_xlabel(r"$\Delta R ~ [Mpc / h]$")
            axs[3, 1].set_ylabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            # max overlap vs chain index
            y = numpy.abs(nsim0 - numpy.array(csiborgtools.summary.get_cross_sims(simname, nsim0, paths, min_logmass, smoothed=smoothed)))  # noqa
            axs[3, 2].scatter(w[k], y, s=3)
            axs[3, 2].set_xlabel(r"$\max_{b \in \mathcal{B}} \mathcal{O}_{a b}$")  # noqa
            axs[3, 2].set_ylabel(r"$\Delta N_{\rm chain}$")

            fig.tight_layout()
            fout = join(
                plt_utils.fout,
                f"vel_{k}_{nsim0}_{simname}_{min_logmass}.png")
            print(f"Saving to `{fout}`.")
            plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
            plt.close()

    with plt.style.context("science"):
        fig, axs = plt.subplots(nrows=2, ncols=1, figsize=(3.5, 1.5 * 2.625),
                                sharex=True)
        fig.subplots_adjust(wspace=0., hspace=0.0)

        x = numpy.asanyarray([numpy.nanmean(w[k]) for k in range(kmax)])
        # y = [csiborgtools.summary.find_peak(yalign[k], w[k])
        #      for k in range(kmax)]
        y = numpy.asanyarray([numpy.nansum(numpy.arccos(yalign[k]) * w[k]) / numpy.nansum(w[k]) for k in range(kmax)])  # noqa
        y *= 180 / numpy.pi
        c = numpy.log10(out_true_mass)

        # plt.errorbar(x, y, xerr=[xlower, xupper], fmt="o", ms=3, lw=0.5,)
        # plt.hexbin(x, y, gridsize=30, mincnt=1, bins="log")
        cax = axs[0].scatter(x, y, s=7.5, c=c, rasterized=True)
        axins = axs[0].inset_axes([0.0, 1.0, 1.0, 0.05])
        fig.colorbar(cax, cax=axins, orientation="horizontal",
                     label=r"$\log M_{\rm tot} ~ [M_\odot / h]$")
        axins.xaxis.tick_top()
        axins.xaxis.set_tick_params(labeltop=True)
        axins.xaxis.set_label_position("top")
       #  fig.colorbar(cax, ax=axs[0], pad=0,
                     # )
        # axs[0].set_colorbar(label=r"$\log M_{\rm tot} ~ [M_\odot / h]$", pad=0)

        # plt.ylim(top=90)

        m = numpy.isfinite(x) & numpy.isfinite(y)
        xbins = numpy.arange(x[m].min(), x[m].max(), 0.1)
        xcent = 0.5 * (xbins[1:] + xbins[:-1])
        ymed, yerr = plt_utils.compute_error_bars(x[m], y[m], xbins, sigma=1)
        axs[0].errorbar(xcent, ymed, yerr=yerr, fmt="o", ms=3, lw=0.5, c="red",
                        capsize=3, ls="--")

        y = numpy.asanyarray([numpy.nansum(ymag[k] * w[k]) / numpy.nansum(w[k]) / numpy.linalg.norm(out_true[k]) for k in range(kmax)])   # noqa

        m = y < 3

        axs[1].scatter(x[m], y[m], s=7.5, c=c[m], rasterized=True)
        m = numpy.isfinite(x) & numpy.isfinite(y)
        xbins = numpy.arange(x[m].min(), x[m].max(), 0.1)
        xcent = 0.5 * (xbins[1:] + xbins[:-1])
        ymed, yerr = plt_utils.compute_error_bars(x[m], y[m], xbins, sigma=1)
        axs[1].errorbar(xcent, ymed, yerr=yerr, fmt="o", ms=3, lw=0.5, c="red",
                        capsize=3, ls="--")

        label = r"$\langle |\textbf{v}_{\mathcal{B}}| \rangle_{\mathcal{B}} / |\textbf{v}_{\rm ref}|$"
        axs[1].set_ylabel(label)

        axs[0].set_ylabel(r"$\langle \theta \rangle_{\mathcal{B}} ~ [\deg]$")
        axs[1].set_xlabel(r"$\langle \max_{b \in \mathcal{B}} \mathcal{O}_{a b} \rangle_{\mathcal{B}}$")  # noqa

        fig.tight_layout(w_pad=0, h_pad=0)
        fout = join(
            plt_utils.fout,
            f"stat_vel_agreement_{kmax}_{nsim0}_{simname}_{min_logmass}.pdf")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                      Max's matching vs overlap success                      #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_matching_max_vs_overlap(simname, nsim0, min_logmass, mult):
    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)

    nsimsx = [nsim for nsim in paths.get_ics(simname) if nsim != nsim0]
    for i in trange(len(nsimsx), desc="Loading data"):
        nsimx = nsimsx[i]
        fpath = paths.match_max(simname, nsim0, nsimx, min_logmass,
                                mult=mult)

        data = numpy.load(fpath, allow_pickle=True)

        if i == 0:
            mass0 = data["mass0"]
            max_overlap = numpy.full((mass0.size, len(nsimsx)), numpy.nan)
            match_overlap = numpy.full((mass0.size, len(nsimsx)), numpy.nan)
            success = numpy.zeros((mass0.size, len(nsimsx)), numpy.bool_)

        max_overlap[:, i] = data["max_overlap"]
        match_overlap[:, i] = data["match_overlap"]
        success[:, i] = data["success"]

    return {"mass0": mass0, "max_overlap": max_overlap,
            "match_overlap": match_overlap, "success": success}


def matching_max_vs_overlap(min_logmass):
    left_edges = numpy.arange(min_logmass, 15, 0.1)

    with plt.style.context("science"):
        # fig, axs = plt.subplots(ncols=2, figsize=(2 * 3.5, 2.625))
        fig, axs = plt.subplots(ncols=1, figsize=(3.5, 2.625))
        axs = [axs]
        ax2 = axs[0].twinx()
        cols = plt.rcParams["axes.prop_cycle"].by_key()["color"]
        for n, mult in enumerate([2.5, 5., 7.5]):

            def make_y1_y2(simname):
                nsims = 100 if simname == "csiborg" else 9
                nsim0 = 7444 if simname == "csiborg" else 0
                x = get_matching_max_vs_overlap(simname,
                                                nsim0, min_logmass, mult=mult)

                mask = numpy.all(numpy.isfinite(x["max_overlap"]), axis=1)
                x["success"][~mask, :] = False

                mass0 = numpy.log10(x["mass0"])
                max_overlap = x["max_overlap"]
                match_overlap = x["match_overlap"]
                success = x["success"]

                nbins = len(left_edges)
                y = numpy.full((nbins, nsims), numpy.nan)
                y2 = numpy.full(nbins, numpy.nan)
                for i in range(nbins):
                    m = mass0 > left_edges[i]
                    for j in range(nsims):
                        y[i, j] = numpy.sum((max_overlap[m, j] == match_overlap[m, j]) & success[m, j])  # noqa
                        y[i, j] /= numpy.sum(success[m, j])

                    y2[i] = success[m, 0].mean()
                return y, y2

            offset = numpy.random.normal(0, 0.015)

            y1_csiborg, y2_csiborg = make_y1_y2("csiborg")

            ysummary = numpy.percentile(y1_csiborg, [16, 50, 84], axis=1)
            axs[0].plot(left_edges + offset, ysummary[1], c=cols[n],
                        label=r"${}~R_{{\rm 200c}}$".format(mult))
            ax2.plot(left_edges + offset, y2_csiborg, c=cols[n], ls="dotted",
                     zorder=0)

        axs[0].set_xlim(left_edges.min(), left_edges.max())
        axs[0].legend(ncols=1, loc="upper left")
        for i in range(1):
            axs[i].set_xlabel(r"$\log M_{\rm tot, min} ~ [M_\odot / h]$")

        axs[0].set_ylabel(r"$f_{\rm agreement}$")
        ax2.set_ylabel(r"$f_{\rm match}$")

        fig.tight_layout()
        fout = join(plt_utils.fout,
                    f"matching_max_agreement_{min_logmass}.pdf")
        print(f"Saving to `{fout}`.")
        fig.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


# --------------------------------------------------------------------------- #
###############################################################################
#                      Mass-concentration relation                            #
###############################################################################
# --------------------------------------------------------------------------- #


@cache_to_disk(120)
def get_mass_concentration(nsim, simname):
    cat = open_cat(nsim, simname)
    mass = cat["m200c"]
    conc = cat["conc"]
    return mass, conc


def mass_concentration(ext="pdf"):

    cosmology.setCosmology('WMAP5')
    xrange = numpy.linspace(12, 15, 100)
    cvir = concentration.concentration(10**xrange, '200c', 0.0,
                                       model='diemer19')

    with plt.style.context("science"):
        plt.figure()

        for nsim in tqdm([7444 + n * 24 for n in (0, 10, 20, 30, 40, 50, 60, 70, 80)]):  # noqa
            x, y = get_mass_concentration(nsim, "csiborg")
            m = numpy.isfinite(x) & numpy.isfinite(y)
            x = x[m]
            y = y[m]
            x = numpy.log10(x)

            xbins = numpy.arange(12, 15, 0.2)
            xcent = 0.5 * (xbins[1:] + xbins[:-1])
            ymed, yerr = plt_utils.compute_error_bars(x, y, xbins, 1)
            plt.plot(xcent, ymed, c="black", lw=0.5)
            # plt.errorbar(xcent, ymed, yerr=yerr, fmt="o", ms=3, lw=0.5,
            #              c="red", capsize=3)

        plt.plot(xrange, cvir, c="blue", lw=1, label="Diemer+19")
        plt.yscale("log")
        plt.legend()

        plt.tight_layout()
        fout = join(plt_utils.fout, "mass_concentration.png")
        print(f"Saving to `{fout}`.")
        plt.savefig(fout, dpi=plt_utils.dpi, bbox_inches="tight")
        plt.close()


if __name__ == "__main__":
    min_logmass = 13.25
    smoothed = True
    nbins = 10
    ext = "pdf"
    plot_quijote = False
    min_maxoverlap = 0.

    funcs = [
        # "get_overlap_summary",
        # "get_max_overlap_agreement",
        # "get_mtot_vs_all_pairoverlap",
        # "get_mtot_vs_maxpairoverlap",
        # "get_mass_vs_separation",
        # "get_expected_mass",
        # "get_expected_key",
        # "get_expected_single",
        # "get_expected_many_topmass",
        # "get_max_overlap_velocity",
        # "get_mass_concentration",
        ]
    for func in funcs:
        print(f"Cleaning up cache for `{func}`.")
        delete_disk_caches_for_function(func)

    if False:
        mtot_vs_all_pairoverlap(7444, "csiborg", min_logmass, smoothed,
                                nbins, ext=ext)
        mtot_vs_maxpairoverlap(7444, "csiborg", "fof_totpartmass", min_logmass,
                               smoothed, nbins, ext=ext)
        if plot_quijote:
            mtot_vs_all_pairoverlap(0, "quijote",  min_logmass, smoothed,
                                    nbins, ext=ext)
            mtot_vs_maxpairoverlap(0, "quijote", "group_mass", min_logmass,
                                   smoothed, nbins, ext=ext)

    if False:
        mtot_vs_maxpairoverlap_fraction(min_logmass, smoothed, nbins, ext=ext)

    if False:
        maximum_overlap_agreement(7444, "csiborg", min_logmass, smoothed)

    if False:
        summed_to_max_overlap(min_logmass, smoothed, nbins, ext=ext)

    if False:
        mtot_vs_summedpairoverlap(7444, "csiborg", min_logmass, smoothed,
                                  nbins, ext)
        if plot_quijote:
            mtot_vs_summedpairoverlap(0, "quijote", min_logmass, smoothed,
                                      nbins, ext)

    if False:
        mtot_vs_expected_mass(7444, "csiborg", min_logmass, smoothed, ext=ext)
        # if plot_quijote:
        #     mtot_vs_expected_mass(0, "quijote", min_logmass, smoothed, nbins,
        #                           max_prob_nomatch=1, ext=ext)

    if False:
        key = "conc"
        mtot_vs_expected_key(7444, "csiborg", min_logmass, key, smoothed, 15)
        # if plot_quijote:
        #     mtot_vs_expected_key(0, "quijote", min_logmass, key, smoothed,
        #                          nbins)

    if False:
        mass_vs_separation(7444, "csiborg", min_logmass, nbins, smoothed,
                           boxsize=677.7)
        # if plot_quijote:
        #     mass_vs_separation(0, 1, "quijote", min_logmass, nbins,
        #                        smoothed, boxsize=1000, plot_std=False)

    if True:
        matching_max_vs_overlap(min_logmass)

    if False:
        # mtot_vs_expected_single("max", 7444, "csiborg", min_logmass,
        #                         "totpartmass", True)
        # mtot_vs_expected_single("maxmass__0", 7444, "csiborg",
        #                         min_logmass, "lambda200c", True)
        for i in range(25):
            mtot_vs_expected_single(f"maxmass__{i}", 7444, "csiborg",
                                    min_logmass, "conc", True)
        # if plot_quijote:
        #     mtot_vs_expected_single("max", 0, "quijote", min_logmass,
        #                             "totpartmass", True, True)

    if False:
        mtot_vs_expected_single_panel("maxmass__0", 7444, "csiborg",
                                      min_logmass, True)

    if False:
        kmax = 100
        expected_in_tolerance(kmax, 7444, "csiborg", min_logmass, "conc", True)

    if False:
        kmax = 1000
        expected_in_velocity(kmax, 7444 + 24 * 30, "csiborg", min_logmass, smoothed)

    if False:
        mass_concentration()
