"""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):
        """Return HTML for 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
