import deprecation
from math import log10

from . import Mp3Exception
from ..utils.binfuncs import bytes2bin, bytes2dec, bin2dec
from ..utils.log import getLogger
from ..__about__ import __version__

log = getLogger(__name__)


def isValidHeader(header):
    """Determine if ``header`` (an integer, 4 bytes compared) is a valid mp3
    frame header."""
    # Test for the mp3 frame sync: 11 set bits.
    sync = (header >> 16)
    if sync & 0xffe0 != 0xffe0:
        # ffe0 is 11 sync bits, 12 are not used in order to support identifying
        # mpeg v2.5 (bits 20,19)
        return False

    # All the remaining tests are not entirely required, but do help in
    # finding false syncs

    version = (header >> 19) & 0x3
    if version == 1:
        # This is a "reserved" version
        log.debug("invalid mpeg version")
        return False

    layer = (header >> 17) & 0x3
    if layer == 0:
        # This is a "reserved" layer
        log.debug("invalid mpeg layer")
        return False

    bitrate = (header >> 12) & 0xf
    if bitrate in (0, 0xf):
        # free and bad bitrate values
        log.debug("invalid mpeg bitrate")
        return False

    sample_rate = (header >> 10) & 0x3
    if sample_rate == 0x3:
        # this is a "reserved" sample rate
        log.debug("invalid mpeg sample rate")
        return False

    return True


def findHeader(fp, start_pos=0):
    """Locate the first mp3 header in file stream ``fp`` starting a offset
    ``start_pos`` (defaults to 0). Returned is a 3-tuple containing the offset
    where the header was found, the header as an integer, and the header as 4
    bytes. If no header is found header_int will equal 0.
    """

    def isBOM(buffer, pos):
        """Check for unicode BOM"""
        try:
            if pos - 1 >= 0:
                if buffer[pos - 1] == 254:
                    return True
            return buffer[pos + 1] == 254
        except IndexError:
            return False

    def find_sync(_fp, _pos=0):
        chunk_sz = 8192  # Measured as optimal

        _fp.seek(_pos)
        data = _fp.read(chunk_sz)

        while data:
            pos = 0
            while 0 <= pos < chunk_sz:
                pos = data.find(b"\xff", pos)
                if pos == -1:
                    break

                if not isBOM(data, pos):
                    h = data[pos:pos + 4]
                    if len(h) == 4:
                        return tuple([_pos + pos, h])

                pos += 1

            _pos += len(data)
            data = _fp.read(chunk_sz)

        return None, None

    sync_pos, header_bytes = find_sync(fp, start_pos)
    while sync_pos is not None:
        header = bytes2dec(header_bytes)
        if isValidHeader(header):
            return tuple([sync_pos, header, header_bytes])
        sync_pos, header_bytes = find_sync(fp, start_pos + sync_pos + 2)

    return None, None, None


def timePerFrame(mp3_header, vbr):
    """Computes the number of seconds per mp3 frame. It can be used to
    compute overall playtime and bitrate. The mp3 layer and sample
    rate from ``mp3_header`` are used to compute the number of seconds
    (fractional float point value) per mp3 frame. Be sure to set ``vbr`` True
    when dealing with VBR, otherwise playtimes may be incorrect."""
    # https://bitbucket.org/nicfit/eyed3/issue/32/mp3audioinfotime_secs-incorrect-for-mpeg2
    if mp3_header.version >= 2.0 and vbr:
        row = _mp3VersionKey(mp3_header.version)
    else:
        row = 0
    return (float(SAMPLES_PER_FRAME_TABLE[row][mp3_header.layer]) /
            float(mp3_header.sample_freq))


@deprecation.deprecated(deprecated_in="0.9a2", removed_in="1.0", current_version=__version__,
                        details="Use timePerFrame instead")
def compute_time_per_frame(mp3_header):
    if mp3_header is not None:
        return timePerFrame(mp3_header, False)


class Mp3Header:
    """Header container for MP3 frames."""
    def __init__(self, header_data=None):
        self.version = None
        self.layer = None
        self.error_protection = None
        self.bit_rate = None
        self.sample_freq = None
        self.padding = None
        self.private_bit = None
        self.copyright = None
        self.original = None
        self.emphasis = None
        self.mode = None
        # This value is left as is: 0<=mode_extension<=3.
        # See http://www.dv.co.yu/mpgscript/mpeghdr.htm for how to interpret
        self.mode_extension = None
        self.frame_length = None

        if header_data:
            self.decode(header_data)

    # This may throw an Mp3Exception if the header is malformed.
    def decode(self, header):
        if not isValidHeader(header):
            raise Mp3Exception("Invalid MPEG header")

        # MPEG audio version from bits 19 and 20.
        version = (header >> 19) & 0x3
        self.version = [2.5, None, 2.0, 1.0][version]
        if self.version is None:
            raise Mp3Exception("Illegal MPEG version")

        # MPEG layer
        self.layer = 4 - ((header >> 17) & 0x3)
        if self.layer == 4:
            raise Mp3Exception("Illegal MPEG layer")

        # Decode some simple values.
        self.error_protection = not (header >> 16) & 0x1
        self.padding = (header >> 9) & 0x1
        self.private_bit = (header >> 8) & 0x1
        self.copyright = (header >> 3) & 0x1
        self.original = (header >> 2) & 0x1

        # Obtain sampling frequency.
        sample_bits = (header >> 10) & 0x3
        self.sample_freq = \
            SAMPLE_FREQ_TABLE[sample_bits][_mp3VersionKey(self.version)]
        if not self.sample_freq:
            raise Mp3Exception("Illegal MPEG sampling frequency")

        # Compute bitrate.
        bit_rate_row = (header >> 12) & 0xf
        if int(self.version) == 1 and self.layer == 1:
            bit_rate_col = 0
        elif int(self.version) == 1 and self.layer == 2:
            bit_rate_col = 1
        elif int(self.version) == 1 and self.layer == 3:
            bit_rate_col = 2
        elif int(self.version) == 2 and self.layer == 1:
            bit_rate_col = 3
        elif int(self.version) == 2 and (self.layer == 2 or
                                         self.layer == 3):
            bit_rate_col = 4
        else:
            raise Mp3Exception("Mp3 version %f and layer %d is an invalid "
                               "combination" % (self.version, self.layer))
        self.bit_rate = BIT_RATE_TABLE[bit_rate_row][bit_rate_col]
        if self.bit_rate is None:
            raise Mp3Exception("Invalid bit rate")
        # We know know the bit rate specified in this frame, but if the file
        # is VBR we need to obtain the average from the Xing header.
        # This is done by the caller since right now all we have is the frame
        # header.

        # Emphasis; whatever that means??
        emph = header & 0x3
        if emph == 0:
            self.emphasis = EMPHASIS_NONE
        elif emph == 1:
            self.emphasis = EMPHASIS_5015
        elif emph == 2:
            self.emphasis = EMPHASIS_CCIT
        else:
            raise Mp3Exception("Illegal mp3 emphasis value: %d" % emph)

        # Channel mode.
        mode_bits = (header >> 6) & 0x3
        if mode_bits == 0:
            self.mode = MODE_STEREO
        elif mode_bits == 1:
            self.mode = MODE_JOINT_STEREO
        elif mode_bits == 2:
            self.mode = MODE_DUAL_CHANNEL_STEREO
        else:
            self.mode = MODE_MONO
        self.mode_extension = (header >> 4) & 0x3

        # Layer II has restrictions wrt to mode and bit rate.  This code
        # enforces them.
        if self.layer == 2:
            m = self.mode
            br = self.bit_rate
            if (br in [32, 48, 56, 80] and (m != MODE_MONO)):
                raise Mp3Exception("Invalid mode/bitrate combination for layer "
                                   "II")
            if (br in [224, 256, 320, 384] and (m == MODE_MONO)):
                raise Mp3Exception("Invalid mode/bitrate combination for layer "
                                   "II")

        br = self.bit_rate * 1000
        sf = self.sample_freq
        p = self.padding
        if self.layer == 1:
            # Layer 1 uses 32 bit slots for padding.
            p = self.padding * 4
            self.frame_length = int((((12 * br) / sf) + p) * 4)
        else:
            # Layer 2 and 3 uses 8 bit slots for padding.
            p = self.padding * 1
            self.frame_length = int(((144 * br) / sf) + p)

        # Dump the state.
        log.debug("MPEG audio version: " + str(self.version))
        log.debug("MPEG audio layer: " + ("I" * self.layer))
        log.debug("MPEG sampling frequency: " + str(self.sample_freq))
        log.debug("MPEG bit rate: " + str(self.bit_rate))
        log.debug("MPEG channel mode: " + self.mode)
        log.debug("MPEG channel mode extension: " + str(self.mode_extension))
        log.debug("MPEG CRC error protection: " + str(self.error_protection))
        log.debug("MPEG original: " + str(self.original))
        log.debug("MPEG copyright: " + str(self.copyright))
        log.debug("MPEG private bit: " + str(self.private_bit))
        log.debug("MPEG padding: " + str(self.padding))
        log.debug("MPEG emphasis: " + str(self.emphasis))
        log.debug("MPEG frame length: " + str(self.frame_length))


class VbriHeader(object):
    def __init__(self):
        self.vbr = True
        self.version = None

    ##
    # \brief Decode the VBRI info from \a frame.
    # http://www.codeproject.com/audio/MPEGAudioInfo.asp#VBRIHeader
    def decode(self, frame):

        # The header is 32 bytes after the end of the first MPEG audio header,
        # therefore 4 + 32 = 36
        offset = 36
        head = frame[offset:offset + 4]
        if head != 'VBRI':
            return False
        log.debug("VBRI header detected @ %x" % (offset))
        offset += 4

        self.version = bin2dec(bytes2bin(frame[offset:offset + 2]))
        offset += 2

        self.delay = bin2dec(bytes2bin(frame[offset:offset + 2]))
        offset += 2

        self.quality = bin2dec(bytes2bin(frame[offset:offset + 2]))
        offset += 2

        self.num_bytes = bin2dec(bytes2bin(frame[offset:offset + 4]))
        offset += 4

        self.num_frames = bin2dec(bytes2bin(frame[offset:offset + 4]))
        offset += 4

        return True


class XingHeader:
    """Header class for the Xing header extensions."""

    def __init__(self):
        self.numFrames = int()
        self.numBytes = int()
        self.toc = [0] * 100
        self.vbrScale = int()

    # Pass in the first mp3 frame from the file as a byte string.
    # If an Xing header is present in the file it'll be in the first mp3
    # frame.  This method returns true if the Xing header is found in the
    # frame, and false otherwise.
    def decode(self, frame):
        # mp3 version
        version = (frame[1] >> 3) & 0x1
        # channel mode.
        mode = (frame[3] >> 6) & 0x3

        # Find the start of the Xing header.
        if version:
            # +4 in all of these to skip initial mp3 frame header.
            if mode != 3:
                pos = 32 + 4
            else:
                pos = 17 + 4
        else:
            if mode != 3:
                pos = 17 + 4
            else:
                pos = 9 + 4
        head = frame[pos:pos + 4]
        self.vbr = (head == b'Xing') and True or False
        if head not in [b'Xing', b'Info']:
            return False
        log.debug("%s header detected @ %x" % (head, pos))
        pos += 4

        # Read Xing flags.
        headFlags = bin2dec(bytes2bin(frame[pos:pos + 4]))
        pos += 4
        log.debug("%s header flags: 0x%x" % (head, headFlags))

        # Read frames header flag and value if present
        if headFlags & FRAMES_FLAG:
            self.numFrames = bin2dec(bytes2bin(frame[pos:pos + 4]))
            pos += 4
            log.debug("%s numFrames: %d" % (head, self.numFrames))

        # Read bytes header flag and value if present
        if headFlags & BYTES_FLAG:
            self.numBytes = bin2dec(bytes2bin(frame[pos:pos + 4]))
            pos += 4
            log.debug("%s numBytes: %d" % (head, self.numBytes))

        # Read TOC header flag and value if present
        if headFlags & TOC_FLAG:
            self.toc = frame[pos:pos + 100]
            pos += 100
            log.debug("%s TOC (100 bytes): PRESENT" % head)
        else:
            log.debug("%s TOC (100 bytes): NOT PRESENT" % head)

        # Read vbr scale header flag and value if present
        if headFlags & VBR_SCALE_FLAG and head == b'Xing':
            self.vbrScale = bin2dec(bytes2bin(frame[pos:pos + 4]))
            pos += 4
            log.debug("%s vbrScale: %d" % (head, self.vbrScale))

        return True


class LameHeader(dict):
    """ Mp3 Info tag (AKA LAME Tag)

    Lame (and some other encoders) write a tag containing various bits of info
    about the options used at encode time.  If available, the following are
    parsed and stored in the LameHeader dict:

    * encoder_version: short encoder version [str]
    * tag_revision:    revision number of the tag [int]
    * vbr_method:      VBR method used for encoding [str]
    * lowpass_filter:  lowpass filter frequency in Hz [int]
    * replaygain:      if available, radio and audiofile gain (see below) [dict]
    * encoding_flags:  encoding flags used [list]
    * nogap:           location of gaps when --nogap was used [list]
    * ath_type:        ATH type [int]
    * bitrate:         bitrate and type (Constant, Target, Minimum) [tuple]
    * encoder_delay:   samples added at the start of the mp3 [int]
    * encoder_padding: samples added at the end of the mp3 [int]
    * noise_shaping:   noise shaping method [int]
    * stereo_mode:     stereo mode used [str]
    * unwise_settings: whether unwise settings were used [boolean]
    * sample_freq:     source sample frequency [str]
    * mp3_gain:        mp3 gain adjustment (rarely used) [float]
    * preset:          preset used [str]
    * surround_info:   surround information [str]
    * music_length:    length in bytes of original mp3 [int]
    * music_crc:       CRC-16 of the mp3 music data [int]
    * infotag_crc:     CRC-16 of the info tag [int]

    Prior to ~3.90, Lame simply stored the encoder version in the first frame.
    If the infotag_crc is invalid, then we try to read this version string.  A
    simple way to tell if the LAME Tag is complete is to check for the
    infotag_crc key.

    Replay Gain data is only available since Lame version 3.94b.  If set, the
    replaygain dict has the following structure:

    .. code-block::

       peak_amplitude: peak signal amplitude [float]
       radio:
          name:       name of the gain adjustment [str]
          adjustment: gain adjustment [float]
          originator: originator of the gain adjustment [str]
       audiofile: [same as radio]

    Note that as of 3.95.1, Lame uses 89dB as a reference level instead of the
    83dB that is specified in the Replay Gain spec.  This is not automatically
    compensated for.  You can do something like this if you want:

    .. code-block::

       import eyeD3
       af = eyeD3.mp3.Mp3AudioFile('/path/to/some.mp3')
       lamever = af.lameTag['encoder_version']
       name, ver = lamever[:4], lamever[4:]
       gain = af.lameTag['replaygain']['radio']['adjustment']
       if name == 'LAME' and eyeD3.mp3.lamevercmp(ver, '3.95') > 0:
           gain -= 6

    Radio and Audiofile Replay Gain are often referred to as Track and Album
    gain, respectively.  See https://replaygain.hydrogenaudio.org/ for further
    details on Replay Gain.

    See http://gabriel.mp3-tech.org/mp3infotag.html for the gory details of the
    LAME Tag.
    """

    # from the LAME source:
    # http://lame.cvs.sourceforge.net/*checkout*/lame/lame/libmp3lame/VbrTag.c
    _crc16_table = [
      0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
      0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
      0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
      0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
      0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
      0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
      0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
      0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
      0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
      0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
      0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
      0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
      0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
      0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
      0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
      0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
      0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
      0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
      0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
      0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
      0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
      0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
      0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
      0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
      0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
      0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
      0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
      0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
      0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
      0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
      0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
      0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040]

    ENCODER_FLAGS = {
      'NSPSYTUNE': 0x0001,
      'NSSAFEJOINT': 0x0002,
      'NOGAP_NEXT': 0x0004,
      'NOGAP_PREV': 0x0008}

    PRESETS = {
      0: 'Unknown',
      # 8 to 320 are reserved for ABR bitrates
      410: 'V9',
      420: 'V8',
      430: 'V7',
      440: 'V6',
      450: 'V5',
      460: 'V4',
      470: 'V3',
      480: 'V2',
      490: 'V1',
      500: 'V0',
      1000: 'r3mix',
      1001: 'standard',
      1002: 'extreme',
      1003: 'insane',
      1004: 'standard/fast',
      1005: 'extreme/fast',
      1006: 'medium',
      1007: 'medium/fast'}

    REPLAYGAIN_NAME = {
      0: 'Not set',
      1: 'Radio',
      2: 'Audiofile'}

    REPLAYGAIN_ORIGINATOR = {
      0: 'Not set',
      1: 'Set by artist',
      2: 'Set by user',
      3: 'Set automatically',
      100: 'Set by simple RMS average'}

    SAMPLE_FREQUENCIES = {
      0: '<= 32 kHz',
      1: '44.1 kHz',
      2: '48 kHz',
      3: '> 48 kHz'}

    STEREO_MODES = {
      0: 'Mono',
      1: 'Stereo',
      2: 'Dual',
      3: 'Joint',
      4: 'Force',
      5: 'Auto',
      6: 'Intensity',
      7: 'Undefined'}

    SURROUND_INFO = {
      0: 'None',
      1: 'DPL encoding',
      2: 'DPL2 encoding',
      3: 'Ambisonic encoding',
      8: 'Reserved'}

    VBR_METHODS = {
      0: 'Unknown',
      1: 'Constant Bitrate',
      2: 'Average Bitrate',
      3: 'Variable Bitrate method1 (old/rh)',
      4: 'Variable Bitrate method2 (mtrh)',
      5: 'Variable Bitrate method3 (mt)',
      6: 'Variable Bitrate method4',
      8: 'Constant Bitrate (2 pass)',
      9: 'Average Bitrate (2 pass)',
      15: 'Reserved'}

    def __init__(self, frame):
        """Read the LAME info tag.
        frame should be the first frame of an mp3.
        """
        super().__init__()
        self.decode(frame)

    def _crc16(self, data, val=0):
        """Compute a CRC-16 checksum on a data stream."""
        for c in [bytes([b]) for b in data]:
            val = self._crc16_table[ord(c) ^ (val & 0xff)] ^ (val >> 8)
        return val

    def decode(self, frame):
        """Decode the LAME info tag."""
        try:
            pos = frame.index(b"LAME")
        except ValueError:
            return

        log.debug(f"Lame info tag found at position {pos}")

        # check the info tag crc. If it's not valid, no point parsing much more.
        lamecrc = bin2dec(bytes2bin(frame[190:192]))
        if self._crc16(frame[:190]) != lamecrc:
            log.warning("Lame tag CRC check failed")
        else:
            log.debug("Lame tag CRC OK")

        try:
            # Encoder short VersionString, 9 bytes
            self['encoder_version'] = str(frame[pos:pos + 9].rstrip(), "latin1")
            log.debug('Lame Encoder Version: %s' % self['encoder_version'])
            pos += 9

            # Info Tag revision + VBR method, 1 byte
            self['tag_revision'] = bin2dec(bytes2bin(frame[pos:pos + 1])[:5])
            vbr_method = bin2dec(bytes2bin(frame[pos:pos + 1])[5:])
            self['vbr_method'] = self.VBR_METHODS.get(vbr_method, 'Unknown')
            log.debug('Lame info tag version: %s' % self['tag_revision'])
            log.debug('Lame VBR method: %s' % self['vbr_method'])
            pos += 1

            # Lowpass filter value, 1 byte
            self['lowpass_filter'] = bin2dec(
                                       bytes2bin(frame[pos:pos + 1])) * 100
            log.debug('Lame Lowpass filter value: %s Hz' %
                      self['lowpass_filter'])
            pos += 1

            # Replay Gain, 8 bytes total
            replaygain = {}

            # Peak signal amplitude, 4 bytes
            peak = bin2dec(bytes2bin(frame[pos:pos + 4])) << 5
            if peak > 0:
                peak /= float(1 << 28)
                db = 20 * log10(peak)
                replaygain['peak_amplitude'] = peak
                log.debug('Lame Peak signal amplitude: %.8f (%+.1f dB)' %
                          (peak, db))
            pos += 4

            # Radio and Audiofile Gain, AKA track and album, 2 bytes each
            for gaintype in ['radio', 'audiofile']:
                name = bin2dec(bytes2bin(frame[pos:pos + 2])[:3])
                orig = bin2dec(bytes2bin(frame[pos:pos + 2])[3:6])
                sign = bin2dec(bytes2bin(frame[pos:pos + 2])[6:7])
                adj = bin2dec(bytes2bin(frame[pos:pos + 2])[7:]) / 10.0
                if sign:
                    adj *= -1

                # Lame 3.95.1 and above use 89dB as a reference instead of 83dB as defined by the
                # Replay Gain spec. This will be compensated for with `adj -= 6`
                lamever = self['encoder_version']
                if lamever[:4] == 'LAME' and lamevercmp(lamever[4:], "3.95") > 0:
                    adj -= 6

                if orig:
                    name = self.REPLAYGAIN_NAME.get(name, 'Unknown')
                    orig = self.REPLAYGAIN_ORIGINATOR.get(orig, 'Unknown')
                    replaygain[gaintype] = {'name': name, 'adjustment': adj,
                                            'originator': orig}
                    log.debug('Lame %s Replay Gain: %s dB (%s)' %
                              (name, adj, orig))
                pos += 2
            if replaygain:
                self['replaygain'] = replaygain

            # Encoding flags + ATH Type, 1 byte
            encflags = bin2dec(bytes2bin(frame[pos:pos + 1])[:4])
            (self['encoding_flags'],
             self['nogap']) = self._parse_encflags(encflags)
            self['ath_type'] = bin2dec(bytes2bin(frame[pos:pos + 1])[4:])
            log.debug('Lame Encoding flags: %s' %
                      ' '.join(self['encoding_flags']))
            if self['nogap']:
                log.debug('Lame No gap: %s' % ' and '.join(self['nogap']))
            log.debug('Lame ATH type: %s' % self['ath_type'])
            pos += 1

            # if ABR {specified bitrate} else {minimal bitrate}, 1 byte
            btype = 'Constant'
            if 'Average' in self['vbr_method']:
                btype = 'Target'
            elif 'Variable' in self['vbr_method']:
                btype = 'Minimum'
            # bitrate may be modified below after preset is read
            self['bitrate'] = (bin2dec(bytes2bin(frame[pos:pos + 1])), btype)
            log.debug('Lame Bitrate (%s): %s' % (btype, self['bitrate'][0]))
            pos += 1

            # Encoder delays, 3 bytes
            self['encoder_delay'] = bin2dec(bytes2bin(frame[pos:pos + 3])[:12])
            self['encoder_padding'] = bin2dec(
                                        bytes2bin(frame[pos:pos + 3])[12:])
            log.debug('Lame Encoder delay: %s samples' % self['encoder_delay'])
            log.debug('Lame Encoder padding: %s samples' %
                      self['encoder_padding'])
            pos += 3

            # Misc, 1 byte
            sample_freq = bin2dec(bytes2bin(frame[pos:pos + 1])[:2])
            unwise_settings = bin2dec(bytes2bin(frame[pos:pos + 1])[2:3])
            stereo_mode = bin2dec(bytes2bin(frame[pos:pos + 1])[3:6])
            self['noise_shaping'] = bin2dec(bytes2bin(frame[pos:pos + 1])[6:])
            self['sample_freq'] = self.SAMPLE_FREQUENCIES.get(sample_freq,
                                                              'Unknown')
            self['unwise_settings'] = bool(unwise_settings)
            self['stereo_mode'] = self.STEREO_MODES.get(stereo_mode, 'Unknown')
            log.debug('Lame Source Sample Frequency: %s' % self['sample_freq'])
            log.debug('Lame Unwise settings used: %s' % self['unwise_settings'])
            log.debug('Lame Stereo mode: %s' % self['stereo_mode'])
            log.debug('Lame Noise Shaping: %s' % self['noise_shaping'])
            pos += 1

            # MP3 Gain, 1 byte
            sign = bytes2bin(frame[pos:pos + 1])[0]
            gain = bin2dec(bytes2bin(frame[pos:pos + 1])[1:])
            if sign:
                gain *= -1
            self['mp3_gain'] = gain
            db = gain * 1.5
            log.debug('Lame MP3 Gain: %s (%+.1f dB)' % (self['mp3_gain'], db))
            pos += 1

            # Preset and surround info, 2 bytes
            surround = bin2dec(bytes2bin(frame[pos:pos + 2])[2:5])
            preset = bin2dec(bytes2bin(frame[pos:pos + 2])[5:])
            if preset in range(8, 321):
                if self['bitrate'][0] >= 255:
                    # the value from preset is better in this case
                    self['bitrate'] = (preset, btype)
                    log.debug('Lame Bitrate (%s): %s' %
                              (btype, self['bitrate'][0]))
                if 'Average' in self['vbr_method']:
                    preset = 'ABR %s' % preset
                else:
                    preset = 'CBR %s' % preset
            else:
                preset = self.PRESETS.get(preset, preset)
            self['surround_info'] = self.SURROUND_INFO.get(surround, surround)
            self['preset'] = preset
            log.debug('Lame Surround Info: %s' % self['surround_info'])
            log.debug('Lame Preset: %s' % self['preset'])
            pos += 2

            # MusicLength, 4 bytes
            self['music_length'] = bin2dec(bytes2bin(frame[pos:pos + 4]))
            log.debug('Lame Music Length: %s bytes' % self['music_length'])
            pos += 4

            # MusicCRC, 2 bytes
            self['music_crc'] = bin2dec(bytes2bin(frame[pos:pos + 2]))
            log.debug('Lame Music CRC: %04X' % self['music_crc'])
            pos += 2

            # CRC-16 of Info Tag, 2 bytes
            self['infotag_crc'] = lamecrc  # we read this earlier
            log.debug('Lame Info Tag CRC: %04X' % self['infotag_crc'])
            pos += 2
        except IndexError:
            log.warning("Truncated LAME info header, values incomplete.")

    def _parse_encflags(self, flags):
        """Parse encoder flags.

        Returns a tuple containing lists of encoder flags and nogap data in
        human readable format.
        """

        encoder_flags, nogap = [], []

        if not flags:
            return encoder_flags, nogap

        if flags & self.ENCODER_FLAGS['NSPSYTUNE']:
            encoder_flags.append('--nspsytune')
        if flags & self.ENCODER_FLAGS['NSSAFEJOINT']:
            encoder_flags.append('--nssafejoint')

        NEXT = self.ENCODER_FLAGS['NOGAP_NEXT']
        PREV = self.ENCODER_FLAGS['NOGAP_PREV']
        if flags & (NEXT | PREV):
            encoder_flags.append('--nogap')
            if flags & PREV:
                nogap.append('before')
            if flags & NEXT:
                nogap.append('after')
        return encoder_flags, nogap


def lamevercmp(x, y):
    """Compare LAME version strings.

    alpha and beta versions are considered older.
    Versions with sub minor parts or end with 'r' are considered newer.

    :param x: The first version to compare.
    :param y: The second version to compare.
    :returns: Return negative if x<y, zero if x==y, positive if x>y.
    """

    def cmp(a, b):
        # This is Python2's built-in `cmp`, which was removed from Python3
        # And depends on bool - bool yielding the integer -1, 0, 1
        return (a > b) - (a < b)

    x = x.ljust(5)
    y = y.ljust(5)
    if x[:5] == y[:5]:
        return 0
    ret = cmp(x[:4], y[:4])
    if ret:
        return ret
    xmaj, xmin = x.split('.')[:2]
    ymaj, ymin = y.split('.')[:2]
    minparts = ['.']
    # lame 3.96.1 added the use of r in the very short version for post releases
    if (xmaj == '3' and xmin >= '96') or (ymaj == '3' and ymin >= '96'):
        minparts.append('r')
    if x[4] in minparts:
        return 1
    if y[4] in minparts:
        return -1
    if x[4] == ' ':
        return 1
    if y[4] == ' ':
        return -1
    return cmp(x[4], y[4])


#                     MPEG1  MPEG2  MPEG2.5
SAMPLE_FREQ_TABLE = ((44100, 22050, 11025),
                     (48000, 24000, 12000),
                     (32000, 16000, 8000),
                     (None, None, None))

#              V1/L1  V1/L2 V1/L3 V2/L1 V2/L2&L3
BIT_RATE_TABLE = ((0,    0,    0,    0,    0),                          # noqa
                  (32,   32,   32,   32,   8),                          # noqa
                  (64,   48,   40,   48,   16),                         # noqa
                  (96,   56,   48,   56,   24),                         # noqa
                  (128,  64,   56,   64,   32),                         # noqa
                  (160,  80,   64,   80,   40),                         # noqa
                  (192,  96,   80,   96,   48),                         # noqa
                  (224,  112,  96,   112,  56),                         # noqa
                  (256,  128,  112,  128,  64),                         # noqa
                  (288,  160,  128,  144,  80),                         # noqa
                  (320,  192,  160,  160,  96),                         # noqa
                  (352,  224,  192,  176,  112),                        # noqa
                  (384,  256,  224,  192,  128),                        # noqa
                  (416,  320,  256,  224,  144),                        # noqa
                  (448,  384,  320,  256,  160),                        # noqa
                  (None, None, None, None, None))

# Rows 1 and 2 (mpeg 2.x) are only used for those versions *and* VBR.
#                                  L1   L2   L3
SAMPLES_PER_FRAME_TABLE = ((None, 384, 1152, 1152),  # MPEG 1
                           (None, 384, 1152, 576),   # MPEG 2
                           (None, 384, 1152, 576),   # MPEG 2.5
                          )

# Emphasis constants
EMPHASIS_NONE = "None"
EMPHASIS_5015 = "50/15 ms"
EMPHASIS_CCIT = "CCIT J.17"

# Mode constants
MODE_STEREO = "Stereo"
MODE_JOINT_STEREO = "Joint stereo"
MODE_DUAL_CHANNEL_STEREO = "Dual channel stereo"
MODE_MONO = "Mono"

# Xing flag bits
FRAMES_FLAG = 0x0001
BYTES_FLAG = 0x0002
TOC_FLAG = 0x0004
VBR_SCALE_FLAG = 0x0008


def _mp3VersionKey(version):
    """Map mp3 version float to a data structure index.
    1 -> 0, 2 -> 1, 2.5 -> 2
    """
    if version == 2.5:
        key = 2
    else:
        key = int(version - 1)

    if not 0 <= key <= 2:
        raise ValueError(f"Invalid mp3 version key value: {key}")

    return key
