# Copyright (C) 2022 Richard Stiskalek, Harry Desmond
# 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.
"""
CSiBORG paths manager.
"""
import datetime
from glob import glob
from os import listdir, makedirs
from os.path import exists, getmtime, isdir, join
from re import search
from warnings import warn

import numpy


def check_directory(path):
    if not isdir(path):
        raise IOError(f"Invalid directory `{path}`!")


def try_create_directory(fdir):
    if not isdir(fdir):
        makedirs(fdir)
        warn(f"Created directory `{fdir}`.")


class Paths:
    """
    Paths manager for CSiBORG and Quijote simulations.

    Parameters
    ----------
    csiborg1_srcdir : str
        Path to the CSiBORG1 simulation directory.
    csiborg2_main_srcdir : str
        Path to the CSiBORG2 main simulation directory.
    csiborg2_random_srcdir : str
        Path to the CSiBORG2 random simulation directory.
    csiborg2_varysmall_srcdir : str
        Path to the CSiBORG2 varysmall simulation directory.
    manticore_srcdir : str
        Path to the MANTICORE simulation directory.
    postdir : str
        Path to the CSiBORG post-processing directory.
    quijote_dir : str
        Path to the Quijote simulation directory.
    borg1_dir : str
        Path to the BORG1 simulation directory.
    borg2_dir : str
        Path to the BORG2 simulation directory.
    tng300_1_dir : str
        Path to the TNG300-1 simulation directory.
    aux_cat_dir : str
        Path to the directory containing auxiliary catalogues.
    CF4_dir : str
        Path to the CosmicFlows4 directory with density & velocity fields.
    """
    def __init__(self,
                 csiborg1_srcdir,
                 csiborg2_main_srcdir,
                 csiborg2_random_srcdir,
                 csiborg2_varysmall_srcdir,
                 manticore_dir,
                 postdir,
                 quijote_dir,
                 borg1_dir,
                 borg2_dir,
                 tng300_1_dir,
                 aux_cat_dir,
                 CF4_dir,
                 ):
        self.csiborg1_srcdir = csiborg1_srcdir
        self.csiborg2_main_srcdir = csiborg2_main_srcdir
        self.csiborg2_random_srcdir = csiborg2_random_srcdir
        self.csiborg2_varysmall_srcdir = csiborg2_varysmall_srcdir
        self.manticore_dir = manticore_dir
        self.quijote_dir = quijote_dir
        self.borg1_dir = borg1_dir
        self.borg2_dir = borg2_dir
        self.tng300_1_dir = tng300_1_dir
        self.postdir = postdir
        self.aux_cat_dir = aux_cat_dir
        self.CF4_dir = CF4_dir

    def get_ics(self, simname):
        """Get available IC realisation IDs for a given simulation."""
        if simname == "csiborg1" or simname == "borg1":
            files = glob(join(self.csiborg1_srcdir, "chain_*"))
            files = [int(search(r'chain_(\d+)', f).group(1)) for f in files]
        elif simname == "csiborg2_main" or simname == "borg2":
            files = glob(join(self.csiborg2_main_srcdir, "chain_*"))
            files = [int(search(r'chain_(\d+)', f).group(1)) for f in files]
        elif simname == "borg2_all":
            files = glob(join(self.borg2_dir, "mcmc_*"))
            files = [int(search(r'mcmc_(\d+)', f).group(1)) for f in files]
        elif simname == "csiborg2_random":
            files = glob(join(self.csiborg2_random_srcdir, "chain_*"))
            files = [int(search(r'chain_(\d+)', f).group(1)) for f in files]
        elif simname == "csiborg2_varysmall":
            files = glob(join(self.csiborg2_varysmall_srcdir, "chain_*"))
            files = [int(search(r'chain_16417_(\d+)', f).group(1))
                     for f in files]
        elif "manticore" in simname:
            fdir = self.manticore_dir
            if simname == "manticore_2MPP_N128_DES_V1":
                fdir = join(fdir, "2MPP_N128_DES_V1", "resimulations", "R512")
                files = glob(join(fdir, "mcmc_*"))
                files = [int(search(r'mcmc_(\d+)', f).group(1)) for f in files]
            else:
                raise ValueError(f"Unknown MANTICORE simulation `{simname}`.")
        elif simname == "csiborg2X":
            # NOTE this too is preliminary
            fname = "/mnt/extraspace/rstiskalek/MANTICORE/resimulations/fields/2MPP_N128_DES_PROD/R512"  # noqa
            fdirs = listdir(fname)
            files = [int(search(r'\d+', fdir).group())
                     for fdir in fdirs if search(r'\d+', fdir)]
        elif simname == "quijote":
            files = glob(join(self.quijote_dir, "fiducial_processed",
                              "chain_*"))
            files = [int(search(r'chain_(\d+)', f).group(1)) for f in files]
        elif simname == "CF4":
            files = glob(join(self.CF4_dir, "CF4_new_128-z008_realization*_delta.fits"))  # noqa
            files = [search(r'realization(\d+)_delta\.fits', file).group(1)
                     for file in files if search(r'realization(\d+)_delta\.fits', file)]  # noqa
            files = [int(file) for file in files]
        elif simname in ["Carrick2015", "Lilow2024", "no_field", "CLONES"]:
            files = [0]
        elif "IndranilVoid" in simname:
            files = [0]
        else:
            raise ValueError(f"Unknown simulation name `{simname}`.")

        return numpy.sort(files)

    def get_snapshots(self, nsim, simname):
        """List available snapshots of simulation."""
        if simname == "csiborg1":
            snaps = glob(join(self.csiborg1_srcdir, f"chain_{nsim}",
                              "snapshot_*"))
            snaps = [int(search(r'snapshot_(\d+)', f).group(1)) for f in snaps]
            snaps = sorted(snaps)
        elif simname == "csiborg2_main":
            snaps = glob(join(self.csiborg2_main_srcdir, f"chain_{nsim}",
                              "output", "snapshot_*"))
            snaps = [int(search(r'snapshot_(\d+)', f).group(1))
                     for f in snaps]
            snaps = sorted(snaps)
        elif simname == "csiborg2_random":
            snaps = glob(join(self.csiborg2_random_srcdir, f"chain_{nsim}",
                              "output", "snapshot_*"))
            snaps = [int(search(r'snapshot_(\d+)', f).group(1))
                     for f in snaps]
            snaps = sorted(snaps)
        elif simname == "csiborg2_varysmall":
            snaps = glob(join(self.csiborg2_random_srcdir,
                              f"chain_16417_{str(nsim).zfill(3)}",
                              "snapshot_*"))
            snaps = [int(search(r'snapshot_16417_(\d+)', f).group(1))
                     for f in snaps]
            snaps = sorted(snaps)
        elif simname == "csiborg2X":
            raise ValueError("Snapshots not available for CSiBORG2X.")
        elif "manticore" in simname:
            raise ValueError("Snapshots not available for MANTICORE.")
        elif simname == "quijote":
            snaps = glob(join(self.quijote_dir, "fiducial_processed",
                              f"chain_{nsim}", "snapshot_*"))
            has_ics = any("snapshot_ICs" in f for f in snaps)
            snaps = [int(match.group(1))
                     for f in snaps if (match := search(r'snapshot_(\d+)', f))]
            snaps = sorted(snaps)
            if has_ics:
                snaps.insert(0, "ICs")
        else:
            raise ValueError(f"Unsupported simulation name `{simname}`.")
        return snaps

    def snapshot(self, nsnap, nsim, simname):
        """Path to a simulation snapshot."""
        if simname == "csiborg1":
            fpath = join(self.csiborg1_srcdir, f"chain_{nsim}",
                         f"snapshot_{str(nsnap).zfill(5)}.hdf5")
        elif simname == "csiborg2_main":
            fpath = join(self.csiborg2_main_srcdir, f"chain_{nsim}", "output",
                         f"snapshot_{str(nsnap).zfill(3)}.hdf5")
        elif simname == "csiborg2_random":
            fpath = join(self.csiborg2_random_srcdir, f"chain_{nsim}",
                         "output", f"snapshot_{str(nsnap).zfill(3)}.hdf5")
        elif simname == "csiborg2_varysmall":
            fpath = join(self.csiborg2_varysmall_srcdir,
                         f"chain_16417_{str(nsim).zfill(3)}", "output",
                         f"snapshot_{str(nsnap).zfill(3)}.hdf5")
        elif simname == "csiborg2X":
            raise ValueError("Snapshots not available for CSiBORG2X based on "
                             "Stephen's ICs.")
        elif "manticore" in simname:
            fdir = self.manticore_dir
            if simname == "manticore_2MPP_N128_DES_V1":
                if nsnap != 1:
                    raise ValueError("Only snapshot 1 is available for "
                                     "MANTICORE 2MPP_N128_DES_V1.")

                fdir = join(fdir, "2MPP_N128_DES_V1", "resimulations", "R512")
                nsnap_str = str(nsnap).zfill(4)
                fpath = join(fdir, f"mcmc_{nsim}", "swift_monofonic",
                             f"snap_{nsnap_str}", f"snap_{nsnap_str}.hdf5")
            else:
                raise ValueError(f"Unknown MANTICORE simulation `{simname}`.")
        elif simname == "quijote":
            fpath = join(self.quijote_dir, "fiducial_processed",
                         f"chain_{nsim}",
                         f"snapshot_{str(nsnap).zfill(3)}.hdf5")
        else:
            raise ValueError(f"Unsupported simulation name `{simname}`.")

        return fpath

    def snapshot_catalogue(self, nsnap, nsim, simname):
        """Path to the halo catalogue of a simulation snapshot."""
        if simname == "csiborg1":
            return join(self.csiborg1_srcdir, f"chain_{nsim}",
                        f"fof_{str(nsnap).zfill(5)}.hdf5")
        elif simname == "csiborg2_main":
            return join(self.csiborg2_main_srcdir, f"chain_{nsim}", "output",
                        f"fof_subhalo_tab_{str(nsnap).zfill(3)}.hdf5")
        elif simname == "csiborg2_random":
            return join(self.csiborg2_random_srcdir, f"chain_{nsim}", "output",
                        f"fof_subhalo_tab_{str(nsnap).zfill(3)}.hdf5")
        elif simname == "csiborg2_varysmall":
            return join(self.csiborg2_varysmall_srcdir,
                        f"chain_16417_{str(nsim).zfill(3)}", "output",
                        f"fof_subhalo_tab_{str(nsnap).zfill(3)}.hdf5")
        elif simname == "csiborg2X":
            raise ValueError("Snapshots not available for CSiBORG2X.")
        elif simname == "quijote":
            return join(self.quijote_dir, "fiducial_processed",
                        f"chain_{nsim}",
                        f"fof_{str(nsnap).zfill(3)}.hdf5")
        else:
            raise ValueError(f"Unsupported simulation name `{simname}`.")

    def external_halo_catalogue(self, name):
        """Path to an external halo catalogue."""
        if name == "MDPL2":
            return join(self.aux_cat_dir, "MDPL2_FOF_125.hdf5")
        else:
            raise ValueError(f"Unknown external FOF catalogue `{name}`.")

    def initial_lagpatch(self, nsim, simname):
        """
        Path to the Lagrangain patch information of a simulation for halos
        defined at z = 0.
        """
        if simname == "csiborg1":
            return join(self.csiborg1_srcdir, f"chain_{nsim}",
                        "initial_lagpatch.npy")
        elif simname == "csiborg2_main":
            return join(self.csiborg2_main_srcdir, "catalogues",
                        f"initial_lagpatch_{nsim}.npy")
        elif simname == "csiborg2_random":
            return join(self.csiborg2_random_srcdir, "catalogues",
                        f"initial_lagpatch_{nsim}.npy")
        elif simname == "csiborg2_varysmall":
            return join(self.csiborg2_varysmall_srcdir, "catalogues",
                        f"initial_lagpatch_{nsim}.npy")
        elif simname == "quijote":
            return join(self.quijote_dir, "fiducial_processed",
                        f"chain_{nsim}", "initial_lagpatch.npy")
        else:
            raise ValueError(f"Unsupported simulation name `{simname}`.")

    def trees(self, nsim, simname):
        """Path to the halo trees of a simulation snapshot."""
        if simname == "csiborg1":
            raise ValueError("Trees not available for CSiBORG1.")
        elif simname == "csiborg2_main":
            return join(self.csiborg2_main_srcdir, f"chain_{nsim}", "output",
                        "trees.hdf5")
        elif simname == "csiborg2_random":
            return join(self.csiborg2_random_srcdir, f"chain_{nsim}", "output",
                        "trees.hdf5")
        elif simname == "csiborg2_varysmall":
            return join(self.csiborg2_varysmall_srcdir,
                        f"chain_16417_{str(nsim).zfill(3)}", "output",
                        "trees.hdf5")
        elif simname == "quijote":
            raise ValueError("Trees not available for Quijote.")
        else:
            raise ValueError(f"Unsupported simulation name `{simname}`.")

    def overlap(self, simname, nsim0, nsimx, min_logmass, smoothed):
        """
        Path to the overlap files between two CSiBORG simulations.

        Parameters
        ----------
        simname : str
            Simulation name. Must be one of `csiborg` or `quijote`.
        nsim0 : int
            IC realisation index of the first simulation.
        nsimx : int
            IC realisation index of the second simulation.
        min_logmass : float
            Minimum log mass of halos to consider.
        smoothed : bool
            Whether the overlap is smoothed or not.

        Returns
        -------
        str
        """
        if "csiborg" in simname:
            fdir = join(self.postdir, "overlap")
        elif simname == "quijote":
            fdir = join(self.quijote_dir, "overlap")
        else:
            raise ValueError(f"Unknown simulation name `{simname}`.")

        try_create_directory(fdir)

        nsim0 = str(nsim0).zfill(5)
        nsimx = str(nsimx).zfill(5)
        min_logmass = float('%.4g' % min_logmass)

        fname = f"overlap_{simname}_{nsim0}_{nsimx}_{min_logmass}.npz"
        if smoothed:
            fname = fname.replace("overlap", "overlap_smoothed")
        return join(fdir, fname)

    def random_mah(self, simname, nsim):
        """Path to the files containing MAHs from random simulations."""
        fdir = join(self.postdir, "random_mah")
        try_create_directory(fdir)

        return join(fdir, f"random_mah_{simname}_{nsim}.hdf5")

    def match_max(self, simname, nsim0, nsimx, min_logmass, mult):
        """
        Path to the files containing matching based on [1].

        Parameters
        ----------
        simname : str
            Simulation name.
        nsim0 : int
            IC realisation index of the first simulation.
        nsimx : int
            IC realisation index of the second simulation.
        min_logmass : float
            Minimum log mass of halos to consider.
        mult : float
            Multiplicative search radius factor.

        Returns
        -------
        str

        References
        ----------
        [1] Maxwell L Hutt, Harry Desmond, Julien Devriendt, Adrianne Slyz; The
        effect of local Universe constraints on halo abundance and clustering;
        Monthly Notices of the Royal Astronomical Society, Volume 516, Issue 3,
        November 2022, Pages 3592–3601, https://doi.org/10.1093/mnras/stac2407
        """
        if simname == "csiborg":
            fdir = join(self.postdir, "match_max")
        elif simname == "quijote":
            fdir = join(self.quijote_dir, "match_max")
        else:
            ValueError(f"Unknown simulation name `{simname}`.")

        try_create_directory(fdir)

        nsim0 = str(nsim0).zfill(5)
        nsimx = str(nsimx).zfill(5)
        min_logmass = float('%.4g' % min_logmass)
        fname = f"match_max_{nsim0}_{nsimx}_{min_logmass}_{str(mult)}.npz"

        return join(fdir, fname)

    def field(self, kind, MAS, grid, nsim, simname):
        """Path to the files containing the calculated fields."""
        if simname == "borg2":
            return join(self.borg2_dir, f"mcmc_{nsim}.h5")

        if simname == "borg1":
            # NOTE Replace eventually with non-local paths
            return f"/mnt/zfsusers/hdesmond/BORG_final/mcmc_{nsim}.h5"

        if simname == "csiborg2X":
            basedir = "/mnt/extraspace/rstiskalek/MANTICORE/resimulations/fields/2MPP_N128_DES_PROD/R512"  # noqa
            if kind == "overdensity":
                return join(basedir, f"mcmc_{nsim}", "delta_field.hdf5")
            elif kind == "velocity":
                return join(basedir, f"mcmc_{nsim}", "vel_field.hdf5")
            else:
                raise ValueError(f"Unsupported CSiBORG2X field: `{kind}`.")

        if simname == "manticore_2MPP_N128_DES_V1":
            basedir = join(self.manticore_dir, "2MPP_N128_DES_V1",
                           "fields", "R512")
            return join(basedir, f"SPH_{nsim}.hdf5")

        if simname == "Carrick2015":
            basedir = "/mnt/extraspace/rstiskalek/catalogs"
            if kind == "overdensity":
                return join(basedir, "twompp_density_carrick2015.npy")
            elif kind == "velocity":
                return join(basedir, "twompp_velocity_carrick2015.npy")
            else:
                raise ValueError(f"Unsupported Carrick2015 field: `{kind}`.")

        if simname == "Lilow2024":
            basedir = "/mnt/extraspace/rstiskalek/catalogs"
            if kind == "overdensity":
                return join(basedir, "Lilow2024_density.npy")
            elif kind == "velocity":
                return join(basedir, "Lilow2024_xVelocity.npy")
            else:
                raise ValueError(f"Unsupported Lilow2024 field: `{kind}`.")

        if simname == "CF4":
            basedir = "/mnt/extraspace/rstiskalek/catalogs/CF4"
            if kind == "overdensity":
                return join(
                    basedir, f"CF4_new_128-z008_realization{nsim}_delta.fits")
            elif kind == "velocity":
                return join(
                    basedir,
                    f"CF4_new_128-z008_realization{nsim}_velocity.fits")
            else:
                raise ValueError(f"Unsupported CF4 field: `{kind}`.")

        if MAS == "SPH" and kind in ["density", "velocity"]:
            if simname == "csiborg1":
                return join(self.csiborg1_srcdir, "field",
                            f"sph_ramses_{str(nsim).zfill(5)}_{grid}.hdf5")
            elif simname == "csiborg2_main":
                return join(self.csiborg2_main_srcdir, "field",
                            f"chain_{nsim}_{grid}.hdf5")
            elif simname == "csiborg2_random":
                return join(self.csiborg2_random_srcdir, "field",
                            f"chain_{nsim}_{grid}.hdf5")
            elif simname == "csiborg2_varysmall":
                return join(self.csiborg2_varysmall_srcdir, "field",
                            f"chain_{nsim}_{grid}.hdf5")
            elif simname == "quijote":
                raise ValueError("SPH field not available for CSiBORG1.")
            elif simname == "CLONES":
                # NOTE eventually stop using local paths
                basedir = "/mnt/extraspace/rstiskalek/CLONES/s8"
                fname = "cf2gvpecc1pt5elmo73_sig6distribsbvoldi_RZA3Derrv2_512_500_ss8_zinit60_000.hdf5"  # noqa
                return join(basedir, fname)
            else:
                raise ValueError(f"Unsupported simulation name `{simname}`.")

        fdir = join(self.postdir, "environment")
        try_create_directory(fdir)

        fname = f"{kind}_{simname}_{MAS}_{str(nsim).zfill(5)}_{grid}.npy"

        return join(fdir, fname)

    def observer_peculiar_velocity(self, MAS, grid, nsim, simname):
        """
        Path to the files containing the observer peculiar velocity defined as
        the velocity of the centre of the box.
        """
        fdir = join(self.postdir, "environment")
        try_create_directory(fdir)
        fname = f"observer_peculiar_velocity_{simname}_{MAS}_{str(nsim).zfill(5)}_{grid}.npz"  # noqa
        return join(fdir, fname)

    def field_interpolated(self, survey, simname, nsim, kind, MAS, grid,
                           radial_scatter=None):
        """
        Path to the files containing the interpolated field for a given
        survey.

        Parameters
        ----------
        survey : str
            Survey name.
        simname : str
            Simulation name.
        nsim : int
            IC realisation index.
        kind : str
            Field type.
        MAS : str
           Mass-assignment scheme.
        grid : int
            Grid size.
        radial_scatter : float, optional
            Radial scatter added to the galaxy positions, only supported for
            TNG300-1.

        Returns
        -------
        str
        """
        # # In case the galaxy positions of TNG300-1 were scattered..
        if kind not in ["density", "potential", "radvel"]:
            raise ValueError("Unsupported field type.")

        fdir = join(self.postdir, "field_interpolated")
        try_create_directory(fdir)

        nsim = str(nsim).zfill(5)
        fname = join(fdir, f"{survey}_{simname}_{kind}_{MAS}_{nsim}_{grid}.npz")  # noqa

        if simname == "TNG300-1" and radial_scatter is not None:
            fname = fname.replace(".npz", f"_scatter{radial_scatter}.npz")

        return fname

    def cross_nearest(self, simname, run, kind, nsim=None, nobs=None):
        """
        Path to the files containing distance from a halo in a reference
        simulation to the nearest halo from a cross simulation.

        Parameters
        ----------
        simname : str
            Simulation name. Must be one of: `csiborg`, `quijote`.
        run : str
            Run name.
        kind : str
            Whether raw distances or counts in bins. Must be one of `dist`,
            `bin_dist` or `tot_counts`.
        nsim : int, optional
            IC realisation index.
        nobs : int, optional
            Fiducial observer index in Quijote simulations.

        Returns
        -------
        str
        """
        assert simname in ["csiborg", "quijote"]
        assert kind in ["dist", "bin_dist", "tot_counts"]
        fdir = join(self.postdir, "nearest_neighbour")
        try_create_directory(fdir)

        if nsim is not None:
            if simname == "csiborg":
                nsim = str(nsim).zfill(5)
            else:
                nsim = self.quijote_fiducial_nsim(nsim, nobs)
            return join(fdir, f"{simname}_nn_{kind}_{nsim}_{run}.npz")

        files = glob(join(fdir, f"{simname}_nn_{kind}_*"))
        run = "_" + run
        return [f for f in files if run in f]

    def knnauto(self, simname, run, nsim=None, nobs=None):
        """
        Path to the `knn` auto-correlation files. If `nsim` is not specified
        returns a list of files for this run for all available simulations.

        Parameters
        ----------
        simname : str
            Simulation name. Must be either `csiborg` or `quijote`.
        run : str
            Run name.
        nsim : int, optional
            IC realisation index.
        nobs : int, optional
            Fiducial observer index in Quijote simulations.

        Returns
        -------
        str
        """
        assert simname in ["csiborg", "quijote"]
        fdir = join(self.postdir, "knn", "auto")
        try_create_directory(fdir)

        if nsim is not None:
            if simname == "csiborg":
                nsim = str(nsim).zfill(5)
            else:
                nsim = self.quijote_fiducial_nsim(nsim, nobs)
            return join(fdir, f"{simname}_knncdf_{nsim}_{run}.p")

        files = glob(join(fdir, f"{simname}_knncdf*"))
        run = "_" + run
        return [f for f in files if run in f]

    def knncross(self, simname, run, nsims=None):
        """
        Path to the `knn` cross-correlation files. If `nsims` is not specified
        returns a list of files for this run for all available simulations.

        Parameters
        ----------
        simname : str
            Simulation name. Must be either `csiborg` or `quijote`.
        run : str
            Type of run.
        nsims : len-2 tuple of int, optional
            IC realisation indices.

        Returns
        -------
        str
        """
        fdir = join(self.postdir, "knn", "cross")
        try_create_directory(fdir)

        if nsims is not None:
            assert isinstance(nsims, (list, tuple)) and len(nsims) == 2
            nsim0 = str(nsims[0]).zfill(5)
            nsimx = str(nsims[1]).zfill(5)
            return join(fdir, f"{simname}_knncdf_{nsim0}_{nsimx}__{run}.p")

        files = glob(join(fdir, f"{simname}_knncdf*"))
        run = "_" + run
        return [f for f in files if run in f]

    def tpcfauto(self, simname, run, nsim=None):
        """
        Path to the `tpcf` auto-correlation files. If `nsim` is not specified
        returns a list of files for this run for all available simulations.

        Parameters
        ----------
        simname : str
            Simulation name. Must be either `csiborg` or `quijote`.
        run : str
            Type of run.
        nsim : int, optional
            IC realisation index.

        Returns
        -------
        str
        """
        fdir = join(self.postdir, "tpcf", "auto")
        try_create_directory(fdir)

        if nsim is not None:
            return join(fdir, f"{simname}_tpcf{str(nsim).zfill(5)}_{run}.p")

        files = glob(join(fdir, f"{simname}_tpcf*"))
        run = "__" + run
        return [f for f in files if run in f]

    def tng300_1(self):
        """
        Path to the TNG300-1 simulation directory.

        Returns
        -------
        str
        """
        return self.tng300_1_dir

    def field_los(self, simname, catalogue):
        """
        Path to the files containing the line-of-sight fields.

        Parameters
        ----------
        simname : str
            Simulation name.
        catalogue : str
            Catalogue name.

        Returns
        -------
        str
        """
        fdir = join(self.postdir, "field_los")
        try_create_directory(fdir)
        return join(fdir, f"los_{catalogue}_{simname}.hdf5")

    def flow_validation(self, fdir, simname, catalogue, inference_method,
                        smooth=None, nsim=None, zcmb_min=None, zcmb_max=None,
                        mag_selection=None,  sample_alpha=False,
                        sample_beta=False, no_Vext=None,
                        sample_Vmono=False, sample_mag_dipole=False,
                        sample_curvature=False, absolute_calibration=None,
                        sample_Vmag_vax=False, verbose_print=True):
        """Flow validation file path."""
        if isinstance(catalogue, list) and len(catalogue) == 1:
            catalogue = catalogue[0]

        if isinstance(catalogue, list):
            catalogue = "_".join(catalogue)

        if smooth == 0:
            smooth = None

        fname = f"samples_{simname}_{catalogue}_{inference_method}_"

        keys = ["smooth", "nsim", "zcmb_min", "zcmb_max", "mag_selection",
                "sample_alpha", "sample_beta", "no_Vext", "sample_Vmono",
                "sample_mag_dipole", "sample_curvature",
                "sample_Vmag_vax", "absolute_calibration"]
        values = [smooth, nsim, zcmb_min, zcmb_max, mag_selection,
                  sample_alpha, sample_beta, no_Vext, sample_Vmono,
                  sample_mag_dipole, sample_curvature,
                  sample_Vmag_vax, absolute_calibration]

        for key, value in zip(keys, values):

            if isinstance(value, bool):
                if value:
                    fname += f"{key}_"
            elif value is not None:
                fname += f"{key}_{value}_"

        fname = fname.strip("_")
        fname = join(fdir, f"{fname}.hdf5")
        # Print the last modified time of the file if it exists.
        if verbose_print and exists(fname):
            mtime = getmtime(fname)
            mtime = datetime.datetime.fromtimestamp(mtime)
            mtime = mtime.strftime("%d/%m/%Y %H:%M:%S")
            print(f"File:          {fname}")
            print(f"Last modified: {mtime}")

        return fname

    def field_projected(self, simname, kind):
        """
        Path to the files containing the projected fields on the sky.

        Parameters
        ----------
        simname : str
            Simulation name.
        kind : str
            Field type.

        Returns
        -------
        str
        """
        fdir = join(self.postdir, "field_projected")
        try_create_directory(fdir)
        return join(fdir, f"{simname}_{kind}_volume_weighted.hdf5")
