package zero1hd.rhythmbullet.audio;

import java.util.Comparator;
import java.util.Observable;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Sort;

import zero1hd.rhythmbullet.audio.processor.AudioProcessor;
import zero1hd.rhythmbullet.audio.processor.WAVAudioProcessor;

/**
 * A music list made to store paths to all the songs within a given directory. Can activate the music calling {@link #newAudioProcessor(FileHandle)} and its derivatives.
 * Is observable, meaning, given there are observers, will notify when the list completes a refresh.
 * @author yunya
 */
public class MusicList extends Observable {
	public final class States {
		public final Integer LOADING = new Integer(0), COMPLETE = new Integer(1), EMPTY = new Integer(2);
	}
	public States states = new States();
	private Array<FileHandle> musicList;
	private RecursiveMusicSearchThread searchThread;
	private AudioProcessorFactory audioProcFactory;
	private volatile boolean searched;
	private String searchPath;
	private Comparator<FileHandle> compare;
	
	public MusicList(AudioProcessorFactory audioProcessorFactory, String searchPath) {
		this.audioProcFactory = audioProcessorFactory;
		musicList = new Array<>();
		setSearchPath(searchPath);
		
		compare = new Comparator<FileHandle>() {
			
			@Override
			public int compare(FileHandle o1, FileHandle o2) {
				return o1.nameWithoutExtension().compareTo(o2.nameWithoutExtension());
			}
		};
	}
	
	/**
	 * Asynchronous recursive search on music directory.
	 * Also notifies listeners that are on the main thread.
	 * @param refresh does a search whether or not path has changed and whether or not this list has searched before this.
	 */
	public void attemptAsyncSearch(boolean refresh) {
		if (refresh) {
			notifyObservers(states.LOADING);
			if (searchThread != null) {
				if (!searchThread.start()) {
					searchThread.stop();
					searchThread = new RecursiveMusicSearchThread("Music Search Thread", Gdx.files.absolute(searchPath));
					searchThread.start();
				}
			} else {
				searchThread = new RecursiveMusicSearchThread("Music Search Thread", Gdx.files.absolute(searchPath));
				searchThread.start();
			}
		} else {
			if (!searched || hasChanged()) {
				attemptAsyncSearch(true);
			}
		}
	}
	
	public void setSearchPath(String searchPath) {
		if (this.searchPath != null && this.searchPath.equals(searchPath)) return;
		this.searchPath = searchPath;
		setChanged();
	}
	
	public String getSearchPath() {
		return searchPath;
	}
	
	/**
	 * @param file
	 * @return a {@link AudioProcessor} of the given music file. Will return null if theres a format error.
	 */
	public AudioProcessor newAudioProcessor(FileHandle file) {
		switch (SupportedFormats.valueOf(file.extension().toUpperCase())) {
		case MP3:
			return audioProcFactory.newMP3AudioProcessor(file);
		case WAV:
			return new WAVAudioProcessor(file);
		default:
			break;
		}
		return null;
	}
	
	/**
	 * 
	 * @param file the music file that you need the header for.
	 * @return the header containing minimal info of the song.
	 */
	public MinimalAudioHeader newMinimalAudioHeader(FileHandle file) {
		return new MinimalAudioHeader(file);
	}
	
	public AudioProcessor newAudioProcessorFromIndex(int index) {
		if (!searched) {
			Gdx.app.debug("MusicList", "Warning, this list has not completed it's search...");
			Thread.dumpStack();
		}
		if (musicList.size == 0) {
			return null;
		}
		return newAudioProcessor(musicList.get(index));
	}
	
	
	public FileHandle getSongFileHandleFromIndex(int index) {
		if (!searched) {
			Gdx.app.debug("MusicList", "Warning, this list has not completed it's search...");
			Thread.dumpStack();
		}
		if (musicList.size == 0) {
			return null;
		}
		return musicList.get(index);
	}
	
	public FileHandle getAudioFileHandle(int index) {
		return musicList.get(index);
	}
	
	public int getIndexOfFileHandle(FileHandle file) {
		return musicList.indexOf(file, true);
	}
	
	public boolean isSearched() {
		return searched;
	}
	
	/**
	 * 
	 * @return the amount of audio files discovered.
	 */
	public int getTotal() {
		return musicList.size;
	}
	
	private class RecursiveMusicSearchThread implements Runnable {
		private Thread thread;
		private String threadName;
		private FileHandle directory;
		private volatile boolean work;

		public RecursiveMusicSearchThread(String name, FileHandle searchDirectory) {
			this.threadName = name;
			this.directory = searchDirectory;
		}
		
		@Override
		public void run() {
			Gdx.app.debug("MusicList", "recursive async search beginning.");
			Array<FileHandle> obtainedAudioFiles = recursiveMusicSearch(directory);
			Sort.instance().sort(obtainedAudioFiles, compare);
			if (work) {
				musicList = obtainedAudioFiles;
				searched = true;
				Gdx.app.debug("MusicList", "recursive async search completed.");
				setChanged();
				if (musicList.size != 0) {
					notifyObservers(states.COMPLETE);
				} else {
					notifyObservers(states.EMPTY);
				}
			}
		}
		
		public boolean start() {
			if (thread == null) {
				work = true;
				thread = new Thread(this, threadName);
				thread.setDaemon(true);
				thread.start();
				return true;
			} else {
				return false;
			}
		}
		
		public void stop() {
			work = false;
		}
		
		private Array<FileHandle> recursiveMusicSearch(FileHandle fileHandle) {
			Array<FileHandle> musicFiles = new Array<>();
			FileHandle[] files = fileHandle.list();
			for (int i = 0; i < files.length && work; i++) {
				if (files[i].isDirectory()) {
					musicFiles.addAll(recursiveMusicSearch(files[i]));
				} else {
					try {
						SupportedFormats.valueOf(files[i].extension().toUpperCase());
						musicFiles.add(files[i]);
					} catch (IllegalArgumentException e) {
						Gdx.app.log("MusicList", "Unsupported file format: " + files[i].name());
					}
				}
			}
			return musicFiles;
		}
	}
}
