#!/usr/bin/env python3

from collections import OrderedDict # For sorting the Hot 100
import tekore as tk
import pyml_config as config
import json
import re
import requests
from typing import Union # For CompactJSONEncoder

conf = (config.client_id, config.client_secret)
token = tk.request_client_token(*conf)

spotify = tk.Spotify(token)


## Takes a trackid URI and turns it into a track object
# \param uri	A Spotify trackid, in the form of a URI (ie. spotify:track:SOMEUUID)
def track_from_id(uri):
	track = spotify.track(trackid)
	return track


def trackname(track):
	print(track)
	name = track.dict().get('name')
	return name


## Takes a track and tries to determine the primary artist on that track
# \param track	A Spotify track
# \return artist	A string that's hopefully the primary artist's name
def primary_artist(track):
	if track is None:
		#print(bcolors.FAIL+"Uhh, nothing here, skipping, hope that's cool!"+bcolors.ENDC)
		return
	#print(bcolors.HEADER+"Now checking this track: "+track.dict().get("name")+bcolors.ENDC)
	albuminfo = track.dict().get("album")
	albumartist = albuminfo.get("artists")[0]["name"] #TODO: Handle when an album has more than one artist!
	#print("Album artist is "+albumartist)
	#print(track.dict())
	#print(albuminfo)
	artistlist = []
	for a in track.artists:
		artistlist += [a.dict().get("name")]
		#print("Found artist "+a.dict().get("name"))
	if albumartist in artistlist:
		#print("One of the track's artists is also the album artist, "+albumartist)
		if len(track.artists) == 1:
			artist = albumartist
		else:
			#print("Well, there's other artists, but going with the one whose name is on the album...")
			artist = track.artists[0].dict().get("name")
	else:
		#print(bcolors.WARNING+"Danger, Will Robinson! No track artist matches the album artist "+albumartist+"!"+bcolors.ENDC)
		if "Various Artists" in albumartist:
			#print("Taking 'Various Artists' as evidence that this is some sort of compilation, and defaulting to the first track artist listed")
			artist = track.artists[0].dict().get("name")
		elif "Cast" in albumartist:
			#print("Album artist has 'Cast' in its name, presuming this is a musical and that'll make more sense than crediting the individual performer as is weirdly common")
			artist = albumartist
		elif "Cast" in albuminfo["name"]:
			#print("Album title has 'Cast' in its name, presuming this is a musical and that'll make more sense than crediting the individual performer as is weirdly common")
			artist = albumartist
		else:
			# If we can't match anything else, default to track artist
			artist = track.artists[0].dict().get("name")
			#print(bcolors.FAIL+"Oh no! Seems like I didn't catch a potential scenario, couldn't determine artist for "+track.dict().get("name")+bcolors.ENDC)
		#print(" Going with "+bcolors.OKCYAN+artist+bcolors.ENDC)
	return artist


# From Blender by way of https://stackoverflow.com/a/287944/2808933, should do something better
class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


## Takes a URL or URI and figures out if it's a URL or a URI
# \param ur	A Spotify URL or URI
def ur_type(ur):
	spotify_url_prefix = "https://open.spotify.com/"
	spotify_uri_prefix = "spotify:"
	spotify_new_prefix = "https://spotify.link"
	u = "unknown"
	
	if (ur.startswith(spotify_url_prefix)):
		u = "url"
		print("found url")
	elif (ur.startswith(spotify_uri_prefix)):
		u = "uri"
		print("found uri")
	elif (ur.startswith(spotify_new_prefix)):
		u = "new"
		print("Found url (new format)")
	else:
		print("Didn't match???")
	
	return u


## Translate new Spotify URL format into full verbose ID
# \param newurl	A Spotify URL of the new type
def newurl_expand(newurl):
	req = requests.get(newurl)
	# If Spotify just actually used HTTP like God intended, we could just use
	# return(r.url)
	# Intead, we're gonna have to parse the page text for the URL to the track that their JavaScript will do the reload for (kindof insane, on a desktop you don't even see the page)
	pagetext = req.text
	# print(pagetext)
	r = re.search(r'validateProtocol\("https://open.spotify.com/(.*)\?', pagetext)
	if r:
		url = "https://open.spotify.com/"+r[1]
	if not r:
		# print("Didn't find the expected link, trying a different match hoping it's a different type of dumb redirect page Spotify does")
		r = re.search('href="/track/([a-zA-Z0-9_]+)">', pagetext)
		url = "https://open.spotify.com/track/"+r[1]

	return(url)


## Takes a URL or URI and figures out if it's a URL or a URI, then figures out and returns what type of entity (track, album, playlist, artist, whatever) it is
def spotify_type(ur):
	types = ["playlist", "album", "track", "artist"]
	this_is = "dunno"
	
	# Test UR* type and use tk and lml functions accordingly
	ut = ur_type(ur)
	if ut == "uri":
		f = tk.from_uri(ur)
		this_is = f[0]
	elif ut == "url":
		f = tk.from_url(ur)
		this_is = f[0]
	elif ut == "new":
		ur = newurl_expand(ur)
		f = tk.from_url(ur)
		this_is = f[0]
	
	# for t in types:
	# 	if (("spotify:"+t+":" in ur) or ("spotify.com/"+t+"/" in ur)):
	# 		this_is = t
	
	return this_is


## Takes a URL or URI and figures out if it's a URL or a URI, then figures out and returns what type of entity (track, album, playlist, artist, whatever) it is alongside the ID (there's probably a Tekore function I'm missing that does this)
def spotify_type_and_id(ur):
	types = ["playlist", "album", "track", "artist"]
	this_is = "dunno"
	
	# Test UR* type and use tk and lml functions accordingly
	ut = ur_type(ur)
	if ut == "uri":
		f = tk.from_uri(ur)
	elif ut == "url":
		f = tk.from_url(ur)
	elif ut == "new":
		ur = newurl_expand(ur)
		f = tk.from_url(ur)
	
	return f


# Takes a UR*, determines and deciphers type, then returns a track object
def return_spotify_track(ur):
	# Test UR* type and use tk and lml functions accordingly
	ut = ur_type(ur)
	if ut == "uri":
		f = tk.from_uri(ur)
	elif ut == "url":
		f = tk.from_url(ur)
	elif ut == "new":
		f = tk.from_url(newurl_expand(ur))
	
	if f[0] == "track":
		t = spotify.track(f[1])
	else:
		print("not a track?")
	return t



def play_list(uri):
	info = spotify.playlist(uri)
	playlist_id = uri
	title = info.dict().get("name")
	tracks = spotify.playlist_items(playlist_id)
	tracks = spotify.all_items(tracks)
	items = []
	for t in tracks:
		#print(t.track.dict().get('name'))
		artist = primary_artist(t.track)
		items.append(t)
	return title, items


def playlist_as_textlist(playlist):
	l = []
	distinction = " - "
	for i in playlist[1]:
		name = trackname(i.track)
		artist = primary_artist(i.track)
		l.append(artist+distinction+name)
	return l

def print_playlist(playlist):
	l = []
	title = playlist[0]
	distinction = " - "
	titleprefix = "###"
	itemprefix = "####"
	for i in playlist[1]:
		name = trackname(i.track)
		artist = primary_artist(i.track)
		l.append(artist+distinction+name)
	print(titleprefix+' '+title, end = '')
	print('', *l, sep='\n'+itemprefix+' ')

def print_playlist_with_trackids(playlist):
	l = []
	title = playlist[0]
	distinction = " - "
	titleprefix = "###"
	itemprefix = "####"
	for i in playlist[1]:
		name = trackname(i.track)
		artist = primary_artist(i.track)
		l.append(i.track.id+distinction+artist+distinction+name)
	print(titleprefix+' '+title, end = '')
	print('', *l, sep='\n'+itemprefix+' ')


def print_hot(playlist):
	l = []
	title = playlist[0]
	distinction = " - "
	for i in playlist[1]:
		l.append([f'{i.track.popularity:03d}'+"\t"+primary_artist(i.track)+"\t"+trackname(i.track)])
		# l.append([f'{i.track.popularity:03d}')+" | "+trackname(i.track)+" by "+primary_artist(i.track)])
	# l.sort(key=operator.itemgetter('popularity'))
	# print(titleprefix+' '+title, end = '')
	# print('', *l, sep='\n'+itemprefix+' ')
	# print(l)
	l.sort()
	#print('', *l, sep='\n')
	for line in l:
		print(line[0])


def get_available_markets(track):
	available_markets = track.dict()['available_markets']

	# Get the ISRC code to check for duplicate tracks that will be substituted automatically by Spotify if the submitted version isn't available in a jurisdiction
	isrc = track.dict()['external_ids']['isrc']

	# Now check for those additional versions, and append them to the available_markets array. I might be doing this in a reasonable way now?
	search_results = spotify.search('isrc:{}'.format(isrc), types=('track',))
	for p in search_results:
		for i in p.items:
			available_markets += i.dict()['available_markets']

	# TODO: Duplicate the standalone manual check of additional markets maybe?

	# Clear out duplicates
	available_markets = list(dict.fromkeys(available_markets))
	# Order it
	available_markets.sort()
	
	return available_markets

# From https://gist.github.com/jannismain/e96666ca4f059c3e5bc28abb711b5c92 because I want inner lists not to have newlines splattered about
class CompactJSONEncoder(json.JSONEncoder):
    """A JSON Encoder that puts small containers on single lines."""

    CONTAINER_TYPES = (list, tuple, dict)
    """Container datatypes include primitives or other containers."""

    MAX_WIDTH = 10000
    """Maximum width of a container that might be put on a single line."""

    MAX_ITEMS = 1000
    """Maximum number of items in container that might be put on single line."""

    INDENTATION_CHAR = " "

    def __init__(self, *args, **kwargs):
        # using this class without indentation is pointless
        if kwargs.get("indent") is None:
            kwargs.update({"indent": 4})
        super().__init__(*args, **kwargs)
        self.indentation_level = 0

    def encode(self, o):
        """Encode JSON object *o* with respect to single line lists."""
        if isinstance(o, (list, tuple)):
            if self._put_on_single_line(o):
                return "[" + ", ".join(self.encode(el) for el in o) + "]"
            else:
                self.indentation_level += 1
                output = [self.indent_str + self.encode(el) for el in o]
                self.indentation_level -= 1
                return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
        elif isinstance(o, dict):
            if o:
                if self._put_on_single_line(o):
                    return "{ " + ", ".join(f"{self.encode(k)}: {self.encode(el)}" for k, el in o.items()) + " }"
                else:
                    self.indentation_level += 1
                    output = [self.indent_str + f"{json.dumps(k)}: {self.encode(v)}" for k, v in o.items()]
                    self.indentation_level -= 1
                    return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
            else:
                return "{}"
        elif isinstance(o, float):  # Use scientific notation for floats, where appropiate
            return format(o, "g")
        elif isinstance(o, str):  # escape newlines
            o = o.replace("\n", "\\n")
            return f'"{o}"'
        else:
            return json.dumps(o)

    def iterencode(self, o, **kwargs):
        """Required to also work with `json.dump`."""
        return self.encode(o)

    def _put_on_single_line(self, o):
        return self._primitives_only(o) and len(o) <= self.MAX_ITEMS and len(str(o)) - 2 <= self.MAX_WIDTH

    def _primitives_only(self, o: Union[list, tuple, dict]):
        if isinstance(o, (list, tuple)):
            return not any(isinstance(el, self.CONTAINER_TYPES) for el in o)
        elif isinstance(o, dict):
            return not any(isinstance(el, self.CONTAINER_TYPES) for el in o.values())

    @property
    def indent_str(self) -> str:
        return self.INDENTATION_CHAR*(self.indentation_level*self.indent)
