import logging
import os
import platform
import requests
import shutil
import time
import tempfile
import traceback
import unicodedata

from threading import Thread


logger = None

DEBUG = False


def catch_exceptions(func):
    """Decorator to catch exceptions and print it in DEBUG mode.

    All threaded functions should be decorated with this,
    otherwise exceptions will go uncaught.
    """
    def wrapper(*args, **kwargs):
        """Wrapper to catch and print exceptions."""
        try:
            return func(*args, **kwargs)
        except BaseException:
            clear()
            traceback.print_exc()
            os._exit(1)

    return wrapper if DEBUG else func


def asynchronously(func):
    """Decorator to execute a function asynchronously."""
    @catch_exceptions
    def wrapper(*args, **kwargs):
        Thread(target=func, args=args, kwargs=kwargs).start()

    return wrapper


def is_windows():
    return platform.system() == "Windows"


def is_linux():
    return platform.system() == "Linux"


def is_mac(): 
    return platform.system() == 'Darwin'


def clear():
    """Clear the terminal."""
    if is_windows():
        os.system('cls')
    elif is_linux():
        os.system("reset")
    elif is_mac():
        os.system('reset')
    elif is_linux():
        os.system("reset")


def is_int(n):
    """Returns True if 'n' is an integer.

    Args:
        n (anything): The variable to check.

    Returns:
        bool: True if it is an integet.
    """
    try:
        int(n)
        return True
    except (ValueError, TypeError):
        return False


def in_range(n, list):
    """Returns True if n is in range of the list.

    Args:
        n (int): The selection.
        list (list): The list.

    Returns:
        bool: True if n is in range.
    """
    return (0 <= n) and (n < len(list))


def ascii(string):
    """Return an ascii encoded version of the string.

    Args:
        string (str, bytes): The string to encode.

    Returns:
        str: The ascii encoded string.
    """
    return unicodedata.normalize("NFKD", string).encode("ascii", "ignore").decode("ascii")


def clamp(value, low, high):
    """Clamp value between low and high (inclusive).

    Args:
        value (int, float): The value.
        low (int, float): Lower bound.
        high (int, float): Upper bound.

    Returns
        int, float: Value such that low <= value <= high.
    """
    return max(low, min(value, high))


def ensure_dir(func):
    """Decorator to ensure a path exists before returning it."""

    @catch_exceptions
    def wrapper(*args, **kwargs):
        """A wrapper that ensures the dir exists before returning it."""
        path = func(*args, **kwargs)
        if not os.path.exists(path):
            os.makedirs(path)
            if logger:
                logger.debug("Created %s", path)
        return path

    return wrapper


@ensure_dir
def get_app_dir():
    """Return the application's directory.

    Returns:
        str: The full path to the directory.
    """
    return os.path.join(tempfile.gettempdir(), "spotifyterminal")


def get_app_file_path(*args):
    """Return the path of a file in the application's directory.

    Args:
        args (tuple): The file paths.

    Returns:
        str: The full path to the file.
    """
    return os.path.join(get_app_dir(), *args)


@ensure_dir
def get_user_dir(username):
    """Return dir for the user.

    Args:
        username (str): The user name.

    Returns:
        str: The path to the user's cache.
    """
    return get_app_file_path(username)


def get_user_file_path(username, *args):
    """Return the path from the user's directory.

    Args:
        args (tuple): The file paths.

    Returns:
        str: The full path to the file.
    """
    return os.path.join(get_user_dir(username), *args)


@ensure_dir
def get_cache(username):
    """Return the directory of the user's cache.

    Args:
        username (str): The user name.

    Returns:
        str: The path to the user's cache.
    """
    return get_user_file_path(username, ".cache")


def get_file_from_cache(username, file):
    """Return a file from a user's cache.

    Args:
        username (str): The user.
        file (str): The file.

    Returns:
        str: The fill path to the file.
    """
    return os.path.join(get_cache(username), file)


def clear_cache(username):
    """Clear the user's cache.

    Args:
        username (str): The user name.
    """
    user_cache = get_cache(username)
    try:
        shutil.rmtree(user_cache)
    except Exception as e:
        logger.debug("Could not clear %s", user_cache)
        logger.debug("%s", e)


def get_auth_filename(username):
    """Return the path to the auth filename.

    Returns:
        str: The full path to the auth filename
    """
    return get_user_file_path(username, "auth")


def clear_auth(username):
    """Clear the user's authorization tokens.

    Args:
        username (str): The user name.
    """
    auth_filename = get_auth_filename(username)
    try:
        os.remove(auth_filename)
    except Exception as e:
        logger.debug("Could not clear %s", auth_filename)
        logger.debug("%s", e)


def extract_version(stream):
    version = None
    for line in stream:
        line = line.decode()
        if "." in line:
            version = line.strip()
            break
    version = tuple(int(n.strip()) for n in version.split("."))
    return version


def get_version():
    try:
        version_file = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            ".version"
        )
        # Read in as bytes. extract_version will decode it.
        # This maintains compatibility with get_master_version
        # which relies on bytes from the Request response.
        with open(version_file, "rb") as f:
            return extract_version(f)
    except BaseException as e:
        logger.info("Could not get current version %s", e)


def get_master_version():
    try:
        resp = requests.get(
            "https://raw.githubusercontent.com/marcdjulien/spotify-terminal/master/spotify_terminal/.version",
            timeout=1
        )
        return extract_version(resp)
    except BaseException as e:
        logger.info("Could not get latest version %s", e)


class ContextDuration(object):
    """Measured the duration of the context."""

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, exception, value, traceback):
        self.end = time.time()
        self.duration = self.end - self.start
        if not exception:
            return self


SPOTIFY_BANNER = """
   _____             __  _ ____
  / ___/____  ____  / /_(_/ ____  __
  \__ \/ __ \/ __ \/ __/ / /_/ / / /
 ___/ / /_/ / /_/ / /_/ / __/ /_/ /
/____/ .___/\____/\__/_/_/  \__, /
    /_/                    /____/
       ______                    _             __
      /_  _____  _________ ___  (_)___  ____ _/ /
       / / / _ \/ ___/ __ `__ \/ / __ \/ __ `/ /
      / / /  __/ /  / / / / / / / / / / /_/ / /
     /_/  \___/_/  /_/ /_/ /_/_/_/ /_/\__,_/_/

  [marcdjulien]
"""


TITLE = """

{}
   Loading...

   v{}.{}.{}
""".format(SPOTIFY_BANNER, *get_version())


PEACE = """

{}

   Saving...

""".format(SPOTIFY_BANNER)

SAVED_TRACKS_CONTEXT_URI = "spotify_terminal:saved_tracks:context"

ARTIST_ALL_TRACKS_CONTEXT_URI = "spotify_terminal:artist:all_tracks_context"

ARTIST_ALL_TRACKS_CONTEXT = {'uri': ARTIST_ALL_TRACKS_CONTEXT_URI}

ALL_ARTIST_TRACKS_CONTEXT_TYPE = "all_artist_tracks"

def get_all_tracks_context(artist):
    return {'uri': ARTIST_ALL_TRACKS_CONTEXT_URI,
            'artist': artist,
            'type': ALL_ARTIST_TRACKS_CONTEXT_TYPE}


def is_all_tracks_context(context):
    return context['uri'] == ARTIST_ALL_TRACKS_CONTEXT_URI


logging.basicConfig(filename=get_app_file_path("log"),
                    filemode='w',
                    format='[%(asctime)s][%(levelname)s][%(name)s] %(message)s',
                    level=logging.DEBUG)


logger = logging.getLogger(__name__)
import logging
import os
import platform
import requests
import shutil
import time
import tempfile
import traceback
import unicodedata

from threading import Thread


logger = None

DEBUG = False


def catch_exceptions(func):
    """Decorator to catch exceptions and print it in DEBUG mode.

    All threaded functions should be decorated with this,
    otherwise exceptions will go uncaught.
    """
    def wrapper(*args, **kwargs):
        """Wrapper to catch and print exceptions."""
        try:
            return func(*args, **kwargs)
        except BaseException:
            clear()
            traceback.print_exc()
            os._exit(1)

    return wrapper if DEBUG else func


def asynchronously(func):
    """Decorator to execute a function asynchronously."""
    @catch_exceptions
    def wrapper(*args, **kwargs):
        Thread(target=func, args=args, kwargs=kwargs).start()

    return wrapper


def is_windows():
    return platform.system() == "Windows"


def is_linux():
    return platform.system() == "Linux"


def is_mac(): 
    return platform.system() == 'Darwin'


def clear():
    """Clear the terminal."""
    if is_windows():
        os.system('cls')
    elif is_linux():
        os.system("reset")
    elif is_mac():
        os.system('reset')
    elif is_linux():
        os.system("reset")


def is_int(n):
    """Returns True if 'n' is an integer.

    Args:
        n (anything): The variable to check.

    Returns:
        bool: True if it is an integet.
    """
    try:
        int(n)
        return True
    except (ValueError, TypeError):
        return False


def in_range(n, list):
    """Returns True if n is in range of the list.

    Args:
        n (int): The selection.
        list (list): The list.

    Returns:
        bool: True if n is in range.
    """
    return (0 <= n) and (n < len(list))


def ascii(string):
    """Return an ascii encoded version of the string.

    Args:
        string (str, bytes): The string to encode.

    Returns:
        str: The ascii encoded string.
    """
    return unicodedata.normalize("NFKD", string).encode("ascii", "ignore").decode("ascii")


def clamp(value, low, high):
    """Clamp value between low and high (inclusive).

    Args:
        value (int, float): The value.
        low (int, float): Lower bound.
        high (int, float): Upper bound.

    Returns
        int, float: Value such that low <= value <= high.
    """
    return max(low, min(value, high))


def ensure_dir(func):
    """Decorator to ensure a path exists before returning it."""

    @catch_exceptions
    def wrapper(*args, **kwargs):
        """A wrapper that ensures the dir exists before returning it."""
        path = func(*args, **kwargs)
        if not os.path.exists(path):
            os.makedirs(path)
            if logger:
                logger.debug("Created %s", path)
        return path

    return wrapper


@ensure_dir
def get_app_dir():
    """Return the application's directory.

    Returns:
        str: The full path to the directory.
    """
    return os.path.join(tempfile.gettempdir(), "spotifyterminal")


def get_app_file_path(*args):
    """Return the path of a file in the application's directory.

    Args:
        args (tuple): The file paths.

    Returns:
        str: The full path to the file.
    """
    return os.path.join(get_app_dir(), *args)


@ensure_dir
def get_user_dir(username):
    """Return dir for the user.

    Args:
        username (str): The user name.

    Returns:
        str: The path to the user's cache.
    """
    return get_app_file_path(username)


def get_user_file_path(username, *args):
    """Return the path from the user's directory.

    Args:
        args (tuple): The file paths.

    Returns:
        str: The full path to the file.
    """
    return os.path.join(get_user_dir(username), *args)


@ensure_dir
def get_cache(username):
    """Return the directory of the user's cache.

    Args:
        username (str): The user name.

    Returns:
        str: The path to the user's cache.
    """
    return get_user_file_path(username, ".cache")


def get_file_from_cache(username, file):
    """Return a file from a user's cache.

    Args:
        username (str): The user.
        file (str): The file.

    Returns:
        str: The fill path to the file.
    """
    return os.path.join(get_cache(username), file)


def clear_cache(username):
    """Clear the user's cache.

    Args:
        username (str): The user name.
    """
    user_cache = get_cache(username)
    try:
        shutil.rmtree(user_cache)
    except Exception as e:
        logger.debug("Could not clear %s", user_cache)
        logger.debug("%s", e)


def get_auth_filename(username):
    """Return the path to the auth filename.

    Returns:
        str: The full path to the auth filename
    """
    return get_user_file_path(username, "auth")


def clear_auth(username):
    """Clear the user's authorization tokens.

    Args:
        username (str): The user name.
    """
    auth_filename = get_auth_filename(username)
    try:
        os.remove(auth_filename)
    except Exception as e:
        logger.debug("Could not clear %s", auth_filename)
        logger.debug("%s", e)


def extract_version(stream):
    version = None
    for line in stream:
        line = line.decode()
        if "." in line:
            version = line.strip()
            break
    version = tuple(int(n.strip()) for n in version.split("."))
    return version


def get_version():
    try:
        version_file = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            ".version"
        )
        # Read in as bytes. extract_version will decode it.
        # This maintains compatibility with get_master_version
        # which relies on bytes from the Request response.
        with open(version_file, "rb") as f:
            return extract_version(f)
    except BaseException as e:
        logger.info("Could not get current version %s", e)


def get_master_version():
    try:
        resp = requests.get(
            "https://raw.githubusercontent.com/marcdjulien/spotify-terminal/master/spotify_terminal/.version",
            timeout=1
        )
        return extract_version(resp)
    except BaseException as e:
        logger.info("Could not get latest version %s", e)


class ContextDuration(object):
    """Measured the duration of the context."""

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, exception, value, traceback):
        self.end = time.time()
        self.duration = self.end - self.start
        if not exception:
            return self


SPOTIFY_BANNER = """
   _____             __  _ ____
  / ___/____  ____  / /_(_/ ____  __
  \__ \/ __ \/ __ \/ __/ / /_/ / / /
 ___/ / /_/ / /_/ / /_/ / __/ /_/ /
/____/ .___/\____/\__/_/_/  \__, /
    /_/                    /____/
       ______                    _             __
      /_  _____  _________ ___  (_)___  ____ _/ /
       / / / _ \/ ___/ __ `__ \/ / __ \/ __ `/ /
      / / /  __/ /  / / / / / / / / / / /_/ / /
     /_/  \___/_/  /_/ /_/ /_/_/_/ /_/\__,_/_/

  [marcdjulien]
"""


TITLE = """

{}
   Loading...

   v{}.{}.{}
""".format(SPOTIFY_BANNER, *get_version())


PEACE = """

{}

   Saving...

""".format(SPOTIFY_BANNER)

SAVED_TRACKS_CONTEXT_URI = "spotify_terminal:saved_tracks:context"

ARTIST_ALL_TRACKS_CONTEXT_URI = "spotify_terminal:artist:all_tracks_context"

ARTIST_ALL_TRACKS_CONTEXT = {'uri': ARTIST_ALL_TRACKS_CONTEXT_URI}

ALL_ARTIST_TRACKS_CONTEXT_TYPE = "all_artist_tracks"

def get_all_tracks_context(artist):
    return {'uri': ARTIST_ALL_TRACKS_CONTEXT_URI,
            'artist': artist,
            'type': ALL_ARTIST_TRACKS_CONTEXT_TYPE}


def is_all_tracks_context(context):
    return context['uri'] == ARTIST_ALL_TRACKS_CONTEXT_URI


logging.basicConfig(filename=get_app_file_path("log"),
                    filemode='w',
                    format='[%(asctime)s][%(levelname)s][%(name)s] %(message)s',
                    level=logging.DEBUG)


logger = logging.getLogger(__name__)
Decorator to catch exceptions and print it in DEBUG mode.      All threaded functions should be decorated with this,     otherwise exceptions will go uncaught. Wrapper to catch and print exceptions. Decorator to execute a function asynchronously. Clear the terminal. Returns True if 'n' is an integer.      Args:         n (anything): The variable to check.      Returns:         bool: True if it is an integet. Returns True if n is in range of the list.      Args:         n (int): The selection.         list (list): The list.      Returns:         bool: True if n is in range. Return an ascii encoded version of the string.      Args:         string (str, bytes): The string to encode.      Returns:         str: The ascii encoded string. Clamp value between low and high (inclusive).      Args:         value (int, float): The value.         low (int, float): Lower bound.         high (int, float): Upper bound.      Returns         int, float: Value such that low <= value <= high. Decorator to ensure a path exists before returning it. A wrapper that ensures the dir exists before returning it. Return the application's directory.      Returns:         str: The full path to the directory. Return the path of a file in the application's directory.      Args:         args (tuple): The file paths.      Returns:         str: The full path to the file. Return dir for the user.      Args:         username (str): The user name.      Returns:         str: The path to the user's cache. Return the path from the user's directory.      Args:         args (tuple): The file paths.      Returns:         str: The full path to the file. Return the directory of the user's cache.      Args:         username (str): The user name.      Returns:         str: The path to the user's cache. Return a file from a user's cache.      Args:         username (str): The user.         file (str): The file.      Returns:         str: The fill path to the file. Clear the user's cache.      Args:         username (str): The user name. Return the path to the auth filename.      Returns:         str: The full path to the auth filename Clear the user's authorization tokens.      Args:         username (str): The user name. # Read in as bytes. extract_version will decode it. # This maintains compatibility with get_master_version # which relies on bytes from the Request response. Measured the duration of the context. _____             __  _ ____   / ___/____  ____  / /_(_/ ____  __   \__ \/ __ \/ __ \/ __/ / /_/ / / /  ___/ / /_/ / /_/ / /_/ / __/ /_/ / /____/ .___/\____/\__/_/_/  \__, /     /_/                    /____/        ______                    _             __       /_  _____  _________ ___  (_)___  ____ _/ /        / / / _ \/ ___/ __ `__ \/ / __ \/ __ `/ /       / / /  __/ /  / / / / / / / / / / /_/ / /      /_/  \___/_/  /_/ /_/ /_/_/_/ /_/\__,_/_/    [marcdjulien] {}    Loading...     v{}.{}.{} {}     Saving...