# Scoria MDA is a program for manipulating 3D molecular models
# that includes MDAnalysis support.
# 
# Copyright (C) 2017  Jacob D. Durrant
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Note that this version of Scoria (with MDAnalysis integration) is
# derived from the original Scoria source code, which retains its
# Apache 2.0 license.

from __future__ import absolute_import
from __future__ import print_function
from scoria_mda import dumbpy as numpy
import sys
from .six.moves import range

class Selections():
    """
    A class for selecting atoms. Subclass to the
    :py:class:`scoria_mda.Molecule` class.
    """

    ######## selections ########
    def __init__(self, parent_molecule_object):
        """
        Initializes the scoria_mda.Selections class.

        :param scoria_mda.Molecule parent_molecule_object: The scoria_mda.Molecule object
                    associated with this class.
        """

        self.__parent_molecule = parent_molecule_object

    def select_atoms(self, selection_criteria):
        """
        Select a set of atoms based on user-specified criteria.

        Should be called via the wrapper function :meth:`~scoria_mda.Molecule.Molecule.select_atoms`.

        :param dict selection_criteria: A dictionary, where the keys correspond
                    to keys in the
                    self.__parent_Information.Information.get_atom_information()
                    structured numpy array, and the values are lists of
                    acceptable matches. The selection is a logical "AND"
                    between dictionary entries, but "OR" within the value lists
                    themselves. For example: {'atom':['CA', 'O'], 'chain':'A',
                    'resname_padded':'PRO'} would select all atoms with the names CA
                    or O that are located in the PRO residues of chain A.

        :returns: A numpy.array containing the indices of the atoms of the
                    selection.
        """

        #try:
        # start assuming everything is selected
        selection = numpy.ones(
            len(self.__parent_molecule.get_atom_information()),
            dtype = bool
        )

        for key in selection_criteria.keys():

            vals = selection_criteria[key]

            # make sure the vals are in a list
            # if it's a single value, put it in a list
            if not type(vals) is list and not type(vals) is tuple and not type(vals) is numpy.ndarray:
                vals = [vals]

            # make sure the vals are in the right format
            const = self.__parent_molecule.get_constants()
            if key in const['f8_fields']:
                vals = [float(v) for v in vals]
            elif key in const['i8_fields']:
                vals = [int(v) for v in vals]
            else:
                vals = vals
                # vals = [v.strip() for v in vals]
                # This was causing issues when looking for unstripped fields.

            # "or" all the vals together
            # start assuming nothing is selected
            atm_inf = self.__parent_molecule.get_atom_information()
            subselection = numpy.zeros(
                len(atm_inf), dtype = bool
            )

            try:
                for val in vals:
                    subselection = numpy.logical_or(
                        subselection,
                        (atm_inf[key] == val)
                    )
            except ValueError:
                print("""A non-existant field was selected for.
                         The selectable fields are: """, atm_inf.dtype.names)


            # now "and" that with everything else
            selection = numpy.logical_and(selection, subselection)

        # now get the indices of the selection
        return numpy.nonzero(selection)[0]
        #except:
        #    print "ERROR: Could not make the selection:"
        #    print "\t" + str(selection_criteria)
        #    print "Existing fields:"
        #    print "\t" + ", ".join(atm_inf.dtype.names)
        #    sys.exit(0)

    def select_atoms_in_bounding_box(self, bounding_box):
        """
        Selects all the atoms that are within a bounding box.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.select_atoms_in_bounding_box`.

        :param numpy.array bounding_box: A 2x3 numpy.array containing the minimum and
                    maximum points of the bounding box. Example:
                    numpy.array( [[min_x, min_y, min_z], [max_x, max_y, max_z]] ).

        :returns: A numpy.array containing the indices of the atoms that are
                    within the bounding box.
        """

        if not numpy.class_dependency("select atoms in bounding box", "NUMPY"):
            return

        min_pt = bounding_box[0]
        max_pt = bounding_box[1]
        coordinates = self.__parent_molecule.get_coordinates()
        sel1 = numpy.nonzero((coordinates[:, 0] >= min_pt[0]))[0]
        sel2 = numpy.nonzero((coordinates[:, 0] <= max_pt[0]))[0]
        sel3 = numpy.nonzero((coordinates[:, 1] >= min_pt[1]))[0]
        sel4 = numpy.nonzero((coordinates[:, 1] <= max_pt[1]))[0]
        sel5 = numpy.nonzero((coordinates[:, 2] >= min_pt[2]))[0]
        sel6 = numpy.nonzero((coordinates[:, 2] <= max_pt[2]))[0]
        sel = numpy.intersect1d(sel1, sel2)
        sel = numpy.intersect1d(sel, sel3)
        sel = numpy.intersect1d(sel, sel4)
        sel = numpy.intersect1d(sel, sel5)
        sel = numpy.intersect1d(sel, sel6)

        return sel

    def select_all_atoms_bound_to_selection(self, selection):
        """
        Selects all the atoms that are bound to a user-specified selection.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function 
        :meth:`~scoria_mda.Molecule.Molecule.select_all_atoms_bound_to_selection`.
        
        :param numpy.array selection: A numpy.array containing the indices of the
                    user-specified selection.

        :returns: A numpy.array containing the indices of the atoms that are
                bound to the user-specified selection. Note that this new
                selection does not necessarily include the indices of the
                original user-specified selection.
        """

        if not numpy.class_dependency("select all atoms bound to a selection", "NUMPY"):
            return

        if self.__parent_molecule.information.get_bonds() is None:
            print("You need to define the bonds to use" +
                  "select_all_atoms_bound_to_selection().")
            return

        #print 1, self
        #print 2, self.__parent_molecule
        #print 3, self.__parent_molecule.get_bonds()
        #print 4, selection

        # t = self.__parent_molecule.get_bonds()
        bonds_to_consider = self.__parent_molecule.get_bonds()[selection]
        t = numpy.nonzero(bonds_to_consider)
        if len(t) > 1:
            return numpy.unique(t[1])
        else:
            return numpy.array([])

    def select_branch(self, root_atom_index, directionality_atom_index):
        """
        Identify an isolated "branch" of a molecular model. Assumes the
        atoms with indices root_atom_index and directionality_atom_index are
        bound to one another and that the branch starts at root_atom_index one
        and "points" in the direction of directionality_atom_index.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.select_branch`.

        :param int root_atom_index: An int, the index of the first atom in the
                branch (the "root").
        :param int directionality_atom_index: An int, the index of the second atom
                in the branch, used to establish directionality

        :returns: A numpy array containing the indices of the atoms of the branch.
        """

        if not numpy.class_dependency("select a branch", "NUMPY"):
            return

        # note that this function is mostly retained for legacy reasons. the
        # old version of scoria had a branch-identification function.

        if self.__parent_molecule.get_bonds() is None:
            print("To identify atoms in the same molecule as the atoms of " +
                  "a selection, you need to define the bonds.")
            return

        # Make sure atoms are neighboring
        if (not directionality_atom_index in
                self.select_all_atoms_bound_to_selection(
                    numpy.array([root_atom_index]))
           ):

            print("The root and directionality atoms, with indices " +
                  str(root_atom_index) + " and " +
                  str(directionality_atom_index) +
                  ", respectively, are not neighboring atoms.")

            return

        # first, set up the two indices need to manage the growing list of
        # connected atoms. current_index is the index in the list that you're
        # currently considering
        current_index = 1

        # create an "empty" array to store the indices of the connected atoms
        # can't know ahead of time what size, so let's use a python list #
        # -99999 *
        # numpy.ones(len(self.__parent_molecule.information.get_coordinates()),
        # dtype=int) # assume initially that all the atoms belong to this
        # molecule. this list will be shortened, possibly, later if that
        # assumption is incorrect.
        indices_of_this_branch = [root_atom_index, directionality_atom_index]

        while True:
            # get all the neighbors of the current atom
            try:
                current_atom_index = indices_of_this_branch[current_index]
            except:
                # this error because you've reached the end of the larger
                # molecule
                break

            neighbors_indices = (
                self.__parent_molecule.select_all_atoms_bound_to_selection(
                    numpy.array([current_atom_index])
                )
            )

            # get the ones in neighbors_indices that are not in
            # indices_of_this_molecule
            new_ones = numpy.setdiff1d(
                neighbors_indices, indices_of_this_branch
            )

            indices_of_this_branch.extend(new_ones)

            # prepare to look at the next atom in the list
            current_index = current_index + 1

        return numpy.array(indices_of_this_branch)

    def select_atoms_from_same_molecule(self, selection):
        """
        Selects all the atoms that belong to the same molecule as a
        user-defined selection, assuming that the scoria_mda.Molecule object
        actually contains multiple physically distinct molecules that are not
        bound to each other via covalent bonds.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.select_atoms_from_same_molecule`.

        :param numpy.array selection: A numpy.array containing the indices of the
                    user-defined selection.

        :returns: A numpy.array containing the indices of the atoms belonging to
                    the same molecules as the atoms of the user-defined
                    selection.
        """

        if not numpy.class_dependency("select atoms from the same molecule", "NUMPY"):
            return

        # If your "Molecule" object actually contains several molecules, this
        # one selects all the atoms from any molecule containing any atom in
        # the selection note that bonds must be defined

        if self.__parent_molecule.get_bonds() is None:
            print("To identify atoms in the same molecule as the atoms of " +
                  "a selection, you need to define the bonds.")
            return

        indices = []
        for index in selection:

            # first, set up the two indices need to manage the growing list of
            # connected atoms.
            # current_index is the index in the list that you're currently
            # considering
            current_index = 0

            # create an "empty" array to store the indices of the connected
            # atoms
            # can't know ahead of time what size, so let's use a python list #
            # -99999 *
            # numpy.ones(len(self.__parent_molecule.information.
            # get_coordinates()), dtype=int) # assume initially that all the
            # atoms belong to this molecule. this list will be shortened,
            # possibly, later if that assumption is incorrect.
            indices_of_this_molecule = [index]

            while True:
                # get all the neighbors of the current atom
                try:
                    current_atom_index = (
                        indices_of_this_molecule[current_index]
                    )
                except:
                    # this error because you've reached the end of the larger
                    # molecule
                    break

                prnt = self.__parent_molecule
                neighbors_indices = prnt.select_all_atoms_bound_to_selection(
                    numpy.array([current_atom_index])
                )

                # get the ones in neighbors_indices that are not in
                # indices_of_this_molecule
                new_ones = numpy.setdiff1d(neighbors_indices,
                                           indices_of_this_molecule)

                indices_of_this_molecule.extend(new_ones)

                # prepare to look at the next atom in the list
                current_index = current_index + 1

            # indices_of_this_molecule =
            # indices_of_this_molecule[:current_index-1] # so the list is
            # prunes down.
            indices.append(indices_of_this_molecule)

        # now merge and remove redundancies
        return numpy.unique(numpy.hstack(indices))

    def selections_of_constituent_molecules(self):
        """
        Identifies the indices of atoms belonging to separate molecules,
        assuming that the scoria_mda.Molecule object actually contains multiple
        physically distinct molecules that are not bound to each other via
        covalent bonds.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.selections_of_constituent_molecules`.

        :Returns: A python list of numpy.array objects containing the indices of
                    the atoms belonging to each molecule of the composite
                    scoria_mda.Molecule object.
        """

        if not numpy.class_dependency("select atoms of constituent molecules", "NUMPY"):
            return

        # If your scoria_mda.Molecule object contains multiple molecules (e.g.,
        # several chains), this will return a list of selections corresponding
        # to the atoms of each molecule.

        atoms_not_yet_considered = self.select_all()
        selections = []

        while len(atoms_not_yet_considered) > 0:
            # add the atoms in the same molecule as the first atom in
            # atoms_not_yet_considered
            this_molecule_atoms = self.select_atoms_from_same_molecule(
                numpy.array([atoms_not_yet_considered[0]])
            )

            # now remove these from the atoms_not_yet_considered list
            atoms_not_yet_considered = numpy.setxor1d(
                this_molecule_atoms, atoms_not_yet_considered, True
            )

            # save the atoms of this molecule
            selections.append(this_molecule_atoms)

        return selections

    def select_atoms_near_other_selection(self, selection, cutoff):
        """
        Selects all atoms that are near the atoms of a user-defined
        selection.

        Requires the :any:`numpy` and :any:`scipy<scipy.spatial>` libraries.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.select_atoms_near_other_selection`.

        :param numpy.array selection: A numpy.array containing the indices of the
                    user-defined selection.
        :param float cutoff: A float, the distance cutoff (in Angstroms).

        :returns: A numpy.array containing the indices of all atoms near the
                    user-defined selection, not including the atoms of the
                    user-defined selection themselves.
        """

        if not numpy.class_dependency("select atoms near another selection", "NUMPY"):
            return

        if not numpy.class_dependency("select atoms near another selection", "SCIPY"):
            return

        # note that this does not return a selection that includes the input
        # selection. merge selections as required to get a selection that also
        # includes the input.

        invert_selection = self.invert_selection(selection)

        prnt = self.__parent_molecule

        selection_coors = prnt.get_coordinates()[selection]
        inversion_coors = prnt.get_coordinates()[invert_selection]

        indices_of_nearby = invert_selection[numpy.unique(
            numpy.nonzero(numpy.cdist(inversion_coors, selection_coors) < cutoff)[0]
        )]

        return indices_of_nearby

    def select_atoms_in_same_residue(self, selection):
        """
        Selects all atoms that are in the same residue as any of the atoms
        of a user-defined seleciton. Residues are considered unique if they
        have a unique combination of resname, resseq, and chainid fields.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.select_atoms_in_same_residue`.

        :param numpy.array selection: A numpy.array containing the indices of the
                    user-defined selection.

        :returns: A numpy.array containing the indices of all atoms in the same
                    residue as any of the atoms of the user-defined selection.
        """


        # get string ids representing the residues of all atoms
        atm_inf = self.__parent_molecule.get_atom_information()

        keys = numpy.defchararray_add(atm_inf['resname'], '-')

        keys = numpy.defchararray_add(
            keys, numpy.array([str(t) for t in atm_inf['resseq']])
        )

        keys = numpy.defchararray_add(keys, '-')

        keys = numpy.defchararray_add(keys, atm_inf['chainid'])

        # get the unique keys of the selection
        unique_keys_of_selection = numpy.unique(keys[selection])

        # now get all the atoms of these selection keys

        # the below works, but is slow for large systems
        #residues = self.__parent_molecule.selections_of_residues()
        #new_selection = numpy.array([], dtype=int)
        #for key in unique_keys_of_selection:
        #    print key
        #    new_selection = numpy.append(new_selection, residues[key])

        # let's use this instead, faster for large systems.
        new_selection = numpy.array([], dtype = int)
        for key in unique_keys_of_selection:
            new_selection = numpy.append(
                new_selection, numpy.nonzero(keys == key)[0]
            )

        return new_selection

    def invert_selection(self, selection):
        """
        Inverts a user-defined selection (i.e., identifies all atoms that
        are not in the seleciton).

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.invert_selection`.

        :param numpy.array selection: A numpy.array containing the indices of the
                    user-defined selection.

        :returns: A numpy.array containing the indices of all atoms that are not
                    in the user-defined seleciton.
        """

        # selection is a list of atom indices
        all_atoms = numpy.arange(
            0, len(self.__parent_molecule.get_atom_information()),
            1, dtype = int
        )

        remaining_indicies = numpy.delete(all_atoms, selection)
        return remaining_indicies

    def select_all(self):
        """
        Selects all the atoms in a scoria_mda.Molecule object.

        Should be called via the wrapper function :meth:`~scoria_mda.Molecule.Molecule.select_all`.
        
        :returns: A numpy.array containing the indices of all atoms in the
                    scoria_mda.Molecule object.
        """

        return self.select_atoms({})

    def select_close_atoms_from_different_molecules(self, other_mol, cutoff,
                                                    pairwise_comparison = True,
                                                    terminate_early = False):
        """
        Effectively detects steric clashes between self and another
        scoria_mda.Molecule.

        Requires the :any:`numpy` and :any:`scipy<scipy.spatial>` libraries.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.select_close_atoms_from_different_molecules`.
        
        :param scoria_mda.Molecule other_mol: A scoria_mda.Molecule object of the other
                    molecule.
        :param float cutoff: A float, the user-defined distance cutoff in
                    Angstroms.
        :param bool pairwise_comparison: An optional boolean, whether or not to
                    perform a simple pairwise distance comparison (if True) or
                    to use a more sophisitcated method (if False). True by
                    default.
        :param bool terminate_early: An optional boolean, whether or not to stop
                    looking for steric clashes once one is found. False by
                    default.

        :returns: A tuple containing two elements. The first is a numpy.array
                    containing the indices of all nearby atoms from this
                    scoria_mda.Molecule object (self). The second is a
                    numpy.array containing the indices of all nearby atoms from
                    the other molecule.
        """

        if not numpy.class_dependency("select close atoms from a different molecule", "NUMPY"):
            return

        if not numpy.class_dependency("select close atoms from a different molecule", "SCIPY"):
            return

        if pairwise_comparison == True:

            dists = numpy.cdist(self.__parent_molecule.get_coordinates(),
                                other_mol.get_coordinates())
            close_ones = numpy.nonzero(dists < cutoff)
            close_ones_from_mol_parent_molecule = numpy.unique(close_ones[0])
            close_ones_from_mol_other_mol = numpy.unique(close_ones[1])

            return (close_ones_from_mol_parent_molecule,
                    close_ones_from_mol_other_mol)

        else:
            # so do the more complex hierarchical comparison
            # first, do some quick and easy checks
            self_coordinates = self.__parent_molecule.get_coordinates()
            other_coordinates = other_mol.get_coordinates()
            slf_hrachy = self.__parent_molecule.get_hierarchy()
            othr_hrachy = other_mol.get_hierarchy()

            margin = numpy.array([cutoff, cutoff, cutoff])
            self_min = numpy.min(self_coordinates, 0) - margin
            other_mol_max = numpy.max(other_coordinates, 0) + margin

            if self_min[0] > other_mol_max[0]:
                return (numpy.array([]), numpy.array([]))

            if self_min[1] > other_mol_max[1]:
                return (numpy.array([]), numpy.array([]))

            if self_min[2] > other_mol_max[2]:
                return (numpy.array([]), numpy.array([]))

            self_max = numpy.max(self_coordinates, 0) + margin
            other_mol_min = numpy.min(other_coordinates, 0) - margin

            if other_mol_min[0] > self_max[0]:
                return (numpy.array([]), numpy.array([]))

            if other_mol_min[1] > self_max[1]:
                return (numpy.array([]), numpy.array([]))

            if other_mol_min[2] > self_max[2]:
                return (numpy.array([]), numpy.array([]))

            # now assign spheres to the whole molecule, the chains, the
            # residues note that this won't recalculate the data if it's
            # already been calculated.
            prnt = self.__parent_molecule
            prnt.define_molecule_chain_residue_spherical_boundaries()
            other_mol.define_molecule_chain_residue_spherical_boundaries()

            # if the whole molecules are too far away, give up
            self_cent = slf_hrachy['spheres']['molecule']['center']
            self_rad = slf_hrachy['spheres']['molecule']['radius']
            other_cent = othr_hrachy['spheres']['molecule']['center']
            other_rad = othr_hrachy['spheres']['molecule']['radius']
            mol_dist = numpy.linalg.norm(self_cent - other_cent)

            if mol_dist > self_rad + other_rad + cutoff:
                # the molecules are too far away to clash
                return (numpy.array([]), numpy.array([]))

            # check the chains
            chain_distances = numpy.cdist(
                slf_hrachy['spheres']['chains']['centers'],
                other_mol.get_hierarchy()['spheres']['chains']['centers']
            )

            sum1_matrix = numpy.hstack(
                [
                    numpy.array(
                        [slf_hrachy['spheres']['chains']['radii']]
                    ).T for t in
                    range(len(othr_hrachy['spheres']['chains']['radii']))
                ]
            )

            sum2_matrix = numpy.vstack(
                [
                    numpy.array(
                        [othr_hrachy['spheres']['chains']['radii']]
                    ) for t in
                    range(len(slf_hrachy['spheres']['chains']['radii']))
                ]
            )

            sum_matrix = sum1_matrix + sum2_matrix + cutoff

            indicies_of_clashing_chains = numpy.nonzero(chain_distances <
                                                        sum_matrix)

            if len(indicies_of_clashing_chains[0]) == 0:
                # the chains don't clash, so no atoms can either
                return (numpy.array([]), numpy.array([]))

            # check the residues
            residue_distances = numpy.cdist(
                slf_hrachy['spheres']['residues']['centers'],
                other_mol.get_hierarchy()['spheres']['residues']['centers']
            )

            sum1_matrix = numpy.hstack(
                [
                    numpy.array(
                        [slf_hrachy['spheres']['residues']['radii']]
                    ).T for t in
                    range(len(othr_hrachy['spheres']['residues']['radii']))
                ]
            )

            sum2_matrix = numpy.vstack(
                [
                    numpy.array(
                        [othr_hrachy['spheres']['residues']['radii']]
                    ) for t in
                    range(len(slf_hrachy['spheres']['residues']['radii']))
                ]
            )

            sum_matrix = sum1_matrix + sum2_matrix + cutoff

            indicies_of_clashing_residues = numpy.nonzero(residue_distances <
                                                          sum_matrix)

            if len(indicies_of_clashing_residues[0]) == 0:
                # the residues don't clash, so no atoms can either
                return (numpy.array([]), numpy.array([]))

            # now time to check the atoms
            self_close_atom_indices = numpy.array([], dtype = int)
            other_close_atom_indices = numpy.array([], dtype = int)

            for i in range(len(indicies_of_clashing_residues[0])):
                self_res_index = indicies_of_clashing_residues[0][i]
                other_res_index = indicies_of_clashing_residues[1][i]

                self_res_name = (
                    slf_hrachy['spheres']['residues']['keys'][self_res_index]
                )

                other_res_name = (
                    othr_hrachy['spheres']['residues']['keys'][other_res_index]
                )

                self_res_indicies = (
                    slf_hrachy['residues']['indices'][self_res_name]
                )

                other_res_indicies = (
                    othr_hrachy['residues']['indices'][other_res_name]
                )

                self_coors = self_coordinates[self_res_indicies]
                other_coors = other_coordinates[other_res_indicies]

                some_self_indices, some_other_indices = numpy.nonzero(
                    numpy.cdist(self_coors, other_coors) < cutoff
                )

                if len(some_self_indices) != 0 or len(some_other_indices) != 0:
                    # so there are some
                    self_close_atom_indices = numpy.append(
                        self_close_atom_indices,
                        self_res_indicies[some_self_indices]
                    )

                    other_close_atom_indices = numpy.append(
                        other_close_atom_indices,
                        other_res_indicies[some_other_indices]
                    )

                    if terminate_early == True:
                        # so don't keep looking once you've found something
                        return (self_close_atom_indices,
                                other_close_atom_indices)

            # so nothing was found in the end
            return (numpy.unique(self_close_atom_indices),
                    numpy.unique(other_close_atom_indices))

    def get_molecule_from_selection(self, selection, serial_reindex = True,
                                    resseq_reindex = False):
        """
        Creates a scoria_mda.Molecule from a user-defined atom selection.

        Should be called via the wrapper function :meth:`~scoria_mda.Molecule.Molecule.get_molecule_from_selection`.

        :param numpy.array selection: A numpy.array containing the indices of the atoms
                    in the user-defined selection.
        :param bool serial_reindex: An optional boolean, whether or not to
                    reindex the atom serial fields. Default is True.
        :param bool resseq_reindex: An optional boolean, whether or not to
                    reindex the atom resseq fields. Default is False.

        :returns: A scoria_mda.Molecule object containing the atoms of the
                    user-defined selection.
        """

        from .Molecule import Molecule
        new_mol = Molecule()

        trajectory_frames = self.__parent_molecule.get_trajectory_frame_count()
        for frame in range(0, trajectory_frames):
            new_mol.set_coordinates(
                self.__parent_molecule.get_coordinates(frame)[selection],
                frame
            )

        # try to get the undo coordinates as well, though they may not have
        # been  set
        try:
            new_mol.information.set_coordinates_undo_point(
                self.__parent_molecule.get_coordinates_undo_point()[selection]
            )
        except:
            new_mol.information.set_coordinates_undo_point(None)

        new_mol.set_atom_information(
            self.__parent_molecule.get_atom_information()[selection]
        )

        if not self.__parent_molecule.get_bonds() is None:
            new_mol.set_bonds(self.__parent_molecule.get_bonds()[selection])
            new_mol.set_bonds(new_mol.get_bonds()[:, selection])
        else:
            new_mol.set_bonds(None)

        # note that hierarchy will have to be recalculated

        if serial_reindex == True: new_mol.information.serial_reindex()
        if resseq_reindex == True: new_mol.information.resseq_reindex()
        return new_mol

    def selections_of_chains(self):
        """
        Identifies the atom selections of each chain.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function :meth:`~scoria_mda.Molecule.Molecule.selections_of_chains`.
        
        :returns: A dictionary. The keys of the dictionary correspond to the
                    chainids, and the values are numpy.array objects containing
                    the indices of the associated chain atoms.
        """

        if not numpy.class_dependency("select chains", "NUMPY"):
            return

        prnt = self.__parent_molecule

        if not 'chains' in list(prnt.get_hierarchy().keys()):
            # so it hasn't already been calculated
            unique_chainids = numpy.unique(
                prnt.get_atom_information()['chainid']
            )

            prnt.get_hierarchy()['chains'] = {}
            prnt.get_hierarchy()['chains']['indices'] = {}
            for chainid in unique_chainids:
                prnt.get_hierarchy()['chains']['indices'][chainid] = (
                    prnt.select_atoms({'chainid': chainid})
                )

        return prnt.get_hierarchy()['chains']['indices']

    def selections_of_residues(self):
        """
        Identifies the atom selections of each residue.

        Requires the :any:`numpy` library.

        Should be called via the wrapper function
        :meth:`~scoria_mda.Molecule.Molecule.selections_of_residues`.
        
        :returns: A dictionary. The keys of this dictionary correspond to the
                    unique resname-resseq-chainid residue identifiers, and the
                    values are numpy.array objects containing the indices of
                    the associated residue atoms.
        """

        if not numpy.class_dependency("select residues", "NUMPY"):
            return

        prnt = self.__parent_molecule
        atm_inf = prnt.get_atom_information()

        if not 'residues' in list(prnt.get_hierarchy().keys()) :
            # so it hasn't already been calculated

            keys = numpy.defchararray_add(
                atm_inf['resname'], '-'
            )

            keys = numpy.defchararray_add(
                keys, numpy.array([str(t) for t in atm_inf['resseq']])
            )

            keys = numpy.defchararray_add(keys, '-')

            keys = numpy.defchararray_add(
                keys, atm_inf['chainid']
            )

            unique_resnames = numpy.unique(keys)

            prnt.get_hierarchy()['residues'] = {}

            prnt.get_hierarchy()['residues']['indices'] = {}

            for key in unique_resnames:
                resname, resseq, chainid = key.split('-')
                resseq = int(resseq)

                prnt.get_hierarchy()['residues']['indices'][key] = (
                    prnt.select_atoms({
                        'chainid': chainid,
                        'resname': resname,
                        'resseq': resseq})
                )

        return prnt.get_hierarchy()['residues']['indices']