/*
 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.FileOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;

import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.util.Log;

import net.nullsum.audinaut.domain.MusicDirectory;
import net.nullsum.audinaut.util.BufferFile;
import net.nullsum.audinaut.util.CacheCleaner;
import net.nullsum.audinaut.util.Constants;
import net.nullsum.audinaut.util.FileUtil;
import net.nullsum.audinaut.util.SilentBackgroundTask;
import net.nullsum.audinaut.util.Util;

import org.apache.http.Header;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;

/**
 * @author Sindre Mehus
 * @version $Id$
 */
public class DownloadFile implements BufferFile {
    private static final String TAG = DownloadFile.class.getSimpleName();
    private static final int MAX_FAILURES = 5;
    private final Context context;
    private final MusicDirectory.Entry song;
    private final File partialFile;
    private final File completeFile;
    private final File saveFile;

    private final MediaStoreService mediaStoreService;
    private DownloadTask downloadTask;
    private boolean save;
	private boolean failedDownload = false;
    private int failed = 0;
    private int bitRate;
	private boolean isPlaying = false;
	private boolean saveWhenDone = false;
	private boolean completeWhenDone = false;
	private Long contentLength = null;
	private long currentSpeed = 0;
	private boolean rateLimit = false;

    public DownloadFile(Context context, MusicDirectory.Entry song, boolean save) {
        this.context = context;
        this.song = song;
        this.save = save;
        saveFile = FileUtil.getSongFile(context, song);
        bitRate = getActualBitrate();
        partialFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
                ".partial." + FileUtil.getExtension(saveFile.getName()));
        completeFile = new File(saveFile.getParent(), FileUtil.getBaseName(saveFile.getName()) +
                ".complete." + FileUtil.getExtension(saveFile.getName()));
        mediaStoreService = new MediaStoreService(context);
    }

    public MusicDirectory.Entry getSong() {
        return song;
    }
	public boolean isSong() {
		return song.isSong();
	}

	public Context getContext() {
		return context;
	}

    /**
     * Returns the effective bit rate.
     */
    public int getBitRate() {
		if(!partialFile.exists()) {
			bitRate = getActualBitrate();
		}
        if (bitRate > 0) {
            return bitRate;
        }
        return song.getBitRate() == null ? 160 : song.getBitRate();
    }
	private int getActualBitrate() {
		int br = Util.getMaxBitrate(context);
		if(br == 0 && song.getTranscodedSuffix() != null && "mp3".equals(song.getTranscodedSuffix().toLowerCase())) {
			if(song.getBitRate() != null) {
				br = Math.min(320, song.getBitRate());
			} else {
				br = 320;
			}
		} else if(song.getSuffix() != null && (song.getTranscodedSuffix() == null || song.getSuffix().equals(song.getTranscodedSuffix()))) {
			// If just downsampling, don't try to upsample (ie: 128 kpbs -> 192 kpbs)
			if(song.getBitRate() != null && (br == 0 || br > song.getBitRate())) {
				br = song.getBitRate();
			}
		}

		return br;
	}
	
	public Long getContentLength() {
		return contentLength;
	}

	public long getCurrentSize() {
		if(partialFile.exists()) {
			return partialFile.length();
		} else {
			File file = getCompleteFile();
			if(file.exists()) {
				return file.length();
			} else {
				return 0L;
			}
		}
	}

	@Override
	public long getEstimatedSize() {
		if(contentLength != null) {
			return contentLength;
		}

		File file = getCompleteFile();
		if(file.exists()) {
			return file.length();
		} else if(song.getDuration() == null) {
			return 0;
		} else {
			int br = (getBitRate() * 1000) / 8;
			int duration = song.getDuration();
			return br * duration;
		}
	}

	public long getBytesPerSecond() {
		return currentSpeed;
	}

    public synchronized void download() {
    	rateLimit = false;
        preDownload();
        downloadTask.execute();
    }
    public synchronized void downloadNow(MusicService musicService) {
    	rateLimit = true;
    	preDownload();
		downloadTask.setMusicService(musicService);
		try {
			downloadTask.doInBackground();
		} catch(InterruptedException e) {
			// This should never be reached
		}
    }
    private void preDownload() {
    	FileUtil.createDirectoryForParent(saveFile);
        failedDownload = false;
		if(!partialFile.exists()) {
			bitRate = getActualBitrate();
		}
		downloadTask = new DownloadTask(context);
    }

    public synchronized void cancelDownload() {
        if (downloadTask != null) {
            downloadTask.cancel();
        }
    }

	@Override
	public File getFile() {
		if (saveFile.exists()) {
			return saveFile;
		} else if (completeFile.exists()) {
			return completeFile;
		} else {
			return partialFile;
		}
	}

    public File getCompleteFile() {
        if (saveFile.exists()) {
            return saveFile;
        }

        if (completeFile.exists()) {
            return completeFile;
        }

        return saveFile;
    }
	public File getSaveFile() {
		return saveFile;
	}

    public File getPartialFile() {
        return partialFile;
    }

    public boolean isSaved() {
        return saveFile.exists();
    }

    public synchronized boolean isCompleteFileAvailable() {
        return saveFile.exists() || completeFile.exists();
    }

	@Override
    public synchronized boolean isWorkDone() {
        return saveFile.exists() || (completeFile.exists() && !save) || saveWhenDone || completeWhenDone;
    }

	@Override
	public void onStart() {
		setPlaying(true);
	}

	@Override
	public void onStop() {
		setPlaying(false);
	}

	@Override
	public synchronized void onResume() {
		if(!isWorkDone() && !isFailedMax() && !isDownloading() && !isDownloadCancelled()) {
			download();
		}
	}

	public synchronized boolean isDownloading() {
        return downloadTask != null && downloadTask.isRunning();
    }

    public synchronized boolean isDownloadCancelled() {
        return downloadTask != null && downloadTask.isCancelled();
    }

    public boolean shouldSave() {
        return save;
    }

    public boolean isFailed() {
        return failedDownload;
    }
    public boolean isFailedMax() {
    	return failed > MAX_FAILURES;
    }

    public void delete() {
        cancelDownload();
        
        // Remove from mediaStore BEFORE deleting file since it calls getCompleteFile
		deleteFromStore();
		
		// Delete all possible versions of the file
		File parent = partialFile.getParentFile();
        Util.delete(partialFile);
        Util.delete(completeFile);
        Util.delete(saveFile);
		FileUtil.deleteEmptyDir(parent);
    }

    public void unpin() {
        if (saveFile.exists()) {
        	// Delete old store entry before renaming to pinned file
            saveFile.renameTo(completeFile);
			renameInStore(saveFile, completeFile);
        }
    }

    public boolean cleanup() {
        boolean ok = true;
        if (completeFile.exists() || saveFile.exists()) {
            ok = Util.delete(partialFile);
        }
        if (saveFile.exists()) {
            ok &= Util.delete(completeFile);
        }
        return ok;
    }

    // In support of LRU caching.
    public void updateModificationDate() {
        updateModificationDate(saveFile);
        updateModificationDate(partialFile);
        updateModificationDate(completeFile);
    }

    private void updateModificationDate(File file) {
        if (file.exists()) {
            boolean ok = file.setLastModified(System.currentTimeMillis());
            if (!ok) {
                Log.w(TAG, "Failed to set last-modified date on " + file);
            }
        }
    }
	
	public void setPlaying(boolean isPlaying) {
		try {
			if(saveWhenDone && !isPlaying) {
				Util.renameFile(completeFile, saveFile);
				renameInStore(completeFile, saveFile);
				saveWhenDone = false;
			} else if(completeWhenDone && !isPlaying) {
				if(save) {
					Util.renameFile(partialFile, saveFile);
                    saveToStore();
				} else {
					Util.renameFile(partialFile, completeFile);
					saveToStore();
				}
				completeWhenDone = false;
			}
		} catch(IOException ex) {
			Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile, ex);
		}
		
		this.isPlaying = isPlaying;
	}
	public void renamePartial() {
		try {
			Util.renameFile(partialFile, completeFile);
			saveToStore();
		} catch(IOException ex) {
			Log.w(TAG, "Failed to rename file " + partialFile + " to " + completeFile, ex);
		}
	}
	public boolean getPlaying() {
		return isPlaying;
	}
	
	private void deleteFromStore() {
		try {
			mediaStoreService.deleteFromMediaStore(this);
		} catch(Exception e) {
			Log.w(TAG, "Failed to remove from store", e);
		}
	}
	private void saveToStore() {
		if(!Util.getPreferences(context).getBoolean(Constants.PREFERENCES_KEY_HIDE_MEDIA, false)) {
			try {
				mediaStoreService.saveInMediaStore(this);
			} catch(Exception e) {
				Log.w(TAG, "Failed to save in media store", e);
			}
		}
	}
	private void renameInStore(File start, File end) {
		try {
			mediaStoreService.renameInMediaStore(start, end);
		} catch(Exception e) {
			Log.w(TAG, "Failed to rename in store", e);
		}
	}

    @Override
    public String toString() {
        return "DownloadFile (" + song + ")";
    }

	// Don't do this.  Causes infinite loop if two instances of same song
	/*@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}

		DownloadFile downloadFile = (DownloadFile) o;
		return Util.equals(this.getSong(), downloadFile.getSong());
	}*/

    private class DownloadTask extends SilentBackgroundTask<Void> {
		private MusicService musicService;

		public DownloadTask(Context context) {
			super(context);
		}

        @Override
        public Void doInBackground() throws InterruptedException {
            InputStream in = null;
            FileOutputStream out = null;
            PowerManager.WakeLock wakeLock = null;
			WifiManager.WifiLock wifiLock = null;
            try {

                if (Util.isScreenLitOnDownload(context)) {
                    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                    wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, toString());
                    wakeLock.acquire();
                }
				
				wifiLock = Util.createWifiLock(context, toString());
				wifiLock.acquire();

                if (saveFile.exists()) {
                    Log.i(TAG, saveFile + " already exists. Skipping.");
                    checkDownloads();
                    return null;
                }
                if (completeFile.exists()) {
                    if (save) {
						if(isPlaying) {
							saveWhenDone = true;
						} else {
							Util.renameFile(completeFile, saveFile);
							renameInStore(completeFile, saveFile);
						}
                    } else {
                        Log.i(TAG, completeFile + " already exists. Skipping.");
                    }
                    checkDownloads();
                    return null;
                }

				if(musicService == null) {
                	musicService = MusicServiceFactory.getMusicService(context);
				}

				// Some devices seem to throw error on partial file which doesn't exist
				boolean compare;
				try {
					compare = (bitRate == 0) || (song.getDuration() == 0) || (partialFile.length() == 0) || (bitRate * song.getDuration() * 1000 / 8) > partialFile.length();
				} catch(Exception e) {
					compare = true;
				}
				if(compare) {
					// Attempt partial HTTP GET, appending to the file if it exists.
					HttpResponse response = musicService.getDownloadInputStream(context, song, partialFile.length(), bitRate, DownloadTask.this);
					Header contentLengthHeader = response.getFirstHeader("Content-Length");
					if(contentLengthHeader != null) {
						String contentLengthString = contentLengthHeader.getValue();
						if(contentLengthString != null) {
							Log.i(TAG, "Content Length: " + contentLengthString);
							contentLength = Long.parseLong(contentLengthString);
						}
					}
					in = response.getEntity().getContent();
					boolean partial = response.getStatusLine().getStatusCode() == HttpStatus.SC_PARTIAL_CONTENT;
					if (partial) {
						Log.i(TAG, "Executed partial HTTP GET, skipping " + partialFile.length() + " bytes");
					}

					out = new FileOutputStream(partialFile, partial);
					long n = copy(in, out);
					Log.i(TAG, "Downloaded " + n + " bytes to " + partialFile);
					out.flush();
					out.close();

					if (isCancelled()) {
						throw new Exception("Download of '" + song + "' was cancelled");
					} else if(partialFile.length() == 0) {
						throw new Exception("Download of '" + song + "' failed.  File is 0 bytes long.");
					}

					downloadAndSaveCoverArt(musicService);
				}

				if(isPlaying) {
					completeWhenDone = true;
				} else {
					if(save) {
						Util.renameFile(partialFile, saveFile);
					} else {
						Util.renameFile(partialFile, completeFile);
					}
					DownloadFile.this.saveToStore();
				}

            } catch(InterruptedException x) {
				throw x;
			} catch(FileNotFoundException x) {
				Util.delete(completeFile);
				Util.delete(saveFile);
				if(!isCancelled()) {
					failed = MAX_FAILURES + 1;
					failedDownload = true;
					Log.w(TAG, "Failed to download '" + song + "'.", x);
				}
			} catch(IOException x) {
				Util.delete(completeFile);
				Util.delete(saveFile);
				if(!isCancelled()) {
					failedDownload = true;
					Log.w(TAG, "Failed to download '" + song + "'.", x);
				}
			} catch (Exception x) {
                Util.delete(completeFile);
                Util.delete(saveFile);
                if (!isCancelled()) {
                	failed++;
                    failedDownload = true;
                    Log.w(TAG, "Failed to download '" + song + "'.", x);
                }
            } finally {
                Util.close(in);
                Util.close(out);
                if (wakeLock != null) {
                    wakeLock.release();
                    Log.i(TAG, "Released wake lock " + wakeLock);
                }
				if (wifiLock != null) {
					wifiLock.release();
				}
			}
			
			// Only run these if not interrupted, ie: cancelled
			DownloadService downloadService = DownloadService.getInstance();
			if(downloadService != null && !isCancelled()) {
				new CacheCleaner(context, downloadService).cleanSpace();
				checkDownloads();
			}

			return null;
        }
        
        private void checkDownloads() {
        	DownloadService downloadService = DownloadService.getInstance();
        	if(downloadService != null) {
        		downloadService.checkDownloads();
        	}
        }

        @Override
        public String toString() {
            return "DownloadTask (" + song + ")";
        }

		public void setMusicService(MusicService musicService) {
			this.musicService = musicService;
		}

        private void downloadAndSaveCoverArt(MusicService musicService) throws Exception {
            try {
                if (song.getCoverArt() != null) {
					// Check if album art already exists, don't want to needlessly load into memory
					File albumArtFile = FileUtil.getAlbumArtFile(context, song);
					if(!albumArtFile.exists()) {
						musicService.getCoverArt(context, song, 0, null, null);
					}
                }
            } catch (Exception x) {
                Log.e(TAG, "Failed to get cover art.", x);
            }
        }

        private long copy(final InputStream in, OutputStream out) throws IOException, InterruptedException {

            // Start a thread that will close the input stream if the task is
            // cancelled, thus causing the copy() method to return.
            new Thread("DownloadFile_copy") {
                @Override
                public void run() {
                    while (true) {
                        Util.sleepQuietly(3000L);
                        if (isCancelled()) {
                            Util.close(in);
                            return;
                        }
                        if (!isRunning()) {
                            return;
                        }
                    }
                }
            }.start();

            byte[] buffer = new byte[1024 * 16];
            long count = 0;
            int n;
            long lastLog = System.currentTimeMillis();
			long lastCount = 0;

			boolean activeLimit = rateLimit;
            while (!isCancelled() && (n = in.read(buffer)) != -1) {
                out.write(buffer, 0, n);
                count += n;
				lastCount += n;

                long now = System.currentTimeMillis();
                if (now - lastLog > 3000L) {  // Only every so often.
                    Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
					currentSpeed = lastCount / ((now - lastLog) / 1000L);
                    lastLog = now;
					lastCount = 0;
					
					// Re-establish every few seconds whether screen is on or not
					if(rateLimit) {
						PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
						if(pm.isScreenOn()) {
							activeLimit = true;
						} else {
							activeLimit = false;
						}
					}
                }
                
                // If screen is on and rateLimit is true, stop downloading from exhausting bandwidth
                if(activeLimit) {
                	Thread.sleep(10L);
                }
            }
            return count;
        }
    }
}
