from collections.abc import Mapping
from ctypes import c_double, c_int, c_int32, c_char_p, c_size_t, POINTER
from weakref import WeakValueDictionary

import numpy as np
from numpy.ctypeslib import as_array

from openmc import ParticleType
from openmc.exceptions import AllocationError, InvalidIDError
from . import _dll
from .core import _FortranObjectWithID
from .error import _error_handler
from .filter import EnergyFilter, MeshFilter, ParticleFilter
from .mesh import _get_mesh
from .mesh import meshes


__all__ = ['WeightWindows', 'weight_windows']

_dll.openmc_extend_weight_windows.argtypes = [c_int32, POINTER(c_int32), POINTER(c_int32)]

_dll.openmc_weight_windows_update_magic.argtypes = 2*[c_int32] + [c_char_p] + 2*[c_double]
_dll.openmc_weight_windows_update_magic.restype = c_int
_dll.openmc_weight_windows_update_magic.errcheck = _error_handler

_dll.openmc_weight_windows_size.restype = c_size_t

_dll.openmc_get_weight_windows_index.argtypes = [c_int32, POINTER(c_int32)]
_dll.openmc_get_weight_windows_index.restype = c_int
_dll.openmc_get_weight_windows_index.errcheck = _error_handler

_dll.openmc_weight_windows_get_id.argtypes = [c_int32, POINTER(c_int32)]
_dll.openmc_weight_windows_get_id.restype = c_int
_dll.openmc_weight_windows_get_id.errcheck = _error_handler

_dll.openmc_weight_windows_set_id.argtypes = [c_int32, c_int32]
_dll.openmc_weight_windows_set_id.restype = c_int
_dll.openmc_weight_windows_set_id.errcheck = _error_handler

_dll.openmc_weight_windows_set_mesh.argtypes = [c_int32, c_int32]
_dll.openmc_weight_windows_set_mesh.restype = c_int
_dll.openmc_weight_windows_set_mesh.errcheck = _error_handler

_dll.openmc_weight_windows_get_mesh.argtypes = [c_int32, POINTER(c_int32)]
_dll.openmc_weight_windows_get_mesh.restype = c_int
_dll.openmc_weight_windows_get_mesh.errcheck = _error_handler

_dll.openmc_weight_windows_set_energy_bounds.argtypes = [c_int32, POINTER(c_double), c_size_t]
_dll.openmc_weight_windows_set_energy_bounds.restype = c_int
_dll.openmc_weight_windows_set_energy_bounds.errcheck = _error_handler

_dll.openmc_weight_windows_get_energy_bounds.argtypes = [c_int32, POINTER(POINTER(c_double)), POINTER(c_size_t)]
_dll.openmc_weight_windows_get_energy_bounds.restype = c_int
_dll.openmc_weight_windows_get_energy_bounds.errcheck = _error_handler

_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int]
_dll.openmc_weight_windows_set_particle.restype = c_int
_dll.openmc_weight_windows_set_particle.errcheck = _error_handler

_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int)]
_dll.openmc_weight_windows_get_particle.restype = c_int
_dll.openmc_weight_windows_get_particle.errcheck = _error_handler

_dll.openmc_weight_windows_set_bounds.argtypes = [c_int32, POINTER(c_double), POINTER(c_double), c_size_t]
_dll.openmc_weight_windows_set_bounds.restype = c_int
_dll.openmc_weight_windows_set_bounds.errcheck = _error_handler

_dll.openmc_weight_windows_get_bounds.argtypes = [c_int32, POINTER(POINTER(c_double)), POINTER(POINTER(c_double)), POINTER(c_size_t)]
_dll.openmc_weight_windows_get_bounds.restype = c_int
_dll.openmc_weight_windows_get_bounds.errcheck = _error_handler

_dll.openmc_weight_windows_get_survival_ratio.argtypes = [c_int32, POINTER(c_double)]
_dll.openmc_weight_windows_get_survival_ratio.restype = c_int
_dll.openmc_weight_windows_get_survival_ratio.errcheck = _error_handler

_dll.openmc_weight_windows_set_survival_ratio.argtypes = [c_int32, c_double]
_dll.openmc_weight_windows_set_survival_ratio.restype = c_int
_dll.openmc_weight_windows_set_survival_ratio.errcheck = _error_handler

_dll.openmc_weight_windows_get_max_lower_bound_ratio.argtypes = [c_int32, POINTER(c_double)]
_dll.openmc_weight_windows_get_max_lower_bound_ratio.restype = c_int
_dll.openmc_weight_windows_get_max_lower_bound_ratio.errcheck = _error_handler

_dll.openmc_weight_windows_set_max_lower_bound_ratio.argtypes = [c_int32, c_double]
_dll.openmc_weight_windows_set_max_lower_bound_ratio.restype = c_int
_dll.openmc_weight_windows_set_max_lower_bound_ratio.errcheck = _error_handler

_dll.openmc_weight_windows_get_weight_cutoff.argtypes = [c_int32, POINTER(c_double)]
_dll.openmc_weight_windows_get_weight_cutoff.restype = c_int
_dll.openmc_weight_windows_get_weight_cutoff.errcheck = _error_handler

_dll.openmc_weight_windows_set_weight_cutoff.argtypes = [c_int32, c_double]
_dll.openmc_weight_windows_set_weight_cutoff.restype = c_int
_dll.openmc_weight_windows_set_weight_cutoff.errcheck = _error_handler

_dll.openmc_weight_windows_get_max_split.argtypes = [c_int32, POINTER(c_int)]
_dll.openmc_weight_windows_get_max_split.restype = c_int
_dll.openmc_weight_windows_get_max_split.errcheck = _error_handler

_dll.openmc_weight_windows_set_max_split.argtypes = [c_int32, c_int]
_dll.openmc_weight_windows_set_max_split.restype = c_int
_dll.openmc_weight_windows_set_max_split.errcheck = _error_handler


class WeightWindows(_FortranObjectWithID):
    """WeightWindows stored internally.

    This class exposes a weight windows object that is stored internally in the
    OpenMC library. To obtain a view of a weight windows object with a given ID,
    use the :data:`openmc.lib.weight_windows` mapping.

    .. versionadded:: 0.14.0

    Parameters
    ----------
    id : int or None
        Unique ID of the weight windows
    new : bool
        When `index` is None, this argument controls whether a new object is
        created or a view of an existing object is returned.
    index : int or None
        Index in the `weight_windows` array.

    Attributes
    ----------
    id : int
        ID of the weight windows object
    mesh : openmc.lib.Mesh
        Mesh used for the weight windows
    particle : openmc.ParticleType
        The particle type to which these weight windows apply
    energy_bounds : numpy.ndarray
        The energy bounds for the weight windows
    bounds : numpy.ndarray
        The weight window bounds
    """
    __instances = WeakValueDictionary()

    def __new__(cls, id=None, new=True, index=None):
        mapping = weight_windows

        if index is None:
            if new:
                # Determine ID to assign
                if id is None:
                    id = max(mapping, default=0) + 1
                else:
                    if id in mapping:
                        raise AllocationError(f'A weight windows object with ID={id} '
                                              'has already been allocated.')

                index = c_int32()
                _dll.openmc_extend_weight_windows(1, index, None)
                index = index.value
            else:
                index = mapping[id]._index

        if index not in cls.__instances:
            instance = super().__new__(cls)
            instance._index = index
            if id is not None:
                instance.id = id
            cls.__instances[index] = instance

        return cls.__instances[index]

    @property
    def id(self):
        ww_id = c_int32()
        _dll.openmc_weight_windows_get_id(self._index, ww_id)
        return ww_id.value

    @id.setter
    def id(self, ww_id):
        _dll.openmc_weight_windows_set_id(self._index, ww_id)

    @property
    def mesh(self):
        mesh_idx = c_int32()
        _dll.openmc_weight_windows_get_mesh(self._index, mesh_idx)
        return _get_mesh(mesh_idx.value)

    @mesh.setter
    def mesh(self, mesh):
        _dll.openmc_weight_windows_set_mesh(
            weight_windows[self.id]._index, meshes[mesh.id]._index)

    @property
    def energy_bounds(self):
        data = POINTER(c_double)()
        n = c_size_t()
        _dll.openmc_weight_windows_get_energy_bounds(self._index, data, n)
        return as_array(data, (n.value,))

    @energy_bounds.setter
    def energy_bounds(self, e_bounds):
        e_bounds_arr = np.asarray(e_bounds, dtype=float)
        e_bounds_ptr = e_bounds_arr.ctypes.data_as(POINTER(c_double))
        _dll.openmc_weight_windows_set_energy_bounds(
            self._index, e_bounds_ptr, e_bounds_arr.size)

    @property
    def particle(self):
        val = c_int()
        _dll.openmc_weight_windows_get_particle(self._index, val)
        return ParticleType(val.value)

    @particle.setter
    def particle(self, p):
        if isinstance(p, str):
            p = ParticleType.from_string(p)
        else:
            p = ParticleType(p)
        _dll.openmc_weight_windows_set_particle(self._index, int(p))

    @property
    def bounds(self):
        upper = POINTER(c_double)()
        lower = POINTER(c_double)()
        size = c_size_t()
        _dll.openmc_weight_windows_get_bounds(self._index, lower, upper, size)
        lower_arr = as_array(lower, (size.value,))
        upper_arr = as_array(upper, (size.value,))
        return (lower_arr, upper_arr)

    @bounds.setter
    def bounds(self, bounds):
        lower = np.asarray(bounds[0])
        upper = np.asarray(bounds[1])

        lower_p = lower.ctypes.data_as(POINTER(c_double))
        upper_p = upper.ctypes.data_as(POINTER(c_double))

        _dll.openmc_weight_windows_set_bounds(self._index, lower_p, upper_p, lower.size)

    @property
    def survival_ratio(self):
        ratio = c_double()
        _dll.openmc_weight_windows_get_survival_ratio(self._index, ratio)
        return ratio.value

    @survival_ratio.setter
    def survival_ratio(self, ratio):
        _dll.openmc_weight_windows_set_survival_ratio(self._index, ratio)

    @property
    def max_lower_bound_ratio(self):
        lb_ratio = c_double()
        _dll.openmc_weight_windows_get_max_lower_bound_ratio(self._index, lb_ratio)
        return lb_ratio.value

    @max_lower_bound_ratio.setter
    def max_lower_bound_ratio(self, lb_ratio):
        _dll.openmc_weight_windows_set_max_lower_bound_ratio(self._index, lb_ratio)

    @property
    def weight_cutoff(self):
        cutoff = c_double()
        _dll.openmc_weight_windows_get_weight_cutoff(self._index, cutoff)
        return cutoff.value

    @weight_cutoff.setter
    def weight_cutoff(self, cutoff):
        _dll.openmc_weight_windows_set_weight_cutoff(self._index, cutoff)

    @property
    def max_split(self):
        max_split = c_int()
        _dll.openmc_weight_windows_get_max_split(self._index, max_split)
        return max_split.value

    @max_split.setter
    def max_split(self, max_split):
        _dll.openmc_weight_windows_set_max_split(self._index, max_split)

    def update_magic(self, tally, value='mean', threshold=1.0, ratio=5.0):
        """Update weight window values using the MAGIC method

        Reference: https://inis.iaea.org/search/48022314

        Parameters
        ----------
        tally : openmc.lib.Tally object
            The tally used to update weight window information
        value : str
            Value type used to generate weight windows. One of {'mean', 'rel_err'}.
        threshold : float
            Threshold for relative error of results used to generate weight window bounds
        ratio : float
            Ratio of the lower to upper weight window bounds

        """
        _dll.openmc_weight_windows_update_magic(self._index,
                                                tally._index,
                                                c_char_p(value.encode()),
                                                threshold,
                                                ratio)

    @classmethod
    def from_tally(cls, tally, particle=ParticleType.NEUTRON):
        """Create an instance of the WeightWindows class based on the specified tally.

        Parameters
        ----------
        tally : openmc.lib.Tally
            The tally used to create the WeightWindows instance.
        particle : openmc.ParticleType or str, optional
            The particle type to use for the WeightWindows instance. Should be
            specified as an instance of ParticleType or as a string with a value of
            'neutron' or 'photon'.

        Returns
        -------
        WeightWindows
            The WeightWindows instance created from the specified tally.

        Raises
        ------
        ValueError
            If the particle parameter is not an instance of ParticleType or a string.
        ValueError
            If the particle parameter is not a valid particle type (i.e., not 'neutron'
            or 'photon').
        ValueError
            If the specified particle is not included in the bins of the ParticleFilter
            of the tally.
        ValueError
            If the tally does not have a MeshFilter.
        """
        # do some checks on particle value
        if not isinstance(particle, (ParticleType, str)):
            raise ValueError(f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon').")

        # convert particle type if needed
        if isinstance(particle, str):
            particle = ParticleType.from_string(particle)

        if particle not in (ParticleType.NEUTRON, ParticleType.PHOTON):
            raise ValueError('Weight windows can only be applied for neutrons or photons')

        try:
            particle_filter = tally.find_filter(ParticleFilter)
        except ValueError:
            particle_filter = None

        # ensure that the tally won't filter out the specified particle
        if particle_filter is not None and particle not in particle_filter.bins:
            raise ValueError(f'Specified tally for weight windows (Tally {tally.id})'
                             f' does not track the requested particle: "{particle}"')

        # tally must have a mesh filter
        mesh_filter = tally.find_filter(MeshFilter)

        # create a new weight windows instance
        out = cls()

        # set mesh and particle
        out.mesh = mesh_filter.mesh
        out.particle = particle

        # set energy bounds if needed
        try:
            energy_filter = tally.find_filter(EnergyFilter)
        except ValueError:
            energy_filter = None

        if energy_filter is not None:
            out.energy_bounds = energy_filter.bins

        return out


class _WeightWindowsMapping(Mapping):
    def __getitem__(self, key):
        index = c_int32()
        try:
            _dll.openmc_get_weight_windows_index(key, index)
        except (AllocationError, InvalidIDError) as e:
            raise KeyError(str(e))
        return WeightWindows(index=index.value)

    def __iter__(self):
        for i in range(len(self)):
            yield WeightWindows(index=i).id

    def __len__(self):
        return _dll.openmc_weight_windows_size()

    def __repr__(self):
        return repr(dict(self))

    def __delitem__(self):
        raise NotImplementedError("WeightWindows object remove not implemented")

weight_windows = _WeightWindowsMapping()
