#
# Copyright (C) 2013-2022 The ESPResSo project
#
# This file is part of ESPResSo.
#
# ESPResSo 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.
#
# ESPResSo 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, see <http://www.gnu.org/licenses/>.
#
include "myconfig.pxi"

cimport numpy as np
import numpy as np
from cython.operator cimport dereference
from . cimport particle_data
from .interactions import BondedInteraction
from .interactions import BondedInteractions
from .interactions cimport bonded_ia_params_zero_based_type
from .analyze cimport max_seen_particle_type
from copy import copy
import collections
import functools
from .utils import nesting_level, array_locked, is_valid_type, handle_errors
from .utils cimport make_array_locked, make_const_span, check_type_or_throw_except
from .utils cimport Vector3i, Vector3d, Vector4d
from .utils cimport make_Vector3d
from .utils cimport make_Vector3i
from .grid cimport box_geo, folded_position, unfolded_position
import itertools


# List of particle attributes for pickle and the like
# Autogenerated from the class. Everything which is of the same
# type as ParticleHandle.pos (getter_wrapper)
particle_attributes = []
for d in dir(ParticleHandle):
    if type(getattr(ParticleHandle, d)) == type(ParticleHandle.pos):
        if d != "pos_folded":
            particle_attributes.append(d)

cdef class ParticleHandle:
    def __cinit__(self, int _id):
        self._id = _id

    cdef int update_particle_data(self) except -1:
        self.particle_data = &get_particle_data(self._id)

    def to_dict(self):
        """
        Returns the particle's attributes as a dictionary.

        It includes the content of ``particle_attributes``, minus a few exceptions:

        - :attr:`~ParticleHandle.dip`, :attr:`~ParticleHandle.director`:
          Setting only the director will overwrite the orientation of the
          particle around the axis parallel to dipole moment/director.
          Quaternions contain the full info.
        - :attr:`~ParticleHandle.image_box`, :attr:`~ParticleHandle.node`

        """

        pickle_attr = copy(particle_attributes)
        for i in ["director", "dip", "image_box", "node", "lees_edwards_flag"]:
            if i in pickle_attr:
                pickle_attr.remove(i)
        IF MASS == 0:
            pickle_attr.remove("mass")
        pdict = {}

        for property_ in pickle_attr:
            pdict[property_] = ParticleHandle(
                self.id).__getattribute__(property_)
        return pdict

    def __str__(self):
        res = collections.OrderedDict()
        # Id and pos first, then the rest
        res["id"] = self.id
        res["pos"] = self.pos
        for a in particle_attributes:
            tmp = getattr(self, a)
            # Remove array type names from output
            if isinstance(tmp, array_locked):
                res[a] = tuple(tmp)
            else:
                res[a] = tmp

        # Get rid of OrderedDict in output
        return str(res).replace("OrderedDict(", "ParticleHandle(")

    # The individual attributes of a particle are implemented as properties.
    property id:
        """Integer particle id

        """

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.id()

    # The individual attributes of a particle are implemented as properties.

    # Particle Type
    property type:
        """
        The particle type for nonbonded interactions.

        type : :obj:`int`

        .. note::
           The value of ``type`` has to be an integer >= 0.

        """

        def __set__(self, _type):
            if is_valid_type(_type, int) and _type >= 0:
                set_particle_type(self._id, _type)
            else:
                raise ValueError("type must be an integer >= 0")

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.type()

    # Particle MolId
    property mol_id:
        """
        The molecule id of the Particle.

        mol_id : :obj:`int`

        The particle ``mol_id`` is used to differentiate between
        particles belonging to different molecules, e.g. when virtual
        sites are used, or object-in-fluid cells. The default
        ``mol_id`` for all particles is 0.

        .. note::
           The value of ``mol_id`` has to be an integer >= 0.

        """

        def __set__(self, _mol_id):
            if is_valid_type(_mol_id, int) and _mol_id >= 0:
                set_particle_mol_id(self._id, _mol_id)
            else:
                raise ValueError("mol_id must be an integer >= 0")

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.mol_id()

    # Position
    property pos:
        """
        The unwrapped (not folded into central box) particle position.

        pos : (3,) array_like of :obj:`float`

        """

        def __set__(self, _pos):
            if np.isnan(_pos).any() or np.isinf(_pos).any():
                raise ValueError("invalid particle position")
            check_type_or_throw_except(
                _pos, 3, float, "Position must be 3 floats")
            place_particle(self._id, make_Vector3d(_pos))

        def __get__(self):
            self.update_particle_data()
            return make_array_locked(unfolded_position(
                self.particle_data.pos(),
                self.particle_data.image_box(),
                box_geo.length()))

    property pos_folded:
        """
        The wrapped (folded into central box) position vector of a particle.

        pos : (3,) array_like of :obj:`float`

        .. note::
           Setting the folded position is ambiguous and is thus not possible, please use ``pos``.

        Examples
        --------
        >>> import espressomd
        >>> system = espressomd.System(box_l=[10, 10, 10])
        >>> system.part.add(pos=(5, 0, 0))
        >>> system.part.add(pos=(10, 0, 0))
        >>> system.part.add(pos=(25, 0, 0))
        >>> for p in system.part:
        ...     print(p.pos)
        [ 5.  0.  0.]
        [ 10.   0.   0.]
        [ 25.   0.   0.]
        >>> for p in system.part:
        ...     print(p.pos_folded)
        [5.0, 0.0, 0.0]
        [0.0, 0.0, 0.0]
        [5.0, 0.0, 0.0]

        """

        def __set__(self, pos_folded):
            raise AttributeError(
                "setting a folded position is not implemented")

        def __get__(self):
            self.update_particle_data()
            return make_array_locked(folded_position(
                self.particle_data.pos(), box_geo))

    property image_box:
        """
        The image box the particles is in.

        This is the number of times the particle position has been folded by
        the box length in each direction.
        """

        def __get__(self):
            self.update_particle_data()
            cdef Vector3i image_box = self.particle_data.image_box()
            return array_locked([image_box[0], image_box[1], image_box[2]])

    property lees_edwards_offset:
        """
        The accumulated Lees-Edwards offset.
        Can be used to reconstruct continuous trajectories.

        offset : (3,) array_like of :obj:`float`

        """

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.lees_edwards_offset()

        def __set__(self, value):
            set_particle_lees_edwards_offset(self._id, value)

    property lees_edwards_flag:
        """
        The Lees-Edwards flag that indicate if the particle crossed
        the upper or lower boundary.

        """

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.lees_edwards_flag()

    # Velocity
    property v:
        """
        The particle velocity in the lab frame.

        v : (3,) array_like of :obj:`float`

        .. note::
           The velocity remains variable and will be changed during integration.

        """

        def __set__(self, _v):
            check_type_or_throw_except(
                _v, 3, float, "Velocity has to be floats")
            set_particle_v(self._id, make_Vector3d(_v))

        def __get__(self):
            self.update_particle_data()
            return make_array_locked(self.particle_data.v())

    # Force
    property f:
        """
        The instantaneous force acting on this particle.

        f : (3,) array_like of :obj:`float`

        .. note::
           Whereas the velocity is modified with respect to the velocity you set
           upon integration, the force it recomputed during the integration step and any
           force set in this way is immediately lost at the next integration step.

        """

        def __set__(self, _f):
            check_type_or_throw_except(_f, 3, float, "Force has to be floats")
            set_particle_f(self._id, make_Vector3d(_f))

        def __get__(self):
            self.update_particle_data()
            return make_array_locked(self.particle_data.force())

    property bonds:
        """
        The bonds stored by this particle. Note that bonds are only stored by
        one partner. You need to define a bonded interaction.

        bonds : list/tuple of tuples/lists

        A bond tuple is specified as a bond identifier associated with
        a particle ``(bond_ID, part_ID)``. A single particle may contain
        multiple such tuples.

        See Also
        --------
        espressomd.particle_data.ParticleHandle.add_bond : Method to add bonds to a ``Particle``
        espressomd.particle_data.ParticleHandle.delete_bond : Method to remove bonds from a ``Particle``

        .. note::
           Bond ids have to be an integer >= 0.

        """

        def __set__(self, _bonds):
            # Assigning to the bond property means replacing the existing value
            # i.e., we delete all existing bonds
            delete_particle_bonds(self._id)

            # Empty list? only delete
            if _bonds:
                nlvl = nesting_level(_bonds)
                if nlvl == 1:  # Single item
                    self.add_bond(_bonds)
                elif nlvl == 2:  # List of items
                    for bond in _bonds:
                        self.add_bond(bond)
                else:
                    raise ValueError(
                        "Bonds have to specified as lists of tuples/lists or a single list.")

        def __get__(self):
            bonds = []
            part_bonds = get_particle_bonds(self._id)
            # Go through the bond list of the particle
            for part_bond in part_bonds:
                bond_id = part_bond.bond_id()
                partners = part_bond.partner_ids()
                partner_ids = [partners[i] for i in range(partners.size())]
                bonds.append((BondedInteractions()[bond_id], *partner_ids))

            return tuple(bonds)

    property node:
        """
        The node the particle is on, identified by its MPI rank.
        """

        def __get__(self):
            return get_particle_node(self._id)

    # Properties that exist only when certain features are activated
    # MASS
    property mass:
        """
        Particle mass.

        mass : :obj:`float`

        See Also
        --------
        :meth:`espressomd.thermostat.Thermostat.set_langevin` : Setting the parameters of the Langevin thermostat

        """

        def __set__(self, _mass):
            IF MASS == 1:
                check_type_or_throw_except(
                    _mass, 1, float, "Mass has to be 1 float")
                if _mass <= 0.:
                    raise ValueError("mass must be a float > 0")
                set_particle_mass(self._id, _mass)
            ELSE:
                raise AttributeError("You are trying to set the particle mass "
                                     "but the MASS feature is not compiled in.")

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.mass()

    IF ROTATION:
        property omega_lab:
            """
            The particle angular velocity the lab frame.

            omega_lab : (3,) array_like of :obj:`float`

            .. note::
               This needs the feature ``ROTATION``.

               If you set the angular velocity of the particle in the lab
               frame, the orientation of the particle
               (:attr:`~espressomd.particle_data.ParticleHandle.quat`) must be
               set before setting ``omega_lab``, otherwise the conversion from
               lab to body frame will not be handled properly.

            See Also
            ---------
            :attr:`~espressomd.particle_data.ParticleHandle.omega_body`

            """

            def __set__(self, _o):
                check_type_or_throw_except(
                    _o, 3, float, "Omega_lab has to be 3 floats.")
                set_particle_omega_lab(self._id, make_Vector3d(_o))

            def __get__(self):
                self.update_particle_data()
                return array_locked(
                    self.convert_vector_body_to_space(self.omega_body))

        property quat:
            """
            Quaternion representation of the particle rotational position.

            quat : (4,) array_like of :obj:`float`

            .. note::
               This needs the feature ``ROTATION``.

            """

            def __set__(self, _q):
                cdef Quaternion[double] q
                check_type_or_throw_except(
                    _q, 4, float, "Quaternions has to be 4 floats.")
                if np.linalg.norm(_q) == 0.:
                    raise ValueError("quaternion is zero")
                for i in range(4):
                    q[i] = _q[i]
                set_particle_quat(self._id, q)

            def __get__(self):
                self.update_particle_data()

                cdef Quaternion[double] q = self.particle_data.quat()
                return array_locked([q[0], q[1], q[2], q[3]])

        property director:
            """
            The particle director.

            The ``director`` defines the the z-axis in the body-fixed frame.
            If particle rotations happen, the director, i.e., the body-fixed
            coordinate system co-rotates. Properties such as the angular
            velocity :attr:`espressomd.particle_data.ParticleHandle.omega_body`
            are evaluated in this body-fixed coordinate system.
            When using particle dipoles, the dipole moment is co-aligned with
            the particle director. Setting the director thus modifies the
            dipole moment orientation (:attr:`espressomd.particle_data.ParticleHandle.dip`)
            and vice versa.
            See also :ref:`Rotational degrees of freedom and particle anisotropy`.

            director : (3,) array_like of :obj:`float`

            .. note::
               This needs the feature ``ROTATION``.

            """

            def __set__(self, _d):
                check_type_or_throw_except(
                    _d, 3, float, "Particle director has to be 3 floats.")
                set_particle_director(self._id, make_Vector3d(_d))

            def __get__(self):
                self.update_particle_data()
                return make_array_locked(self.particle_data.calc_director())

        property omega_body:
            """
            The particle angular velocity in body frame.

            omega_body : (3,) array_like of :obj:`float`

            This property sets the angular momentum of this particle in the
            particles co-rotating frame (or body frame).

            .. note::
               This needs the feature ``ROTATION``.

            """

            def __set__(self, _o):
                check_type_or_throw_except(
                    _o, 3, float, "Omega_body has to be 3 floats.")
                set_particle_omega_body(self._id, make_Vector3d(_o))

            def __get__(self):
                self.update_particle_data()
                return make_array_locked(self.particle_data.omega())

        property torque_lab:
            """
            The particle torque in the lab frame.

            torque_lab : (3,) array_like of :obj:`float`

            This property defines the torque of this particle
            in the fixed frame (or laboratory frame).

            .. note::
               The orientation of the particle
               (:attr:`~espressomd.particle_data.ParticleHandle.quat`) must be
               set before setting this property, otherwise the conversion from
               lab to body frame will not be handled properly.

            """

            def __set__(self, _t):
                check_type_or_throw_except(
                    _t, 3, float, "Torque has to be 3 floats.")
                set_particle_torque_lab(self._id, make_Vector3d(_t))

            def __get__(self):
                self.update_particle_data()
                cdef Vector3d torque_body
                cdef Vector3d torque_space
                torque_body = self.particle_data.torque()
                torque_space = convert_vector_body_to_space(
                    dereference(self.particle_data), torque_body)

                return make_array_locked(torque_space)

    IF ROTATIONAL_INERTIA:
        property rinertia:
            """
            The particle rotational inertia.

            rinertia : (3,) array_like of :obj:`float`

            Sets the diagonal elements of this particle's rotational inertia
            tensor. These correspond with the inertial moments along the
            coordinate axes in the particle's co-rotating coordinate system.
            When the particle's quaternions are set to ``[1, 0, 0, 0,]``, the
            co-rotating and the fixed (lab) frames are co-aligned.

            .. note::
               This needs the feature ``ROTATIONAL_INERTIA``.

            """

            def __set__(self, _rinertia):
                check_type_or_throw_except(
                    _rinertia, 3, float, "Rotation_inertia has to be 3 floats.")
                set_particle_rotational_inertia(
                    self._id, make_Vector3d(_rinertia))

            def __get__(self):
                self.update_particle_data()
                return make_array_locked(self.particle_data.rinertia())

    # Charge
    property q:
        """
        Particle charge.

        q : :obj:`float`

        .. note::
           This needs the feature ``ELECTROSTATICS``.

        """

        def __set__(self, q):
            check_type_or_throw_except(
                q, 1, float, "Charge has to be a float.")
            set_particle_q(self._id, q)

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.q()

    IF LB_ELECTROHYDRODYNAMICS:
        property mu_E:
            """
            Particle electrophoretic velocity.

            mu_E : :obj:`float`

            This effectively acts as a velocity offset between
            a lattice-Boltzmann fluid and the particle. Has only
            an effect if LB is turned on.

            .. note::
               This needs the feature ``LB_ELECTROHYDRODYNAMICS``.

            """

            def __set__(self, mu_E):
                check_type_or_throw_except(
                    mu_E, 3, float, "mu_E has to be 3 floats.")
                set_particle_mu_E(self._id, make_Vector3d(mu_E))

            def __get__(self):
                self.update_particle_data()
                return make_array_locked(self.particle_data.mu_E())

    property virtual:
        """Virtual flag.

        Declares the particles as virtual (``True``) or non-virtual
        (``False``, default).

        virtual : :obj:`bool`

        .. note::
           This needs the feature ``VIRTUAL_SITES``

        """

        def __set__(self, _v):
            IF VIRTUAL_SITES:
                if is_valid_type(_v, int):
                    set_particle_virtual(self._id, < bint > _v)
                else:
                    raise ValueError("virtual must be a boolean.")
            ELSE:
                if _v:
                    raise AttributeError(
                        "To make a particle virtual, VIRTUAL_SITES has to be defined in myconfig.hpp")

        def __get__(self):
            self.update_particle_data()
            return self.particle_data.is_virtual()

    IF VIRTUAL_SITES_RELATIVE:
        property vs_quat:
            """ Virtual site quaternion.

            This quaternion describes the virtual particles orientation in the
            body fixed frame of the related real particle.

            vs_quat : (4,) array_like of :obj:`float`

            .. note::
               This needs the feature ``VIRTUAL_SITES_RELATIVE``.

            """

            def __set__(self, q):
                check_type_or_throw_except(
                    q, 4, float, "vs_quat has to be an array-like of length 4")
                if np.linalg.norm(q) == 0.:
                    raise ValueError("quaternion is zero")
                cdef Quaternion[double] _q
                for i in range(4):
                    _q[i] = q[i]
                set_particle_vs_quat(self._id, _q)

            def __get__(self):
                self.update_particle_data()
                cdef Quaternion[double] q
                q = get_particle_vs_quat(self.particle_data)
                return array_locked([q[0], q[1], q[2], q[3]])

        property vs_relative:
            """
            Virtual sites relative parameters.

            Allows for manual access to the attributes of virtual sites in the "relative"
            implementation. PID denotes the id of the particle to which this virtual site
            is related and distance the distance between non-virtual and virtual particle.
            The relative orientation is specified as a quaternion of 4 floats.

            vs_relative : tuple (PID, distance, quaternion)

            .. note::
               This needs the feature ``VIRTUAL_SITES_RELATIVE``

            """

            def __set__(self, x):
                if len(x) != 3:
                    raise ValueError(
                        "vs_relative needs input in the form [id, distance, quaternion].")
                rel_to, dist, quat = x
                check_type_or_throw_except(
                    rel_to, 1, int, "The particle id has to be given as an int.")
                check_type_or_throw_except(
                    dist, 1, float, "The distance has to be given as a float.")
                check_type_or_throw_except(
                    quat, 4, float, "The quaternion has to be given as a tuple of 4 floats.")
                if np.linalg.norm(quat) == 0.:
                    raise ValueError("quaternion is zero")
                cdef Quaternion[double] q
                for i in range(4):
                    q[i] = quat[i]

                set_particle_vs_relative(self._id, rel_to, dist, q)

            def __get__(self):
                self.update_particle_data()
                cdef int rel_to = -1
                cdef double dist = 0.
                cdef Quaternion[double] q
                q = get_particle_vs_relative(self.particle_data, rel_to, dist)
                return (rel_to, dist, array_locked([q[0], q[1], q[2], q[3]]))

        # vs_auto_relate_to
        def vs_auto_relate_to(self, rel_to):
            """
            Setup this particle as virtual site relative to the particle
            in argument ``rel_to``. A particle cannot relate to itself.

            Parameters
            -----------
            rel_to : :obj:`int` or :obj:`ParticleHandle`
                Particle to relate to (either particle id or particle object).

            """
            # If rel_to is of type ParticleHandle,
            # resolve id of particle which to relate to
            if isinstance(rel_to, ParticleHandle):
                rel_to = rel_to.id
            check_type_or_throw_except(
                rel_to, 1, int, "Argument of vs_auto_relate_to has to be of type ParticleHandle or int.")
            vs_relate_to(self._id, rel_to)
            handle_errors('vs_auto_relate_to')

    IF DIPOLES:
        property dip:
            """
            The orientation of the dipole axis.

            dip : (3,) array_like of :obj:`float`

            .. note::
               This needs the feature ``DIPOLES``.

            """

            def __set__(self, _dip):
                check_type_or_throw_except(
                    _dip, 3, float, "Dipole moment vector has to be 3 floats.")
                set_particle_dip(self._id, make_Vector3d(_dip))

            def __get__(self):
                self.update_particle_data()
                return make_array_locked(self.particle_data.calc_dip())

        # Scalar magnitude of dipole moment
        property dipm:
            """
            The magnitude of the dipole moment.

            dipm : :obj:`float`

            .. note::
               This needs the feature ``DIPOLES``.

            """

            def __set__(self, dipm):
                check_type_or_throw_except(
                    dipm, 1, float, "Magnitude of dipole moment has to be 1 float.")
                set_particle_dipm(self._id, dipm)

            def __get__(self):
                self.update_particle_data()
                return self.particle_data.dipm()

    IF EXTERNAL_FORCES:
        property ext_force:
            """
            An additional external force applied to the particle.

            ext_force : (3,) array_like of :obj:`float`

            .. note::
               This needs the feature ``EXTERNAL_FORCES``.

            """

            def __set__(self, _ext_f):
                check_type_or_throw_except(
                    _ext_f, 3, float, "External force vector has to be 3 floats.")
                set_particle_ext_force(self._id, make_Vector3d(_ext_f))

            def __get__(self):
                self.update_particle_data()
                return make_array_locked(
                    self.particle_data.ext_force())

        property fix:
            """
            Fixes the particle motion in the specified cartesian directions.

            fix : (3,) array_like of :obj:`bool`

            Fixes the particle in space. By supplying a set of 3 bools as
            arguments it is possible to fix motion in x, y, or z coordinates
            independently. For example::

                part[<INDEX>].fix = [False, False, True]

            will fix motion for particle with index ``INDEX`` only in z.

            .. note::
               This needs the feature ``EXTERNAL_FORCES``.

            """

            def __set__(self, flag):
                check_type_or_throw_except(
                    flag, 3, int, "Property 'fix' has to be 3 bools.")
                set_particle_fix(self._id, make_Vector3i(flag))

            def __get__(self):
                self.update_particle_data()
                cdef Vector3i flag = get_particle_fix(self.particle_data)
                ext_flag = np.array([flag[0], flag[1], flag[2]], dtype=int)
                return array_locked(ext_flag)

        IF ROTATION:
            property ext_torque:
                """
                An additional external torque is applied to the particle.

                ext_torque : (3,) array_like of :obj:`float`

                ..  note::
                    * This torque is specified in the laboratory frame!
                    * This needs features ``EXTERNAL_FORCES`` and ``ROTATION``.

                """

                def __set__(self, _ext_t):
                    check_type_or_throw_except(
                        _ext_t, 3, float, "External force vector has to be 3 floats.")
                    set_particle_ext_torque(self._id, make_Vector3d(_ext_t))

                def __get__(self):
                    self.update_particle_data()
                    return make_array_locked(self.particle_data.ext_torque())

    IF THERMOSTAT_PER_PARTICLE:
        IF PARTICLE_ANISOTROPY:
            property gamma:
                """
                The body-fixed frictional coefficient used in the Langevin
                and Brownian thermostats.

                gamma : :obj:`float` or (3,) array_like of :obj:`float`

                .. note::
                    This needs features ``PARTICLE_ANISOTROPY`` and
                    ``THERMOSTAT_PER_PARTICLE``.

                See Also
                ----------
                :meth:`espressomd.thermostat.Thermostat.set_langevin` : Setting the parameters of the Langevin thermostat

                """

                def __set__(self, _gamma):
                    # We accept a single number by just repeating it
                    if not isinstance(_gamma, collections.abc.Iterable):
                        _gamma = 3 * [_gamma]
                    check_type_or_throw_except(
                        _gamma, 3, float, "Friction has to be 3 floats.")
                    set_particle_gamma(self._id, make_Vector3d(_gamma))

                def __get__(self):
                    self.update_particle_data()
                    return make_array_locked(
                        get_particle_gamma(self.particle_data))

        ELSE:
            property gamma:
                """
                The translational frictional coefficient used in the Langevin
                and Brownian thermostats.

                gamma : :obj:`float`

                .. note::
                   This needs the feature ``THERMOSTAT_PER_PARTICLE``.

                See Also
                ----------
                :meth:`espressomd.thermostat.Thermostat.set_langevin.set_langevin` : Setting the parameters of the Langevin thermostat

                """

                def __set__(self, _gamma):
                    check_type_or_throw_except(
                        _gamma, 1, float, "Gamma has to be a float.")
                    set_particle_gamma(self._id, _gamma)

                def __get__(self):
                    self.update_particle_data()
                    return get_particle_gamma(self.particle_data)

        IF ROTATION:
            IF PARTICLE_ANISOTROPY:
                property gamma_rot:
                    """
                    The particle translational frictional coefficient used in
                    the Langevin and Brownian thermostats.

                    gamma_rot : :obj:`float` or (3,) array_like of :obj:`float`

                    .. note::
                        This needs features ``ROTATION``, ``PARTICLE_ANISOTROPY``
                        and ``THERMOSTAT_PER_PARTICLE``.

                    """

                    def __set__(self, _gamma_rot):
                        # We accept a single number by just repeating it
                        if not isinstance(
                                _gamma_rot, collections.abc.Iterable):
                            _gamma_rot = 3 * [_gamma_rot]
                        check_type_or_throw_except(
                            _gamma_rot, 3, float, "Rotational friction has to be 3 floats.")
                        set_particle_gamma_rot(
                            self._id, make_Vector3d(_gamma_rot))

                    def __get__(self):
                        self.update_particle_data()
                        return make_array_locked(
                            get_particle_gamma_rot(self.particle_data))
            ELSE:
                property gamma_rot:
                    """
                    The particle rotational frictional coefficient used in the
                    Langevin and Brownian thermostats.

                    gamma_rot : :obj:`float`

                    .. note::
                        This needs features ``ROTATION`` and
                        ``THERMOSTAT_PER_PARTICLE``.

                    """

                    def __set__(self, _gamma_rot):
                        check_type_or_throw_except(
                            _gamma_rot, 1, float, "gamma_rot has to be a float.")
                        set_particle_gamma_rot(self._id, _gamma_rot)

                    def __get__(self):
                        self.update_particle_data()
                        return get_particle_gamma_rot(self.particle_data)

    IF ROTATION:
        property rotation:
            """
            Switches the particle's rotational degrees of freedom in the
            Cartesian axes in the body-fixed frame. The content of the torque
            and omega variables are meaningless for the co-ordinates for which
            rotation is disabled.

            The default is not to integrate any rotational degrees of freedom.

            rotation : (3,) array_like of :obj:`bool`

            .. note::
                This needs the feature ``ROTATION``.

            """

            def __set__(self, flag):
                check_type_or_throw_except(
                    flag, 3, int, "Property 'rotation' has to be 3 bools.")
                set_particle_rotation(self._id, make_Vector3i(flag))

            def __get__(self):
                self.update_particle_data()
                cdef Vector3i flag = get_particle_rotation(self.particle_data)
                rot_flag = np.array([flag[0], flag[1], flag[2]], dtype=int)
                return array_locked(rot_flag)

    IF EXCLUSIONS:
        property exclusions:
            """
            The exclusion list of particles where non-bonded interactions are ignored.

            .. note::
                This needs the feature ``EXCLUSIONS``.

            exclusions : (N,) array_like of :obj:`int`

            """

            def __set__(self, partners):
                # Delete all
                for e in self.exclusions:
                    self.delete_exclusion(e)

                nlvl = nesting_level(partners)

                if nlvl == 0:  # Single item
                    self.add_exclusion(partners)
                elif nlvl == 1:  # List of items
                    for partner in partners:
                        self.add_exclusion(partner)
                else:
                    raise ValueError(
                        "Exclusions have to be specified as a lists of partners or a single item.")

            def __get__(self):
                self.update_particle_data()
                return array_locked(self.particle_data.exclusions_as_vector())

        def add_exclusion(self, partner):
            """
            Exclude non-bonded interactions with the given partner.

            .. note::
                This needs the feature ``EXCLUSIONS``.

            Parameters
            -----------
            partner : :class:`~espressomd.particle_data.ParticleHandle` or :obj:`int`
                Particle to exclude.

            """
            if isinstance(partner, ParticleHandle):
                p_id = partner.id
            else:
                p_id = partner
            check_type_or_throw_except(
                p_id, 1, int, "Argument 'partner' has to be a ParticleHandle or int.")
            self.update_particle_data()
            if self.particle_data.has_exclusion(p_id):
                raise RuntimeError(
                    f"Particle with id {p_id} is already in exclusion list of particle with id {self._id}")
            add_particle_exclusion(self._id, p_id)

        def delete_exclusion(self, partner):
            """
            Remove exclusion of non-bonded interactions with the given partner.

            .. note::
                This needs the feature ``EXCLUSIONS``.

            Parameters
            -----------
            partner : :class:`~espressomd.particle_data.ParticleHandle` or :obj:`int`
                Particle to remove from exclusions.

            """
            if isinstance(partner, ParticleHandle):
                p_id = partner.id
            else:
                p_id = partner
            check_type_or_throw_except(
                p_id, 1, int, "Argument 'partner' has to be a ParticleHandle or int.")
            self.update_particle_data()
            if not self.particle_data.has_exclusion(p_id):
                raise RuntimeError(
                    f"Particle with id {p_id} is not in exclusion list of particle with id {self._id}")
            remove_particle_exclusion(self._id, p_id)

    IF ENGINE:
        property swimming:
            """
            Set swimming parameters.

            This property takes a dictionary with a different number of
            entries depending whether there is an implicit fluid (i.e. with the
            Langevin thermostat) of an explicit fluid (with LB).

            Swimming enables the particle to be self-propelled in the direction
            determined by its quaternion. For setting the quaternion of the
            particle see :attr:`~espressomd.particle_data.ParticleHandle.quat`. The
            self-propulsion speed will relax to a constant velocity, that is specified by
            ``v_swim``. Alternatively it is possible to achieve a constant velocity by
            imposing a constant force term ``f_swim`` that is balanced by friction of a
            (Langevin) thermostat. The way the velocity of the particle decays to the
            constant terminal velocity in either of these methods is completely
            determined by the friction coefficient. You may only set one of the
            possibilities ``v_swim`` *or* ``f_swim`` as you cannot relax to constant force
            *and* constant velocity at the same time. Setting both ``v_swim`` and
            ``f_swim`` to 0.0 disables swimming. This option applies to all
            non-lattice-Boltzmann thermostats. Note that there is no real difference
            between ``v_swim`` and ``f_swim`` since the latter may always be chosen such that
            the same terminal velocity is achieved for a given friction coefficient.


            Parameters
            ----------
            f_swim : :obj:`float`
                Achieve a constant velocity by imposing a constant
                force term ``f_swim`` that is balanced by friction of a
                (Langevin) thermostat. This excludes the option ``v_swim``.
            v_swim : :obj:`float`
                Achieve a constant velocity by imposing a constant terminal
                velocity ``v_swim``. This excludes the option ``f_swim``.
            mode : :obj:`str`, {'pusher', 'puller', 'N/A'}
                The LB flow field can be generated by a pushing or a
                pulling mechanism, leading to change in the sign of the
                dipolar flow field with respect to the direction of motion.
            dipole_length : :obj:`float`
                This determines the distance of the source of
                propulsion from the particle's center.

            Notes
            -----
            This needs the feature ``ENGINE``.  The keys ``'mode'``,
            and ``'dipole_length'`` are only
            available if ``ENGINE`` is used with LB or ``CUDA``.

            Examples
            --------
            >>> import espressomd
            >>> system = espressomd.System(box_l=[10, 10, 10])
            >>> # Langevin swimmer
            >>> system.part.add(pos=[1, 0, 0], swimming={'f_swim': 0.03})
            >>> # LB swimmer
            >>> system.part.add(pos=[2, 0, 0], swimming={'f_swim': 0.01,
            ...     'mode': 'pusher', 'dipole_length': 2.0})

            """

            def __set__(self, _params):
                cdef particle_parameters_swimming swim

                swim.swimming = True
                swim.v_swim = 0.0
                swim.f_swim = 0.0
                swim.push_pull = 0
                swim.dipole_length = 0.0

                if type(_params) == type(True):
                    if _params:
                        raise Exception(
                            "To enable swimming supply a dictionary of parameters.")
                else:
                    if 'f_swim' in _params and 'v_swim' in _params:
                        if _params["f_swim"] == 0 or _params["v_swim"] == 0:
                            pass
                        else:
                            raise Exception(
                                "You can't set v_swim and f_swim at the same time.")
                    if 'f_swim' in _params:
                        check_type_or_throw_except(
                            _params['f_swim'], 1, float, "f_swim has to be a float.")
                        swim.f_swim = _params['f_swim']
                    if 'v_swim' in _params:
                        check_type_or_throw_except(
                            _params['v_swim'], 1, float, "v_swim has to be a float.")
                        swim.v_swim = _params['v_swim']

                    if 'mode' in _params:
                        if _params['mode'] == "pusher":
                            swim.push_pull = -1
                        elif _params['mode'] == "puller":
                            swim.push_pull = 1
                        elif _params['mode'] == "N/A":
                            swim.push_pull = 0
                        else:
                            raise Exception(
                                "'mode' has to be either 'pusher', 'puller' or 'N/A'.")

                    if 'dipole_length' in _params:
                        check_type_or_throw_except(
                            _params['dipole_length'], 1, float, "dipole_length has to be a float.")
                        swim.dipole_length = _params['dipole_length']

                set_particle_swimming(self._id, swim)

            def __get__(self):
                self.update_particle_data()
                swim = {}
                mode = "N/A"
                cdef particle_parameters_swimming _swim
                _swim = self.particle_data.swimming()

                if _swim.push_pull == -1:
                    mode = 'pusher'
                elif _swim.push_pull == 1:
                    mode = 'puller'
                swim = {
                    'v_swim': _swim.v_swim,
                    'f_swim': _swim.f_swim,
                    'mode': mode,
                    'dipole_length': _swim.dipole_length
                }

                return swim

    def remove(self):
        """
        Delete the particle.

        See Also
        --------
        espressomd.particle_data.ParticleList.add
        espressomd.particle_data.ParticleList.clear

        """
        remove_particle(self._id)
        del self

    def add_verified_bond(self, bond):
        """
        Add a bond, the validity of which has already been verified.

        See Also
        --------
        add_bond : Add an unverified bond to the ``Particle``.
        bonds : ``Particle`` property containing a list of all current bonds held by ``Particle``.

        """

        # If someone adds bond types with more than four partners, this has to
        # be changed
        cdef int bond_info[5]
        bond_info[0] = bond[0]._bond_id
        for i in range(1, len(bond)):
            bond_info[i] = bond[i]
        if self._id in bond[1:]:
            raise Exception(
                f"Bond partners {bond[1:]} include the particle {self._id} itself.")
        add_particle_bond(self._id, make_const_span[int](bond_info, len(bond)))

    def delete_verified_bond(self, bond):
        """
        Delete a single bond from the particle. The validity of which has already been verified.

        Parameters
        ----------
        bond : :obj:`tuple`
            tuple where the first element is either a bond ID of a bond type,
            and the last element is the ID of the partner particle to be bonded
            to.

        See Also
        --------
        delete_bond : Delete an unverified bond held by the ``Particle``.
        bonds : ``Particle`` property containing a list of all current bonds held by ``Particle``.

        """

        cdef int bond_info[5]
        bond_info[0] = bond[0]._bond_id
        for i in range(1, len(bond)):
            bond_info[i] = bond[i]

        delete_particle_bond(
            self._id, make_const_span[int](bond_info, len(bond)))

    def normalize_and_check_bond_or_throw_exception(self, bond):
        """
        Checks the validity of the given bond:

        - If the bondtype is given as an object or a numerical id
        - If all partners are of type :obj:`int`
        - If the number of partners satisfies the bond
        - If the bond type used exists (is lower than ``n_bonded_ia``)
        - If the number of bond partners fits the bond type

        Throws an exception if any of these are not met.

        Normalize the bond, i.e. replace bond ids by bond objects and particle
        objects by particle ids.

        """
        # Has it []-access
        if not hasattr(bond, "__getitem__"):
            raise ValueError(
                "Bond needs to be a tuple or list containing bond type and partners.")

        bond = list(bond)
        # Bond type or numerical bond id
        if is_valid_type(bond[0], int):
            bond[0] = BondedInteractions()[bond[0]]
        elif not isinstance(bond[0], BondedInteraction):
            raise Exception(
                f"1st element of Bond has to be of type BondedInteraction or int, got {type(bond[0])}.")
        # Check the bond is in the list of active bonded interactions
        if bond[0]._bond_id == -1:
            raise Exception(
                "The bonded interaction has not yet been added to the list of active bonds in ESPResSo.")
        # Validity of the numeric id
        if not bonded_ia_params_zero_based_type(bond[0]._bond_id):
            raise ValueError(
                f"The bond type {bond[0]._bond_id} does not exist.")
        # Number of partners
        expected_num_partners = bond[0].call_method('get_num_partners')
        if len(bond) - 1 != expected_num_partners:
            raise ValueError(
                f"Bond {bond[0]} needs {expected_num_partners} partners.")
        # Type check on partners
        for i in range(1, len(bond)):
            if isinstance(bond[i], ParticleHandle):
                # Put the particle id instead of the particle handle
                bond[i] = bond[i].id
            elif not is_valid_type(bond[i], int):
                raise ValueError(
                    "Bond partners have to be of type integer or ParticleHandle.")
        return tuple(bond)

    def add_bond(self, bond):
        """
        Add a single bond to the particle.

        Parameters
        ----------
        bond : :obj:`tuple`
            tuple where the first element is either a bond ID or a bond object,
            and the next elements are particle ids or particle objects to be
            bonded to.

        See Also
        --------
        bonds : ``Particle`` property containing a list of all current bonds held by ``Particle``.

        Examples
        --------
        >>> import espressomd.interactions
        >>>
        >>> system = espressomd.System(box_l=3 * [10])
        >>>
        >>> # define a harmonic potential and add it to the system
        >>> harm_bond = espressomd.interactions.HarmonicBond(r_0=1, k=5)
        >>> system.bonded_inter.add(harm_bond)
        >>>
        >>> # add two particles
        >>> p1 = system.part.add(pos=(1, 0, 0))
        >>> p2 = system.part.add(pos=(2, 0, 0))
        >>>
        >>> # bond them via the bond type
        >>> p1.add_bond((harm_bond, p2))
        >>> # or via the bond index (zero in this case since it is the first one added)
        >>> p1.add_bond((0, p2))

        """
        _bond = self.normalize_and_check_bond_or_throw_exception(bond)
        if _bond in self.bonds:
            raise RuntimeError(
                f"Bond {_bond} already exists on particle {self._id}.")
        self.add_verified_bond(_bond)

    def delete_bond(self, bond):
        """
        Delete a single bond from the particle.

        Parameters
        ----------
        bond : :obj:`tuple`
            tuple where the first element is either a bond ID or a bond object,
            and the next elements are particle ids or particle objects that are
            bonded to.

        See Also
        --------
        bonds : ``Particle`` property containing a list of all bonds currently held by ``Particle``.


        Examples
        --------

        >>> import espressomd.interactions
        >>>
        >>> system = espressomd.System(box_l=3 * [10])
        >>>
        >>> # define a harmonic potential and add it to the system
        >>> harm_bond = espressomd.interactions.HarmonicBond(r_0=1, k=5)
        >>> system.bonded_inter.add(harm_bond)
        >>>
        >>> # bond two particles to the first one
        >>> p0 = system.part.add(pos=(1, 0, 0))
        >>> p1 = system.part.add(pos=(2, 0, 0))
        >>> p2 = system.part.add(pos=(1, 1, 0))
        >>> p0.add_bond((harm_bond, p1))
        >>> p0.add_bond((harm_bond, p2))
        >>>
        >>> print(p0.bonds)
        ((HarmonicBond(0): {'r_0': 1.0, 'k': 5.0, 'r_cut': 0.0}, 1),
         (HarmonicBond(0): {'r_0': 1.0, 'k': 5.0, 'r_cut': 0.0}, 2))
        >>> # delete the first bond
        >>> p0.delete_bond(p0.bonds[0])
        >>> print(p0.bonds)
        ((HarmonicBond(0): {'r_0': 1.0, 'k': 5.0, 'r_cut': 0.0}, 2),)

        """

        _bond = self.normalize_and_check_bond_or_throw_exception(bond)
        if _bond not in self.bonds:
            raise RuntimeError(
                f"Bond {_bond} doesn't exist on particle {self._id}.")
        self.delete_verified_bond(_bond)

    def delete_all_bonds(self):
        """
        Delete all bonds from the particle.

        See Also
        ----------
        delete_bond : Delete an unverified bond held by the particle.
        bonds : ``Particle`` property containing a list of all current bonds held by ``Particle``.

        """

        delete_particle_bonds(self._id)

    def update(self, new_properties):
        """
        Update properties of a particle.

        Parameters
        ----------
        new_properties : :obj:`dict`
            Map particle property names to values. All properties except
            for the particle id can be changed.

        Examples
        --------

        >>> import espressomd
        >>> system = espressomd.System(box_l=[10, 10, 10])
        >>> p = system.part.add(pos=[1, 2, 3], q=1, virtual=True)
        >>> print(p.pos, p.q, p.virtual)
        [1. 2. 3.] 1.0 True
        >>> p.update({'pos': [4, 5, 6], 'virtual': False, 'q': 0})
        >>> print(p.pos, p.q, p.virtual)
        [4. 5. 6.] 0.0 False

        """
        if "id" in new_properties:
            raise Exception("Cannot change particle id.")

        for k in ("quat", "director", "dip"):
            if k in new_properties:
                setattr(self, k, new_properties[k])
                break
        for k, v in new_properties.items():
            if k in ("quat", "director", "dip"):
                continue
            setattr(self, k, v)

    IF ROTATION:
        def convert_vector_body_to_space(self, vec):
            """Converts the given vector from the particle's body frame to the space frame"""
            self.update_particle_data()
            return np.array(make_array_locked(convert_vector_body_to_space(
                dereference(self.particle_data), make_Vector3d(vec))))

        def convert_vector_space_to_body(self, vec):
            """Converts the given vector from the space frame to the particle's body frame"""
            self.update_particle_data()
            return np.array(make_array_locked(convert_vector_space_to_body(
                dereference(self.particle_data), make_Vector3d(vec))))

        def rotate(self, axis, angle):
            """Rotates the particle around the given axis

            Parameters
            ----------
            axis : (3,) array_like of :obj:`float`

            angle : :obj:`float`

            """
            rotate_particle(self._id, make_Vector3d(axis), angle)

cdef class _ParticleSliceImpl:
    """Handles slice inputs.

    This base class should not be used directly. Use
    :class:`espressomd.particle_data.ParticleSlice` instead, which contains
    all the particle properties.

    """

    def __cinit__(self, slice_, prefetch_chunk_size=10000):
        # Chunk size for pre-fetch cache
        self._chunk_size = prefetch_chunk_size

        # We distinguish two cases:
        # * ranges and slices only specify lower and upper bounds for particle
        #   ids and optionally a step. Gaps in the id range are tolerated.
        # * Explicit list/tuple/ndarray containing particle ids. Here
        #   all particles have to exist and the result maintains the order
        #   specified in the list.
        if isinstance(slice_, (slice, range)):
            self.id_selection = self._id_selection_from_slice(slice_)
        elif isinstance(slice_, (list, tuple, np.ndarray)):
            self._validate_pid_list(slice_)
            self.id_selection = np.array(slice_, dtype=int)
        else:
            raise TypeError(
                f"ParticleSlice must be initialized with an instance of "
                f"slice or range, or with a list, tuple, or ndarray of ints, "
                f"but got {slice_} of type {type(slice_)}")

    def _id_selection_from_slice(self, slice_):
        """Returns an ndarray of particle ids to be included in the
        ParticleSlice for a given range or slice object.
        """
        # Prevent negative bounds
        if (slice_.start is not None and slice_.start < 0) or \
           (slice_.stop is not None and slice_.stop < 0):
            raise IndexError(
                "Negative start and end ids are not supported on ParticleSlice")

        # We start with a full list of possible particle ids and then
        # remove ids of non-existing particles
        id_list = np.arange(get_maximal_particle_id() + 1, dtype=int)
        id_list = id_list[slice_]

        # Generate a mask which will remove ids of non-existing particles
        mask = np.empty(len(id_list), dtype=type(True))
        mask[:] = True
        for i, id in enumerate(id_list):
            if not particle_exists(id):
                mask[i] = False
        # Return the id list filtered by the mask
        return id_list[mask]

    def _validate_pid_list(self, pid_list):
        """Check that all entries are integers and the corresponding particles exist. Throw, otherwise."""
        # Check that all entries are some flavor of integer
        for pid in pid_list:
            if not is_valid_type(pid, int):
                raise TypeError(
                    f"Particle id must be an integer but got {pid}")
            if not particle_exists(pid):
                raise IndexError(f"Particle does not exist {pid}")

    def __iter__(self):
        return self._id_gen()

    def _id_gen(self):
        """Generator for chunked and prefetched iteration of particles.
        """
        for chunk in self.chunks(self.id_selection, self._chunk_size):
            prefetch_particle_data(chunk)
            for i in chunk:
                yield ParticleHandle(i)

    def chunks(self, l, n):
        """Generator returning chunks of length n from l.
        """
        for i in range(0, len(l), n):
            yield l[i:i + n]

    def __len__(self):
        return len(self.id_selection)

    property pos_folded:
        """
        Particle position (folded into central image).

        """

        def __get__(self):
            pos_array = np.zeros((len(self.id_selection), 3))
            for i in range(len(self.id_selection)):
                pos_array[i, :] = ParticleHandle(
                    self.id_selection[i]).pos_folded
            return pos_array

    IF EXCLUSIONS:
        def add_exclusion(self, _partner):
            for i in self.id_selection:
                ParticleHandle(i).add_exclusion(_partner)

        def delete_exclusion(self, _partner):
            for i in self.id_selection:
                ParticleHandle(i).delete_exclusion(_partner)

    def __str__(self):
        res = ""
        pl = ParticleList()
        for i in self.id_selection:
            if pl.exists(i):
                res += f"{pl.by_id(i)}, "
        # Remove final comma
        return f"ParticleSlice([{res[:-2]}])"

    def update(self, new_properties):
        if "id" in new_properties:
            raise Exception("Cannot change particle id.")

        for k, v in new_properties.items():
            setattr(self, k, v)

    # Bond related methods
    def add_bond(self, _bond):
        """
        Add a single bond to the particles.

        """
        for i in self.id_selection:
            ParticleHandle(i).add_bond(_bond)

    def delete_bond(self, _bond):
        """
        Delete a single bond from the particles.

        """
        for i in self.id_selection:
            ParticleHandle(i).delete_bond(_bond)

    def delete_all_bonds(self):
        for i in self.id_selection:
            ParticleHandle(i).delete_all_bonds()

    def remove(self):
        """
        Delete the particles.

        See Also
        --------
        :meth:`espressomd.particle_data.ParticleList.add`

        """
        for id in self.id_selection:
            ParticleHandle(id).remove()


class ParticleSlice(_ParticleSliceImpl):

    """
    Handles slice inputs e.g. ``part[0:2]``. Sets values for selected slices or
    returns values as a single list.

    """

    def __setattr__(self, name, value):
        if name != "_chunk_size" and not hasattr(ParticleHandle, name):
            raise AttributeError(
                f"ParticleHandle does not have the attribute {name}.")
        super().__setattr__(name, value)

    def to_dict(self):
        """
        Returns the particles attributes as a dictionary.

        It can be used to save the particle data and recover it by using

        >>> p = system.part.add(...)
        >>> particle_dict = p.to_dict()
        >>> system.part.add(particle_dict)

        It includes the content of ``particle_attributes``, minus a few exceptions:

        - :attr:`~ParticleHandle.dip`, :attr:`~ParticleHandle.director`:
          Setting only the director will overwrite the orientation of the
          particle around the axis parallel to dipole moment/director.
          Quaternions contain the full info.
        - :attr:`~ParticleHandle.image_box`, :attr:`~ParticleHandle.node`

        """

        odict = {}
        key_list = [p.id for p in self]
        for particle_number in key_list:
            pdict = ParticleHandle(particle_number).to_dict()
            for p_key, p_value in pdict.items():
                if p_key in odict:
                    odict[p_key].append(p_value)
                else:
                    odict[p_key] = [p_value]
        return odict


cdef class ParticleList:
    """
    Provides access to the particles.

    """

    def by_id(self, id):
        """
        Access a particle by its integer id.
        """
        return ParticleHandle(id)

    def by_ids(self, ids):
        """
        Get a slice of particles by their integer ids.
        """
        return ParticleSlice(ids)

    def all(self):
        """
        Get a slice containing all particles.
        """
        all_ids = get_particle_ids()
        return self.by_ids(all_ids)

    # __getstate__ and __setstate__ define the pickle interaction
    def __getstate__(self):
        """Attributes to pickle.

        Content of ``particle_attributes``, minus a few exceptions:

        - :attr:`~ParticleHandle.dip`, :attr:`~ParticleHandle.director`:
          Setting only the director will overwrite the orientation of the
          particle around the axis parallel to dipole moment/director.
          Quaternions contain the full info.
        - :attr:`~ParticleHandle.id`: The particle id is used as the
          storage key when pickling all particles via :class:`ParticleList`,
          and the interface (rightly) does not support changing of the id
          after the particle was created.
        - :attr:`~ParticleHandle.image_box`, :attr:`~ParticleHandle.node`

        """

        odict = {}
        for p in self:
            pdict = p.to_dict()
            del pdict["id"]
            odict[p.id] = pdict
        return odict

    def __setstate__(self, params):
        exclusions = collections.OrderedDict()
        for particle_number in params.keys():
            params[particle_number]["id"] = particle_number
            IF EXCLUSIONS:
                exclusions[particle_number] = params[particle_number][
                    "exclusions"]
                del params[particle_number]["exclusions"]
            self._place_new_particle(params[particle_number])
        IF EXCLUSIONS:
            for pid in exclusions:
                self.by_id(pid).exclusions = exclusions[pid]

    def __len__(self):
        return get_n_part()

    def add(self, *args, **kwargs):
        """
        Adds one or several particles to the system

        Parameters
        ----------
        Either a dictionary or a bunch of keyword args.

        Returns
        -------
        Returns an instance of :class:`espressomd.particle_data.ParticleHandle` for each added particle.

        See Also
        --------
        :meth:`espressomd.particle_data.ParticleHandle.remove`

        Examples
        --------

        >>> import espressomd
        >>> system = espressomd.System(box_l=[10, 10, 10])
        >>> # add two particles
        >>> system.part.add(id=0, pos=(1, 0, 0))
        >>> system.part.add(id=1, pos=(2, 0, 0))

        ``pos`` is mandatory, ``id`` can be omitted, in which case it is assigned automatically.
        Several particles can be added by passing one value per particle to each property::

            system.part.add(pos=((1, 2, 3), (4, 5, 6)), q=(1, -1))

        """

        # Did we get a dictionary
        if len(args) == 1 and isinstance(
                args[0], (dict, collections.OrderedDict)):
            particles_dict = args[0]
        else:
            if len(args) == 0 and len(kwargs) != 0:
                particles_dict = kwargs
            else:
                raise ValueError(
                    "add() takes either a dictionary or a bunch of keyword args.")

        # Check for presence of pos attribute
        if "pos" not in particles_dict:
            raise ValueError(
                "pos attribute must be specified for new particle")

        if len(np.array(particles_dict["pos"]).shape) == 2:
            return self._place_new_particles(particles_dict)
        else:
            return self._place_new_particle(particles_dict)

    def _place_new_particle(self, p_dict):
        # Handling of particle id
        if "id" not in p_dict:
            # Generate particle id
            p_dict["id"] = get_maximal_particle_id() + 1
        else:
            if particle_exists(p_dict["id"]):
                raise Exception(f"Particle {p_dict['id']} already exists.")

        # Prevent setting of contradicting attributes
        IF DIPOLES:
            if 'dip' in p_dict and 'dipm' in p_dict:
                raise ValueError("Contradicting attributes: 'dip' and 'dipm'. Setting \
'dip' is sufficient as the length of the vector defines the scalar dipole moment.")
            IF ROTATION:
                for key in ('quat', 'director'):
                    if 'dip' in p_dict and key in p_dict:
                        raise ValueError(f"Contradicting attributes: 'dip' and '{key}'. \
Setting 'dip' overwrites the rotation of the particle around the dipole axis. \
Set '{key}' and 'dipm' instead.")
        IF ROTATION:
            if 'director' in p_dict and 'quat' in p_dict:
                raise ValueError("Contradicting attributes: 'director' and 'quat'. \
Setting 'quat' is sufficient as it defines the director.")

        # The ParticleList can not be used yet, as the particle
        # doesn't yet exist. Hence, the setting of position has to be
        # done here.
        check_type_or_throw_except(
            p_dict["pos"], 3, float, "Position must be 3 floats.")
        place_particle(p_dict["id"], make_Vector3d(p_dict["pos"]))

        # position is taken care of
        del p_dict["pos"]
        pid = p_dict.pop("id")

        if "type" not in p_dict:
            p_dict["type"] = 0

        if p_dict != {}:
            self.by_id(pid).update(p_dict)

        return self.by_id(pid)

    def _place_new_particles(self, p_list_dict):
        # Check if all entries have the same length
        n_parts = len(p_list_dict["pos"])
        if not all(np.array(v, dtype=object).shape and len(v) ==
                   n_parts for v in p_list_dict.values()):
            raise ValueError(
                "When adding several particles at once, all lists of attributes have to have the same size")

        # If particle ids haven't been provided, use free ones
        # beyond the highest existing one
        if "id" not in p_list_dict:
            first_id = get_maximal_particle_id() + 1
            p_list_dict["id"] = range(first_id, first_id + n_parts)

        # Place the particles
        for i in range(n_parts):
            p_dict = {k: v[i] for k, v in p_list_dict.items()}
            self._place_new_particle(p_dict)

        # Return slice of added particles
        return self.by_ids(p_list_dict["id"])

    # Iteration over all existing particles
    def __iter__(self):
        ids = get_particle_ids()

        for i in ids:
            yield self.by_id(i)

    def exists(self, idx):
        if is_valid_type(idx, int):
            return particle_exists(idx)
        if isinstance(idx, (slice, tuple, list, np.ndarray)):
            tf_array = np.zeros(len(idx), dtype=type(True))
            for i in range(len(idx)):
                tf_array[i] = particle_exists(idx[i])
            return tf_array

    def clear(self):
        """
        Removes all particles.

        See Also
        --------
        add
        :meth:`espressomd.particle_data.ParticleHandle.remove`

        """

        remove_all_particles()

    def __str__(self):
        return "ParticleList([" + ",".join(get_particle_ids()) + "])"

    def writevtk(self, fname, types='all'):
        """
        Write the positions and velocities of particles with specified
        types to a VTK file.

        Parameters
        ----------
        fname: :obj:`str`
            Filename of the target output file
        types: list of :obj:`int` or the string 'all', optional (default: 'all')
            A list of particle types which should be output to 'fname'

        Examples
        --------

        >>> import espressomd
        >>> system = espressomd.System(box_l=[10, 10, 10])
        >>> # add several particles
        >>> system.part.add(pos=0.5 * system.box_l, v=[1, 0, 0], type=0)
        >>> system.part.add(pos=0.4 * system.box_l, v=[0, 2, 0], type=1)
        >>> system.part.add(pos=0.7 * system.box_l, v=[2, 0, 1], type=1)
        >>> system.part.add(pos=0.1 * system.box_l, v=[0, 0, 1], type=2)
        >>> # write to VTK
        >>> system.part.writevtk("part_type_0_1.vtk", types=[0, 1])
        >>> system.part.writevtk("part_type_2.vtk", types=[2])
        >>> system.part.writevtk("part_all.vtk")

        .. todo:: move to ``./io/writer/``

        """

        global box_l
        if not hasattr(types, '__iter__'):
            types = [types]

        n = 0
        for p in self:
            if types == 'all' or p.type in types:
                n += 1

        with open(fname, "w") as vtk:
            vtk.write("# vtk DataFile Version 2.0\n")
            vtk.write("particles\n")
            vtk.write("ASCII\n")
            vtk.write("DATASET UNSTRUCTURED_GRID\n")
            vtk.write("POINTS {} floats\n".format(n))
            for p in self:
                if types == 'all' or p.type in types:
                    vtk.write("{} {} {}\n".format(*(p.pos_folded)))

            vtk.write("POINT_DATA {}\n".format(n))
            vtk.write("SCALARS velocity float 3\n")
            vtk.write("LOOKUP_TABLE default\n")
            for p in self:
                if types == 'all' or p.type in types:
                    vtk.write("{} {} {}\n".format(*p.v))

    property highest_particle_id:
        """
        Largest particle id.

        """

        def __get__(self):
            return get_maximal_particle_id()

    property n_part_types:
        """
        Number of particle types.

        """

        def __get__(self):
            return max_seen_particle_type

    property n_rigidbonds:
        """
        Number of rigid bonds.

        """

        def __get__(self):
            return n_rigidbonds

    def pairs(self):
        """
        Generator returns all pairs of particles.

        """

        ids = get_particle_ids()
        id_pairs = itertools.combinations(ids, 2)
        for id_pair in id_pairs:
            yield (self.by_id(id_pair[0]), self.by_id(id_pair[1]))

    def select(self, *args, **kwargs):
        """Generates a particle slice by filtering particles via a user-defined criterion

        Parameters:

        Either: a keyword arguments in which the keys are names of particle
        properties and the values are the values to filter for. E.g.,::

            system.part.select(type=0, q=1)

        Or: a function taking a ParticleHandle as argument and returning True if
        the particle is to be filtered for. E.g.,::

            system.part.select(lambda p: p.pos[0] < 0.5)

        Returns
        -------
        :class:`ParticleSlice` :
            An instance of :class:`ParticleSlice` containing the selected particles

        """

        # Ids of the selected particles
        ids = []
        # Did we get a function as argument?
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # Go over all particles and pass them to the user-provided function
            for p in self:
                if args[0](p):
                    ids.append(p.id)
            return ParticleSlice(ids)

        # Did we get a set of keyword args?
        elif len(args) == 0:
            for p in self:
                select = True
                # Check, if the particle fails any required criteria
                for k in kwargs:
                    # Fetch user-provided value and value in particle
                    val1 = kwargs[k]
                    val2 = getattr(p, k)
                    # Get tolerance from numerical accuracy limits
                    tol = max(
                        np.amax(np.spacing(val1)), np.amax(np.spacing(val2)))

                    # Compare
                    if not np.allclose(val1, val2, atol=tol):
                        select = False
                        break
                if select:
                    ids.append(p.id)
            return ParticleSlice(ids)
        else:
            raise Exception(
                "select() takes either selection function as positional argument or a set of keyword arguments.")


def set_slice_one_for_all(particle_slice, attribute, values):
    for i in particle_slice.id_selection:
        setattr(ParticleHandle(i), attribute, values)


def set_slice_one_for_each(particle_slice, attribute, values):
    for i, v in zip(particle_slice.id_selection, values):
        setattr(ParticleHandle(i), attribute, v)


def _add_particle_slice_properties():
    """
    Automatically add all of ParticleHandle's properties to ParticleSlice.

    """

    def set_attribute(particle_slice, values, attribute):
        """
        Setter function that sets attribute on every member of particle_slice.
        If values contains only one element, all members are set to it. If it
        contains as many elements as there are members, each of them gets set
        to the corresponding one. For attributes that are lists of various length,
        (bonds, exclusions) the nesting level decides if it is one-for-all or one-for-each.

        """

        N = len(particle_slice.id_selection)

        if N == 0:
            raise AttributeError(
                "Cannot set properties of an empty ParticleSlice")

        # Special attributes
        if attribute == "bonds":
            nlvl = nesting_level(values)
            if nlvl == 1 or nlvl == 2:
                set_slice_one_for_all(particle_slice, attribute, values)
            elif nlvl == 3 and len(values) == N:
                set_slice_one_for_each(particle_slice, attribute, values)
            else:
                raise Exception("Failed to set bonds for particle slice.")

            return

        elif attribute == "exclusions":
            nlvl = nesting_level(values)
            if nlvl == 0 or nlvl == 1:
                set_slice_one_for_all(particle_slice, attribute, values)
            elif nlvl == 2 and len(values) == N:
                set_slice_one_for_each(particle_slice, attribute, values)
            else:
                raise Exception("Failed to set exclusions for particle slice.")

            return

        elif attribute == "vs_relative":
            nlvl = nesting_level(values)
            if nlvl in [1, 2]:
                set_slice_one_for_all(particle_slice, attribute, values)
            elif nlvl == 3 and len(values) == N:
                set_slice_one_for_each(particle_slice, attribute, values)
            else:
                raise Exception(
                    "Failed to set vs_relative for particle slice.")

            return

        else:
            target = getattr(
                ParticleHandle(particle_slice.id_selection[0]), attribute)
            target_shape = np.shape(target)

            if not target_shape:  # scalar quantity
                if not np.shape(values):
                    set_slice_one_for_all(particle_slice, attribute, values)
                elif np.shape(values)[0] == N:
                    set_slice_one_for_each(particle_slice, attribute, values)
                else:
                    raise Exception(
                        f"Value shape {np.shape(values)} does not broadcast to attribute shape {target_shape}.")

                return

            else:  # fixed length vector quantity
                if target_shape == np.shape(values):
                    set_slice_one_for_all(particle_slice, attribute, values)
                elif target_shape == tuple(np.shape(values)[1:]) and np.shape(values)[0] == N:
                    set_slice_one_for_each(particle_slice, attribute, values)
                else:
                    raise Exception(
                        f"Value shape {np.shape(values)} does not broadcast to attribute shape {target_shape}.")

                return

    def get_attribute(particle_slice, attribute):
        """
        Getter function that copies attribute from every member of
        particle_slice into an array (if possible).
        For special properties, a tuple of tuples is used.

        """

        N = len(particle_slice.id_selection)
        if N == 0:
            return np.empty(0, dtype=type(None))

        # get first slice member to determine its type
        target = getattr(ParticleHandle(
            particle_slice.id_selection[0]), attribute)
        if type(target) is array_locked:  # vectorial quantity
            target_type = target.dtype
        else:  # scalar quantity
            target_type = type(target)

        if attribute in ["exclusions", "bonds", "vs_relative", "swimming"]:
            values = []
            for part in particle_slice._id_gen():
                values.append(getattr(part, attribute))
        else:
            values = np.empty((N,) + np.shape(target), dtype=target_type)
            i = 0
            for part in particle_slice._id_gen():
                values[i] = getattr(part, attribute)
                i += 1

        return values

    for attribute_name in particle_attributes:
        if attribute_name in dir(ParticleSlice):
            continue

        # synthesize a new property
        new_property = property(
            functools.partial(get_attribute, attribute=attribute_name),
            functools.partial(set_attribute, attribute=attribute_name),
            doc=getattr(ParticleHandle, attribute_name).__doc__)
        # attach the property to ParticleSlice
        setattr(ParticleSlice, attribute_name, new_property)


_add_particle_slice_properties()
