# -*- coding: utf-8 -*-

import subprocess
import time
import re
import fcntl
import os
import hashlib
import math
from pydub import AudioSegment
import mutagen
import mutagen.mp3
import mutagen.mp4
import mutagen.easymp4
import mutagen.monkeysaudio
import mutagen.asf
import mutagen.flac
import mutagen.wavpack
import chromaprint
from collections import namedtuple
from PIL import Image
from bard.config import config
from bard.terminalcolors import TerminalColors
from bard import bard_audiofile
from pydub.utils import db_to_float
import itertools
import io
# import tempfile

ImageDataTuple = namedtuple('ImageDataTuple', ['image', 'data'])

DecodedAudioPropertiesTuple = namedtuple('DecodedAudioPropertiesTuple',
                                         ['codec', 'format',
                                          'container_duration',
                                          'decoded_duration',
                                          'container_bitrate',
                                          'stream_bitrate',
                                          'stream_sample_format',
                                          'stream_bytes_per_sample',
                                          'stream_bits_per_raw_sample',
                                          'decoded_sample_format',
                                          'decoded_bytes_per_sample',
                                          'is_planar',
                                          'samples',
                                          'channels',
                                          'sample_rate',
                                          'library_versions',
                                          'messages'])


class DecodeMessageRecord(namedtuple('DecodeMessageRecord',
                                     ['time_position', 'level', 'msg'])):
    __slots__ = ()

    @staticmethod
    def level_to_string(level):
        mapping = {0: 'Critical',
                   1: 'Fatal',
                   2: 'Error',
                   3: 'Warning',
                   4: 'Info',
                   5: 'Verbose',
                   6: 'Debug',
                   7: 'Trace'}
        try:
            return mapping[level]
        except KeyError:
            return 'Unknown level'

    @staticmethod
    def color_for_level(level):
        if level <= 2:
            return TerminalColors.Error
        if level <= 4:
            return TerminalColors.Highlight
        return ''

    def level_as_string(self):
        return self.level_to_string(self.level)

    def level_color(self):
        return self.color_for_level(self.level)

    def __str__(self):
        return f'%.3f (%s): %s' % (self.time_position, self.level_color() +
                                   self.level_as_string() +
                                   TerminalColors.ENDC, self.msg)


def detect_silence_at_beginning_and_end(audio_segment, min_silence_len=1000,
                                        silence_thresh=-16, seek_step=1):
    seg_len = len(audio_segment)

    # you can't have a silent portion of a sound that is longer than the sound
    if seg_len < min_silence_len:
        return []

    # convert silence threshold to a float value (so we can compare it to rms)
    silence_thresh = (db_to_float(silence_thresh) *
                      audio_segment.max_possible_amplitude)

    # check successive (1 sec by default) chunk of sound for silence
    # try a chunk at every "seek step" (or every chunk for a seek step == 1)
    last_slice_start = seg_len - min_silence_len
    slice_starts = range(0, last_slice_start + 1, seek_step)

    # guarantee last_slice_start is included in the range
    # to make sure the last portion of the audio is seached
    if last_slice_start % seek_step:
        slice_starts = itertools.chain(slice_starts, [last_slice_start])

    song_start = 0
    song_end = seg_len
    for i in slice_starts:
        audio_slice = audio_segment[i:i + min_silence_len]
        if audio_slice.rms > silence_thresh:
            if i == 0:
                song_start = 0
            else:
                song_start = i + min_silence_len
            break
    else:
        return [[0, 0], [song_end, song_end]]

    for i in reversed(slice_starts):
        audio_slice = audio_segment[i:i + min_silence_len]
        if audio_slice.rms > silence_thresh:
            if song_end == slice_starts[-1]:
                song_end = seg_len
            else:
                song_end = i
            break

    return [[0, song_start], [song_end, seg_len]]


def fingerprint_AudioSegment(audio_segment, maxlength=120000):
    """Fingerprint audio data given a pydub AudioSegment object.

    Raises a FingerprintGenerationError if anything goes wrong.
    Based on acoustid.py's fingerprint function.
    """
    maxlength /= 1000
    endposition = audio_segment.frame_rate * audio_segment.channels * maxlength
    try:
        fper = chromaprint.Fingerprinter()

        fper.start(audio_segment.frame_rate, audio_segment.channels)

        position = 0  # Samples of audio fed to the fingerprinter.
        for start in range(0, len(audio_segment.raw_data), 4096):
            block = audio_segment.raw_data[start:start + 4096]
            fper.feed(block)
            position += len(block) // 2  # 2 bytes/sample.
            if position >= endposition:
                break
        return fper.finish()
    except chromaprint.FingerprintError:
        raise chromaprint.FingerprintGenerationError("fingerprint calculation "
                                                     "failed")


def printSongsInfo(song1, song2,
                   useColors=(TerminalColors.First, TerminalColors.Second)):
    song1.calculateCompleteness()
    song2.calculateCompleteness()

    print(useColors[0] + (song1.path() or song1.description()) +
          TerminalColors.ENDC)
    print(useColors[1] + (song2.path() or song2.description()) +
          TerminalColors.ENDC)

    song1.loadMetadataInfo()
    song2.loadMetadataInfo()
    printDictsDiff(song1.metadata, song2.metadata, forcePrint=True)

    print('Completeness: %s%d%s <-> %s%d%s)' % (
          useColors[0], song1.completeness, TerminalColors.ENDC,
          useColors[1], song2.completeness, TerminalColors.ENDC))

    if song1.metadata == song2.metadata:
        print('Songs have identical metadata!')

    printPropertiesDiff(song1, song2, forcePrint=True)


def loadImageFromData(data):
    if not data:
        return None
    image = Image.open(io.BytesIO(data))
    return ImageDataTuple(image, data)


def loadImageFromAPEBinaryValue(obj):
    data = obj.value[obj.value.find(b'\x00') + 1:]
    image = Image.open(io.BytesIO(data))
    return ImageDataTuple(image, data)


def loadImageFromASFByteArrayAttribute(obj):
    try:
        data = obj.value[obj.value.find(b'\x00\x00\x00\x00\x00') + 5:]
        image = Image.open(io.BytesIO(data))
    except OSError as e:
        print("Error reading image from ASFByteArrayAttribute (%s):" % obj, e)
        raise
    return ImageDataTuple(image, data)


def extractAnyImageFromList(values):
    expandedList = [(key, val) for key, val in values.items()
                    if not isinstance(val, list)]
    for key, value in values.items():
        if key in ['WM/MCDI', 'WM/UserWebURL', 'CT_Custom', 'CT_MY_RATING']:
            continue

        if isinstance(value, list):
            for val in value:
                expandedList.append((key, val))
        else:
            expandedList.append((key, value))

    for key, value in expandedList:
        if isinstance(value, mutagen.apev2.APEBinaryValue):
            return loadImageFromAPEBinaryValue(value)

        if isinstance(value, mutagen.asf._attrs.ASFByteArrayAttribute):
            return loadImageFromASFByteArrayAttribute(value)

        if isinstance(value, mutagen.mp4.MP4Cover):
            return loadImageFromData(value)

        if isinstance(value, mutagen.id3.APIC) and value.data:
            return loadImageFromData(value.data)

    return None


def extractFrontCover(mutagenFile):
    for pic in getattr(mutagenFile, 'pictures', []):
        if pic.type == mutagen.id3.PictureType.COVER_FRONT:
            image = Image.open(io.BytesIO(pic.data))
            return ImageDataTuple(image, pic.data)

    if isinstance(getattr(mutagenFile, 'Cover Art (Front)', None),
                  mutagen.apev2.APEBinaryValue):
        return loadImageFromAPEBinaryValue(mutagenFile['Cover Art (Front)'])

    # print(mutagenFile)
    if ('WM/Picture' in mutagenFile and
       isinstance(mutagenFile['WM/Picture'][0],
                  mutagen.asf._attrs.ASFByteArrayAttribute)):
        return loadImageFromASFByteArrayAttribute(mutagenFile['WM/Picture'][0])

    if 'covr' in mutagenFile and isinstance(mutagenFile['covr'], list):
        return loadImageFromData(mutagenFile['covr'][0])

    if 'APIC:' in mutagenFile and isinstance(mutagenFile['APIC:'],
                                             mutagen.id3.APIC):
        return loadImageFromData(mutagenFile['APIC:'].data)

    return extractAnyImageFromList(mutagenFile)


def fixAPETextValuesWithEmptyMultipleValues(mutagenFile):
    for k, v in mutagenFile.items():
        if isinstance(v, mutagen.apev2.APETextValue) and v.value[-1] == '\x00':
            mutagenFile[k] = v.value[:-1]


def fixBrokenImages(mutagenFile):
    for k, v in mutagenFile.items():
        try:
            extractAnyImageFromList({k: v})
        except IOError:
            del mutagenFile[k]
            # mutagenFile['TPE1'] = mutagen.id3.TPE1(mutagen.id3.Encoding.UTF8,
            #                                        'test')


def printDictsDiff(dict1, dict2, forcePrint=False):
    # Calculate changes
    removedKeys = [x for x in dict1.keys() if x not in dict2.keys()]

    def is_changed(x):
        try:
            return (x in dict1 and dict1.get(x, None) != dict2.get(x, None))
        except ValueError:
            return True

    def is_in_dict1(x):
        try:
            return x in dict1.keys()
        except ValueError:
            return False

    changedKeys = [x for x in dict2.keys() if is_changed(x)]

    newKeys = [x for x in dict2.keys() if not is_in_dict1(x)]

#    changedKeys = [x for x in dict2.keys()
#                   if x in dict1 and dict1.get(x, None) != dict2.get(x, None)]
#    newKeys = [x for x in dict2.keys() if x not in dict1.keys()]

    if not forcePrint and not removedKeys and not changedKeys and not newKeys:
        return False

    allKeys = list(dict1.keys()) + [x for x in dict2.keys()
                                    if x not in dict1.keys()]
    allKeys.sort()
    # print('removed:', removedKeys)
    # print('changed:', changedKeys)
    # print('new    :', newKeys)

    # print(dict1.get('COMM::eng', None))
    # print(dict2.get('COMM::eng', None))
    def use_str_or_repr(x):
        return {True: str, False: repr}[all(hasattr(z, 'text') for z in x)]

    for k in sorted(allKeys):
        if k in changedKeys:
            try:
                str_repr = use_str_or_repr((dict1[k], dict2[k]))
            except ValueError:
                print(f'Error getting value for {k}')
                str_repr = '~~~'
            else:
                print(str(k), ':', TerminalColors.Highlight,
                      str_repr(dict1[k])[:100], TerminalColors.ENDC,
                      ' -> ', TerminalColors.Highlight,
                      str_repr(dict2[k])[:100], TerminalColors.ENDC)
        elif k in removedKeys:
            str_repr = use_str_or_repr((dict1[k],))
            print(str(k), ':', TerminalColors.First, str_repr(dict1[k])[:200],
                  TerminalColors.ENDC)
        elif k in newKeys:
            str_repr = use_str_or_repr((dict2[k],))
            print(str(k), ':', TerminalColors.Second, str_repr(dict2[k])[:200],
                  TerminalColors.ENDC)
        else:
            str_repr = use_str_or_repr((dict1[k],))
            print(str(k), ':', str_repr(dict1[k])[:200])

    return True


def formatLength(seconds):
    hours, remainder = divmod(seconds, 3600)
    minutes, remseconds = divmod(remainder, 60)
    string = ''
    if hours:
        minformat = '%02d'
    else:
        minformat = '%d'

    if hours or minutes:
        minseconds = '%02d'
    else:
        minseconds = '%d'

    string = ':'.join([y % x for x, y in [(hours, '%d'), (minutes, minformat),
                                          (remseconds, minseconds)] if x])
    miliseconds = math.modf(remseconds)[0]
    if miliseconds:
        string += '{0:.3f}'.format(miliseconds)[1:]
    return string


def printPropertiesDiff(song1, song2, forcePrint=False):
    properties = [('', '_format', str),
                  (' s', 'length', formatLength),
                  (' bits/s', 'bitrate', str),
                  (' bits/sample', 'bits_per_sample', str),
                  (' channels', 'channels', str),
                  (' Hz', 'sample_rate', str)]
    values1 = []
    values2 = []
    for suffix, prop, propformatter in properties:
        try:
            val1 = getattr(song1.metadata.info, prop)
        except AttributeError:
            val1 = getattr(song1, prop)
        if callable(val1):
            val1 = val1()
        try:
            val2 = getattr(song2.metadata.info, prop)
        except AttributeError:
            val2 = getattr(song2, prop)
        if callable(val2):
            val2 = val2()
        if val1 and val2 and val1 == val2:
            values1.append(propformatter(val1) + suffix)
            values2.append(propformatter(val2) + suffix)
            continue
        if not val1:
            values1.append('-' + suffix)
        else:
            values1.append(TerminalColors.First + propformatter(val1) +
                           TerminalColors.ENDC + suffix)

        if not val2:
            values2.append('-' + suffix)
        else:
            values2.append(TerminalColors.Second + propformatter(val2) +
                           TerminalColors.ENDC + suffix)
    print('Properties: ' + ', '.join(values1))
    print('Properties: ' + ', '.join(values2))


def colorizeAll(color, text):
    return color + text + TerminalColors.ENDC


def colorizeTime(color, text):
    pos = text.find('.')
    if pos == -1:
        return colorizeAll(color, text)
    return color + text[:pos] + TerminalColors.ENDC + text[pos:]


def colorizeBps(color, text):
    pos = text.find('.')
    if pos == -1:
        return colorizeAll(color, text)
    pos -= 3
    return color + text[:pos] + TerminalColors.ENDC + text[pos:]


def getPropertiesAsString(song, colors={}):
    properties = [('', '_format', str, colorizeAll),
                  (' s', 'length', formatLength, colorizeTime),
                  (' s (w/o silences)', 'durationWithoutSilences',
                   formatLength, colorizeTime),
                  (' bits/s', 'bitrate', str, colorizeBps),
                  (' bits/sample', 'bits_per_sample', str, colorizeAll),
                  (' channels', 'channels', str, colorizeAll),
                  (' Hz', 'sample_rate', str, colorizeAll)]
    values = []
    for suffix, prop, propformatter, propcolorizer in properties:
        try:
            color = colors[prop]
        except KeyError:
            color = TerminalColors.Highlight
        try:
            val = getattr(song.metadata.info, prop)
        except AttributeError:
            val = getattr(song, prop)
        if callable(val):
            val = val()
        if not val:
            values.append(color + '-' + TerminalColors.ENDC + suffix)
        else:
            values.append(propcolorizer(color, propformatter(val)) + suffix)
    return ', '.join(values)


def printProperties(song):
    print('Properties: ' + getPropertiesAsString(song))


def fixTags(mutagenFile):
    # Save original values
    originalValues = {}
    originalValues.update(mutagenFile)

    # Apply fixes
    fixAPETextValuesWithEmptyMultipleValues(mutagenFile)
    fixBrokenImages(mutagenFile)

    # Print changes
    if not printDictsDiff(originalValues, mutagenFile):
        print('Nothing to be done for %s' % mutagenFile.filename)
        return False

    # print('Before:')
    # for k, v in originalValues.items():
    #     msg = '%s : %s' % (str(k), repr(v)[:100])
    #     if k in changedKeys:
    #         print(TerminalColors.Highlight + msg + TerminalColors.ENDC)
    #     elif k in removedKeys:
    #         print(TerminalColors.First + msg + TerminalColors.ENDC)
    #     else:
    #         print(msg)
    #
    #  print('')
    # print('After:')
    # for k, v in mutagenFile.items():
    #     msg = '%s : %s' % (str(k), repr(v)[:100])
    #     if k in changedKeys:
    #         print(TerminalColors.Highlight + msg + TerminalColors.ENDC)
    #     else:
    #         print(msg)
    # print('')

    key = input('Do you want to write the changes? (y/n) ')
    if key == 'y':
        mutagenFile.save()

    return True


def md5(fname):
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096 * 1024), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()


def md5FromData(data):
    hash_md5 = hashlib.md5()
    hash_md5.update(data)
    return hash_md5.hexdigest()


def calculateFileSHA256(filething):
    def calculate(fileobj):
        hash_sha256 = hashlib.sha256()
        for chunk in iter(lambda: fileobj.read(4096 * 1024), b""):
            hash_sha256.update(chunk)
        return hash_sha256.hexdigest()

    if isinstance(filething, str):
        with open(filething, "rb") as f:
            sha256 = calculate(f)
    else:
        f = filething
        filething.seek(0)
        sha256 = calculate(f)

    return sha256


def calculateSHA256(filelike):
    hash_sha256 = hashlib.sha256()
    for chunk in iter(lambda: filelike.read(4096 * 1024), b""):
        hash_sha256.update(chunk)
    return hash_sha256.hexdigest()


def calculateSHA256_data(data):
    hash_sha256 = hashlib.sha256()
    hash_sha256.update(data)
    return hash_sha256.hexdigest()


def removeAllTagsFromPath(path):
    # subprocess.check_output(['id3v2', '--delete-all', path])
    mutagenFile = mutagen.File(path)
    print(type(mutagenFile), path)

    if isinstance(mutagenFile, mutagen.flac.FLAC):
        mutagenFile.clear_pictures()
        mutagenFile.delete(path)
        # mutagenFile.save(path, deleteid3=True, padding=lambda x: 0)
        return
    elif isinstance(mutagenFile, mutagen.id3.ID3FileType):
        mutagenFile.delete(path)
        return
    elif isinstance(mutagenFile, mutagen.apev2.APEv2File):
        mutagenFile.delete(path)
        return

    mutagenFile.delete(path)


def removeAllTags(filelike, recurse=True):
    try:
        filelike.seek(0)
        id3 = mutagen.id3.ID3(filelike)
    except mutagen.id3._util.ID3NoHeaderError:
        pass
    else:
        filelike.seek(0)
        id3.delete(filelike)

    filelike.seek(0)
    mutagenFile = mutagen.File(filelike)
    # print(type(mutagenFile), filelike.name)

    if isinstance(mutagenFile, mutagen.flac.FLAC) and mutagenFile.pictures:
        mutagenFile.clear_pictures()
        filelike.seek(0)
        mutagenFile.save(filelike, padding=lambda x: 0)

    filelike.seek(0)
    if mutagenFile:
        mutagenFile.delete(filelike)


def calculateAudioTrackSHA256(path, tmpdir='/tmp'):
    # extension=path[path.rfind('.'):]
    # (fn, tmpfilename) = tempfile.mkstemp(suffix=extension, dir=tmpdir)

    filelike = io.BytesIO(open(path, 'rb').read())
    filelike.name = path
    # filelike.filename = path
    # print(path, tmpfilename)
    # try:
    removeAllTags(filelike)
    # shutil.copyfile(path, tmpfilename)
    # removeAllTags(tmpfilename)
    # if os.path.getsize(tmpfilename) >= os.path.getsize(path):
    #     print('Error removing tags from %s (%d >= %d)' % \
    #           (path, os.path.getsize(tmpfilename), os.path.getsize(path)))
    print(len(filelike.getvalue()))
    # open('/tmp/output9.mp3','wb').write(filelike.getvalue())
    filelike.seek(0)
    return calculateSHA256(filelike)
    # finally:
    #     os.close(fn)
    #     os.unlink(tmpfilename)

    # return None


def calculateAudioTrackSHA256_pydub(path):
    audio_segment = AudioSegment.from_file(path)
    audioSha256sum = calculateSHA256_data(audio_segment.raw_data)
    # print('size:', len(audio_segment.raw_data))
    return audioSha256sum


def DecodedAudioPropertiesTupleFromDict(properties):
    messages = []
    for time_position, level, message in properties['messages']:
        messages.append(DecodeMessageRecord(time_position, level,
                                            message.decode("utf-8", "ignore")))
    return DecodedAudioPropertiesTuple(**properties)._replace(
        messages=messages)


def decodeAudio(filething):
    if hasattr(filething, 'seek'):
        filething.seek(0)
        filecontents = filething.read()
        data, properties = bard_audiofile.decode(data=filecontents)
    else:
        data, properties = bard_audiofile.decode(path=filething)

    if config['enable_internal_checks']:
        FILES_PYDUB_CANT_DECODE_RIGHT = \
            ['/mnt/DD8/media/mp3/id13/k3/software_libre-hq.ogg']
        if hasattr(filething, 'seek'):
            filething.seek(0)
        audio_segment = AudioSegment.from_file(filething)
        if (audio_segment.raw_data != data and
                filething not in FILES_PYDUB_CANT_DECODE_RIGHT):
            with open('/tmp/decoded-song-pydub.raw', 'wb') as f:
                f.write(audio_segment.raw_data)
            with open('/tmp/decoded-song-bard_audiofile.raw', 'wb') as f:
                f.write(data)
            raise Exception('DECODED AUDIO IS DIFFERENT BETWEEN '
                            'BARD_AUDIOFILE AND PYDUB')
        print('bard_audiofile/pydub decode check ' +
              TerminalColors.Ok + 'OK' + TerminalColors.ENDC)
    return data, DecodedAudioPropertiesTupleFromDict(properties)


class CaptureFD:
    def __init__(self, fd, max_size=65535):
        """Create a CaptureFD context manager."""
        self.redirected_fd = fd
        self.max_size = max_size

    def __enter__(self):
        self.fd_copy = os.dup(self.redirected_fd)
        self.piper, self.pipew = os.pipe()
        # os.close(self.redirected_fd)
        os.dup2(self.pipew, self.redirected_fd)
        # self.temp_fd_holder = os.open('/dev/null', os.O_WRONLY)

    def __exit__(self, exc_type, exc_value, traceback):
        flags = fcntl.fcntl(self.piper, fcntl.F_GETFL)
        flags = flags | os.O_NONBLOCK
        fcntl.fcntl(self.piper, fcntl.F_SETFL, flags)

        try:
            self.output = os.read(self.piper, self.max_size)
        except BlockingIOError:
            self.output = None
        # os.close(self.temp_fd_holder)
        os.dup2(self.fd_copy, self.redirected_fd)
        os.close(self.fd_copy)
        os.close(self.pipew)
        os.close(self.piper)


def audioSamplesFromAudioFile(path):
    decode_errors = []
    captureStdErr = CaptureFD(2)
    import av
    # import contextlib
    # captureStdErr = contextlib.nullcontext()
    with captureStdErr:
        container = av.open(path)
        a_stream = container.streams.get(audio=0)[0]

        format_name = a_stream.format.name

        if (format_name == 'fltp' and
                a_stream.codec_context.name in ('mp3float', 'aac')):
            format_name = 's16'
        else:
            format_name = 's' + str(a_stream.format.bits)

        audio_format = av.audio.format.AudioFormat(format_name)
        audio_layout = av.audio.layout.AudioLayout(a_stream.channels)
        sample_rate = a_stream.codec_context.sample_rate
        resampler = av.audio.resampler.AudioResampler(audio_format,
                                                      audio_layout,
                                                      sample_rate)

        # iprop = InputAudioPropertiesTuple(codec=a_stream.codec.name,
        #                                   format_name=a_stream.format.name,
        #                                   duration=float(a_stream.duration *
        #                                                  a_stream.time_base),
        #                                   stream_bitrate=a_stream.bit_rate,
        #                                   container_bitrate=container.bit_rate,
        #                                   is_valid=True,
        #                                   decode_errors=None,
        #                                   decode_messages=None)

        # prop = AudioPropertiesTuple(channels=a_stream.channels,
        #                             sample_rate=a_stream.sample_rate,
        #                             bits_per_sample=audio_format.bits,
        #                             bytes_per_sample=audio_format.bytes)

        bytespersample = a_stream.channels * audio_format.bytes
        outputbytes = bytearray(b'')
        # frame_generator = container.decode(audio=0)

        demuxer = container.demux(a_stream)

        while True:
            try:
                packet = next(demuxer)
            except av.AVError as exc:
                decode_errors.append(str(exc))
                print('error demuxing', exc)
                continue
            except StopIteration:
                break

            try:
                frames = packet.decode()
            except av.AVError as exc:
                decode_errors.append(str(exc))
                print('error decoding', exc)
                import pdb
                pdb.set_trace()
                continue
            for frame in frames:
                frame.pts = None
                p = resampler.resample(frame)
                for x in p.planes:
                    outputbytes.extend(x.to_bytes()[:bytespersample *
                                                    p.samples])

    messages = None
    if captureStdErr.output:
        messages = captureStdErr.output.decode('utf-8').strip('\n')

    decode_errors = '\n'.join(decode_errors).strip('\n') or None
    # iprop = iprop._replace(is_valid=bool(decode_errors),
    #                       decode_messages=messages,
    #                       decode_errors=decode_errors)

    return bytes(outputbytes), (None, None)  # (iprop, prop)


def calculateAudioTrackSHA256_pyav(path):
    data, properties = audioSamplesFromAudioFile(path)
    audioSha256sum = calculateSHA256_data(data)
    # print('size:', len(audio_segment.raw_data))
    if config['enable_internal_checks']:
        if hasattr(path, 'seek'):
            path.seek(0)
        audio_segment = AudioSegment.from_file(path)
        pydubAudioSha256sum = calculateSHA256_data(audio_segment.raw_data)
        if audio_segment.raw_data != data or \
                pydubAudioSha256sum != audioSha256sum:
            raise Exception('SHA256sum IS DIFFERENT BETWEEN PYAV AND PYDUB')
        print('pyav/pydub decode check ' +
              TerminalColors.Ok + 'OK' + TerminalColors.ENDC)
    return audioSha256sum, data, properties


def audioSegmentFromDataProperties(data, properties):
    return AudioSegment(data=data,
                        sample_width=properties.decoded_bytes_per_sample,
                        frame_rate=properties.sample_rate,
                        channels=properties.channels)

# def calculateAudioTrackSHA256_audioread(path):
#    hash_sha256 = hashlib.sha256()
#    with audioread.audio_open(path) as audiofile:
#        c = 0
#        for block in audiofile:
#            c += len(block)
#            hash_sha256.update(block)
#        print('size:', c)
#    return hash_sha256.hexdigest()


def windowsList():
    process = subprocess.run(['wmctrl', '-l'], stdout=subprocess.PIPE)
    lines = [x.split(maxsplit=3) for x in
             process.stdout.decode('utf-8').split('\n') if x]
    return [(x[0], x[3]) for x in lines]


def waitForWindowToOpen(title):
    while title not in [x[1] for x in windowsList()]:
        time.sleep(0.5)


def waitForWindowToClose(title):
    while title in [x[1] for x in windowsList()]:
        time.sleep(0.5)


def analyzeAudio(cmd, path):
    if cmd not in ['spek', 'audacity']:
        return None

    command = [cmd, path]
    process = subprocess.Popen(command, stdout=subprocess.DEVNULL,
                               stderr=subprocess.DEVNULL)
    return process


def manualAudioCmp(path1, path2, useColors=None):
    proc1 = analyzeAudio('spek', path1)
    proc2 = analyzeAudio('spek', path2)
    otherAction = ('a', '(A)udacity')
    omsg = 'Choose the preferred option (%s1%s/%s2%s/0 (equal)'
    if useColors:
        omsg = omsg % (useColors[0], TerminalColors.ENDC,
                       useColors[1], TerminalColors.ENDC)
    else:
        omsg = omsg % ('', '', '', '')

    omsg += '/%s/(Q)uit):'

    msg = omsg % otherAction[1]

    while True:
        option = input(msg).lower()
        if option == '1':
            r = -1
            break
        elif option == '2':
            r = 1
            break
        elif option == '0':
            r = 0
            break
        elif option == 'q':
            r = None
            break
        elif option == otherAction[0]:
            proc1.terminate()
            proc2.terminate()
            if option == 'a':
                proc1 = analyzeAudio('audacity', path1)
                waitForWindowToOpen('.aup')
                time.sleep(1)
                waitForWindowToClose('Recuperación automática')
                subprocess.run(['wmctrl', '-r', '.aup', '-N', 'Song 1'])
                proc2 = analyzeAudio('audacity', path2)
                waitForWindowToOpen('.aup')
                subprocess.run(['wmctrl', '-r', '.aup', '-N', 'Song 2'])
                otherAction = ('s', '(S)pek')
            elif option == 's':
                proc1 = analyzeAudio('spek', path1)
                proc2 = analyzeAudio('spek', path2)
                otherAction = ('a', '(A)udacity')
            msg = omsg % otherAction[1]

    proc1.terminate()
    proc2.terminate()

    return r


def simple_find_matching_square_bracket(txt, initial):
    count = 0
    for idx, c in enumerate(txt[initial:]):
        if c == '[':
            count += 1
        elif c == ']':
            count -= 1
            if count == 0:
                return idx + initial

    return None


def printableLen(text):
    """Return length of printable characters in string."""
    strip_ANSI_pat = re.compile(r"""\x1b\[[;\d]*[A-Za-z]""", re.VERBOSE)
    try:
        return len(strip_ANSI_pat.sub("", text))
    except TypeError:
        print(text, type(text))
        raise


def alignColumns(lines, col_alignments=None):
    """Align columns of text.

    lines is a list containing lists of columns. The function returns a list
    of strings where the respective columns have been aligned. col_alignments
    can be passed as a list of boolean values for every column which specify
    if the column should be left aligned (True) or right aligned (False).
    """
    aligned = []
    if not lines:
        return []
    maxlengths = [max([printableLen(y) for y in x]) for x in zip(*lines)]
    maxlengths[-1] = 0

    if not col_alignments:
        col_alignments = [True for x in maxlengths]

    for cols in lines:
        newline = ''
        for col, maxlength, alignleft in zip(cols, maxlengths, col_alignments):
            num_spaces = maxlength - printableLen(col)
            if alignleft:
                newline += col + ' ' * (num_spaces + 1)
            else:
                newline += ' ' * num_spaces + col + ' '
        aligned.append(newline)
    return aligned


def cutStringAtMaxBytesLength(s, length):
    s = s[:length]
    while len(s.encode('utf-8')) > length:
        s = s[:-1]
    return s