# Copyright (C) 2002, Thomas Hamelryck (thamelry@binf.ku.dk)
# This code is part of the Biopython distribution and governed by its
# license.  Please see the LICENSE file that should have been included
# as part of this package.

"""
Base class for Residue, Chain, Model and Structure classes.

It is a simple container class, with list and dictionary like properties.
"""

from copy import copy

from Bio.PDB.PDBExceptions import PDBConstructionException


class Entity(object):
    """
    Basic container object. Structure, Model, Chain and Residue
    are subclasses of Entity. It deals with storage and lookup.
    """
    def __init__(self, id):
        self.id = id
        self.full_id = None
        self.parent = None
        self.child_list = []
        self.child_dict = {}
        # Dictionary that keeps additional properties
        self.xtra = {}

    # Special methods

    def __len__(self):
        "Return the number of children."
        return len(self.child_list)

    def __getitem__(self, id):
        "Return the child with given id."
        return self.child_dict[id]

    def __delitem__(self, id):
        "Remove a child."
        return self.detach_child(id)

    def __contains__(self, id):
        "True if there is a child element with the given id."
        return (id in self.child_dict)

    def __iter__(self):
        "Iterate over children."
        for child in self.child_list:
            yield child

    # Public methods

    def get_level(self):
        """Return level in hierarchy.

        A - atom
        R - residue
        C - chain
        M - model
        S - structure
        """
        return self.level

    def set_parent(self, entity):
        "Set the parent Entity object."
        self.parent = entity

    def detach_parent(self):
        "Detach the parent."
        self.parent = None

    def detach_child(self, id):
        "Remove a child."
        child = self.child_dict[id]
        child.detach_parent()
        del self.child_dict[id]
        self.child_list.remove(child)

    def add(self, entity):
        "Add a child to the Entity."
        entity_id = entity.get_id()
        if self.has_id(entity_id):
            raise PDBConstructionException(
                "%s defined twice" % str(entity_id))
        entity.set_parent(self)
        self.child_list.append(entity)
        self.child_dict[entity_id] = entity

    def insert(self, pos, entity):
        "Add a child to the Entity at a specified position."
        entity_id = entity.get_id()
        if self.has_id(entity_id):
            raise PDBConstructionException(
                "%s defined twice" % str(entity_id))
        entity.set_parent(self)
        self.child_list[pos:pos] = [entity]
        self.child_dict[entity_id] = entity

    def get_iterator(self):
        "Return iterator over children."
        for child in self.child_list:
            yield child

    def get_list(self):
        "Return a copy of the list of children."
        return copy(self.child_list)

    def has_id(self, id):
        """True if a child with given id exists."""
        return (id in self.child_dict)

    def get_parent(self):
        "Return the parent Entity object."
        return self.parent

    def get_id(self):
        "Return the id."
        return self.id

    def get_full_id(self):
        """Return the full id.

        The full id is a tuple containing all id's starting from
        the top object (Structure) down to the current object. A full id for
        a Residue object e.g. is something like:

        ("1abc", 0, "A", (" ", 10, "A"))

        This corresponds to:

        Structure with id "1abc"
        Model with id 0
        Chain with id "A"
        Residue with id (" ", 10, "A")

        The Residue id indicates that the residue is not a hetero-residue
        (or a water) because it has a blank hetero field, that its sequence
        identifier is 10 and its insertion code "A".
        """
        if self.full_id is None:
            entity_id = self.get_id()
            l = [entity_id]
            parent = self.get_parent()
            while parent is not None:
                entity_id = parent.get_id()
                l.append(entity_id)
                parent = parent.get_parent()
            l.reverse()
            self.full_id = tuple(l)
        return self.full_id

    def transform(self, rot, tran):
        """
        Apply rotation and translation to the atomic coordinates.

        Example:
                >>> rotation=rotmat(pi, Vector(1, 0, 0))
                >>> translation=array((0, 0, 1), 'f')
                >>> entity.transform(rotation, translation)

        @param rot: A right multiplying rotation matrix
        @type rot: 3x3 Numeric array

        @param tran: the translation vector
        @type tran: size 3 Numeric array
        """
        for o in self.get_list():
            o.transform(rot, tran)

    def copy(self):
        shallow = copy(self)

        shallow.child_list = []
        shallow.child_dict = {}
        shallow.xtra = copy(self.xtra)

        shallow.detach_parent()

        for child in self.child_list:
            shallow.add(child.copy())
        return shallow


class DisorderedEntityWrapper(object):
    """
    This class is a simple wrapper class that groups a number of equivalent
    Entities and forwards all method calls to one of them (the currently selected
    object). DisorderedResidue and DisorderedAtom are subclasses of this class.

    E.g.: A DisorderedAtom object contains a number of Atom objects,
    where each Atom object represents a specific position of a disordered
    atom in the structure.
    """
    def __init__(self, id):
        self.id = id
        self.child_dict = {}
        self.selected_child = None
        self.parent = None

    # Special methods

    def __getattr__(self, method):
        "Forward the method call to the selected child."
        if not hasattr(self, 'selected_child'):
            # Avoid problems with pickling
            # Unpickling goes into infinite loop!
            raise AttributeError
        return getattr(self.selected_child, method)

    def __getitem__(self, id):
        "Return the child with the given id."
        return self.selected_child[id]

    # XXX Why doesn't this forward to selected_child?
    # (NB: setitem was here before getitem, iter, len, sub)
    def __setitem__(self, id, child):
        "Add a child, associated with a certain id."
        self.child_dict[id] = child

    def __contains__(self, id):
        "True if the child has the given id."
        return (id in self.selected_child)

    def __iter__(self):
        "Return the number of children."
        return iter(self.selected_child)

    def __len__(self):
        "Return the number of children."
        return len(self.selected_child)

    def __sub__(self, other):
        """Subtraction with another object."""
        return self.selected_child - other

    # Public methods

    def get_id(self):
        "Return the id."
        return self.id

    def disordered_has_id(self, id):
        """True if there is an object present associated with this id."""
        return (id in self.child_dict)

    def detach_parent(self):
        "Detach the parent"
        self.parent = None
        for child in self.disordered_get_list():
            child.detach_parent()

    def get_parent(self):
        "Return parent."
        return self.parent

    def set_parent(self, parent):
        "Set the parent for the object and its children."
        self.parent = parent
        for child in self.disordered_get_list():
            child.set_parent(parent)

    def disordered_select(self, id):
        """Select the object with given id as the currently active object.

        Uncaught method calls are forwarded to the selected child object.
        """
        self.selected_child = self.child_dict[id]

    def disordered_add(self, child):
        "This is implemented by DisorderedAtom and DisorderedResidue."
        raise NotImplementedError

    def is_disordered(self):
        """
        Return 2, indicating that this Entity is a collection of Entities.
        """
        return 2

    def disordered_get_id_list(self):
        "Return a list of id's."
        # sort id list alphabetically
        return sorted(self.child_dict)

    def disordered_get(self, id=None):
        """Get the child object associated with id.

        If id is None, the currently selected child is returned.
        """
        if id is None:
            return self.selected_child
        return self.child_dict[id]

    def disordered_get_list(self):
        "Return list of children."
        return list(self.child_dict.values())
