package me.despawningbone.discordbot.command.music;

import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;

import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.music.AudioTrackHandler.TrackData;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;

/**
 * This class schedules tracks for the audio player. It contains the queue of
 * tracks.
 */
public class TrackScheduler extends AudioEventAdapter {
	private final AudioPlayer player;
	private final BlockingQueue<AudioTrack> queue;
	private AudioTrackHandler ap;
	private Guild guild;

	public String loop = null;  //way too lazy to use getter setters lmao

	/**
	 * @param player
	 *            The audio player this scheduler uses
	 */
	public TrackScheduler(Guild parent, AudioTrackHandler handler, AudioPlayer player) {
		this.player = player;
		this.queue = new LinkedBlockingQueue<>();
		this.ap = handler;
		this.guild = parent;
	}

	/**
	 * Add the next track to queue or play right away if nothing is in the
	 * queue.
	 *
	 * @param track
	 *            The track to play or add to queue.
	 */
	public void queue(AudioTrack track) {
		// Calling startTrack with the noInterrupt set to true will start the track only if nothing is currently playing. If
		// something is playing, it returns false and does nothing. In that case the player was already playing so this
		// track goes to the queue instead.
		if (!player.startTrack(track, true)) {
			queue.offer(track);
		} else if (loop != null && loop.equals("autoplay")) {  //i dont think this is needed as people need to play something before autoplay can be toggled anyways
			queueAutoplay(track);
		}
	}

	/**
	 * Start the next track, stopping the current one if it is playing.
	 */
	public void nextTrack() {  //DONE rewrite to not include q.remove here so that stuff like interrupted wont break the queue?
		// Start the next track, regardless of if something is already playing or not. In case queue was empty, we are
		// giving null to startTrack, which is a valid argument and will simply stop the player.
		AudioTrack track = queue.poll();
		player.startTrack(track, false);
		if(track == null) {
			//System.out.println("finished");   //debug
			loop = null;
			delayCloseConnection(player);  //required because if not it will throw InterruptedException
		}
	} //seems to be called internally somehow; even when mayStartNext is false (REPLACED, STOPPED etc) this still fires 
	//NVM ITS CALLED FROM AudioTrackHandler.skipTrack() LOL

	public void queueAutoplay(AudioTrack track) {  //check duplicate please, some can get into a dead loop like cosMo@暴走P - WalpurgisNacht and Ice - 絶  //well randoming it works
		ap.ex.submit(() -> {  //async so it can free up the event 
			try (HttpInterface httpInterface = ap.httpInterfaceManager.getInterface()) {
				//System.out.println("autoplay");
				
				URI orig = new URIBuilder("https://www.youtube.com/watch").addParameter("v", track.getIdentifier()).build();
				
				try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(orig))) {  //connection code borrowed from lavaplayer's YoutubeSearchProvider
					int statusCode = response.getStatusLine().getStatusCode();
					if (!HttpClientTools.isSuccessWithContent(statusCode)) {
						throw new IOException("Invalid status code for search response: " + statusCode);
					}
					
					//start parsing
					String data = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
					data = data.substring(data.indexOf("window[\"ytInitialData\"] = ") + 26);
					
					for (int i = 0; i < 5 && !data.startsWith("{"); i++) { //reconnect in case it doesnt fetch correctly; give up after 5 tries (should be more than enough)
						response.close();
						try(CloseableHttpResponse retry = httpInterface.execute(new HttpGet(orig))) {
							data = IOUtils.toString(retry.getEntity().getContent(), "UTF-8");
							data = data.substring(data.indexOf("window[\"ytInitialData\"] = ") + 26);	
						}
					}
					
					JSONArray arr = new JSONObject(new JSONTokener(data.substring(0, data.indexOf(";\n"))))
							.getJSONObject("contents").getJSONObject("twoColumnWatchNextResults").getJSONObject("secondaryResults")
							.getJSONObject("secondaryResults").getJSONArray("results");
					
					//DONE reimplement random selection between top 2 results to alleviate infinite loops
					JSONObject result = ThreadLocalRandom.current().nextBoolean() ?
							arr.getJSONObject(0).getJSONObject("compactAutoplayRenderer").getJSONArray("contents").getJSONObject(0).getJSONObject("compactVideoRenderer")  //get autoplay video renderer
							: null;
					if(result == null)
						for(Object obj : arr) {  //else find first video renderer
							JSONObject renderer = (JSONObject) obj;
							if(renderer.has("compactVideoRenderer")) {
								result = renderer.getJSONObject("compactVideoRenderer");
							}
						}
					
					//JSONObject result = new JSONObject(new JSONTokener(data.substring(0, data.indexOf(";\n")))).getJSONObject("webWatchNextResponseExtensionData")
					//		.getJSONObject("contents").getJSONObject("twoColumnWatchNextResults").getJSONObject("secondaryResults").getJSONObject("secondaryResults")
					//		.getJSONObject("results").getJSONObject("compactAutoplayRenderer").getJSONArray("contents").getJSONObject(0).getJSONObject("compactVideoRenderer");
					String url = "https://youtube.com/watch?v=" + result.getString("videoId");
					
					//load
					Member user = guild.getMemberById(DiscordBot.BotID); //no need to use original track user as it would introduce performance overhead and other complications when leaving channels
					ap.load(user, url, (n, l) -> {
						AudioTrack auto = l.get(0);
						auto.setUserData(new TrackData(auto.getInfo().uri, "Autoplay", auto.getDuration()));  //change name to autoplay
						ap.queueTracks(l, user);  //no need sublist coz l is always a single video
					}, ex -> {
						ex.printStackTrace();
						DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track: " + ex.getMessage()).queue();
						loop = null;
					});
				}
			} catch (Exception e) {
				e.printStackTrace();
				DiscordBot.lastMusicCmd.get(guild.getId()).sendMessage("Something went wrong when loading next autoplay track. Is the last track a youtube video?").queue();
				loop = null;
			}
		});
	}

	public List<AudioTrack> findTracks(int startIndex, int range) {
		AudioTrack[] a = queue.toArray(new AudioTrack[queue.size()]);
		int i = startIndex - 1 + range < 0 ? Integer.MAX_VALUE : startIndex - 1 + range;  //prevent overflow
		AudioTrack[] t = Arrays.copyOfRange(a, startIndex - 1, Math.min(queue.size() + 1, i));  //accounts for first track player thats not in queue
		
		return Arrays.asList(t);
	}

	public AudioTrack removeTrack(int num) {
		Iterator<AudioTrack> i = queue.iterator();
		num = num - 1;

		for (int times = 0; i.hasNext(); times++) {
			AudioTrack removed = i.next();
			if (num == times) {
				i.remove();
				return removed;
			}
		}

		return null;
	}
	
	public AudioTrack moveTrack(int from, int to) {
		List<AudioTrack> q = new ArrayList<>(Arrays.asList(queue.toArray(new AudioTrack[queue.size()])));
		AudioTrack track = q.remove(from);
		q.add(to, track);
		synchronized (queue) {  //obtain lock and operate before releasing or else queue might not be complete
			queue.clear();
			for(AudioTrack t : q) queue.offer(t);
		}
		return track;
	}
	
	public void shuffleQueue() {
		List<AudioTrack> q = Arrays.asList(queue.toArray(new AudioTrack[queue.size()]));
		Collections.shuffle(q);
		synchronized (queue) {  //obtain lock and operate before releasing or else queue might not be complete
			queue.clear();
			for(AudioTrack t : q) queue.offer(t);
		}
	}

	public void clearSchedulerQueue() {
		queue.clear();
	}

	public int getQueueSize() {
		return queue.size();
	}


	@Override
	public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
		// Only start the next track if the end reason is suitable for it (FINISHED or LOAD_FAILED or REPLACED)
		//System.out.println(endReason);  //debug  
		
		boolean mayStartNext = endReason.mayStartNext;

		if (mayStartNext) {
			//doesnt queue clone if skipped
			if (loop != null && loop.equals("loop")) { //so what the hecc if loop is null and i do loop.equals("Loop") it freezes the thread
				AudioTrack clone = track.makeClone();
				TrackData origData = clone.getUserData(TrackData.class);
				clone.setUserData(new TrackData(origData.getUrl(), origData.getUserWithDiscriminator(), clone.getDuration()));  //wipe votes
				queue.offer(clone);
			}
			
			nextTrack();
		}
		
		if(mayStartNext || endReason == AudioTrackEndReason.REPLACED) {  //queues new if skipped too
			if (loop != null && loop.equals("autoplay") && queue.size() < 1) {
				queueAutoplay(player.getPlayingTrack());
			}
		}
	}

	private void delayCloseConnection(AudioPlayer player) {  //TODO configurable delay; dont leave if new things play within the delay
		ap.ex.schedule(() -> guild.getAudioManager().closeAudioConnection(), 1, TimeUnit.MILLISECONDS);
	}

}