import os
import re
import weakref

import numpy as np

from yt.data_objects.index_subobjects.grid_patch import AMRGridPatch
from yt.data_objects.static_output import Dataset
from yt.fields.field_info_container import FieldInfoContainer
from yt.funcs import mylog, setdefaultattr
from yt.geometry.api import Geometry
from yt.geometry.grid_geometry_handler import GridIndex
from yt.utilities.file_handler import HDF5FileHandler, valid_hdf5_signature
from yt.utilities.lib.misc_utilities import get_box_grids_level
from yt.utilities.on_demand_imports import _h5py as h5py
from yt.utilities.parallel_tools.parallel_analysis_interface import parallel_root_only

from .fields import (
    ChomboFieldInfo,
    ChomboPICFieldInfo1D,
    ChomboPICFieldInfo2D,
    ChomboPICFieldInfo3D,
    Orion2FieldInfo,
    PlutoFieldInfo,
)


def is_chombo_hdf5(fn):
    try:
        with h5py.File(fn, mode="r") as fileh:
            valid = "Chombo_global" in fileh["/"]
    except (KeyError, OSError, ImportError):
        return False
    return valid


class ChomboGrid(AMRGridPatch):
    _id_offset = 0
    __slots__ = ["_level_id", "stop_index"]

    def __init__(self, id, index, level, start, stop):
        AMRGridPatch.__init__(self, id, filename=index.index_filename, index=index)
        self._parent_id = []
        self._children_ids = []
        self.Level = level
        self.ActiveDimensions = stop - start + 1

    def get_global_startindex(self):
        """
        Return the integer starting index for each dimension at the current
        level.

        """
        if self.start_index is not None:
            return self.start_index
        if self.Parent is None:
            iLE = self.LeftEdge - self.ds.domain_left_edge
            start_index = iLE / self.dds
            return np.rint(start_index).astype("int64").ravel()
        pdx = self.Parent[0].dds
        start_index = (self.Parent[0].get_global_startindex()) + np.rint(
            (self.LeftEdge - self.Parent[0].LeftEdge) / pdx
        )
        self.start_index = (start_index * self.ds.refine_by).astype("int64").ravel()
        return self.start_index

    def _setup_dx(self):
        # has already been read in and stored in index
        self.dds = self.ds.arr(self.index.dds_list[self.Level], "code_length")

    @property
    def Parent(self):
        if len(self._parent_id) == 0:
            return None
        return [self.index.grids[pid - self._id_offset] for pid in self._parent_id]

    @property
    def Children(self):
        return [self.index.grids[cid - self._id_offset] for cid in self._children_ids]


class ChomboHierarchy(GridIndex):
    grid = ChomboGrid
    _data_file = None

    def __init__(self, ds, dataset_type="chombo_hdf5"):
        self.domain_left_edge = ds.domain_left_edge
        self.domain_right_edge = ds.domain_right_edge
        self.dataset_type = dataset_type
        self.field_indexes = {}
        self.dataset = weakref.proxy(ds)
        # for now, the index file is the dataset!
        self.index_filename = os.path.abspath(self.dataset.parameter_filename)
        self.directory = ds.directory
        self._handle = ds._handle

        self._levels = [key for key in self._handle.keys() if key.startswith("level")]

        GridIndex.__init__(self, ds, dataset_type)

        self._read_particles()

    def _read_particles(self):
        # only do anything if the dataset contains particles
        if not any(f[1].startswith("particle_") for f in self.field_list):
            return

        self.num_particles = 0
        particles_per_grid = []
        for key, val in self._handle.items():
            if key.startswith("level"):
                level_particles = val["particles:offsets"][:]
                self.num_particles += level_particles.sum()
                particles_per_grid = np.concatenate(
                    (particles_per_grid, level_particles)
                )

        for i, _grid in enumerate(self.grids):
            self.grids[i].NumberOfParticles = particles_per_grid[i]
            self.grid_particle_count[i] = particles_per_grid[i]

        assert self.num_particles == self.grid_particle_count.sum()

    # Chombo datasets, by themselves, have no "known" fields. However,
    # we will look for "fluid" fields by finding the string "component" in
    # the output file, and "particle" fields by finding the string "particle".
    def _detect_output_fields(self):
        # look for fluid fields
        output_fields = []
        for key, val in self._handle.attrs.items():
            if key.startswith("component"):
                output_fields.append(val.decode("ascii"))
        self.field_list = [("chombo", c) for c in output_fields]

        # look for particle fields
        particle_fields = []
        for key, val in self._handle.attrs.items():
            if key.startswith("particle"):
                particle_fields.append(val.decode("ascii"))
        self.field_list.extend([("io", c) for c in particle_fields])

    def _count_grids(self):
        self.num_grids = 0
        for lev in self._levels:
            d = self._handle[lev]
            if "Processors" in d:
                self.num_grids += d["Processors"].len()
            elif "boxes" in d:
                self.num_grids += d["boxes"].len()
            else:
                raise RuntimeError("Unknown file specification")

    def _parse_index(self):
        f = self._handle  # shortcut
        self.max_level = f.attrs["num_levels"] - 1

        grids = []
        self.dds_list = []
        i = 0
        D = self.dataset.dimensionality
        for lev_index, lev in enumerate(self._levels):
            level_number = int(re.match(r"level_(\d+)", lev).groups()[0])
            try:
                boxes = f[lev]["boxes"][()]
            except KeyError:
                boxes = f[lev]["particles:boxes"][()]
            dx = f[lev].attrs["dx"]
            self.dds_list.append(dx * np.ones(3))

            if D == 1:
                self.dds_list[lev_index][1] = 1.0
                self.dds_list[lev_index][2] = 1.0

            if D == 2:
                self.dds_list[lev_index][2] = 1.0

            for level_id, box in enumerate(boxes):
                si = np.array([box[f"lo_{ax}"] for ax in "ijk"[:D]])
                ei = np.array([box[f"hi_{ax}"] for ax in "ijk"[:D]])

                if D == 1:
                    si = np.concatenate((si, [0.0, 0.0]))
                    ei = np.concatenate((ei, [0.0, 0.0]))

                if D == 2:
                    si = np.concatenate((si, [0.0]))
                    ei = np.concatenate((ei, [0.0]))

                pg = self.grid(len(grids), self, level=level_number, start=si, stop=ei)
                grids.append(pg)
                grids[-1]._level_id = level_id
                self.grid_levels[i] = level_number
                self.grid_left_edge[i] = self.dds_list[lev_index] * si.astype(
                    self.float_type
                )
                self.grid_right_edge[i] = self.dds_list[lev_index] * (
                    ei.astype(self.float_type) + 1
                )
                self.grid_particle_count[i] = 0
                self.grid_dimensions[i] = ei - si + 1
                i += 1
        self.grids = np.empty(len(grids), dtype="object")
        for gi, g in enumerate(grids):
            self.grids[gi] = g

    def _populate_grid_objects(self):
        self._reconstruct_parent_child()
        for g in self.grids:
            g._prepare_grid()
            g._setup_dx()

    def _setup_derived_fields(self):
        self.derived_field_list = []

    def _reconstruct_parent_child(self):
        mask = np.empty(len(self.grids), dtype="int32")
        mylog.debug("First pass; identifying child grids")
        for i, grid in enumerate(self.grids):
            get_box_grids_level(
                self.grid_left_edge[i, :],
                self.grid_right_edge[i, :],
                self.grid_levels[i].item() + 1,
                self.grid_left_edge,
                self.grid_right_edge,
                self.grid_levels,
                mask,
            )
            ids = np.where(mask.astype("bool"))  # where is a tuple
            grid._children_ids = ids[0] + grid._id_offset
        mylog.debug("Second pass; identifying parents")
        for i, grid in enumerate(self.grids):  # Second pass
            for child in grid.Children:
                child._parent_id.append(i + grid._id_offset)


class ChomboDataset(Dataset):
    _load_requirements = ["h5py"]
    _index_class = ChomboHierarchy
    _field_info_class: type[FieldInfoContainer] = ChomboFieldInfo

    def __init__(
        self,
        filename,
        dataset_type="chombo_hdf5",
        storage_filename=None,
        ini_filename=None,
        units_override=None,
        unit_system="cgs",
        default_species_fields=None,
    ):
        self.fluid_types += ("chombo",)
        self._handle = HDF5FileHandler(filename)
        self.dataset_type = dataset_type

        self.geometry = Geometry.CARTESIAN
        self.ini_filename = ini_filename
        self.fullplotdir = os.path.abspath(filename)
        Dataset.__init__(
            self,
            filename,
            self.dataset_type,
            units_override=units_override,
            unit_system=unit_system,
            default_species_fields=default_species_fields,
        )
        self.storage_filename = storage_filename
        self.cosmological_simulation = False

        # These are parameters that I very much wish to get rid of.
        self.parameters["HydroMethod"] = "chombo"
        self.parameters["DualEnergyFormalism"] = 0
        self.parameters["EOSType"] = -1  # default

    def _set_code_unit_attributes(self):
        if not hasattr(self, "length_unit"):
            mylog.warning("Setting code length unit to be 1.0 cm")
        if not hasattr(self, "mass_unit"):
            mylog.warning("Setting code mass unit to be 1.0 g")
        if not hasattr(self, "time_unit"):
            mylog.warning("Setting code time unit to be 1.0 s")
        setdefaultattr(self, "length_unit", self.quan(1.0, "cm"))
        setdefaultattr(self, "mass_unit", self.quan(1.0, "g"))
        setdefaultattr(self, "time_unit", self.quan(1.0, "s"))
        setdefaultattr(self, "magnetic_unit", self.quan(np.sqrt(4.0 * np.pi), "gauss"))
        setdefaultattr(self, "velocity_unit", self.length_unit / self.time_unit)

    def _localize(self, f, default):
        if f is None:
            return os.path.join(self.directory, default)
        return f

    def _parse_parameter_file(self):
        self.dimensionality = self._handle["Chombo_global/"].attrs["SpaceDim"]
        self.domain_left_edge = self._calc_left_edge()
        self.domain_right_edge = self._calc_right_edge()
        self.domain_dimensions = self._calc_domain_dimensions()

        # if a lower-dimensional dataset, set up pseudo-3D stuff here.
        if self.dimensionality == 1:
            self.domain_left_edge = np.concatenate((self.domain_left_edge, [0.0, 0.0]))
            self.domain_right_edge = np.concatenate(
                (self.domain_right_edge, [1.0, 1.0])
            )
            self.domain_dimensions = np.concatenate((self.domain_dimensions, [1, 1]))

        if self.dimensionality == 2:
            self.domain_left_edge = np.concatenate((self.domain_left_edge, [0.0]))
            self.domain_right_edge = np.concatenate((self.domain_right_edge, [1.0]))
            self.domain_dimensions = np.concatenate((self.domain_dimensions, [1]))

        self.refine_by = self._handle["/level_0"].attrs["ref_ratio"]
        self._determine_periodic()
        self._determine_current_time()

    def _determine_current_time(self):
        # some datasets will not be time-dependent, and to make
        # matters worse, the simulation time is not always
        # stored in the same place in the hdf file! Make
        # sure we handle that here.
        try:
            self.current_time = self._handle.attrs["time"]
        except KeyError:
            try:
                self.current_time = self._handle["/level_0"].attrs["time"]
            except KeyError:
                self.current_time = 0.0

    def _determine_periodic(self):
        # we default to true unless the HDF5 file says otherwise
        is_periodic = np.array([True, True, True])
        for dir in range(self.dimensionality):
            try:
                is_periodic[dir] = self._handle["/level_0"].attrs[
                    "is_periodic_%d" % dir
                ]
            except KeyError:
                is_periodic[dir] = True
        self._periodicity = tuple(is_periodic)

    def _calc_left_edge(self):
        fileh = self._handle
        dx0 = fileh["/level_0"].attrs["dx"]
        D = self.dimensionality
        LE = dx0 * ((np.array(list(fileh["/level_0"].attrs["prob_domain"])))[0:D])
        return LE

    def _calc_right_edge(self):
        fileh = self._handle
        dx0 = fileh["/level_0"].attrs["dx"]
        D = self.dimensionality
        RE = dx0 * ((np.array(list(fileh["/level_0"].attrs["prob_domain"])))[D:] + 1)
        return RE

    def _calc_domain_dimensions(self):
        fileh = self._handle
        D = self.dimensionality
        L_index = (np.array(list(fileh["/level_0"].attrs["prob_domain"])))[0:D]
        R_index = (np.array(list(fileh["/level_0"].attrs["prob_domain"])))[D:] + 1
        return R_index - L_index

    @classmethod
    def _is_valid(cls, filename: str, *args, **kwargs) -> bool:
        if cls._missing_load_requirements():
            return False

        if not is_chombo_hdf5(filename):
            return False

        pluto_ini_file_exists = False
        orion2_ini_file_exists = False

        if isinstance(filename, str):
            dir_name = os.path.dirname(os.path.abspath(filename))
            pluto_ini_filename = os.path.join(dir_name, "pluto.ini")
            orion2_ini_filename = os.path.join(dir_name, "orion2.ini")
            pluto_ini_file_exists = os.path.isfile(pluto_ini_filename)
            orion2_ini_file_exists = os.path.isfile(orion2_ini_filename)

        if not (pluto_ini_file_exists or orion2_ini_file_exists):
            try:
                fileh = h5py.File(filename, mode="r")
                valid = "Chombo_global" in fileh["/"]
                # ORION2 simulations should always have this:
                valid = valid and "CeilVA_mass" not in fileh.attrs.keys()
                valid = valid and "Charm_global" not in fileh.keys()
                fileh.close()
                return valid
            except Exception:
                pass
        return False

    @parallel_root_only
    def print_key_parameters(self):
        for a in [
            "current_time",
            "domain_dimensions",
            "domain_left_edge",
            "domain_right_edge",
        ]:
            if not hasattr(self, a):
                mylog.error("Missing %s in parameter file definition!", a)
                continue
            v = getattr(self, a)
            mylog.info("Parameters: %-25s = %s", a, v)


class PlutoHierarchy(ChomboHierarchy):
    def __init__(self, ds, dataset_type="chombo_hdf5"):
        ChomboHierarchy.__init__(self, ds, dataset_type)

    def _parse_index(self):
        f = self._handle  # shortcut
        self.max_level = f.attrs["num_levels"] - 1

        grids = []
        self.dds_list = []
        i = 0
        D = self.dataset.dimensionality
        for lev_index, lev in enumerate(self._levels):
            level_number = int(re.match(r"level_(\d+)", lev).groups()[0])
            try:
                boxes = f[lev]["boxes"][()]
            except KeyError:
                boxes = f[lev]["particles:boxes"][()]
            dx = f[lev].attrs["dx"]
            self.dds_list.append(dx * np.ones(3))

            if D == 1:
                self.dds_list[lev_index][1] = 1.0
                self.dds_list[lev_index][2] = 1.0

            if D == 2:
                self.dds_list[lev_index][2] = 1.0

            for level_id, box in enumerate(boxes):
                si = np.array([box[f"lo_{ax}"] for ax in "ijk"[:D]])
                ei = np.array([box[f"hi_{ax}"] for ax in "ijk"[:D]])

                if D == 1:
                    si = np.concatenate((si, [0.0, 0.0]))
                    ei = np.concatenate((ei, [0.0, 0.0]))

                if D == 2:
                    si = np.concatenate((si, [0.0]))
                    ei = np.concatenate((ei, [0.0]))

                pg = self.grid(len(grids), self, level=level_number, start=si, stop=ei)
                grids.append(pg)
                grids[-1]._level_id = level_id
                self.grid_levels[i] = level_number
                self.grid_left_edge[i] = (
                    self.dds_list[lev_index] * si.astype(self.float_type)
                    + self.domain_left_edge.value
                )
                self.grid_right_edge[i] = (
                    self.dds_list[lev_index] * (ei.astype(self.float_type) + 1)
                    + self.domain_left_edge.value
                )
                self.grid_particle_count[i] = 0
                self.grid_dimensions[i] = ei - si + 1
                i += 1
        self.grids = np.empty(len(grids), dtype="object")
        for gi, g in enumerate(grids):
            self.grids[gi] = g


class PlutoDataset(ChomboDataset):
    _index_class = PlutoHierarchy
    _field_info_class = PlutoFieldInfo

    def __init__(
        self,
        filename,
        dataset_type="chombo_hdf5",
        storage_filename=None,
        ini_filename=None,
        units_override=None,
        unit_system="cgs",
        default_species_fields=None,
    ):
        ChomboDataset.__init__(
            self,
            filename,
            dataset_type,
            storage_filename,
            ini_filename,
            units_override=units_override,
            unit_system=unit_system,
            default_species_fields=default_species_fields,
        )

    def _parse_parameter_file(self):
        """
        Check to see whether a 'pluto.ini' file
        exists in the plot file directory. If one does, attempt to parse it.
        Otherwise grab the dimensions from the hdf5 file.
        """

        pluto_ini_file_exists = False
        dir_name = os.path.dirname(os.path.abspath(self.fullplotdir))
        pluto_ini_filename = os.path.join(dir_name, "pluto.ini")
        pluto_ini_file_exists = os.path.isfile(pluto_ini_filename)

        self.dimensionality = self._handle["Chombo_global/"].attrs["SpaceDim"]
        self.domain_dimensions = self._calc_domain_dimensions()
        self.refine_by = self._handle["/level_0"].attrs["ref_ratio"]

        if pluto_ini_file_exists:
            lines = [line.strip() for line in open(pluto_ini_filename)]
            domain_left_edge = np.zeros(self.dimensionality)
            domain_right_edge = np.zeros(self.dimensionality)
            for il, ll in enumerate(
                lines[
                    lines.index("[Grid]") + 2 : lines.index("[Grid]")
                    + 2
                    + self.dimensionality
                ]
            ):
                domain_left_edge[il] = float(ll.split()[2])
                domain_right_edge[il] = float(ll.split()[-1])
            self._periodicity = [0] * 3
            for il, ll in enumerate(
                lines[
                    lines.index("[Boundary]") + 2 : lines.index("[Boundary]")
                    + 2
                    + 6 : 2
                ]
            ):
                self.periodicity[il] = ll.split()[1] == "periodic"
            self._periodicity = tuple(self.periodicity)
            for ll in lines[lines.index("[Parameters]") + 2 :]:
                if ll.split()[0] == "GAMMA":
                    self.gamma = float(ll.split()[1])
            self.domain_left_edge = domain_left_edge
            self.domain_right_edge = domain_right_edge
        else:
            self.domain_left_edge = self._calc_left_edge()
            self.domain_right_edge = self._calc_right_edge()
            self._periodicity = (True, True, True)

        # if a lower-dimensional dataset, set up pseudo-3D stuff here.
        if self.dimensionality == 1:
            self.domain_left_edge = np.concatenate((self.domain_left_edge, [0.0, 0.0]))
            self.domain_right_edge = np.concatenate(
                (self.domain_right_edge, [1.0, 1.0])
            )
            self.domain_dimensions = np.concatenate((self.domain_dimensions, [1, 1]))

        if self.dimensionality == 2:
            self.domain_left_edge = np.concatenate((self.domain_left_edge, [0.0]))
            self.domain_right_edge = np.concatenate((self.domain_right_edge, [1.0]))
            self.domain_dimensions = np.concatenate((self.domain_dimensions, [1]))

        self._determine_current_time()

    @classmethod
    def _is_valid(cls, filename: str, *args, **kwargs) -> bool:
        if cls._missing_load_requirements():
            return False

        if not is_chombo_hdf5(filename):
            return False

        pluto_ini_file_exists = False

        if isinstance(filename, str):
            dir_name = os.path.dirname(os.path.abspath(filename))
            pluto_ini_filename = os.path.join(dir_name, "pluto.ini")
            pluto_ini_file_exists = os.path.isfile(pluto_ini_filename)

        if pluto_ini_file_exists:
            return True

        return False


class Orion2Hierarchy(ChomboHierarchy):
    def __init__(self, ds, dataset_type="orion_chombo_native"):
        ChomboHierarchy.__init__(self, ds, dataset_type)

    def _detect_output_fields(self):
        # look for fluid fields
        output_fields = []
        for key, val in self._handle.attrs.items():
            if key.startswith("component"):
                output_fields.append(val.decode("ascii"))
        self.field_list = [("chombo", c) for c in output_fields]

        # look for particle fields
        self.particle_filename = self.index_filename[:-4] + "sink"
        if not os.path.exists(self.particle_filename):
            return
        pfield_list = [("io", str(c)) for c in self.io.particle_field_index.keys()]
        self.field_list.extend(pfield_list)

    def _read_particles(self):
        if not os.path.exists(self.particle_filename):
            return
        with open(self.particle_filename) as f:
            lines = f.readlines()
            self.num_stars = int(lines[0].strip().split(" ")[0])
            for num, line in enumerate(lines[1:]):
                particle_position_x = float(line.split(" ")[1])
                particle_position_y = float(line.split(" ")[2])
                particle_position_z = float(line.split(" ")[3])
                coord = [particle_position_x, particle_position_y, particle_position_z]
                # for each particle, determine which grids contain it
                # copied from object_finding_mixin.py
                mask = np.ones(self.num_grids)
                for i in range(len(coord)):
                    np.choose(
                        np.greater(self.grid_left_edge.d[:, i], coord[i]),
                        (mask, 0),
                        mask,
                    )
                    np.choose(
                        np.greater(self.grid_right_edge.d[:, i], coord[i]),
                        (0, mask),
                        mask,
                    )
                ind = np.where(mask == 1)
                selected_grids = self.grids[ind]
                # in orion, particles always live on the finest level.
                # so, we want to assign the particle to the finest of
                # the grids we just found
                if len(selected_grids) != 0:
                    grid = sorted(selected_grids, key=lambda grid: grid.Level)[-1]
                    ind = np.where(self.grids == grid)[0][0]
                    self.grid_particle_count[ind] += 1
                    self.grids[ind].NumberOfParticles += 1

                    # store the position in the *.sink file for fast access.
                    try:
                        self.grids[ind]._particle_line_numbers.append(num + 1)
                    except AttributeError:
                        self.grids[ind]._particle_line_numbers = [num + 1]


class Orion2Dataset(ChomboDataset):
    _load_requirements = ["h5py"]
    _index_class = Orion2Hierarchy
    _field_info_class = Orion2FieldInfo

    def __init__(
        self,
        filename,
        dataset_type="orion_chombo_native",
        storage_filename=None,
        ini_filename=None,
        units_override=None,
        default_species_fields=None,
    ):
        ChomboDataset.__init__(
            self,
            filename,
            dataset_type,
            storage_filename,
            ini_filename,
            units_override=units_override,
            default_species_fields=default_species_fields,
        )

    def _parse_parameter_file(self):
        """
        Check to see whether an 'orion2.ini' file
        exists in the plot file directory. If one does, attempt to parse it.
        Otherwise grab the dimensions from the hdf5 file.
        """

        orion2_ini_file_exists = False
        dir_name = os.path.dirname(os.path.abspath(self.fullplotdir))
        orion2_ini_filename = os.path.join(dir_name, "orion2.ini")
        orion2_ini_file_exists = os.path.isfile(orion2_ini_filename)

        if orion2_ini_file_exists:
            self._parse_inputs_file("orion2.ini")
        self.dimensionality = 3
        self.domain_left_edge = self._calc_left_edge()
        self.domain_right_edge = self._calc_right_edge()
        self.domain_dimensions = self._calc_domain_dimensions()
        self.refine_by = self._handle["/level_0"].attrs["ref_ratio"]
        self._determine_periodic()
        self._determine_current_time()

    def _parse_inputs_file(self, ini_filename):
        self.fullplotdir = os.path.abspath(self.parameter_filename)
        self.ini_filename = self._localize(self.ini_filename, ini_filename)
        lines = open(self.ini_filename).readlines()
        # read the file line by line, storing important parameters
        for line in lines:
            try:
                param, sep, vals = line.partition("=")
                if not sep:
                    # No = sign present, so split by space instead
                    param, sep, vals = line.partition(" ")
                param = param.strip()
                vals = vals.strip()
                if not param:  # skip blank lines
                    continue
                if param[0] == "#":  # skip comment lines
                    continue
                if param[0] == "[":  # skip stanza headers
                    continue
                vals = vals.partition("#")[0]  # strip trailing comments
                try:
                    self.parameters[param] = np.int64(vals)
                except ValueError:
                    try:
                        self.parameters[param] = np.float64(vals)
                    except ValueError:
                        self.parameters[param] = vals
            except ValueError:
                mylog.error("ValueError: '%s'", line)
            if param == "GAMMA":
                self.gamma = np.float64(vals)

    @classmethod
    def _is_valid(cls, filename: str, *args, **kwargs) -> bool:
        if cls._missing_load_requirements():
            return False

        if not is_chombo_hdf5(filename):
            return False

        pluto_ini_file_exists = False
        orion2_ini_file_exists = False

        if isinstance(filename, str):
            dir_name = os.path.dirname(os.path.abspath(filename))
            pluto_ini_filename = os.path.join(dir_name, "pluto.ini")
            orion2_ini_filename = os.path.join(dir_name, "orion2.ini")
            pluto_ini_file_exists = os.path.isfile(pluto_ini_filename)
            orion2_ini_file_exists = os.path.isfile(orion2_ini_filename)

        if orion2_ini_file_exists:
            return True

        if not pluto_ini_file_exists:
            try:
                fileh = h5py.File(filename, mode="r")
                valid = "CeilVA_mass" in fileh.attrs.keys()
                valid = (
                    "Chombo_global" in fileh["/"] and "Charm_global" not in fileh["/"]
                )
                valid = valid and "CeilVA_mass" in fileh.attrs.keys()
                fileh.close()
                return valid
            except Exception:
                pass
        return False


class ChomboPICHierarchy(ChomboHierarchy):
    def __init__(self, ds, dataset_type="chombo_hdf5"):
        ChomboHierarchy.__init__(self, ds, dataset_type)


class ChomboPICDataset(ChomboDataset):
    _load_requirements = ["h5py"]
    _index_class = ChomboPICHierarchy
    _field_info_class = ChomboPICFieldInfo3D

    def __init__(
        self,
        filename,
        dataset_type="chombo_hdf5",
        storage_filename=None,
        ini_filename=None,
        units_override=None,
    ):
        ChomboDataset.__init__(
            self,
            filename,
            dataset_type,
            storage_filename,
            ini_filename,
            units_override=units_override,
        )

        if self.dimensionality == 1:
            self._field_info_class = ChomboPICFieldInfo1D

        if self.dimensionality == 2:
            self._field_info_class = ChomboPICFieldInfo2D

    @classmethod
    def _is_valid(cls, filename: str, *args, **kwargs) -> bool:
        if not valid_hdf5_signature(filename):
            return False

        if cls._missing_load_requirements():
            return False

        if not is_chombo_hdf5(filename):
            return False

        pluto_ini_file_exists = False
        orion2_ini_file_exists = False

        if isinstance(filename, str):
            dir_name = os.path.dirname(os.path.abspath(filename))
            pluto_ini_filename = os.path.join(dir_name, "pluto.ini")
            orion2_ini_filename = os.path.join(dir_name, "orion2.ini")
            pluto_ini_file_exists = os.path.isfile(pluto_ini_filename)
            orion2_ini_file_exists = os.path.isfile(orion2_ini_filename)

        if orion2_ini_file_exists:
            return False

        if pluto_ini_file_exists:
            return False

        try:
            fileh = h5py.File(filename, mode="r")
            valid = "Charm_global" in fileh["/"]
            fileh.close()
            return valid
        except Exception:
            pass
        return False
