# -*- coding: utf-8 -*-
from io import BytesIO
from codecs import ascii_encode
from collections import namedtuple

from .. import core
from ..utils import requireUnicode, requireBytes
from ..utils.binfuncs import (bin2bytes, bin2dec, bytes2bin, dec2bin,
                              bytes2dec, dec2bytes)
from ..compat import unicode, UnicodeType, BytesType, byteiter
from .. import Error
from . import ID3_V2, ID3_V2_3, ID3_V2_4
from . import (LATIN1_ENCODING, UTF_8_ENCODING, UTF_16BE_ENCODING,
               UTF_16_ENCODING, DEFAULT_LANG)
from .headers import FrameHeader


from ..utils.log import getLogger
log = getLogger(__name__)


class FrameException(Error):
    pass


TITLE_FID          = b"TIT2"                                            # noqa
SUBTITLE_FID       = b"TIT3"                                            # noqa
ARTIST_FID         = b"TPE1"                                            # noqa
ALBUM_ARTIST_FID   = b"TPE2"                                            # noqa
ALBUM_FID          = b"TALB"                                            # noqa
TRACKNUM_FID       = b"TRCK"                                            # noqa
GENRE_FID          = b"TCON"                                            # noqa
COMMENT_FID        = b"COMM"                                            # noqa
USERTEXT_FID       = b"TXXX"                                            # noqa
OBJECT_FID         = b"GEOB"                                            # noqa
UNIQUE_FILE_ID_FID = b"UFID"                                            # noqa
LYRICS_FID         = b"USLT"                                            # noqa
DISCNUM_FID        = b"TPOS"                                            # noqa
IMAGE_FID          = b"APIC"                                            # noqa
USERURL_FID        = b"WXXX"                                            # noqa
PLAYCOUNT_FID      = b"PCNT"                                            # noqa
BPM_FID            = b"TBPM"                                            # noqa
PUBLISHER_FID      = b"TPUB"                                            # noqa
CDID_FID           = b"MCDI"                                            # noqa
PRIVATE_FID        = b"PRIV"                                            # noqa
TOS_FID            = b"USER"                                            # noqa
POPULARITY_FID     = b"POPM"                                            # noqa

URL_COMMERCIAL_FID = b"WCOM"                                            # noqa
URL_COPYRIGHT_FID  = b"WCOP"                                            # noqa
URL_AUDIOFILE_FID  = b"WOAF"                                            # noqa
URL_ARTIST_FID     = b"WOAR"                                            # noqa
URL_AUDIOSRC_FID   = b"WOAS"                                            # noqa
URL_INET_RADIO_FID = b"WORS"                                            # noqa
URL_PAYMENT_FID    = b"WPAY"                                            # noqa
URL_PUBLISHER_FID  = b"WPUB"                                            # noqa
URL_FIDS           = [URL_COMMERCIAL_FID, URL_COPYRIGHT_FID,            # noqa
                      URL_AUDIOFILE_FID, URL_ARTIST_FID, URL_AUDIOSRC_FID,
                      URL_INET_RADIO_FID, URL_PAYMENT_FID,
                      URL_PUBLISHER_FID]

TOC_FID            = b"CTOC"                                            # noqa
CHAPTER_FID        = b"CHAP"                                            # noqa

DEPRECATED_DATE_FIDS = [b"TDAT", b"TYER", b"TIME", b"TORY", b"TRDA",
                        # Nonstandard v2.3 only
                        b"XDOR",
                       ]
DATE_FIDS = [b"TDEN", b"TDOR", b"TDRC", b"TDRL", b"TDTG"]


class Frame(object):
    @requireBytes(1)
    def __init__(self, id):
        self.id = id
        self.header = None

        self.decompressed_size = 0
        self.group_id = None
        self.encrypt_method = None
        self.data = None
        self.data_len = 0
        self._encoding = None

    @property
    def header(self):
        return self._header

    @header.setter
    def header(self, h):
        self._header = h

    @requireBytes(1)
    def parse(self, data, frame_header):
        self.id = frame_header.id
        self.header = frame_header
        self.data = self._disassembleFrame(data)

    def render(self):
        return self._assembleFrame(self.data)

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

    @staticmethod
    def decompress(data):
        import zlib
        log.debug("before decompression: %d bytes" % len(data))
        data = zlib.decompress(data, 15)
        log.debug("after decompression: %d bytes" % len(data))
        return data

    @staticmethod
    def compress(data):
        import zlib
        log.debug("before compression: %d bytes" % len(data))
        data = zlib.compress(data)
        log.debug("after compression: %d bytes" % len(data))
        return data

    @staticmethod
    def decrypt(data):
        raise NotImplementedError("Frame decryption not yet supported")

    @staticmethod
    def encrypt(data):
        raise NotImplementedError("Frame encryption not yet supported")

    @requireBytes(1)
    def _disassembleFrame(self, data):
        assert(self.header)
        header = self.header
        # Format flags in the frame header may add extra data to the
        # beginning of this data.
        if header.minor_version <= 3:
            # 2.3:  compression(4), encryption(1), group(1)
            if header.compressed:
                self.decompressed_size = bin2dec(bytes2bin(data[:4]))
                data = data[4:]
                log.debug("Decompressed Size: %d" % self.decompressed_size)
            if header.encrypted:
                self.encrypt_method = bin2dec(bytes2bin(data[0:1]))
                data = data[1:]
                log.debug("Encryption Method: %d" % self.encrypt_method)
            if header.grouped:
                self.group_id = bin2dec(bytes2bin(data[0:1]))
                data = data[1:]
                log.debug("Group ID: %d" % self.group_id)
        else:
            # 2.4:  group(1), encrypted(1), data_length_indicator (4,7)
            if header.grouped:
                self.group_id = bin2dec(bytes2bin(data[0:1]))
                log.debug("Group ID: %d" % self.group_id)
                data = data[1:]
            if header.encrypted:
                self.encrypt_method = bin2dec(bytes2bin(data[0:1]))
                data = data[1:]
                log.debug("Encryption Method: %d" % self.encrypt_method)
            if header.data_length_indicator:
                self.data_len = bin2dec(bytes2bin(data[:4], 7))
                data = data[4:]
                log.debug("Data Length: %d" % self.data_len)
                if header.compressed:
                    self.decompressed_size = self.data_len
                    log.debug("Decompressed Size: %d" % self.decompressed_size)

        if header.minor_version == 4 and header.unsync:
            data = deunsyncData(data)
        if header.encrypted:
            data = self.decrypt(data)
        if header.compressed:
            data = self.decompress(data)

        return data

    @requireBytes(1)
    def _assembleFrame(self, data):
        assert(self.header)
        header = self.header

        # eyeD3 never writes unsync'd frames
        header.unsync = False

        format_data = b""
        if header.minor_version == 3:
            if header.compressed:
                format_data += bin2bytes(dec2bin(len(data), 32))
            if header.encrypted:
                format_data += bin2bytes(dec2bin(self.encrypt_method, 8))
            if header.grouped:
                format_data += bin2bytes(dec2bin(self.group_id, 8))
        else:
            if header.grouped:
                format_data += bin2bytes(dec2bin(self.group_id, 8))
            if header.encrypted:
                format_data += bin2bytes(dec2bin(self.encrypt_method, 8))
            if header.compressed or header.data_length_indicator:
                header.data_length_indicator = 1
                format_data += bin2bytes(dec2bin(len(data), 32))

        if header.compressed:
            data = self.compress(data)

        if header.encrypted:
            data = self.encrypt(data)

        self.data = format_data + data
        return header.render(len(self.data)) + self.data

    @property
    def text_delim(self):
        assert(self.encoding is not None)
        return b"\x00\x00" if self.encoding in (UTF_16_ENCODING,
                                                UTF_16BE_ENCODING) else b"\x00"

    def _initEncoding(self):
        assert(self.header.version and len(self.header.version) == 3)

        if self.encoding is not None:
            # Make sure the encoding is valid for this version
            if self.header.version[:2] < (2, 4):
                if self.header.version[0] == 1:
                    self.encoding = LATIN1_ENCODING
                else:
                    if self.encoding > UTF_16_ENCODING:
                        # v2.3 cannot do utf16 BE or utf8
                        self.encoding = UTF_16_ENCODING
        else:
            if self.header.version[:2] < (2, 4):
                if self.header.version[0] == 2:
                    self.encoding = UTF_16_ENCODING
                else:
                    self.encoding = LATIN1_ENCODING
            else:
                self.encoding = UTF_8_ENCODING

    @property
    def encoding(self):
        return self._encoding

    @encoding.setter
    def encoding(self, enc):
        if not isinstance(enc, bytes):
            raise TypeError("encoding argument must be a byte string.")
        elif not (LATIN1_ENCODING <= enc <= UTF_8_ENCODING):
            raise ValueError("encoding argument must be a valid constant.")
        self._encoding = enc


class TextFrame(Frame):
    '''Text frames.
    Data string format: encoding (one byte) + text
    '''
    @requireUnicode("text")
    def __init__(self, id, text=None):
        super(TextFrame, self).__init__(id)
        assert(self.id[0:1] == b'T' or self.id in [b"XSOA", b"XSOP", b"XSOT",
                                                   b"XDOR", b"WFED"])
        self.text = text or u""

    @property
    def text(self):
        return self._text

    @text.setter
    @requireUnicode(1)
    def text(self, txt):
        self._text = txt

    def parse(self, data, frame_header):
        super(TextFrame, self).parse(data, frame_header)

        self.encoding = self.data[0:1]
        self.text = decodeUnicode(self.data[1:], self.encoding)
        log.debug("TextFrame text: %s" % self.text)

    def render(self):
        self._initEncoding()
        self.data = (self.encoding +
                     self.text.encode(id3EncodingToString(self.encoding)))
        assert(type(self.data) == BytesType)
        return super(TextFrame, self).render()


class UserTextFrame(TextFrame):
    @requireUnicode("description", "text")
    def __init__(self, id=USERTEXT_FID, description=u"", text=u""):
        super(UserTextFrame, self).__init__(id, text=text)
        self.description = description

    @property
    def description(self):
        return self._description

    @description.setter
    @requireUnicode(1)
    def description(self, txt):
        self._description = txt

    def parse(self, data, frame_header):
        '''Data string format:
        encoding (one byte) + description + b"\x00" + text '''
        # Calling Frame, not TextFrame implementation here since TextFrame
        # does not know about description
        Frame.parse(self, data, frame_header)

        self.encoding = encoding = self.data[0:1]
        (d, t) = splitUnicode(self.data[1:], encoding)
        self.description = decodeUnicode(d, encoding)
        log.debug("UserTextFrame description: %s" % self.description)
        self.text = decodeUnicode(t, encoding)
        log.debug("UserTextFrame text: %s" % self.text)

    def render(self):
        self._initEncoding()
        data = (self.encoding +
                self.description.encode(id3EncodingToString(self.encoding)) +
                self.text_delim +
                self.text.encode(id3EncodingToString(self.encoding)))
        self.data = data
        # Calling Frame, not the base
        return Frame.render(self)


class DateFrame(TextFrame):
    def __init__(self, id, date=u""):
        assert(id in DATE_FIDS or id in DEPRECATED_DATE_FIDS)
        super(DateFrame, self).__init__(id, text=unicode(date))
        self.date = self.text
        self.encoding = LATIN1_ENCODING

    def parse(self, data, frame_header):
        super(DateFrame, self).parse(data, frame_header)
        try:
            if self.text:
                _ = core.Date.parse(self.text)                        # noqa
        except ValueError:
            # Date is invalid, log it and reset.
            core.parseError(FrameException(u"Invalid date: " + self.text))
            self.text = u''

    @property
    def date(self):
        return core.Date.parse(self.text.encode("latin1")) if self.text \
                                                           else None

    # \a date Either an ISO 8601 date string or a eyed3.core.Date object.
    @date.setter
    def date(self, date):
        if not date:
            self.text = u""
            return

        try:
            if type(date) is str:
                date = core.Date.parse(date)
            elif type(date) is unicode:
                date = core.Date.parse(date.encode("latin1"))
            elif not isinstance(date, core.Date):
                raise TypeError("str, unicode, and eyed3.core.Date type "
                                "expected")
        except ValueError:
            log.warning("Invalid date text: %s" % date)
            self.text = u""
            return

        self.text = unicode(str(date))

    def _initEncoding(self):
        # Dates are always latin1 since they are always represented in ISO 8601
        self.encoding = LATIN1_ENCODING


class UrlFrame(Frame):
    @requireBytes("url")
    def __init__(self, id, url=b""):
        assert(id in URL_FIDS or id == USERURL_FID)
        super(UrlFrame, self).__init__(id)
        self.encoding = LATIN1_ENCODING
        self.url = url

    @property
    def url(self):
        return self._url

    @requireBytes(1)
    @url.setter
    def url(self, url):
        self._url = url

    def parse(self, data, frame_header):
        super(UrlFrame, self).parse(data, frame_header)
        # The URL is ascii, ensure
        try:
            self.url = unicode(self.data, "ascii").encode("ascii")
        except UnicodeDecodeError:
            log.warning("Non ascii url, clearing.")
            self.url = ""

    def render(self):
        self.data = self.url
        return super(UrlFrame, self).render()


class UserUrlFrame(UrlFrame):
    '''
    Data string format:
    encoding (one byte) + description + b"\x00" + url (ascii)
    '''
    @requireUnicode("description")
    def __init__(self, id=USERURL_FID, description=u"", url=b""):
        UrlFrame.__init__(self, id, url=url)
        assert(self.id == USERURL_FID)

        self.description = description

    @property
    def description(self):
        return self._description

    @description.setter
    @requireUnicode(1)
    def description(self, desc):
        self._description = desc

    def parse(self, data, frame_header):
        # Calling Frame and NOT UrlFrame to get the basic disassemble behavior
        # UrlFrame would be confused by the encoding, desc, etc.
        super(UserUrlFrame, self).parse(data, frame_header)
        self.encoding = encoding = self.data[0:1]

        (d, u) = splitUnicode(self.data[1:], encoding)
        self.description = decodeUnicode(d, encoding)
        log.debug("UserUrlFrame description: %s" % self.description)
        # The URL is ascii, ensure
        try:
            self.url = unicode(u, "ascii").encode("ascii")
        except UnicodeDecodeError:
            log.warning("Non ascii url, clearing.")
            self.url = ""
        log.debug("UserUrlFrame text: %s" % self.url)

    def render(self):
        self._initEncoding()
        data = (self.encoding +
                self.description.encode(id3EncodingToString(self.encoding)) +
                self.text_delim + self.url)
        self.data = data
        # Calling Frame, not the base.
        return Frame.render(self)


##
# Data string format:
# <Header for 'Attached picture', ID: "APIC">
#  Text encoding      $xx
#  MIME type          <text string> $00
#  Picture type       $xx
#  Description        <text string according to encoding> $00 (00)
#  Picture data       <binary data>
class ImageFrame(Frame):
    OTHER               = 0x00                                           # noqa
    ICON                = 0x01  # 32x32 png only.                        # noqa
    OTHER_ICON          = 0x02                                           # noqa
    FRONT_COVER         = 0x03                                           # noqa
    BACK_COVER          = 0x04                                           # noqa
    LEAFLET             = 0x05                                           # noqa
    MEDIA               = 0x06  # label side of cd, vinyl, etc.          # noqa
    LEAD_ARTIST         = 0x07                                           # noqa
    ARTIST              = 0x08                                           # noqa
    CONDUCTOR           = 0x09                                           # noqa
    BAND                = 0x0A                                           # noqa
    COMPOSER            = 0x0B                                           # noqa
    LYRICIST            = 0x0C                                           # noqa
    RECORDING_LOCATION  = 0x0D                                           # noqa
    DURING_RECORDING    = 0x0E                                           # noqa
    DURING_PERFORMANCE  = 0x0F                                           # noqa
    VIDEO               = 0x10                                           # noqa
    BRIGHT_COLORED_FISH = 0x11  # There's always room for porno.         # noqa
    ILLUSTRATION        = 0x12                                           # noqa
    BAND_LOGO           = 0x13                                           # noqa
    PUBLISHER_LOGO      = 0x14                                           # noqa
    MIN_TYPE            = OTHER                                          # noqa
    MAX_TYPE            = PUBLISHER_LOGO                                 # noqa

    URL_MIME_TYPE       = b"-->"                                         # noqa
    URL_MIME_TYPE_STR   = u"-->"                                         # noqa
    URL_MIME_TYPE_VALUES = (URL_MIME_TYPE, URL_MIME_TYPE_STR)

    @requireUnicode("description")
    def __init__(self, id=IMAGE_FID, description=u"",
                 image_data=None, image_url=None,
                 picture_type=None, mime_type=None):
        assert(id == IMAGE_FID)
        super(ImageFrame, self).__init__(id)
        self.description = description
        self.image_data = image_data
        self.image_url = image_url

        self.picture_type = picture_type
        self.mime_type = mime_type

    @property
    def description(self):
        return self._description

    @description.setter
    @requireUnicode(1)
    def description(self, d):
        self._description = d

    @property
    def mime_type(self):
        return unicode(self._mime_type, "ascii")

    @mime_type.setter
    def mime_type(self, m):
        m = m or b''
        self._mime_type = m if isinstance(m, BytesType) else m.encode('ascii')

    @property
    def picture_type(self):
        return self._pic_type

    @picture_type.setter
    def picture_type(self, t):
        if t is not None and (t < ImageFrame.MIN_TYPE or
                              t > ImageFrame.MAX_TYPE):
            raise ValueError("Invalid picture_type: %d" % t)
        self._pic_type = t

    def parse(self, data, frame_header):
        super(ImageFrame, self).parse(data, frame_header)

        input = BytesIO(self.data)
        log.debug("APIC frame data size: %d" % len(self.data))
        self.encoding = encoding = input.read(1)

        # Mime type
        self._mime_type = b""
        if frame_header.minor_version != 2:
            ch = input.read(1)
            while ch and ch != b"\x00":
                self._mime_type += ch
                ch = input.read(1)
        else:
            # v2.2 (OBSOLETE) special case
            self._mime_type = input.read(3)
        log.debug("APIC mime type: %s" % self._mime_type)
        if not self._mime_type:
            core.parseError(FrameException("APIC frame does not contain a mime "
                                           "type"))
        if (self._mime_type != self.URL_MIME_TYPE and
                self._mime_type.find(b"/") == -1):
            self._mime_type = b"image/" + self._mime_type

        pt = ord(input.read(1))
        log.debug("Initial APIC picture type: %d" % pt)
        if pt < self.MIN_TYPE or pt > self.MAX_TYPE:
            core.parseError(FrameException("Invalid APIC picture type: %d" %
                                           pt))
            self.picture_type = self.OTHER
        else:
            self.picture_type = pt
        log.debug("APIC picture type: %d" % self.picture_type)

        self.desciption = u""

        # Remaining data is a NULL separated description and image data
        buffer = input.read()
        input.close()

        (desc, img) = splitUnicode(buffer, encoding)
        log.debug("description len: %d" % len(desc))
        log.debug("image len: %d" % len(img))
        self.description = decodeUnicode(desc, encoding)
        log.debug("APIC description: %s" % self.description)

        if self._mime_type.find(self.URL_MIME_TYPE) != -1:
            self.image_data = None
            self.image_url = img
            log.debug("APIC image URL: %s" %
                      len(self.image_url.decode("ascii")))
        else:
            self.image_data = img
            self.image_url = None
            log.debug("APIC image data: %d bytes" % len(self.image_data))
        if not self.image_data and not self.image_url:
            core.parseError(FrameException("APIC frame does not contain image "
                                           "data/url"))

    def render(self):
        # some code has problems with image descriptions encoded <> latin1
        # namely mp3diags: work around the problem by forcing latin1 encoding
        # for empty descriptions, which is by far the most common case anyway
        if self.description:
            self._initEncoding()
        else:
            self.encoding = LATIN1_ENCODING

        if not self.image_data and self.image_url:
            self._mime_type = self.URL_MIME_TYPE

        data = (self.encoding + self._mime_type + b"\x00" +
                bin2bytes(dec2bin(self.picture_type, 8)) +
                self.description.encode(id3EncodingToString(self.encoding)) +
                self.text_delim)

        if self.image_data:
            data += self.image_data
        elif self.image_url:
            data += self.image_url

        self.data = data
        return super(ImageFrame, self).render()

    @staticmethod
    def picTypeToString(t):
        if t == ImageFrame.OTHER:
            return "OTHER"
        elif t == ImageFrame.ICON:
            return "ICON"
        elif t == ImageFrame.OTHER_ICON:
            return "OTHER_ICON"
        elif t == ImageFrame.FRONT_COVER:
            return "FRONT_COVER"
        elif t == ImageFrame.BACK_COVER:
            return "BACK_COVER"
        elif t == ImageFrame.LEAFLET:
            return "LEAFLET"
        elif t == ImageFrame.MEDIA:
            return "MEDIA"
        elif t == ImageFrame.LEAD_ARTIST:
            return "LEAD_ARTIST"
        elif t == ImageFrame.ARTIST:
            return "ARTIST"
        elif t == ImageFrame.CONDUCTOR:
            return "CONDUCTOR"
        elif t == ImageFrame.BAND:
            return "BAND"
        elif t == ImageFrame.COMPOSER:
            return "COMPOSER"
        elif t == ImageFrame.LYRICIST:
            return "LYRICIST"
        elif t == ImageFrame.RECORDING_LOCATION:
            return "RECORDING_LOCATION"
        elif t == ImageFrame.DURING_RECORDING:
            return "DURING_RECORDING"
        elif t == ImageFrame.DURING_PERFORMANCE:
            return "DURING_PERFORMANCE"
        elif t == ImageFrame.VIDEO:
            return "VIDEO"
        elif t == ImageFrame.BRIGHT_COLORED_FISH:
            return "BRIGHT_COLORED_FISH"
        elif t == ImageFrame.ILLUSTRATION:
            return "ILLUSTRATION"
        elif t == ImageFrame.BAND_LOGO:
            return "BAND_LOGO"
        elif t == ImageFrame.PUBLISHER_LOGO:
            return "PUBLISHER_LOGO"
        else:
            raise ValueError("Invalid APIC picture type: %d" % t)

    @staticmethod
    def stringToPicType(s):
        if s == "OTHER":
            return ImageFrame.OTHER
        elif s == "ICON":
            return ImageFrame.ICON
        elif s == "OTHER_ICON":
            return ImageFrame.OTHER_ICON
        elif s == "FRONT_COVER":
            return ImageFrame.FRONT_COVER
        elif s == "BACK_COVER":
            return ImageFrame.BACK_COVER
        elif s == "LEAFLET":
            return ImageFrame.LEAFLET
        elif s == "MEDIA":
            return ImageFrame.MEDIA
        elif s == "LEAD_ARTIST":
            return ImageFrame.LEAD_ARTIST
        elif s == "ARTIST":
            return ImageFrame.ARTIST
        elif s == "CONDUCTOR":
            return ImageFrame.CONDUCTOR
        elif s == "BAND":
            return ImageFrame.BAND
        elif s == "COMPOSER":
            return ImageFrame.COMPOSER
        elif s == "LYRICIST":
            return ImageFrame.LYRICIST
        elif s == "RECORDING_LOCATION":
            return ImageFrame.RECORDING_LOCATION
        elif s == "DURING_RECORDING":
            return ImageFrame.DURING_RECORDING
        elif s == "DURING_PERFORMANCE":
            return ImageFrame.DURING_PERFORMANCE
        elif s == "VIDEO":
            return ImageFrame.VIDEO
        elif s == "BRIGHT_COLORED_FISH":
            return ImageFrame.BRIGHT_COLORED_FISH
        elif s == "ILLUSTRATION":
            return ImageFrame.ILLUSTRATION
        elif s == "BAND_LOGO":
            return ImageFrame.BAND_LOGO
        elif s == "PUBLISHER_LOGO":
            return ImageFrame.PUBLISHER_LOGO
        else:
            raise ValueError("Invalid APIC picture type: %s" % s)

    def makeFileName(self, name=None):
        name = ImageFrame.picTypeToString(self.picture_type) if not name \
                                                             else name
        ext = self.mime_type.split("/")[1]
        if ext == "jpeg":
            ext = "jpg"
        return ".".join([name, ext])


class ObjectFrame(Frame):
    @requireUnicode("description", "filename")
    def __init__(self, id=OBJECT_FID, description=u"", filename=u"",
                 object_data=None, mime_type=None):
        super(ObjectFrame, self).__init__(OBJECT_FID)
        self.description = description
        self.filename = filename
        self.mime_type = mime_type
        self.object_data = object_data

    @property
    def description(self):
        return self._description

    @description.setter
    @requireUnicode(1)
    def description(self, txt):
        self._description = txt

    @property
    def mime_type(self):
        return unicode(self._mime_type, "ascii")

    @mime_type.setter
    def mime_type(self, m):
        m = m or b''
        self._mime_type = m if isinstance(m, BytesType) else m.encode('ascii')

    @property
    def filename(self):
        return self._filename

    @filename.setter
    @requireUnicode(1)
    def filename(self, txt):
        self._filename = txt

    def parse(self, data, frame_header):
        '''Parse the frame from ``data`` bytes using details from
        ``frame_header``.

        Data string format:
        <Header for 'General encapsulated object', ID: "GEOB">
        Text encoding          $xx
        MIME type              <text string> $00
        Filename               <text string according to encoding> $00 (00)
        Content description    <text string according to encoding> $00 (00)
        Encapsulated object    <binary data>
        '''
        super(ObjectFrame, self).parse(data, frame_header)

        input = BytesIO(self.data)
        log.debug("GEOB frame data size: " + str(len(self.data)))
        self.encoding = encoding = input.read(1)

        # Mime type
        self._mime_type = b""
        if self.header.minor_version != 2:
            ch = input.read(1)
            while ch != b"\x00":
                self._mime_type += ch
                ch = input.read(1)
        else:
            # v2.2 (OBSOLETE) special case
            self._mime_type = input.read(3)
        log.debug("GEOB mime type: %s" % self._mime_type)
        if not self._mime_type:
            core.parseError(FrameException("GEOB frame does not contain a "
                                           "mime type"))
        if self._mime_type.find(b"/") == -1:
            core.parseError(FrameException("GEOB frame does not contain a "
                                           "valid mime type"))

        self.filename = u""
        self.description = u""

        # Remaining data is a NULL separated filename, description and object
        # data
        buffer = input.read()
        input.close()

        (filename, buffer) = splitUnicode(buffer, encoding)
        (desc, obj) = splitUnicode(buffer, encoding)
        self.filename = decodeUnicode(filename, encoding)
        log.debug("GEOB filename: " + self.filename)
        self.description = decodeUnicode(desc, encoding)
        log.debug("GEOB description: " + self.description)

        self.object_data = obj
        log.debug("GEOB data: %d bytes " % len(self.object_data))
        if not self.object_data:
            core.parseError(FrameException("GEOB frame does not contain any "
                                           "data"))

    def render(self):
        self._initEncoding()
        data = (self.encoding + self._mime_type + b"\x00" +
                self.filename.encode(id3EncodingToString(self.encoding)) +
                self.text_delim +
                self.description.encode(id3EncodingToString(self.encoding)) +
                self.text_delim +
                (self.object_data or b""))
        self.data = data
        return super(ObjectFrame, self).render()


class PrivateFrame(Frame):
    '''PRIV'''

    def __init__(self, id=PRIVATE_FID, owner_id=b"", owner_data=b""):
        super(PrivateFrame, self).__init__(id)
        assert(id == PRIVATE_FID)
        self.owner_id = owner_id
        self.owner_data = owner_data

    def parse(self, data, frame_header):
        super(PrivateFrame, self).parse(data, frame_header)
        try:
            self.owner_id, self.owner_data = self.data.split(b'\x00', 1)
        except ValueError:
            # If data doesn't contain required \x00
            # all data is taken to be owner_id
            self.owner_id = self.data

    def render(self):
        self.data = self.owner_id + b"\x00" + self.owner_data
        return super(PrivateFrame, self).render()


class MusicCDIdFrame(Frame):

    def __init__(self, id=CDID_FID, toc=b""):
        super(MusicCDIdFrame, self).__init__(id)
        assert(id == CDID_FID)
        self.toc = toc

    @property
    def toc(self):
        return self.data

    @toc.setter
    def toc(self, toc):
        self.data = toc

    def parse(self, data, frame_header):
        super(MusicCDIdFrame, self).parse(data, frame_header)
        self.toc = self.data


class PlayCountFrame(Frame):
    def __init__(self, id=PLAYCOUNT_FID, count=0):
        super(PlayCountFrame, self).__init__(id)
        assert(self.id == PLAYCOUNT_FID)

        if count is None or count < 0:
            raise ValueError("Invalid count value: %s" % str(count))
        self.count = count

    def parse(self, data, frame_header):
        super(PlayCountFrame, self).parse(data, frame_header)
        # data of less then 4 bytes is handled with with 'sz' arg
        if len(self.data) < 4:
            log.warning("Fixing invalid PCNT frame: less than 32 bits")

        self.count = bytes2dec(self.data)

    def render(self):
        self.data = dec2bytes(self.count, 32)
        return super(PlayCountFrame, self).render()


class PopularityFrame(Frame):
    '''Frame type for 'POPM' frames; popularity.
    Frame format:
    <Header for 'Popularimeter', ID: "POPM">
    Email to user   <text string> $00
    Rating          $xx
    Counter         $xx xx xx xx (xx ...)
    '''
    def __init__(self, id=POPULARITY_FID, email=b"", rating=0, count=0):
        super(PopularityFrame, self).__init__(id)
        assert(self.id == POPULARITY_FID)

        self.email = email
        self.rating = rating
        if count is None or count < 0:
            raise ValueError("Invalid count value: %s" % str(count))
        self.count = count

    @property
    def rating(self):
        return self._rating

    @rating.setter
    def rating(self, rating):
        if rating < 0 or rating > 255:
            raise ValueError("Popularity rating must be >= 0 and <=255")
        self._rating = rating

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, email):
        # XXX: becoming a pattern?
        if isinstance(email, UnicodeType):
            self._email = email.encode(ascii_encode)
        elif isinstance(email, BytesType):
            _ = email.decode("ascii")                                # noqa
            self._email = email
        else:
            raise TypeError("bytes, str, unicode email required")

    @property
    def count(self):
        return self._count

    @count.setter
    def count(self, count):
        if count < 0:
            raise ValueError("Popularity count must be > 0")
        self._count = count

    def parse(self, data, frame_header):
        super(PopularityFrame, self).parse(data, frame_header)
        data = self.data

        null_byte = data.find(b'\x00')
        try:
            self.email = data[:null_byte]
        except UnicodeDecodeError:
            core.parseError(FrameException("Invalid (non-ascii) POPM email "
                                           "address. Setting to 'BOGUS'"))
            self.email = b"BOGUS"
        data = data[null_byte + 1:]

        self.rating = bytes2dec(data[0:1])

        data = data[1:]
        if len(self.data) < 4:
            core.parseError(FrameException(
                "Invalid POPM play count: less than 32 bits."))
        self.count = bytes2dec(data)

    def render(self):
        data = (self.email or b"") + b'\x00'
        data += dec2bytes(self.rating)
        data += dec2bytes(self.count, 32)

        self.data = data
        return super(PopularityFrame, self).render()


class UniqueFileIDFrame(Frame):
    def __init__(self, id=UNIQUE_FILE_ID_FID, owner_id=None, uniq_id=None):
        super(UniqueFileIDFrame, self).__init__(id)
        assert(self.id == UNIQUE_FILE_ID_FID)

        self.owner_id = owner_id
        self.uniq_id = uniq_id

    def parse(self, data, frame_header):
        '''
        Data format
        Owner identifier <text string> $00
        Identifier       up to 64 bytes binary data>
        '''
        super(UniqueFileIDFrame, self).parse(data, frame_header)
        split_data = self.data.split(b'\x00', 1)
        if len(split_data) == 2:
            (self.owner_id, self.uniq_id) = split_data
        else:
            self.owner_id, self.uniq_id = b"", split_data[0:1]
        log.debug("UFID owner_id: %s" % self.owner_id)
        log.debug("UFID id: %s" % self.uniq_id)
        if len(self.owner_id) == 0:
            dummy_owner_id = "http://www.id3.org/dummy/ufid.html"
            self.owner_id = dummy_owner_id
            core.parseError(FrameException("Invalid UFID, owner_id is empty. "
                                           "Setting to '%s'" % dummy_owner_id))
        elif 0 <= len(self.uniq_id) > 64:
            core.parseError(FrameException("Invalid UFID, ID is empty or too "
                                           "long: %s" % self.uniq_id))

    def render(self):
        self.data = self.owner_id + b"\x00" + self.uniq_id
        return super(UniqueFileIDFrame, self).render()


class LanguageCodeMixin(object):
    @property
    def lang(self):
        assert self._lang is not None
        return self._lang

    @lang.setter
    @requireBytes(1)
    def lang(self, lang):
        if not lang:
            self._lang = b""
            return

        lang = lang.strip(b"\00")
        lang = lang[:3] if lang else DEFAULT_LANG
        try:
            if lang != DEFAULT_LANG:
                lang.decode("ascii")
        except UnicodeDecodeError:
            lang = DEFAULT_LANG
        assert len(lang) <= 3
        self._lang = lang

    def _renderLang(self):
        lang = self.lang
        if len(lang) < 3:
            lang = lang + (b"\x00" * (3 - len(lang)))
        return lang


class DescriptionLangTextFrame(Frame, LanguageCodeMixin):
    @requireBytes(1, 3)
    @requireUnicode(2, 4)
    def __init__(self, id, description, lang, text):
        super(DescriptionLangTextFrame,
              self).__init__(id)
        self.lang = lang
        self.description = description
        self.text = text

    @property
    def description(self):
        return self._description

    @description.setter
    @requireUnicode(1)
    def description(self, description):
        self._description = description

    @property
    def text(self):
        return self._text

    @text.setter
    @requireUnicode(1)
    def text(self, text):
        self._text = text

    def parse(self, data, frame_header):
        super(DescriptionLangTextFrame, self).parse(data, frame_header)

        self.encoding = encoding = self.data[0:1]
        self.lang = self.data[1:4]
        log.debug("%s lang: %s" % (self.id, self.lang))

        try:
            (d, t) = splitUnicode(self.data[4:], encoding)
            self.description = decodeUnicode(d, encoding)
            log.debug("%s description: %s" % (self.id, self.description))
            self.text = decodeUnicode(t, encoding)
            log.debug("%s text: %s" % (self.id, self.text))
        except ValueError:
            log.warning("Invalid %s frame; no description/text" % self.id)
            self.description = u""
            self.text = u""

    def render(self):
        lang = self._renderLang()

        self._initEncoding()
        data = (self.encoding + lang +
                self.description.encode(id3EncodingToString(self.encoding)) +
                self.text_delim +
                self.text.encode(id3EncodingToString(self.encoding)))
        self.data = data
        return super(DescriptionLangTextFrame, self).render()


class CommentFrame(DescriptionLangTextFrame):
    def __init__(self, id=COMMENT_FID, description=u"", lang=DEFAULT_LANG,
                 text=u""):
        super(CommentFrame, self).__init__(id, description, lang, text)
        assert(self.id == COMMENT_FID)


class LyricsFrame(DescriptionLangTextFrame):
    def __init__(self, id=LYRICS_FID, description=u"", lang=DEFAULT_LANG,
                 text=u""):
        super(LyricsFrame, self).__init__(id, description, lang, text)
        assert(self.id == LYRICS_FID)


class TermsOfUseFrame(Frame, LanguageCodeMixin):
    @requireUnicode("text")
    def __init__(self, id=b"USER", text=u"", lang=DEFAULT_LANG):
        super(TermsOfUseFrame, self).__init__(id)
        self.lang = lang
        self.text = text

    @property
    def text(self):
        return self._text

    @text.setter
    @requireUnicode(1)
    def text(self, text):
        self._text = text

    def parse(self, data, frame_header):
        super(TermsOfUseFrame, self).parse(data, frame_header)

        self.encoding = encoding = self.data[0:1]
        self.lang = self.data[1:4]
        log.debug("%s lang: %s" % (self.id, self.lang))
        self.text = decodeUnicode(self.data[4:], encoding)
        log.debug("%s text: %s" % (self.id, self.text))

    def render(self):
        lang = self._renderLang()
        self._initEncoding()
        self.data = (self.encoding + lang +
                     self.text.encode(id3EncodingToString(self.encoding)))
        return super(TermsOfUseFrame, self).render()


class TocFrame(Frame):
    '''Table of content frame. There may be more than one, but only one may
    have the top-level flag set.

    Data format:
    Element ID: <string>\x00
    TOC flags:  %000000ab
    Entry count: %xx
    Child elem IDs: <string>\x00 (... num entry count)
    Description: TIT2 frame (optional)
    '''
    TOP_LEVEL_FLAG_BIT = 6
    ORDERED_FLAG_BIT = 7

    @requireBytes(1, 2)
    def __init__(self, id=TOC_FID, element_id=None, toplevel=True, ordered=True,
                 child_ids=None, description=None):
        assert(id == TOC_FID)
        super(TocFrame, self).__init__(id)

        self.element_id = element_id
        self.toplevel = toplevel
        self.ordered = ordered
        self.child_ids = child_ids or []
        self.description = description

    def parse(self, data, frame_header):
        super(TocFrame, self).parse(data, frame_header)

        data = self.data
        log.debug("CTOC frame data size: %d" % len(data))

        null_byte = data.find(b'\x00')
        self.element_id = data[0:null_byte]
        data = data[null_byte + 1:]

        flag_bits = bytes2bin(data[0:1])
        self.toplevel = bool(flag_bits[self.TOP_LEVEL_FLAG_BIT])
        self.ordered = bool(flag_bits[self.ORDERED_FLAG_BIT])
        entry_count = bytes2dec(data[1:2])
        data = data[2:]

        self.child_ids = []
        for i in range(entry_count):
            null_byte = data.find(b'\x00')
            self.child_ids.append(data[:null_byte])
            data = data[null_byte + 1:]

        # Any data remaining must be a TIT2 frame
        self.description = None
        if data and data[:4] != b"TIT2":
            log.warning("Invalid toc data, TIT2 frame expected")
            return
        elif data:
            data = BytesIO(data)
            frame_header = FrameHeader.parse(data, self.header.version)
            data = data.read()
            description_frame = TextFrame(TITLE_FID)
            description_frame.parse(data, frame_header)

            self.description = description_frame.text

    def render(self):
        flags = [0] * 8
        if self.toplevel:
            flags[self.TOP_LEVEL_FLAG_BIT] = 1
        if self.ordered:
            flags[self.ORDERED_FLAG_BIT] = 1

        data = (self.element_id + b'\x00' +
                bin2bytes(flags) + dec2bytes(len(self.child_ids)))

        for cid in self.child_ids:
            data += cid + b'\x00'

        if self.description is not None:
            desc_frame = TextFrame(TITLE_FID, self.description)
            desc_frame.header = FrameHeader(TITLE_FID, self.header.version)
            data += desc_frame.render()

        self.data = data
        return super(TocFrame, self).render()


StartEndTuple = namedtuple("StartEndTuple", ["start", "end"])
'''A 2-tuple, with names 'start' and 'end'.'''


class ChapterFrame(Frame):
    '''Frame type for chapter/section of the audio file.
    <ID3v2.3 or ID3v2.4 frame header, ID: "CHAP">           (10 bytes)
    Element ID      <text string> $00
    Start time      $xx xx xx xx
    End time        $xx xx xx xx
    Start offset    $xx xx xx xx
    End offset      $xx xx xx xx
    <Optional embedded sub-frames>
    '''

    NO_OFFSET = 4294967295
    '''No offset value, aka "0xff0xff0xff0xff"'''

    def __init__(self, id=CHAPTER_FID, element_id=None, times=None,
                 offsets=None, sub_frames=None):
        assert(id == CHAPTER_FID)
        super(ChapterFrame, self).__init__(id)
        self.element_id = element_id
        self.times = times or StartEndTuple(None, None)
        self.offsets = offsets or StartEndTuple(None, None)
        self.sub_frames = sub_frames or FrameSet()

    def parse(self, data, frame_header):
        from .headers import TagHeader, ExtendedTagHeader

        super(ChapterFrame, self).parse(data, frame_header)

        data = self.data
        log.debug("CTOC frame data size: %d" % len(data))

        null_byte = data.find(b'\x00')
        self.element_id = data[0:null_byte]
        data = data[null_byte + 1:]

        start = bytes2dec(data[:4])
        data = data[4:]
        end = bytes2dec(data[:4])
        data = data[4:]
        self.times = StartEndTuple(start, end)

        start = bytes2dec(data[:4])
        data = data[4:]
        end = bytes2dec(data[:4])
        data = data[4:]
        self.offsets = StartEndTuple(start if start != self.NO_OFFSET else None,
                                     end if end != self.NO_OFFSET else None)

        if data:
            dummy_tag_header = TagHeader(self.header.version)
            dummy_tag_header.tag_size = len(data)
            _ = self.sub_frames.parse(BytesIO(data), dummy_tag_header,  # noqa
                                            ExtendedTagHeader())
        else:
            self.sub_frames = FrameSet()

    def render(self):
        data = self.element_id + b'\x00'

        for n in self.times + self.offsets:
            if n is not None:
                data += dec2bytes(n, 32)
            else:
                data += b'\xff\xff\xff\xff'

        for f in self.sub_frames.getAllFrames():
            f.header = FrameHeader(f.id, self.header.version)
            data += f.render()

        self.data = data
        return super(ChapterFrame, self).render()

    @property
    def title(self):
        if TITLE_FID in self.sub_frames:
            return self.sub_frames[TITLE_FID][0].text
        return None

    @title.setter
    def title(self, title):
        self.sub_frames.setTextFrame(TITLE_FID, title)

    @property
    def subtitle(self):
        if SUBTITLE_FID in self.sub_frames:
            return self.sub_frames[SUBTITLE_FID][0].text
        return None

    @subtitle.setter
    def subtitle(self, subtitle):
        self.sub_frames.setTextFrame(SUBTITLE_FID, subtitle)

    @property
    def user_url(self):
        if USERURL_FID in self.sub_frames:
            frame = self.sub_frames[USERURL_FID][0]
            # Not returning frame description, it is always the same since it
            # allows only 1 URL.
            return frame.url
        return None

    @user_url.setter
    def user_url(self, url):
        DESCRIPTION = u"chapter url"

        if url is None:
            del self.sub_frames[USERURL_FID]
        else:
            if USERURL_FID in self.sub_frames:
                for frame in self.sub_frames[USERURL_FID]:
                    if frame.description == DESCRIPTION:
                        frame.url = url
                        return

            self.sub_frames[USERURL_FID] = UserUrlFrame(USERURL_FID,
                                                        DESCRIPTION, url)


# XXX: This data structure pretty sucks, or it is beautiful anarchy
class FrameSet(dict):
    def __init__(self):
        dict.__init__(self)

    def parse(self, f, tag_header, extended_header):
        '''Read frames starting from the current read position of the file
        object. Returns the amount of padding which occurs after the tag, but
        before the audio content.  A return valule of 0 does not mean error.'''
        self.clear()

        padding_size = 0
        size_left = tag_header.tag_size - extended_header.size
        consumed_size = 0

        # Handle a tag-level unsync.  Some frames may have their own unsync bit
        # set instead.
        tag_data = f.read(size_left)

        # If the tag is 2.3 and the tag header unsync bit is set then all the
        # frame data is deunsync'd at once, otherwise it will happen on a per
        # frame basis.
        if tag_header.unsync and tag_header.version <= ID3_V2_3:
            log.debug("De-unsynching %d bytes at once (<= 2.3 tag)" %
                      len(tag_data))
            og_size = len(tag_data)
            tag_data = deunsyncData(tag_data)
            size_left = len(tag_data)
            log.debug("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" %
                      (og_size, size_left))

        # Adding bytes to simulate the tag header(s) in the buffer.  This keeps
        # f.tell() values matching the file offsets for logging.
        prepadding = b'\x00' * 10  # Tag header
        prepadding += b'\x00' * extended_header.size
        tag_buffer = BytesIO(prepadding + tag_data)
        tag_buffer.seek(len(prepadding))

        frame_count = 0
        while size_left > 0:
            log.debug("size_left: " + str(size_left))
            if size_left < (10 + 1):  # The size of the smallest frame.
                log.debug("FrameSet: Implied padding (size_left<minFrameSize)")
                padding_size = size_left
                break

            log.debug("+++++++++++++++++++++++++++++++++++++++++++++++++")
            log.debug("FrameSet: Reading Frame #" + str(frame_count + 1))
            frame_header = FrameHeader.parse(tag_buffer, tag_header.version)
            if not frame_header:
                log.debug("No frame found, implied padding of %d bytes" %
                          size_left)
                padding_size = size_left
                break

            # Frame data.
            if frame_header.data_size:
                log.debug("FrameSet: Reading %d (0x%X) bytes of data from byte "
                          "pos %d (0x%X)" % (frame_header.data_size,
                                             frame_header.data_size,
                                             tag_buffer.tell(),
                                             tag_buffer.tell()))
                data = tag_buffer.read(frame_header.data_size)

                log.debug("FrameSet: %d bytes of data read" % len(data))
                consumed_size += (frame_header.size +
                                  frame_header.data_size)
                frame = createFrame(tag_header, frame_header, data)
                self[frame.id] = frame
                frame_count += 1

            # Each frame contains data_size + headerSize bytes.
            size_left -= (frame_header.size +
                          frame_header.data_size)

        return padding_size

    @requireBytes(1)
    def __getitem__(self, fid):
        if fid in self:
            return dict.__getitem__(self, fid)
        else:
            return None

    @requireBytes(1)
    def __setitem__(self, fid, frame):
        assert(fid == frame.id)

        if fid in self:
            self[fid].append(frame)
        else:
            dict.__setitem__(self, fid, [frame])

    def getAllFrames(self):
        '''Return all the frames in the set as a list. The list is sorted
        in an arbitrary but consistent order.'''
        frames = []
        for flist in list(self.values()):
            frames += flist
        frames.sort()
        return frames

    @requireBytes(1)
    @requireUnicode(2)
    def setTextFrame(self, fid, text):
        '''Set a text frame value.
        Text frame IDs must be unique.  If a frame with
        the same Id is already in the list it's value is changed, otherwise
        the frame is added.
        '''
        assert(fid[0:1] == b"T" and (fid in ID3_FRAMES or
                                     fid in NONSTANDARD_ID3_FRAMES))

        if fid in self:
            self[fid][0].text = text
        else:
            if fid in (DATE_FIDS + DEPRECATED_DATE_FIDS):
                self[fid] = DateFrame(fid, date=text)
            else:
                self[fid] = TextFrame(fid, text=text)

    @requireBytes(1)
    def __contains__(self, fid):
        return dict.__contains__(self, fid)


def deunsyncData(data):
    output = []
    safe = True
    for val in byteiter(data):
        if safe:
            output.append(val)
            safe = (val != b'\xff')
        else:
            if val != b'\x00':
                output.append(val)
            safe = True
    return b''.join(output)


# Create and return the appropriate frame.
def createFrame(tag_header, frame_header, data):
    fid = frame_header.id
    FrameClass = None

    if fid in ID3_FRAMES:
        (desc, ver, FrameClass) = ID3_FRAMES[fid]
    elif fid in NONSTANDARD_ID3_FRAMES:
        log.verbose("Non standard frame '%s' encountered" % fid)
        (desc, ver, FrameClass) = NONSTANDARD_ID3_FRAMES[fid]
    else:
        log.warning("Unknown ID3 frame ID: %s" % fid)
        (desc, ver, FrameClass) = ("Unknown", None, Frame)
    log.debug("createFrame (desc:{}) - {} - {}".format(desc, ver, FrameClass))

    # FrameClass may still be None if the frame is standard but does not
    # yet have a concrete type.
    if not FrameClass:
        log.warning("Frame '%s' is not yet supported, using raw Frame to parse"
                    % fid.decode("ascii"))
        FrameClass = Frame

    log.debug("createFrame '%s' with class '%s'" % (fid, FrameClass))
    if tag_header.version[:2] == (2, 4) and tag_header.unsync:
        frame_header.unsync = True

    frame = FrameClass(fid)
    frame.parse(data, frame_header)
    return frame


def decodeUnicode(bites, encoding):
    for obj, obj_name in ((bites, "bites"), (encoding, "encoding")):
        if not isinstance(obj, bytes):
            raise TypeError("%s argument must be a byte string." % obj_name)

    codec = id3EncodingToString(encoding)
    log.debug("Unicode encoding: %s" % codec)
    if (codec.startswith("utf_16") and
            len(bites) % 2 != 0 and bites[-1:] == b"\x00"):
        # Catch and fix bad utf16 data, it is everywhere.
        log.warning("Fixing utf16 data with extra zero bytes")
        bites = bites[:-1]
    return unicode(bites, codec).rstrip("\x00")


def splitUnicode(data, encoding):
    try:
        if encoding == LATIN1_ENCODING or encoding == UTF_8_ENCODING:
            (d, t) = data.split(b"\x00", 1)
        elif encoding == UTF_16_ENCODING or encoding == UTF_16BE_ENCODING:
            # Two null bytes split, but since each utf16 char is also two
            # bytes we need to ensure we found a proper boundary.
            (d, t) = data.split(b"\x00\x00", 1)
            if (len(d) % 2) != 0:
                (d, t) = data.split(b"\x00\x00\x00", 1)
                d += b"\x00"
    except ValueError as ex:
        log.warning("Invalid 2-tuple ID3 frame data: %s", ex)
        d, t = data, b""
    return (d, t)


def id3EncodingToString(encoding):
    if not isinstance(encoding, bytes):
        raise TypeError("encoding argument must be a byte string.")

    if encoding == LATIN1_ENCODING:
        return "latin_1"
    elif encoding == UTF_8_ENCODING:
        return "utf_8"
    elif encoding == UTF_16_ENCODING:
        return "utf_16"
    elif encoding == UTF_16BE_ENCODING:
        return "utf_16_be"
    else:
        raise ValueError("Encoding unknown: %s" % encoding)


def stringToEncoding(s):
    s = s.replace('-', '_')
    if s in ("latin_1", "latin1"):
        return LATIN1_ENCODING
    elif s in ("utf_8", "utf8"):
        return UTF_8_ENCODING
    elif s in ("utf_16", "utf16"):
        return UTF_16_ENCODING
    elif s in ("utf_16_be", "utf16_be"):
        return UTF_16BE_ENCODING
    else:
        raise ValueError("Encoding unknown: %s" % s)


# { frame-id : (frame-description, valid-id3-version, frame-class) }
ID3_FRAMES = {b"AENC": ("Audio encryption",
                        ID3_V2,
                        None),
              b"APIC": ("Attached picture",
                        ID3_V2,
                        ImageFrame),
              b"ASPI": ("Audio seek point index",
                        ID3_V2_4,
                        None),

              b"COMM": ("Comments", ID3_V2, CommentFrame),
              b"COMR": ("Commercial frame", ID3_V2, None),

              b"CTOC": ("Table of contents", ID3_V2, TocFrame),
              b"CHAP": ("Chapter", ID3_V2, ChapterFrame),

              b"ENCR": ("Encryption method registration", ID3_V2, None),
              b"EQUA": ("Equalisation", ID3_V2_3, None),
              b"EQU2": ("Equalisation (2)", ID3_V2_4, None),
              b"ETCO": ("Event timing codes", ID3_V2, None),

              b"GEOB": ("General encapsulated object", ID3_V2, ObjectFrame),
              b"GRID": ("Group identification registration", ID3_V2, None),

              b"IPLS": ("Involved people list", ID3_V2_3, None),

              b"LINK": ("Linked information", ID3_V2, None),

              b"MCDI": ("Music CD identifier", ID3_V2, MusicCDIdFrame),
              b"MLLT": ("MPEG location lookup table", ID3_V2, None),

              b"OWNE": ("Ownership frame", ID3_V2, None),

              b"PRIV": ("Private frame", ID3_V2, PrivateFrame),
              b"PCNT": ("Play counter", ID3_V2, PlayCountFrame),
              b"POPM": ("Popularimeter", ID3_V2, PopularityFrame),
              b"POSS": ("Position synchronisation frame", ID3_V2, None),

              b"RBUF": ("Recommended buffer size", ID3_V2, None),
              b"RVAD": ("Relative volume adjustment", ID3_V2_3, None),
              b"RVA2": ("Relative volume adjustment (2)", ID3_V2_4, None),
              b"RVRB": ("Reverb", ID3_V2, None),

              b"SEEK": ("Seek frame", ID3_V2_4, None),
              b"SIGN": ("Signature frame", ID3_V2_4, None),
              b"SYLT": ("Synchronised lyric/text", ID3_V2, None),
              b"SYTC": ("Synchronised tempo codes", ID3_V2, None),

              b"TALB": ("Album/Movie/Show title", ID3_V2, TextFrame),
              b"TBPM": ("BPM (beats per minute)", ID3_V2, TextFrame),
              b"TCOM": ("Composer", ID3_V2, TextFrame),
              b"TCON": ("Content type", ID3_V2, TextFrame),
              b"TCOP": ("Copyright message", ID3_V2, TextFrame),
              b"TDAT": ("Date", ID3_V2_3, DateFrame),
              b"TDEN": ("Encoding time", ID3_V2_4, DateFrame),
              b"TDLY": ("Playlist delay", ID3_V2, TextFrame),
              b"TDOR": ("Original release time", ID3_V2_4, DateFrame),
              b"TDRC": ("Recording time", ID3_V2_4, DateFrame),
              b"TDRL": ("Release time", ID3_V2_4, DateFrame),
              b"TDTG": ("Tagging time", ID3_V2_4, DateFrame),
              b"TENC": ("Encoded by", ID3_V2, TextFrame),
              b"TEXT": ("Lyricist/Text writer", ID3_V2, TextFrame),
              b"TFLT": ("File type", ID3_V2, TextFrame),
              b"TIME": ("Time", ID3_V2_3, DateFrame),
              b"TIPL": ("Involved people list", ID3_V2_4, TextFrame),
              b"TIT1": ("Content group description", ID3_V2, TextFrame),
              b"TIT2": ("Title/songname/content description", ID3_V2,
                        TextFrame),
              b"TIT3": ("Subtitle/Description refinement", ID3_V2, TextFrame),
              b"TKEY": ("Initial key", ID3_V2, TextFrame),
              b"TLAN": ("Language(s)", ID3_V2, TextFrame),
              b"TLEN": ("Length", ID3_V2, TextFrame),
              b"TMCL": ("Musician credits list", ID3_V2_4, TextFrame),
              b"TMED": ("Media type", ID3_V2, TextFrame),
              b"TMOO": ("Mood", ID3_V2_4, TextFrame),
              b"TOAL": ("Original album/movie/show title", ID3_V2, TextFrame),
              b"TOFN": ("Original filename", ID3_V2, TextFrame),
              b"TOLY": ("Original lyricist(s)/text writer(s)", ID3_V2,
                        TextFrame),
              b"TOPE": ("Original artist(s)/performer(s)", ID3_V2, TextFrame),
              b"TORY": ("Original release year", ID3_V2_3, DateFrame),
              b"TOWN": ("File owner/licensee", ID3_V2, TextFrame),
              b"TPE1": ("Lead performer(s)/Soloist(s)", ID3_V2, TextFrame),
              b"TPE2": ("Band/orchestra/accompaniment", ID3_V2, TextFrame),
              b"TPE3": ("Conductor/performer refinement", ID3_V2, TextFrame),
              b"TPE4": ("Interpreted, remixed, or otherwise modified by",
                        ID3_V2, TextFrame),
              b"TPOS": ("Part of a set", ID3_V2, TextFrame),
              b"TPRO": ("Produced notice", ID3_V2_4, TextFrame),
              b"TPUB": ("Publisher", ID3_V2, TextFrame),
              b"TRCK": ("Track number/Position in set", ID3_V2, TextFrame),
              b"TRDA": ("Recording dates", ID3_V2_3, DateFrame),
              b"TRSN": ("Internet radio station name", ID3_V2, TextFrame),
              b"TRSO": ("Internet radio station owner", ID3_V2, TextFrame),
              b"TSOA": ("Album sort order", ID3_V2_4, TextFrame),
              b"TSOP": ("Performer sort order", ID3_V2_4, TextFrame),
              b"TSOT": ("Title sort order", ID3_V2_4, TextFrame),
              b"TSIZ": ("Size", ID3_V2_3, TextFrame),
              b"TSRC": ("ISRC (international standard recording code)", ID3_V2,
                        TextFrame),
              b"TSSE": ("Software/Hardware and settings used for encoding",
                        ID3_V2, TextFrame),
              b"TSST": ("Set subtitle", ID3_V2_4, TextFrame),
              b"TYER": ("Year", ID3_V2_3, DateFrame),
              b"TXXX": ("User defined text information frame", ID3_V2,
                        UserTextFrame),

              b"UFID": ("Unique file identifier", ID3_V2, UniqueFileIDFrame),
              b"USER": ("Terms of use", ID3_V2, TermsOfUseFrame),
              b"USLT": ("Unsynchronised lyric/text transcription", ID3_V2,
                        LyricsFrame),

              b"WCOM": ("Commercial information", ID3_V2, UrlFrame),
              b"WCOP": ("Copyright/Legal information", ID3_V2, UrlFrame),
              b"WOAF": ("Official audio file webpage", ID3_V2, UrlFrame),
              b"WOAR": ("Official artist/performer webpage", ID3_V2, UrlFrame),
              b"WOAS": ("Official audio source webpage", ID3_V2, UrlFrame),
              b"WORS": ("Official Internet radio station homepage", ID3_V2,
                        UrlFrame),
              b"WPAY": ("Payment", ID3_V2, UrlFrame),
              b"WPUB": ("Publishers official webpage", ID3_V2, UrlFrame),
              b"WXXX": ("User defined URL link frame", ID3_V2, UserUrlFrame),
}


def map2_2FrameId(orig_id):
    if orig_id not in TAGS2_2_TO_TAGS_2_3_AND_4:
        return orig_id
    return TAGS2_2_TO_TAGS_2_3_AND_4[orig_id]


# mapping of 2.2 frames to 2.3/2.4
TAGS2_2_TO_TAGS_2_3_AND_4 = {
    b"TT1": b"TIT1",  # CONTENTGROUP content group description
    b"TT2": b"TIT2",  # TITLE title/songname/content description
    b"TT3": b"TIT3",  # SUBTITLE subtitle/description refinement
    b"TP1": b"TPE1",  # ARTIST lead performer(s)/soloist(s)
    b"TP2": b"TPE2",  # BAND band/orchestra/accompaniment
    b"TP3": b"TPE3",  # CONDUCTOR conductor/performer refinement
    b"TP4": b"TPE4",  # MIXARTIST interpreted, remixed, modified by
    b"TCM": b"TCOM",  # COMPOSER composer
    b"TXT": b"TEXT",  # LYRICIST lyricist/text writer
    b"TLA": b"TLAN",  # LANGUAGE language(s)
    b"TCO": b"TCON",  # CONTENTTYPE content type
    b"TAL": b"TALB",  # ALBUM album/movie/show title
    b"TRK": b"TRCK",  # TRACKNUM track number/position in set
    b"TPA": b"TPOS",  # PARTINSET part of set
    b"TRC": b"TSRC",  # ISRC international standard recording code
    b"TDA": b"TDAT",  # DATE date
    b"TYE": b"TYER",  # YEAR year
    b"TIM": b"TIME",  # TIME time
    b"TRD": b"TRDA",  # RECORDINGDATES recording dates
    b"TOR": b"TORY",  # ORIGYEAR original release year
    b"TBP": b"TBPM",  # BPM beats per minute
    b"TMT": b"TMED",  # MEDIATYPE media type
    b"TFT": b"TFLT",  # FILETYPE file type
    b"TCR": b"TCOP",  # COPYRIGHT copyright message
    b"TPB": b"TPUB",  # PUBLISHER publisher
    b"TEN": b"TENC",  # ENCODEDBY encoded by
    b"TSS": b"TSSE",  # ENCODERSETTINGS software/hardware+settings for encoding
    b"TLE": b"TLEN",  # SONGLEN length (ms)
    b"TSI": b"TSIZ",  # SIZE size (bytes)
    b"TDY": b"TDLY",  # PLAYLISTDELAY playlist delay
    b"TKE": b"TKEY",  # INITIALKEY initial key
    b"TOT": b"TOAL",  # ORIGALBUM original album/movie/show title
    b"TOF": b"TOFN",  # ORIGFILENAME original filename
    b"TOA": b"TOPE",  # ORIGARTIST original artist(s)/performer(s)
    b"TOL": b"TOLY",  # ORIGLYRICIST original lyricist(s)/text writer(s)
    b"TXX": b"TXXX",  # USERTEXT user defined text information frame
    b"WAF": b"WOAF",  # WWWAUDIOFILE official audio file webpage
    b"WAR": b"WOAR",  # WWWARTIST official artist/performer webpage
    b"WAS": b"WOAS",  # WWWAUDIOSOURCE official audion source webpage
    b"WCM": b"WCOM",  # WWWCOMMERCIALINFO commercial information
    b"WCP": b"WCOP",  # WWWCOPYRIGHT copyright/legal information
    b"WPB": b"WPUB",  # WWWPUBLISHER publishers official webpage
    b"WXX": b"WXXX",  # WWWUSER user defined URL link frame
    b"IPL": b"IPLS",  # INVOLVEDPEOPLE involved people list
    b"ULT": b"USLT",  # UNSYNCEDLYRICS unsynchronised lyrics/text transcription
    b"COM": b"COMM",  # COMMENT comments
    b"UFI": b"UFID",  # UNIQUEFILEID unique file identifier
    b"MCI": b"MCDI",  # CDID music CD identifier
    b"ETC": b"ETCO",  # EVENTTIMING event timing codes
    b"MLL": b"MLLT",  # MPEGLOOKUP MPEG location lookup table
    b"STC": b"SYTC",  # SYNCEDTEMPO synchronised tempo codes
    b"SLT": b"SYLT",  # SYNCEDLYRICS synchronised lyrics/text
    b"RVA": b"RVAD",  # VOLUMEADJ relative volume adjustment
    b"EQU": b"EQUA",  # EQUALIZATION equalization
    b"REV": b"RVRB",  # REVERB reverb
    b"PIC": b"APIC",  # PICTURE attached picture
    b"GEO": b"GEOB",  # GENERALOBJECT general encapsulated object
    b"CNT": b"PCNT",  # PLAYCOUNTER play counter
    b"POP": b"POPM",  # POPULARIMETER popularimeter
    b"BUF": b"RBUF",  # BUFFERSIZE recommended buffer size
    b"CRA": b"AENC",  # AUDIOCRYPTO audio encryption
    b"LNK": b"LINK",  # LINKEDINFO linked information
    # Extension workarounds i.e., ignore them
    b"TCP": b"TCMP",  # iTunes "extension" for compilation marking
    b"TST": b"TSOT",  # iTunes "extension" for title sort
    b"TSP": b"TSOP",  # iTunes "extension" for artist sort
    b"TSA": b"TSOA",  # iTunes "extension" for album sort
    b"TS2": b"TSO2",  # iTunes "extension" for album artist sort
    b"TSC": b"TSOC",  # iTunes "extension" for composer sort
    b"TDR": b"TDRL",  # iTunes "extension" for release date
    b"TDS": b"TDES",  # iTunes "extension" for podcast description
    b"TID": b"TGID",  # iTunes "extension" for podcast identifier
    b"WFD": b"WFED",  # iTunes "extension" for podcast feed URL
    b"CM1": b"CM1 ",  # Seems to be some script kiddie tagging the tag.
                      # For example, [rH] join #rH on efnet [rH]
    b"PCS": b"PCST",  # iTunes extension for podcast marking.
}

from . import apple                                                       # noqa
NONSTANDARD_ID3_FRAMES = {
    b"NCON": ("Undefined MusicMatch extension", ID3_V2, Frame),
    b"TCMP": ("iTunes complilation flag extension", ID3_V2, TextFrame),
    b"XSOA": ("Album sort-order string extension for v2.3",
              ID3_V2_3, TextFrame),
    b"XSOP": ("Performer sort-order string extension for v2.3",
              ID3_V2_3, TextFrame),
    b"XSOT": ("Title sort-order string extension for v2.3",
              ID3_V2_3, TextFrame),
    b"XDOR": ("MusicBrainz release date (full) extension for v2.3",
              ID3_V2_3, DateFrame),

    b"TSO2": ("Album artist sort-order used in iTunes and Picard",
              ID3_V2, TextFrame),
    b"TSOC": ("Composer sort-order used in iTunes and Picard",
              ID3_V2, TextFrame),

    b"PCST": ("iTunes extension; marks the file as a podcast",
              ID3_V2, apple.PCST),
    b"TKWD": ("iTunes extension; podcast keywords?",
              ID3_V2, apple.TKWD),
    b"TDES": ("iTunes extension; podcast description?",
              ID3_V2, apple.TDES),
    b"TGID": ("iTunes extension; podcast ?????",
              ID3_V2, apple.TGID),
    b"WFED": ("iTunes extension; podcast feed URL?",
              ID3_V2, apple.WFED),
    b"TCAT": ("iTunes extension; podcast category.",
              ID3_V2, TextFrame),
}
