package me.despawningbone.discordbot.command.music;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.hc.core5.http.ParseException;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.awaitility.Awaitility;

import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist;
import com.wrapper.spotify.SpotifyApi;
import com.wrapper.spotify.enums.ModelObjectType;
import com.wrapper.spotify.exceptions.SpotifyWebApiException;
import com.wrapper.spotify.exceptions.detailed.NotFoundException;
import com.wrapper.spotify.model_objects.credentials.ClientCredentials;
import com.wrapper.spotify.model_objects.specification.Album;
import com.wrapper.spotify.model_objects.specification.Paging;
import com.wrapper.spotify.model_objects.specification.Playlist;
import com.wrapper.spotify.model_objects.specification.PlaylistTrack;
import com.wrapper.spotify.model_objects.specification.Track;
import com.wrapper.spotify.model_objects.specification.TrackSimplified;
import com.wrapper.spotify.requests.data.albums.GetAlbumsTracksRequest;
import com.wrapper.spotify.requests.data.playlists.GetPlaylistsItemsRequest;


/**
 * Code mostly modified from https://github.com/lijamez/tonbot-plugin-music/
 * @author lijamez @ github
 *
 */

public class SpotifyAudioSourceManager implements AudioSourceManager {

	private static final String SPOTIFY_DOMAIN = "open.spotify.com";
	private static final int EXPECTED_PATH_COMPONENTS = 2;

	private SpotifyApi spotifyApi;
	private YoutubeAudioSourceManager manager;

	public SpotifyAudioSourceManager(SpotifyApi spotifyApi, AudioPlayerManager parent, AudioTrackHandler handler) throws Exception {
		this.spotifyApi = spotifyApi;
		handler.ex.submit(() -> {
			Awaitility.await().until(() -> parent.source(YoutubeAudioSourceManager.class) != null);   //needed to ensure its loaded
			this.manager = parent.source(YoutubeAudioSourceManager.class);	
		});
		refreshSpotifyApi(handler);
	}

	private void refreshSpotifyApi(AudioTrackHandler handler) throws Exception {
		ClientCredentials cred = spotifyApi.clientCredentials().build().execute();
		spotifyApi.setAccessToken(cred.getAccessToken());
		handler.ex.schedule(() -> {
			try {
				refreshSpotifyApi(handler);
			} catch (Exception e) {
				e.printStackTrace();  //DONE disable the source manager?
				spotifyApi = null;
			}
		}, cred.getExpiresIn(), TimeUnit.SECONDS);
	}
	
	@Override
	public String getSourceName() {
		return "Spotify Playlist";
	}

	@Override
	public AudioItem loadItem(DefaultAudioPlayerManager manager, AudioReference reference) {
		
		if(spotifyApi == null) return null;  //disabled due to broken api
		
		try {
			URL url = new URL(reference.identifier);

			if (!StringUtils.equals(url.getHost(), SPOTIFY_DOMAIN)) {
				return null;
			}

			AudioItem audioItem = null;
			audioItem = handleAsPlaylist(url, manager);

			if (audioItem == null) {
				audioItem = handleAsTrack(url, manager);
			}

			return audioItem;

		} catch (MalformedURLException e) {
			return null;
		}
	}

	private AudioTrack handleAsTrack(URL url, DefaultAudioPlayerManager man) {
		Path path = Paths.get(url.getPath());

		if (path.getNameCount() < 2) {
			return null;
		}

		if (!StringUtils.equals(path.getName(0).toString(), "track")) {
			return null;
		}

		String trackId = path.getName(1).toString();

		TrackSimplified track;
		try {
			Track t = spotifyApi.getTrack(trackId).build().execute();
			track = new TrackSimplified.Builder().setArtists(t.getArtists()).setName(t.getName()).setDurationMs(t.getDurationMs()).build();
		} catch (IOException | SpotifyWebApiException | ParseException e) {
			throw new IllegalStateException("Unable to fetch track from Spotify API.", e);
		}

		return getAudioTracks(Arrays.asList(track), man).get(0);
	}

	private BasicAudioPlaylist handleAsPlaylist(URL url, DefaultAudioPlayerManager man) {
		String playlistKey;
		try {
			playlistKey = extractPlaylistId(url);
		} catch (IllegalArgumentException e) {
			return null;
		}

		String name;
		List<TrackSimplified> tracks;
		
		try {
			Playlist playlist = spotifyApi.getPlaylist(playlistKey)
					.build().execute();
			name = playlist.getName();
			tracks = getAllPlaylistTracks(playlist).stream().map(pt -> pt.getTrack())
						.filter(pt -> pt.getType() == ModelObjectType.TRACK).map(pt -> {
							Track t = (Track) pt;  //it doesnt have any methods to translate Track to TrackSimplified, so i had to do this; it only uses 3 params anyways
							return new TrackSimplified.Builder().setArtists(t.getArtists()).setName(t.getName()).setDurationMs(t.getDurationMs()).build();
						}).collect(Collectors.toList());
		} catch (IOException | SpotifyWebApiException | ParseException e) {
			if(e instanceof NotFoundException) {  //try searching as album
				try {
					Album album = spotifyApi.getAlbum(playlistKey).build().execute();
					name = album.getName();
					tracks = getAllAlbumTracks(album);
				} catch (ParseException | SpotifyWebApiException | IOException e1) {
					throw new IllegalStateException("Unable to fetch playlist from Spotify API.", e1);
				}
				
			} else {
				throw new IllegalStateException("Unable to fetch playlist from Spotify API.", e);
			}
		}

		

		List<AudioTrack> audioTracks = getAudioTracks(tracks, man);

		return new BasicAudioPlaylist(name, audioTracks, null, false);
	}

	private List<PlaylistTrack> getAllPlaylistTracks(Playlist playlist) {
		List<PlaylistTrack> playlistTracks = new ArrayList<>();

		Paging<PlaylistTrack> currentPage = playlist.getTracks();

		do {
			playlistTracks.addAll(Arrays.asList(currentPage.getItems()));

			if (currentPage.getNext() == null) {
				currentPage = null;
			} else {

				try {
					URI nextPageUri = new URI(currentPage.getNext());
					List<NameValuePair> queryPairs = URLEncodedUtils.parse(nextPageUri, StandardCharsets.UTF_8);

					GetPlaylistsItemsRequest.Builder b = spotifyApi.getPlaylistsItems(playlist.getId());
					for (NameValuePair queryPair : queryPairs) {
						b = b.setBodyParameter(queryPair.getName(), queryPair.getValue());
					}

					currentPage = b.build().execute();
				} catch (IOException | SpotifyWebApiException | ParseException e) {
					throw new IllegalStateException("Unable to query Spotify for playlist tracks.", e);
				} catch (URISyntaxException e) {
					throw new IllegalStateException("Spotify returned an invalid 'next page' URI.", e);
				}
			}
		} while (currentPage != null);

		return playlistTracks;
	}
	
	private List<TrackSimplified> getAllAlbumTracks(Album album) {
		List<TrackSimplified> albumTracks = new ArrayList<>();

		Paging<TrackSimplified> currentPage = album.getTracks();

		do {
			albumTracks.addAll(Arrays.asList(currentPage.getItems()));

			if (currentPage.getNext() == null) {
				currentPage = null;
			} else {

				try {
					URI nextPageUri = new URI(currentPage.getNext());
					List<NameValuePair> queryPairs = URLEncodedUtils.parse(nextPageUri, StandardCharsets.UTF_8);

					GetAlbumsTracksRequest.Builder b = spotifyApi.getAlbumsTracks(album.getId());
					for (NameValuePair queryPair : queryPairs) {
						b = b.setBodyParameter(queryPair.getName(), queryPair.getValue());
					}

					currentPage = b.build().execute();
				} catch (IOException | SpotifyWebApiException | ParseException e) {
					throw new IllegalStateException("Unable to query Spotify for album tracks.", e);
				} catch (URISyntaxException e) {
					throw new IllegalStateException("Spotify returned an invalid 'next page' URI.", e);
				}
			}
		} while (currentPage != null);

		return albumTracks;
	}

	private String extractPlaylistId(URL url) {
		Path path = Paths.get(url.getPath());
		if (path.getNameCount() < EXPECTED_PATH_COMPONENTS) {
			throw new IllegalArgumentException("Not enough path components.");
		}

		if (!Arrays.asList("playlist", "album").contains(path.getName(0).toString())) {
			throw new IllegalArgumentException("URL doesn't appear to be a playlist.");
		}
		
		String playlistId = path.getName(1).toString();
		if (StringUtils.isBlank(playlistId)) {
			throw new IllegalArgumentException("Playlist ID is blank.");
		}

		return playlistId;
	}

	@Override
	public boolean isTrackEncodable(AudioTrack track) {
		return false;
	}

	@Override
	public void encodeTrack(AudioTrack track, DataOutput output) throws IOException {
		throw new UnsupportedOperationException("encodeTrack is unsupported.");
	}

	@Override
	public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException {
		throw new UnsupportedOperationException("decodeTrack is unsupported.");
	}

	@Override
	public void shutdown() {

	}

	private List<AudioTrack> getAudioTracks(List<TrackSimplified> tracks, DefaultAudioPlayerManager manager) {
		return tracks.parallelStream().map(track -> {    //parallelStream made a world of difference in loading times lmao
			String artist = track.getArtists().length < 1 ? "" : track.getArtists()[0].getName();
			AudioItem item = this.manager.loadItem(manager, new AudioReference("ytsearch:" + artist + " " + track.getName(), null));	
			if (item instanceof AudioPlaylist) {
				AudioPlaylist audioPlaylist = (AudioPlaylist) item;

				// The number of matches is limited to reduce the chances of matching against
				// less than optimal results.
				// The best match is the one that has the smallest track duration delta.
				YoutubeAudioTrack bestMatch = audioPlaylist.getTracks().stream().limit(3)
						.map(t -> (YoutubeAudioTrack) t).min((o1, o2) -> {
							long o1TimeDelta = Math.abs(o1.getDuration() - track.getDurationMs());
							long o2TimeDelta = Math.abs(o2.getDuration() - track.getDurationMs());

							return (int) (o1TimeDelta - o2TimeDelta);
						}).orElse(null);
				
				return bestMatch;
			} else if (item instanceof YoutubeAudioTrack) {
				return (YoutubeAudioTrack) item;
			} else if (item instanceof AudioReference) {   //no results; retry once more
				System.out.println("Spotify Source Manager: Retry needed for " + track.getName());
				item = this.manager.loadItem(manager, new AudioReference("ytsearch:" + artist + " " + track.getName(), null));
				
				if(item instanceof AudioPlaylist) item = ((AudioPlaylist) item).getTracks().get(0);  //cba doing the best match lmao
				else if(!(item instanceof YoutubeAudioTrack)) return null;   //if not playlist and track return null again
				
				return (YoutubeAudioTrack) item;
			} else {
				throw new IllegalArgumentException("Unknown AudioItem");  //should never throw
			}
		}).collect(Collectors.toList());	
	}
}
