"""Base class for generic resqml objects."""

import logging
import warnings
from abc import ABCMeta, abstractmethod

import resqpy.olio.uuid as bu
import resqpy.olio.xml_et as rqet

logger = logging.getLogger(__name__)


class BaseResqpy(metaclass = ABCMeta):
    """Base class for generic resqpy classes.

    Implements generic attributes such as uuid, root, part, title, originator.

    Implements generic magic methods, such as pretty printing and testing for
    equality.

    Example use::

        class AnotherResqpyObject(BaseResqpy):

            resqml_type = 'obj_anotherresqmlobjectrepresentation'
    """

    @property
    @abstractmethod
    def resqml_type(self):
        """Definition of which RESQML object the class represents.

        Subclasses must overwrite this abstract attribute.
        """
        raise NotImplementedError

    def __init__(self, model, uuid = None, title = None, originator = None, root_node = None, extra_metadata = None):
        """Load an existing resqml object, or create new.

        Args:
            model (resqpy.model.Model): Parent model
            uuid (str, optional): Load from existing uuid (if given), else create new.
            title (str, optional): Citation title
            originator (str, optional): Creator of object. By default, uses user id.
        """
        self.model = model
        self.title = title  #: Citation title
        self.originator = originator  #: Creator of object. By default, user id.
        self.extra_metadata = {}
        if extra_metadata:
            self.extra_metadata = extra_metadata
            self._standardise_extra_metadata()  # has side effect of making a copy

        if root_node is not None:
            warnings.warn("root_node parameter is deprecated, use uuid instead", DeprecationWarning)
            uuid = rqet.uuid_for_part_root(root_node)

        if uuid is None:
            self.uuid = bu.new_uuid()  #: Unique identifier


#            logger.debug(f"created new uuid for object {self}")
        else:
            self.uuid = uuid
            #            logger.debug(f"loading existing object {self}")
            citation_node = rqet.find_tag(self.root, 'Citation')
            if citation_node is not None:
                self.title = rqet.find_tag_text(citation_node, 'Title')
                self.originator = rqet.find_tag_text(citation_node, 'Originator')
            self.extra_metadata = rqet.load_metadata_from_xml(self.root)
            self._load_from_xml()

    # usually overridden by derived class, unless generic code above handles all attributes
    def _load_from_xml(self):
        pass

    # Define attributes self.part and self.root, using uuid as the primary key

    @property
    def part(self):
        """Standard part name corresponding to self.uuid."""

        # following caused trouble when resqml_type dynamically determined
        #       return rqet.part_name_for_object(self.resqml_type, self.uuid)
        return self.model.part_for_uuid(self.uuid)

    @property
    def root(self):
        """XML node corresponding to self.uuid."""

        return self.model.root_for_uuid(self.uuid)

    @property
    def citation_title(self):
        """Citation block title equivalent to self.title."""

        return self.title

    def try_reuse(self):
        """Look for an equivalent existing RESQML object and modify the uuid of this object if found.

        returns:
           boolean: True if an equivalent object was found, False if not

        note:
           by design this method may change this object's uuid as a side effect
        """

        assert self.uuid is not None
        if self.root is not None:
            return True
        uuid_list = self.model.uuids(obj_type = self.resqml_type)
        for other_uuid in uuid_list:
            if bu.matching_uuids(self.uuid, other_uuid):
                logger.debug(f'reusing existing xml for uuid {other_uuid}')
                return True
            try:
                other = self.__class__(self.model, uuid = other_uuid)
            except Exception:
                return False
            if self == other:
                logger.debug(f'reusing equivalent resqml object with uuid {other_uuid}')
                self.uuid = other_uuid  # NB: change of uuid for this object
                assert self.root is not None
                return True
        return False

    def create_xml(self, title = None, originator = None, extra_metadata = None, add_as_part = False):
        """Write citation block to XML.

        Note:

            `add_as_part` is False by default in this base method. Derived classes should typically
            extend this method to complete the XML representation, and then finally ensure the node
            is added as a part to the model.

        Args:
            title (string): used as the citation Title text
            originator (string, optional): the name of the human being who created the deviation survey part;
                default is to use the login name
            extra_metadata (dict, optional): extra metadata items to be added
            add_as_part (boolean): if True, the newly created xml node is added as a part
                in the model

        Returns:
            node: the newly created root node
        """

        assert self.uuid is not None

        # Create the root node
        node = self.model.new_obj_node(self.resqml_type)
        node.attrib['uuid'] = str(self.uuid)

        # Citation block
        if title:
            self.title = title
        if originator:
            self.originator = originator
        self.model.create_citation(root = node, title = self.title, originator = self.originator)

        # Extra metadata
        if extra_metadata:
            if not hasattr(self, 'extra_metadata'):
                self.extra_metadata = {}
            for key, value in extra_metadata.items():
                self.extra_metadata[str(key)] = str(value)
        if hasattr(self, 'extra_metadata') and self.extra_metadata:
            rqet.create_metadata_xml(node = node, extra_metadata = self.extra_metadata)

        if add_as_part:
            self.model.add_part(self.resqml_type, self.uuid, node)
            assert self.root is not None

        return node

    def append_extra_metadata(self, meta_dict):
        """Append a given dictionary of metadata to the existing metadata."""
        for key in meta_dict:
            self.extra_metadata[key] = meta_dict[key]
        self._standardise_extra_metadata()

    def _standardise_extra_metadata(self):
        if self.extra_metadata:
            em = {}
            for key, value in self.extra_metadata.items():
                em[str(key)] = str(value)
            self.extra_metadata = em

    # Generic magic methods

    def __eq__(self, other):
        """Implements equals operator; uses is_equivalent() otherwise compares class type and uuid."""
        if hasattr(self, 'is_equivalent'):
            return self.is_equivalent(other)
        if not isinstance(other, self.__class__):
            return False
        other_uuid = getattr(other, "uuid", None)
        return bu.matching_uuids(self.uuid, other_uuid)

    def __ne__(self, other):
        """Implements not equal operator."""
        return not self.__eq__(other)

    def __repr__(self):
        """String representation."""
        return f"{self.__class__.__name__}(uuid={self.uuid}, title={self.title})"

    def _repr_html_(self):
        """IPython / Jupyter representation."""

        keys_to_display = ('uuid', 'title', 'originator')
        html = f"<h3>{self.__class__.__name__}</h3>\n"
        for key in keys_to_display:
            html += f"<strong>{key}</strong>: {getattr(self, key)}<br>\n"
        return html

    # Include some aliases for root, but raise warnings if they are used
    # TODO: remove these aliases for self.root

    @property
    def root_node(self):
        """DEPRECATED.

        Alias for root
        """
        warnings.warn("Attribute 'root_node' is deprecated. Use 'root'", DeprecationWarning)
        return self.root

    @property
    def node(self):
        """DEPRECATED.

        Alias for root
        """
        warnings.warn("Attribute 'node' is deprecated. Use 'root'", DeprecationWarning)
        return self.root

"""Base class for generic resqml objects."""

import logging
import warnings
from abc import ABCMeta, abstractmethod

import resqpy.olio.uuid as bu
import resqpy.olio.xml_et as rqet

logger = logging.getLogger(__name__)


class BaseResqpy(metaclass = ABCMeta):
    """Base class for generic resqpy classes.

    Implements generic attributes such as uuid, root, part, title, originator.

    Implements generic magic methods, such as pretty printing and testing for
    equality.

    Example use::

        class AnotherResqpyObject(BaseResqpy):

            resqml_type = 'obj_anotherresqmlobjectrepresentation'
    """

    @property
    @abstractmethod
    def resqml_type(self):
        """Definition of which RESQML object the class represents.

        Subclasses must overwrite this abstract attribute.
        """
        raise NotImplementedError

    def __init__(self, model, uuid = None, title = None, originator = None, root_node = None, extra_metadata = None):
        """Load an existing resqml object, or create new.

        Args:
            model (resqpy.model.Model): Parent model
            uuid (str, optional): Load from existing uuid (if given), else create new.
            title (str, optional): Citation title
            originator (str, optional): Creator of object. By default, uses user id.
        """
        self.model = model
        self.title = title  #: Citation title
        self.originator = originator  #: Creator of object. By default, user id.
        self.extra_metadata = {}
        if extra_metadata:
            self.extra_metadata = extra_metadata
            self._standardise_extra_metadata()  # has side effect of making a copy

        if root_node is not None:
            warnings.warn("root_node parameter is deprecated, use uuid instead", DeprecationWarning)
            uuid = rqet.uuid_for_part_root(root_node)

        if uuid is None:
            self.uuid = bu.new_uuid()  #: Unique identifier


#            logger.debug(f"created new uuid for object {self}")
        else:
            self.uuid = uuid
            #            logger.debug(f"loading existing object {self}")
            citation_node = rqet.find_tag(self.root, 'Citation')
            if citation_node is not None:
                self.title = rqet.find_tag_text(citation_node, 'Title')
                self.originator = rqet.find_tag_text(citation_node, 'Originator')
            self.extra_metadata = rqet.load_metadata_from_xml(self.root)
            self._load_from_xml()

    # usually overridden by derived class, unless generic code above handles all attributes
    def _load_from_xml(self):
        pass

    # Define attributes self.part and self.root, using uuid as the primary key

    @property
    def part(self):
        """Standard part name corresponding to self.uuid."""

        # following caused trouble when resqml_type dynamically determined
        #       return rqet.part_name_for_object(self.resqml_type, self.uuid)
        return self.model.part_for_uuid(self.uuid)

    @property
    def root(self):
        """XML node corresponding to self.uuid."""

        return self.model.root_for_uuid(self.uuid)

    @property
    def citation_title(self):
        """Citation block title equivalent to self.title."""

        return self.title

    def try_reuse(self):
        """Look for an equivalent existing RESQML object and modify the uuid of this object if found.

        returns:
           boolean: True if an equivalent object was found, False if not

        note:
           by design this method may change this object's uuid as a side effect
        """

        assert self.uuid is not None
        if self.root is not None:
            return True
        uuid_list = self.model.uuids(obj_type = self.resqml_type)
        for other_uuid in uuid_list:
            if bu.matching_uuids(self.uuid, other_uuid):
                logger.debug(f'reusing existing xml for uuid {other_uuid}')
                return True
            try:
                other = self.__class__(self.model, uuid = other_uuid)
            except Exception:
                return False
            if self == other:
                logger.debug(f'reusing equivalent resqml object with uuid {other_uuid}')
                self.uuid = other_uuid  # NB: change of uuid for this object
                assert self.root is not None
                return True
        return False

    def create_xml(self, title = None, originator = None, extra_metadata = None, add_as_part = False):
        """Write citation block to XML.

        Note:

            `add_as_part` is False by default in this base method. Derived classes should typically
            extend this method to complete the XML representation, and then finally ensure the node
            is added as a part to the model.

        Args:
            title (string): used as the citation Title text
            originator (string, optional): the name of the human being who created the deviation survey part;
                default is to use the login name
            extra_metadata (dict, optional): extra metadata items to be added
            add_as_part (boolean): if True, the newly created xml node is added as a part
                in the model

        Returns:
            node: the newly created root node
        """

        assert self.uuid is not None

        # Create the root node
        node = self.model.new_obj_node(self.resqml_type)
        node.attrib['uuid'] = str(self.uuid)

        # Citation block
        if title:
            self.title = title
        if originator:
            self.originator = originator
        self.model.create_citation(root = node, title = self.title, originator = self.originator)

        # Extra metadata
        if extra_metadata:
            if not hasattr(self, 'extra_metadata'):
                self.extra_metadata = {}
            for key, value in extra_metadata.items():
                self.extra_metadata[str(key)] = str(value)
        if hasattr(self, 'extra_metadata') and self.extra_metadata:
            rqet.create_metadata_xml(node = node, extra_metadata = self.extra_metadata)

        if add_as_part:
            self.model.add_part(self.resqml_type, self.uuid, node)
            assert self.root is not None

        return node

    def append_extra_metadata(self, meta_dict):
        """Append a given dictionary of metadata to the existing metadata."""
        for key in meta_dict:
            self.extra_metadata[key] = meta_dict[key]
        self._standardise_extra_metadata()

    def _standardise_extra_metadata(self):
        if self.extra_metadata:
            em = {}
            for key, value in self.extra_metadata.items():
                em[str(key)] = str(value)
            self.extra_metadata = em

    # Generic magic methods

    def __eq__(self, other):
        """Implements equals operator; uses is_equivalent() otherwise compares class type and uuid."""
        if hasattr(self, 'is_equivalent'):
            return self.is_equivalent(other)
        if not isinstance(other, self.__class__):
            return False
        other_uuid = getattr(other, "uuid", None)
        return bu.matching_uuids(self.uuid, other_uuid)

    def __ne__(self, other):
        """Implements not equal operator."""
        return not self.__eq__(other)

    def __repr__(self):
        """String representation."""
        return f"{self.__class__.__name__}(uuid={self.uuid}, title={self.title})"

    def _repr_html_(self):
        """IPython / Jupyter representation."""

        keys_to_display = ('uuid', 'title', 'originator')
        html = f"<h3>{self.__class__.__name__}</h3>\n"
        for key in keys_to_display:
            html += f"<strong>{key}</strong>: {getattr(self, key)}<br>\n"
        return html

    # Include some aliases for root, but raise warnings if they are used
    # TODO: remove these aliases for self.root

    @property
    def root_node(self):
        """DEPRECATED.

        Alias for root
        """
        warnings.warn("Attribute 'root_node' is deprecated. Use 'root'", DeprecationWarning)
        return self.root

    @property
    def node(self):
        """DEPRECATED.

        Alias for root
        """
        warnings.warn("Attribute 'node' is deprecated. Use 'root'", DeprecationWarning)
        return self.root

Base class for generic resqml objects. Base class for generic resqpy classes.      Implements generic attributes such as uuid, root, part, title, originator.      Implements generic magic methods, such as pretty printing and testing for     equality.      Example use::          class AnotherResqpyObject(BaseResqpy):              resqml_type = 'obj_anotherresqmlobjectrepresentation' Definition of which RESQML object the class represents.          Subclasses must overwrite this abstract attribute. Load an existing resqml object, or create new.          Args:             model (resqpy.model.Model): Parent model             uuid (str, optional): Load from existing uuid (if given), else create new.             title (str, optional): Citation title             originator (str, optional): Creator of object. By default, uses user id. #: Citation title #: Creator of object. By default, user id. # has side effect of making a copy #: Unique identifier #            logger.debug(f"created new uuid for object {self}") #            logger.debug(f"loading existing object {self}") # usually overridden by derived class, unless generic code above handles all attributes # Define attributes self.part and self.root, using uuid as the primary key Standard part name corresponding to self.uuid. # following caused trouble when resqml_type dynamically determined #       return rqet.part_name_for_object(self.resqml_type, self.uuid) XML node corresponding to self.uuid. Citation block title equivalent to self.title. Look for an equivalent existing RESQML object and modify the uuid of this object if found.          returns:            boolean: True if an equivalent object was found, False if not          note:            by design this method may change this object's uuid as a side effect # NB: change of uuid for this object Write citation block to XML.          Note:              `add_as_part` is False by default in this base method. Derived classes should typically             extend this method to complete the XML representation, and then finally ensure the node             is added as a part to the model.          Args:             title (string): used as the citation Title text             originator (string, optional): the name of the human being who created the deviation survey part;                 default is to use the login name             extra_metadata (dict, optional): extra metadata items to be added             add_as_part (boolean): if True, the newly created xml node is added as a part                 in the model          Returns:             node: the newly created root node # Create the root node # Citation block # Extra metadata Append a given dictionary of metadata to the existing metadata. # Generic magic methods Implements equals operator; uses is_equivalent() otherwise compares class type and uuid. Implements not equal operator. String representation. IPython / Jupyter representation. # Include some aliases for root, but raise warnings if they are used # TODO: remove these aliases for self.root DEPRECATED.          Alias for root DEPRECATED.          Alias for root