# 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.
"""
A script to calculate the bulk flow in Quijote simulations from either
particles or FoF haloes and to also save the resulting smaller halo catalogues.

If `Rmin > 0` the bulk flows computed from projected radial velocities are
wrong, but the 3D volume average bulk flows are still correct.
"""
from datetime import datetime
from os.path import join

import csiborgtools
import numpy as np
from mpi4py import MPI
from taskmaster import work_delegation  # noqa
from warnings import catch_warnings, simplefilter
from h5py import File
from sklearn.neighbors import NearestNeighbors


###############################################################################
#                Read in information about the simulation                     #
###############################################################################


def t():
    return datetime.now()


def get_data(nsim, verbose=True):
    if verbose:
        print(f"{t()}: reading particles of simulation `{nsim}`.")
    reader = csiborgtools.read.QuijoteSnapshot(nsim, 4, paths)
    part_pos = reader.coordinates().astype(np.float64)
    part_vel = reader.velocities().astype(np.float64)

    if verbose:
        print(f"{t()}: reading haloes of simulation `{nsim}`.")
    reader = csiborgtools.read.QuijoteCatalogue(nsim)
    halo_pos = reader.coordinates
    halo_vel = reader.velocities
    halo_mass = reader.totmass

    return part_pos, part_vel, halo_pos, halo_vel, halo_mass


def volume_bulk_flow(rdist, mass, vel, distances):
    out = csiborgtools.field.particles_enclosed_momentum(
        rdist, mass, vel, distances)
    with catch_warnings():
        simplefilter("ignore", category=RuntimeWarning)
        out /= csiborgtools.field.particles_enclosed_mass(
            rdist, mass, distances)[:, np.newaxis]

    return out


###############################################################################
#                       Main & command line interface                         #
###############################################################################


def main(nsim, folder, fname_basis, Rmin, Rmax, subtract_observer_velocity,
         verbose=True):
    boxsize = csiborgtools.simname2boxsize("quijote")
    observers = csiborgtools.read.fiducial_observers(boxsize, Rmax)
    distances = np.linspace(0, Rmax, 101)[1:]
    part_pos, part_vel, halo_pos, halo_vel, halo_mass = get_data(nsim, verbose)

    if verbose:
        print(f"{t()}: Fitting the particle and halo trees of simulation `{nsim}`.")  # noqa
    part_tree = NearestNeighbors().fit(part_pos)
    halo_tree = NearestNeighbors().fit(halo_pos)

    samples = {}
    bf_volume_part = np.full((len(observers), len(distances), 3), np.nan)
    bf_volume_halo = np.full_like(bf_volume_part, np.nan)
    bf_volume_halo_uniform = np.full_like(bf_volume_part, np.nan)
    bf_vrad_weighted_part = np.full_like(bf_volume_part, np.nan)
    bf_vrad_weighted_halo_uniform = np.full_like(bf_volume_part, np.nan)
    bf_vrad_weighted_halo = np.full_like(bf_volume_part, np.nan)
    obs_vel = np.full((len(observers), 3), np.nan)

    for i in range(len(observers)):
        print(f"{t()}: Calculating bulk flow for observer {i + 1} of simulation {nsim}.")  # noqa

        # Select particles within Rmax of the observer
        rdist_part, indxs = part_tree.radius_neighbors(
            np.asarray(observers[i]).reshape(1, -1), Rmax,
            return_distance=True, sort_results=True)
        rdist_part, indxs = rdist_part[0], indxs[0]

        # And only the ones that are above Rmin
        mask = rdist_part > Rmin
        rdist_part = rdist_part[mask]
        indxs = indxs[mask]

        part_pos_current = part_pos[indxs] - observers[i]
        part_vel_current = part_vel[indxs]
        # Quijote particle masses are all equal
        part_mass = np.ones_like(rdist_part)

        # Select haloes within Rmax of the observer
        rdist_halo, indxs = halo_tree.radius_neighbors(
            np.asarray(observers[i]).reshape(1, -1), Rmax,
            return_distance=True, sort_results=True)
        rdist_halo, indxs = rdist_halo[0], indxs[0]
        mask = rdist_halo > Rmin
        rdist_halo = rdist_halo[mask]
        indxs = indxs[mask]

        halo_pos_current = halo_pos[indxs] - observers[i]
        halo_vel_current = halo_vel[indxs]
        halo_mass_current = halo_mass[indxs]

        # Subtract the observer velocity
        rscale = 2.0  # Mpc / h
        weights = np.exp(-0.5 * (rdist_part / rscale)**2)
        obs_vel_x = np.average(part_vel_current[:, 0], weights=weights)
        obs_vel_y = np.average(part_vel_current[:, 1], weights=weights)
        obs_vel_z = np.average(part_vel_current[:, 2], weights=weights)

        obs_vel[i, 0] = obs_vel_x
        obs_vel[i, 1] = obs_vel_y
        obs_vel[i, 2] = obs_vel_z

        if subtract_observer_velocity:
            part_vel_current[:, 0] -= obs_vel_x
            part_vel_current[:, 1] -= obs_vel_y
            part_vel_current[:, 2] -= obs_vel_z

            halo_vel_current[:, 0] -= obs_vel_x
            halo_vel_current[:, 1] -= obs_vel_y
            halo_vel_current[:, 2] -= obs_vel_z

        # Calculate the volume average bulk flows
        bf_volume_part[i, ...] = volume_bulk_flow(
            rdist_part, part_mass, part_vel_current, distances)
        bf_volume_halo[i, ...] = volume_bulk_flow(
            rdist_halo, halo_mass_current, halo_vel_current, distances)
        bf_volume_halo_uniform[i, ...] = volume_bulk_flow(
            rdist_halo, np.ones_like(halo_mass_current), halo_vel_current,
            distances)
        bf_vrad_weighted_part[i, ...] = csiborgtools.field.bulkflow_peery2018(
            rdist_part, part_mass, part_pos_current, part_vel_current,
            distances, weights="1/r^2", verbose=False)

        # Calculate the bulk flow from projected velocities w. 1/r^2 weights
        bf_vrad_weighted_halo_uniform[i, ...] = csiborgtools.field.bulkflow_peery2018(  # noqa
            rdist_halo, np.ones_like(halo_mass_current), halo_pos_current,
            halo_vel_current, distances, weights="1/r^2", verbose=False)
        bf_vrad_weighted_halo[i, ...] = csiborgtools.field.bulkflow_peery2018(
            rdist_halo, halo_mass_current, halo_pos_current,
            halo_vel_current, distances, weights="1/r^2", verbose=False)

        # Store the haloes around this observer
        samples[i] = {
            "halo_pos": halo_pos_current,
            "halo_vel": halo_vel_current,
            "halo_mass": halo_mass_current}

    # Finally save the output
    fname = join(folder, f"{fname_basis}_{nsim}.hdf5")
    if verbose:
        print(f"Saving to `{fname}`.")
    with File(fname, 'w') as f:
        f["distances"] = distances
        f["bf_volume_part"] = bf_volume_part
        f["bf_volume_halo"] = bf_volume_halo
        f["bf_vrad_weighted_part"] = bf_vrad_weighted_part
        f["bf_volume_halo_uniform"] = bf_volume_halo_uniform
        f["bf_vrad_weighted_halo_uniform"] = bf_vrad_weighted_halo_uniform
        f["bf_vrad_weighted_halo"] = bf_vrad_weighted_halo
        f["obs_vel"] = obs_vel

        for i in range(len(observers)):
            g = f.create_group(f"obs_{str(i)}")
            g["halo_pos"] = samples[i]["halo_pos"]
            g["halo_vel"] = samples[i]["halo_vel"]
            g["halo_mass"] = samples[i]["halo_mass"]


if __name__ == "__main__":
    Rmin = 0
    Rmax = 150
    subtract_observer_velocity = True
    folder = "/mnt/extraspace/rstiskalek/quijote/BulkFlow_fiducial"
    fname_basis = "sBF_nsim" if subtract_observer_velocity else "BF_nsim"

    comm = MPI.COMM_WORLD
    rank = comm.Get_rank()

    paths = csiborgtools.read.Paths(**csiborgtools.paths_glamdring)
    nsims = list(paths.get_ics("quijote"))

    def main_wrapper(nsim):
        main(nsim, folder, fname_basis, Rmin, Rmax, subtract_observer_velocity,
             verbose=rank == 0)

    if rank == 0:
        print(f"Running with {len(nsims)} Quijote simulations.")

    comm.Barrier()
    work_delegation(main_wrapper, nsims, comm, master_verbose=True)
