/*
 This file is part of Subsonic.

 Subsonic is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 Subsonic is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with Subsonic.  If not, see <http://www.gnu.org/licenses/>.

 Copyright 2009 (C) Sindre Mehus
 */
package net.nullsum.audinaut.service;

import java.io.File;
import java.io.Reader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.util.Log;

import org.apache.http.HttpResponse;

import net.nullsum.audinaut.domain.Artist;
import net.nullsum.audinaut.domain.Genre;
import net.nullsum.audinaut.domain.Indexes;
import net.nullsum.audinaut.domain.MusicDirectory.Entry;
import net.nullsum.audinaut.domain.PlayerQueue;
import net.nullsum.audinaut.domain.RemoteStatus;
import net.nullsum.audinaut.domain.MusicDirectory;
import net.nullsum.audinaut.domain.MusicFolder;
import net.nullsum.audinaut.domain.Playlist;
import net.nullsum.audinaut.domain.SearchCritera;
import net.nullsum.audinaut.domain.SearchResult;
import net.nullsum.audinaut.domain.User;
import net.nullsum.audinaut.util.Constants;
import net.nullsum.audinaut.util.FileUtil;
import net.nullsum.audinaut.util.Pair;
import net.nullsum.audinaut.util.ProgressListener;
import net.nullsum.audinaut.util.SilentBackgroundTask;
import net.nullsum.audinaut.util.SongDBHandler;
import net.nullsum.audinaut.util.Util;
import java.io.*;
import java.util.Comparator;
import java.util.SortedSet;

/**
 * @author Sindre Mehus
 */
public class OfflineMusicService implements MusicService {
	private static final String TAG = OfflineMusicService.class.getSimpleName();
	private static final String ERRORMSG = "Not available in offline mode";
	private static final Random random = new Random();

	@Override
	public void ping(Context context, ProgressListener progressListener) throws Exception {

	}

    @Override
    public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
        List<Artist> artists = new ArrayList<Artist>();
		List<Entry> entries = new ArrayList<>();
        File root = FileUtil.getMusicDirectory(context);
        for (File file : FileUtil.listFiles(root)) {
            if (file.isDirectory()) {
                Artist artist = new Artist();
                artist.setId(file.getPath());
                artist.setIndex(file.getName().substring(0, 1));
                artist.setName(file.getName());
                artists.add(artist);
            } else if(!file.getName().equals("albumart.jpg") && !file.getName().equals(".nomedia")) {
				entries.add(createEntry(context, file));
			}
        }
		
        Indexes indexes = new Indexes(0L, Collections.<Artist>emptyList(), artists, entries);
		return indexes;
    }

    @Override
    public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
        return getMusicDirectory(id, artistName, refresh, context, progressListener, false);
    }
	private MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener, boolean isPodcast) throws Exception {
		File dir = new File(id);
		MusicDirectory result = new MusicDirectory();
		result.setName(dir.getName());

		Set<String> names = new HashSet<String>();

		for (File file : FileUtil.listMediaFiles(dir)) {
			String name = getName(file);
			if (name != null & !names.contains(name)) {
				names.add(name);
				result.addChild(createEntry(context, file, name, true, isPodcast));
			}
		}
		result.sortChildren(Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_CUSTOM_SORT_ENABLED, true));
		return result;
	}

	@Override
	public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	private String getName(File file) {
        String name = file.getName();
        if (file.isDirectory()) {
            return name;
        }

        if (name.endsWith(".partial") || name.contains(".partial.") || name.equals(Constants.ALBUM_ART_FILE)) {
            return null;
        }

        name = name.replace(".complete", "");
        return FileUtil.getBaseName(name);
    }

	private Entry createEntry(Context context, File file) {
		return createEntry(context, file, getName(file));
	}
	private Entry createEntry(Context context, File file, String name) {
		return createEntry(context, file, name, true);
	}
    private Entry createEntry(Context context, File file, String name, boolean load) {
        return createEntry(context, file, name, load, false);
    }
	private Entry createEntry(Context context, File file, String name, boolean load, boolean isPodcast) {
		Entry entry;
        entry = new Entry();
		entry.setDirectory(file.isDirectory());
		entry.setId(file.getPath());
		entry.setParent(file.getParent());
		entry.setSize(file.length());
		String root = FileUtil.getMusicDirectory(context).getPath();
		if(!file.getParentFile().getParentFile().getPath().equals(root)) {
			entry.setGrandParent(file.getParentFile().getParent());
		}
		entry.setPath(file.getPath().replaceFirst("^" + root + "/" , ""));
		String title = name;
		if (file.isFile()) {
			File artistFolder = file.getParentFile().getParentFile();
			File albumFolder = file.getParentFile();
			if(artistFolder.getPath().equals(root)) {
				entry.setArtist(albumFolder.getName());
			} else {
				entry.setArtist(artistFolder.getName());
			}
			entry.setAlbum(albumFolder.getName());

			int index = name.indexOf('-');
			if(index != -1) {
				try {
					entry.setTrack(Integer.parseInt(name.substring(0, index)));
					title = title.substring(index + 1);
				} catch(Exception e) {
					// Failed parseInt, just means track filled out
				}
			}

			if(load) {
				entry.loadMetadata(file);
			}
		}

		entry.setTitle(title);
		entry.setSuffix(FileUtil.getExtension(file.getName().replace(".complete", "")));

		File albumArt = FileUtil.getAlbumArtFile(context, entry);
		if (albumArt.exists()) {
			entry.setCoverArt(albumArt.getPath());
		}
		return entry;
	}

    @Override
    public Bitmap getCoverArt(Context context, Entry entry, int size, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
		try {
			return FileUtil.getAlbumArtBitmap(context, entry, size);
		} catch(Exception e) {
			return null;
		}
    }

	@Override
	public HttpResponse getDownloadInputStream(Context context, Entry song, long offset, int maxBitrate, SilentBackgroundTask task) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public String getMusicUrl(Context context, Entry song, int maxBitrate) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
    public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
    }

	@Override
	public void startRescan(Context context, ProgressListener listener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
    public SearchResult search(SearchCritera criteria, Context context, ProgressListener progressListener) throws Exception {
		List<Artist> artists = new ArrayList<Artist>();
		List<Entry> albums = new ArrayList<Entry>();
		List<Entry> songs = new ArrayList<Entry>();
        File root = FileUtil.getMusicDirectory(context);
		int closeness = 0;
        for (File artistFile : FileUtil.listFiles(root)) {
			String artistName = artistFile.getName();
            if (artistFile.isDirectory()) {
				if((closeness = matchCriteria(criteria, artistName)) > 0) {
					Artist artist = new Artist();
					artist.setId(artistFile.getPath());
					artist.setIndex(artistFile.getName().substring(0, 1));
					artist.setName(artistName);
					artist.setCloseness(closeness);
					artists.add(artist);
				}
				
				recursiveAlbumSearch(artistName, artistFile, criteria, context, albums, songs);
            }
        }
		
		Collections.sort(artists, new Comparator<Artist>() {
			public int compare(Artist lhs, Artist rhs) {
				if(lhs.getCloseness() == rhs.getCloseness()) {
					return 0;
				}
				else if(lhs.getCloseness() > rhs.getCloseness()) {
					return -1;
				}
				else {
					return 1;
				}
			}
		});
		Collections.sort(albums, new Comparator<Entry>() {
			public int compare(Entry lhs, Entry rhs) {
				if(lhs.getCloseness() == rhs.getCloseness()) {
					return 0;
				}
				else if(lhs.getCloseness() > rhs.getCloseness()) {
					return -1;
				}
				else {
					return 1;
				}
			}
		});
		Collections.sort(songs, new Comparator<Entry>() {
			public int compare(Entry lhs, Entry rhs) {
				if(lhs.getCloseness() == rhs.getCloseness()) {
					return 0;
				}
				else if(lhs.getCloseness() > rhs.getCloseness()) {
					return -1;
				}
				else {
					return 1;
				}
			}
		});

		// Respect counts in search criteria
		int artistCount = Math.min(artists.size(), criteria.getArtistCount());
		int albumCount = Math.min(albums.size(), criteria.getAlbumCount());
		int songCount = Math.min(songs.size(), criteria.getSongCount());
		artists = artists.subList(0, artistCount);
		albums = albums.subList(0, albumCount);
		songs = songs.subList(0, songCount);

		return new SearchResult(artists, albums, songs);
    }

	private void recursiveAlbumSearch(String artistName, File file, SearchCritera criteria, Context context, List<Entry> albums, List<Entry> songs) {
		int closeness;
		for(File albumFile : FileUtil.listMediaFiles(file)) {
			if(albumFile.isDirectory()) {
				String albumName = getName(albumFile);
				if((closeness = matchCriteria(criteria, albumName)) > 0) {
					Entry album = createEntry(context, albumFile, albumName);
					album.setArtist(artistName);
					album.setCloseness(closeness);
					albums.add(album);
				}

				for(File songFile : FileUtil.listMediaFiles(albumFile)) {
					String songName = getName(songFile);
					if(songName == null) {
						continue;
					}

					if(songFile.isDirectory()) {
						recursiveAlbumSearch(artistName, songFile, criteria, context, albums, songs);
					}
					else if((closeness = matchCriteria(criteria, songName)) > 0){
						Entry song = createEntry(context, albumFile, songName);
						song.setArtist(artistName);
						song.setAlbum(albumName);
						song.setCloseness(closeness);
						songs.add(song);
					}
				}
			}
			else {
				String songName = getName(albumFile);
				if((closeness = matchCriteria(criteria, songName)) > 0) {
					Entry song = createEntry(context, albumFile, songName);
					song.setArtist(artistName);
					song.setAlbum(songName);
					song.setCloseness(closeness);
					songs.add(song);
				}
			}
		}
	}
	private int matchCriteria(SearchCritera criteria, String name) {
		if (criteria.getPattern().matcher(name).matches()) {
			return Util.getStringDistance(
				criteria.getQuery().toLowerCase(),
				name.toLowerCase());
		} else {
			return 0;
		}
	}

    @Override
    public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
        List<Playlist> playlists = new ArrayList<Playlist>();
        File root = FileUtil.getPlaylistDirectory(context);
		String lastServer = null;
		boolean removeServer = true;
        for (File folder : FileUtil.listFiles(root)) {
			if(folder.isDirectory()) {
				String server = folder.getName();
				SortedSet<File> fileList = FileUtil.listFiles(folder);
				for(File file: fileList) {
					if(FileUtil.isPlaylistFile(file)) {
						String id = file.getName();
						String filename = FileUtil.getBaseName(id);
						String name = server + ": " + filename;
						Playlist playlist = new Playlist(server, name);
						playlist.setComment(filename);

						Reader reader = null;
						BufferedReader buffer = null;
						int songCount = 0;
						try {
							reader = new FileReader(file);
							buffer = new BufferedReader(reader);

							String line = buffer.readLine();
							while( (line = buffer.readLine()) != null ){
								// No matter what, end file can't have .complete in it
								line = line.replace(".complete", "");
								File entryFile = new File(line);

								// Don't add file to playlist if it doesn't exist as cached or pinned!
								File checkFile = entryFile;
								if(!checkFile.exists()) {
									// If normal file doens't exist, check if .complete version does
									checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName())
											+ ".complete." + FileUtil.getExtension(entryFile.getName()));
								}

								String entryName = getName(entryFile);
								if(checkFile.exists() && entryName != null){
									songCount++;
								}
							}

							playlist.setSongCount(Integer.toString(songCount));
						} catch(Exception e) {
							Log.w(TAG, "Failed to count songs in playlist", e);
						} finally {
							Util.close(buffer);
							Util.close(reader);
						}

						if(songCount > 0) {
							playlists.add(playlist);
						}
					}
				}
				
				if(!server.equals(lastServer) && fileList.size() > 0) {
					if(lastServer != null) {
						removeServer = false;
					}
					lastServer = server;
				}
			} else {
				// Delete legacy playlist files
				try {
					folder.delete();
				} catch(Exception e) {
					Log.w(TAG, "Failed to delete old playlist file: " + folder.getName());
				}
			}
        }
		
		if(removeServer) {
			for(Playlist playlist: playlists) {
				playlist.setName(playlist.getName().substring(playlist.getId().length() + 2));
			}
		}
        return playlists;
    }

    @Override
    public MusicDirectory getPlaylist(boolean refresh, String id, String name, Context context, ProgressListener progressListener) throws Exception {
		DownloadService downloadService = DownloadService.getInstance();
        if (downloadService == null) {
            return new MusicDirectory();
        }
		
        Reader reader = null;
		BufferedReader buffer = null;
		try {
			int firstIndex = name.indexOf(id);
			if(firstIndex != -1) {
				name = name.substring(id.length() + 2);
			}
			
			File playlistFile = FileUtil.getPlaylistFile(context, id, name);
			reader = new FileReader(playlistFile);
			buffer = new BufferedReader(reader);
			
			MusicDirectory playlist = new MusicDirectory();
			String line = buffer.readLine();
	    	if(!"#EXTM3U".equals(line)) return playlist;
			
			while( (line = buffer.readLine()) != null ){
				// No matter what, end file can't have .complete in it
				line = line.replace(".complete", "");
				File entryFile = new File(line);
				
				// Don't add file to playlist if it doesn't exist as cached or pinned!
				File checkFile = entryFile;
				if(!checkFile.exists()) {
					// If normal file doens't exist, check if .complete version does
					checkFile = new File(entryFile.getParent(), FileUtil.getBaseName(entryFile.getName())
						+ ".complete." + FileUtil.getExtension(entryFile.getName()));
				}
				
				String entryName = getName(entryFile);
				if(checkFile.exists() && entryName != null){
					playlist.addChild(createEntry(context, entryFile, entryName, false));
				}
			}
			
			return playlist;
		} finally {
			Util.close(buffer);
			Util.close(reader);
		}
    }

    @Override
    public void createPlaylist(String id, String name, List<Entry> entries, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
    }
	
	@Override
	public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}
	
	@Override
	public void addToPlaylist(String id, List<Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}
	
	@Override
	public void removeFromPlaylist(String id, List<Integer> toRemove, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}
	
	@Override
	public void overwritePlaylist(String id, String name, int toRemove, List<Entry> toAdd, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}
	
	@Override
	public void updatePlaylist(String id, String name, String comment, boolean pub, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

    @Override
    public MusicDirectory getAlbumList(String type, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
    }

	@Override
	public MusicDirectory getAlbumList(String type, String extra, int size, int offset, boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public MusicDirectory getSongList(String type, int size, int offset, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public MusicDirectory getRandomSongs(int size, String artistId, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public List<Genre> getGenres(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}
	
	@Override
	public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public MusicDirectory getTopTrackSongs(String artist, int size, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
    public MusicDirectory getRandomSongs(int size, String folder, String genre, String startYear, String endYear, Context context, ProgressListener progressListener) throws Exception {
        File root = FileUtil.getMusicDirectory(context);
        List<File> children = new LinkedList<File>();
        listFilesRecursively(root, children);
        MusicDirectory result = new MusicDirectory();

        if (children.isEmpty()) {
            return result;
        }
        for (int i = 0; i < size; i++) {
            File file = children.get(random.nextInt(children.size()));
            result.addChild(createEntry(context, file, getName(file)));
        }

        return result;
    }

	@Override
	public String getCoverArtUrl(Context context, Entry entry) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public User getUser(boolean refresh, String username, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public List<User> getUsers(boolean refresh, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public void createUser(User user, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public void updateUser(User user, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public void deleteUser(String username, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public void changeEmail(String username, String email, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public void changePassword(String username, String password, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public Bitmap getBitmap(String url, int size, Context context, ProgressListener progressListener, SilentBackgroundTask task) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public void savePlayQueue(List<Entry> songs, Entry currentPlaying, int position, Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

	@Override
	public PlayerQueue getPlayQueue(Context context, ProgressListener progressListener) throws Exception {
		throw new OfflineException(ERRORMSG);
	}

    @Override
    public void setInstance(Integer instance) throws Exception{
		throw new OfflineException(ERRORMSG);
    }

    private void listFilesRecursively(File parent, List<File> children) {
        for (File file : FileUtil.listMediaFiles(parent)) {
            if (file.isFile()) {
                children.add(file);
            } else {
                listFilesRecursively(file, children);
            }
        }
    }
}
