import os
import sys
import logging

import requests
from mutagen.mp3 import MP3, EasyMP3
from mutagen.id3._frames import TIT1
from mutagen.id3._frames import TIT2
from mutagen.id3._frames import USLT
from mutagen.id3._frames import APIC
from bandcamp_dl.utils.unicode_slugify import slugify

if not sys.version_info[:2] == (3, 6):
    import mock
    from bandcamp_dl.utils import requests_patch

from bandcamp_dl.__init__ import __version__

from bandcamp_dl.utils.clean_print import print_clean


class BandcampDownloader:
    def __init__(self, template, directory, overwrite, embed_lyrics, grouping, embed_art, no_slugify, ok_chars,
                 space_char, ascii_only, keep_space, keep_upper, debugging, urls=None):
        """Initialize variables we will need throughout the Class

        :param urls: list of urls
        :param template: filename template
        :param directory: download location
        :param overwrite: if True overwrite existing files
        """
        self.headers = {'User-Agent': f'bandcamp-dl/{__version__} (https://github.com/iheanyi/bandcamp-dl)'}
        self.session = requests.Session()

        if type(urls) is str:
            self.urls = [urls]

        self.urls = urls
        self.template = template
        self.directory = directory
        self.overwrite = overwrite
        self.grouping = grouping
        self.embed_art = embed_art
        self.embed_lyrics = embed_lyrics
        self.no_slugify = no_slugify
        self.ok_chars = ok_chars
        self.space_char = space_char
        self.ascii_only = ascii_only
        self.keep_space = keep_space
        self.keep_upper = keep_upper
        self.debugging = debugging

    def start(self, album: dict):
        """Start album download process

        :param album: album dict
        """
        if self.debugging:
            logging.basicConfig(level=logging.DEBUG)

        if album['full'] is not True:
            choice = input("Track list incomplete, some tracks may be private, download anyway? (yes/no): ").lower()
            if choice == "yes" or choice == "y":
                print("Starting download process.")
                self.download_album(album)
            else:
                print("Cancelling download process.")
                return None
        else:
            self.download_album(album)

    def template_to_path(self, track: dict, ascii_only, ok_chars, space_char, keep_space, keep_upper) -> str:
        """Create valid filepath based on template

        :param track: track metadata
        :param ok_chars: optional chars to allow
        :param ascii_only: allow only ascii chars in filename
        :param keep_space: retain whitespace in filename
        :param keep_upper: retain uppercase chars in filename
        :param space_char: char to use in place of spaces
        :return: filepath
        """
        logging.debug(" Generating filepath/trackname..")
        path = self.template

        def slugify_preset(content):
            slugged = slugify(content, ok=ok_chars, only_ascii=ascii_only, spaces=keep_space, lower=not keep_upper,
                              space_replacement=space_char)
            return slugged

        if self.no_slugify:
            path = path.replace("%{artist}", track['artist'])
            path = path.replace("%{album}", track['album'])
            path = path.replace("%{title}", track['title'])
            path = path.replace("%{date}", track['date'])
            path = path.replace("%{label}", track['label'])
        else:
            path = path.replace("%{artist}", slugify_preset(track['artist']))
            path = path.replace("%{album}", slugify_preset(track['album']))
            path = path.replace("%{title}", slugify_preset(track['title']))
            path = path.replace("%{date}", slugify_preset(track['date']))
            path = path.replace("%{label}", slugify_preset(track['label']))

        if track['track'] == "None":
            path = path.replace("%{track}", "Single")
        else:
            path = path.replace("%{track}", str(track['track']).zfill(2))

        # Double check that the old issue in Python 2 with unicode strings isn't a problem with f-strings
        # Otherwise find an alternative to u'STRING'
        path = f"{self.directory}/{path}.mp3"

        logging.debug(" filepath/trackname generated..")
        logging.debug(f"\n\tPath: {path}")
        return path

    @staticmethod
    def create_directory(filename: str) -> str:
        """Create directory based on filename if it doesn't exist

        :param filename: full filename
        :return: directory path
        """
        directory = os.path.dirname(filename)
        logging.debug(f" Directory:\n\t{directory}")
        logging.debug(" Directory doesn't exist, creating..")
        if not os.path.exists(directory):
            os.makedirs(directory)

        return directory

    def download_album(self, album: dict) -> bool:
        """Download all MP3 files in the album

        :param album: album dict
        :return: True if successful
        """
        for track_index, track in enumerate(album['tracks']):
            track_meta = {
                "artist": album['artist'],
                "label": album['label'],
                "album": album['title'],
                "title": track['title'],
                "track": track['track'],
                # TODO: Find out why the 'lyrics' key seems to vanish.
                "lyrics": track.get('lyrics', "lyrics unavailable"),
                "date": album['date']
            }

            self.num_tracks = len(album['tracks'])
            self.track_num = track_index + 1

            filepath = self.template_to_path(track_meta, self.ascii_only, self.ok_chars, self.space_char,
                                             self.keep_space, self.keep_upper) + ".tmp"
            filename = filepath.rsplit('/', 1)[1]
            dirname = self.create_directory(filepath)

            logging.debug(f" Current file:\n\t{filepath}")

            if album['art'] and not os.path.exists(dirname + "/cover.jpg"):
                try:
                    with open(dirname + "/cover.jpg", "wb") as f:
                        r = self.session.get(album['art'], headers=self.headers)
                        f.write(r.content)
                    self.album_art = dirname + "/cover.jpg"
                except Exception as e:
                    print(e)
                    print("Couldn't download album art.")

            attempts = 0
            skip = False

            while True:
                try:
                    if not sys.version_info[:2] == (3, 6):
                        with mock.patch('http.client.parse_headers', requests_patch.parse_headers):
                            r = self.session.get(track['url'], headers=self.headers, stream=True)
                    else:
                        r = self.session.get(track['url'], headers=self.headers, stream=True)
                    file_length = int(r.headers.get('content-length', 0))
                    total = int(file_length / 100)
                    # If file exists and is still a tmp file skip downloading and encode
                    if os.path.exists(filepath):
                        self.write_id3_tags(filepath, track_meta)
                        # Set skip to True so that we don't try encoding again
                        skip = True
                        # break out of the try/except and move on to the next file
                        break
                    elif os.path.exists(filepath[:-4]) and self.overwrite is not True:
                        print(f"File: {filename[:-4]} already exists and is complete, skipping..")
                        skip = True
                        break
                    with open(filepath, "wb") as f:
                        if file_length is None:
                            f.write(r.content)
                        else:
                            dl = 0
                            for data in r.iter_content(chunk_size=total):
                                dl += len(data)
                                f.write(data)
                                if not self.debugging:
                                    done = int(50 * dl / file_length)
                                    print_clean(
                                        f'\r({self.track_num}/{self.num_tracks}) [{"=" * done}{" " * (50 - done)}] :: Downloading: {filename[:-8]}')
                    local_size = os.path.getsize(filepath)
                    # if the local filesize before encoding doesn't match the remote filesize redownload
                    if local_size != file_length and attempts != 3:
                        print(f"{filename} is incomplete, retrying..")
                        continue
                    # if the maximum number of retry attempts is reached give up and move on
                    elif attempts == 3:
                        print("Maximum retries reached.. skipping.")
                        # Clean up incomplete file
                        os.remove(filepath)
                        break
                    # if all is well continue the download process for the rest of the tracks
                    else:
                        break
                except Exception as e:
                    print(e)
                    print("Downloading failed..")
                    return False
            if skip is False:
                self.write_id3_tags(filepath, track_meta)

        if os.path.isfile(f"{self.directory}/{__version__}.not.finished"):
            os.remove(f"{self.directory}/{__version__}.not.finished")

        # Remove album art image as it is embedded
        if self.embed_art:
            os.remove(self.album_art)

        return True

    def write_id3_tags(self, filepath: str, meta: dict):
        """Write metadata to the MP3 file

        :param filepath: name of mp3 file
        :param meta: dict of track metadata
        """
        logging.debug(" Encoding process starting..")

        filename = filepath.rsplit('/', 1)[1][:-8]

        if not self.debugging:
            print_clean(f'\r({self.track_num}/{self.num_tracks}) [{"=" * 50}] :: Encoding: {filename}')

        audio = MP3(filepath)
        audio.delete()
        audio["TIT2"] = TIT2(encoding=3, text=["title"])
        audio.save(filename=None, v1=2)

        audio = MP3(filepath)
        if self.grouping and 'label' in meta:
            audio["TIT1"] = TIT1(encoding=3, text=meta["label"])

        if self.embed_lyrics:
            audio["USLT"] = USLT(encoding=3, lang='eng', desc='', text=meta['lyrics'])

        if self.embed_art:
            with open(self.album_art, 'rb') as cover_img:
                cover_bytes = cover_img.read()
                audio["APIC"] = APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=cover_bytes)
        audio.save()

        audio = EasyMP3(filepath)
        audio["tracknumber"] = meta['track']
        audio["title"] = meta["title"]
        audio["artist"] = meta['artist']
        audio["album"] = meta['album']
        audio["date"] = meta["date"]
        audio.save()

        logging.debug(" Encoding process finished..")
        logging.debug(f" Renaming:\n\t{filepath} -to-> {filepath[:-4]}")

        try:
            os.rename(filepath, filepath[:-4])
        except WindowsError:
            os.remove(filepath[:-4])
            os.rename(filepath, filepath[:-4])

        if not self.debugging:
            print_clean(f'\r({self.track_num}/{self.num_tracks}) [{"=" * 50}] :: Finished: {filename}')
