#
# This file is part of pyasn1 software.
#
# Copyright (c) 2005-2020, Ilya Etingof <etingof@gmail.com>
# License: https://pyasn1.readthedocs.io/en/latest/license.html
#
from pyasn1 import error

__all__ = ['tagClassUniversal', 'tagClassApplication', 'tagClassContext',
           'tagClassPrivate', 'tagFormatSimple', 'tagFormatConstructed',
           'tagCategoryImplicit', 'tagCategoryExplicit',
           'tagCategoryUntagged', 'Tag', 'TagSet']

#: Identifier for ASN.1 class UNIVERSAL
tagClassUniversal = 0x00

#: Identifier for ASN.1 class APPLICATION
tagClassApplication = 0x40

#: Identifier for ASN.1 class context-specific
tagClassContext = 0x80

#: Identifier for ASN.1 class private
tagClassPrivate = 0xC0

#: Identifier for "simple" ASN.1 structure (e.g. scalar)
tagFormatSimple = 0x00

#: Identifier for "constructed" ASN.1 structure (e.g. may have inner components)
tagFormatConstructed = 0x20

tagCategoryImplicit = 0x01
tagCategoryExplicit = 0x02
tagCategoryUntagged = 0x04


def _tagIdToStr(tagId):
    # Decimal rendering of a huge tag ID can exceed the interpreter's
    # integer-to-string conversion limit (sys.get_int_max_str_digits(),
    # Python 3.11+) and raise ValueError; hexadecimal is not limited
    try:
        return str(tagId)
    except ValueError:
        return hex(tagId)


class Tag(object):
    """Create ASN.1 tag

    Represents ASN.1 tag that can be attached to a ASN.1 type to make
    types distinguishable from each other.

    *Tag* objects are immutable and duck-type Python :class:`tuple` objects
    holding three integer components of a tag.

    Parameters
    ----------
    tagClass: :py:class:`int`
        Tag *class* value

    tagFormat: :py:class:`int`
        Tag *format* value

    tagId: :py:class:`int`
        Tag ID value
    """
    def __init__(self, tagClass, tagFormat, tagId):
        if tagId < 0:
            raise error.PyAsn1Error(
                'Negative tag ID (%s) not allowed' % _tagIdToStr(tagId))
        self.__tagClass = tagClass
        self.__tagFormat = tagFormat
        self.__tagId = tagId
        self.__tagClassId = tagClass, tagId
        self.__hash = hash(self.__tagClassId)

    def __repr__(self):
        representation = '[%s:%s:%s]' % (
            self.__tagClass, self.__tagFormat, _tagIdToStr(self.__tagId))
        return '<%s object, tag %s>' % (
            self.__class__.__name__, representation)

    def __eq__(self, other):
        return self.__tagClassId == other

    def __ne__(self, other):
        return self.__tagClassId != other

    def __lt__(self, other):
        return self.__tagClassId < other

    def __le__(self, other):
        return self.__tagClassId <= other

    def __gt__(self, other):
        return self.__tagClassId > other

    def __ge__(self, other):
        return self.__tagClassId >= other

    def __hash__(self):
        return self.__hash

    def __getitem__(self, idx):
        if idx == 0:
            return self.__tagClass
        elif idx == 1:
            return self.__tagFormat
        elif idx == 2:
            return self.__tagId
        else:
            raise IndexError

    def __iter__(self):
        yield self.__tagClass
        yield self.__tagFormat
        yield self.__tagId

    def __and__(self, otherTag):
        return self.__class__(self.__tagClass & otherTag.tagClass,
                              self.__tagFormat & otherTag.tagFormat,
                              self.__tagId & otherTag.tagId)

    def __or__(self, otherTag):
        return self.__class__(self.__tagClass | otherTag.tagClass,
                              self.__tagFormat | otherTag.tagFormat,
                              self.__tagId | otherTag.tagId)

    @property
    def tagClass(self):
        """ASN.1 tag class

        Returns
        -------
        : :py:class:`int`
            Tag class
        """
        return self.__tagClass

    @property
    def tagFormat(self):
        """ASN.1 tag format

        Returns
        -------
        : :py:class:`int`
            Tag format
        """
        return self.__tagFormat

    @property
    def tagId(self):
        """ASN.1 tag ID

        Returns
        -------
        : :py:class:`int`
            Tag ID
        """
        return self.__tagId


class TagSet(object):
    """Create a collection of ASN.1 tags

    Represents a combination of :class:`~pyasn1.type.tag.Tag` objects
    that can be attached to a ASN.1 type to make types distinguishable
    from each other.

    *TagSet* objects are immutable and duck-type Python :class:`tuple` objects
    holding arbitrary number of :class:`~pyasn1.type.tag.Tag` objects.

    Parameters
    ----------
    baseTag: :class:`~pyasn1.type.tag.Tag`
        Base *Tag* object. This tag survives IMPLICIT tagging.

    *superTags: :class:`~pyasn1.type.tag.Tag`
        Additional *Tag* objects taking part in subtyping.

    Examples
    --------
    .. code-block:: python

        class OrderNumber(NumericString):
            '''
            ASN.1 specification

            Order-number ::=
                [APPLICATION 5] IMPLICIT NumericString
            '''
            tagSet = NumericString.tagSet.tagImplicitly(
                Tag(tagClassApplication, tagFormatSimple, 5)
            )

        orderNumber = OrderNumber('1234')
    """
    def __init__(self, baseTag=(), *superTags):
        self.__baseTag = baseTag
        self.__superTags = superTags
        self.__superTagsClassId = tuple(
            [(superTag.tagClass, superTag.tagId) for superTag in superTags]
        )
        self.__lenOfSuperTags = len(superTags)
        self.__hash = hash(self.__superTagsClassId)

    def __repr__(self):
        representation = '-'.join(
            ['%s:%s:%s' % (x.tagClass, x.tagFormat, _tagIdToStr(x.tagId))
             for x in self.__superTags])
        if representation:
            representation = 'tags ' + representation
        else:
            representation = 'untagged'

        return '<%s object, %s>' % (self.__class__.__name__, representation)

    def __add__(self, superTag):
        return self.__class__(self.__baseTag, *self.__superTags + (superTag,))

    def __radd__(self, superTag):
        return self.__class__(self.__baseTag, *(superTag,) + self.__superTags)

    def __getitem__(self, i):
        if i.__class__ is slice:
            return self.__class__(self.__baseTag, *self.__superTags[i])
        else:
            return self.__superTags[i]

    def __eq__(self, other):
        return self.__superTagsClassId == other

    def __ne__(self, other):
        return self.__superTagsClassId != other

    def __lt__(self, other):
        return self.__superTagsClassId < other

    def __le__(self, other):
        return self.__superTagsClassId <= other

    def __gt__(self, other):
        return self.__superTagsClassId > other

    def __ge__(self, other):
        return self.__superTagsClassId >= other

    def __hash__(self):
        return self.__hash

    def __len__(self):
        return self.__lenOfSuperTags

    @property
    def baseTag(self):
        """Return base ASN.1 tag

        Returns
        -------
        : :class:`~pyasn1.type.tag.Tag`
            Base tag of this *TagSet*
        """
        return self.__baseTag

    @property
    def superTags(self):
        """Return ASN.1 tags

        Returns
        -------
        : :py:class:`tuple`
            Tuple of :class:`~pyasn1.type.tag.Tag` objects that this *TagSet* contains
        """
        return self.__superTags

    def tagExplicitly(self, superTag):
        """Return explicitly tagged *TagSet*

        Create a new *TagSet* representing callee *TagSet* explicitly tagged
        with passed tag(s). With explicit tagging mode, new tags are appended
        to existing tag(s).

        Parameters
        ----------
        superTag: :class:`~pyasn1.type.tag.Tag`
            *Tag* object to tag this *TagSet*

        Returns
        -------
        : :class:`~pyasn1.type.tag.TagSet`
            New *TagSet* object
        """
        if superTag.tagClass == tagClassUniversal:
            raise error.PyAsn1Error("Can't tag with UNIVERSAL class tag")
        if superTag.tagFormat != tagFormatConstructed:
            superTag = Tag(superTag.tagClass, tagFormatConstructed, superTag.tagId)
        return self + superTag

    def tagImplicitly(self, superTag):
        """Return implicitly tagged *TagSet*

        Create a new *TagSet* representing callee *TagSet* implicitly tagged
        with passed tag(s). With implicit tagging mode, new tag(s) replace the
        last existing tag.

        Parameters
        ----------
        superTag: :class:`~pyasn1.type.tag.Tag`
            *Tag* object to tag this *TagSet*

        Returns
        -------
        : :class:`~pyasn1.type.tag.TagSet`
            New *TagSet* object
        """
        if self.__superTags:
            superTag = Tag(superTag.tagClass, self.__superTags[-1].tagFormat, superTag.tagId)
        return self[:-1] + superTag

    def isSuperTagSetOf(self, tagSet):
        """Test type relationship against given *TagSet*

        The callee is considered to be a supertype of given *TagSet*
        tag-wise if all tags in *TagSet* are present in the callee and
        they are in the same order.

        Parameters
        ----------
        tagSet: :class:`~pyasn1.type.tag.TagSet`
            *TagSet* object to evaluate against the callee

        Returns
        -------
        : :py:class:`bool`
            :obj:`True` if callee is a supertype of *tagSet*
        """
        if len(tagSet) < self.__lenOfSuperTags:
            return False
        return self.__superTags == tagSet[:self.__lenOfSuperTags]

    # Backward compatibility

    def getBaseTag(self):
        return self.__baseTag

def initTagSet(tag):
    return TagSet(tag, tag)
