/*
 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 com.thejoshwa.ultrasonic.androidapp.service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
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 com.thejoshwa.ultrasonic.androidapp.domain.MusicDirectory;
import com.thejoshwa.ultrasonic.androidapp.util.CancellableTask;
import com.thejoshwa.ultrasonic.androidapp.util.FileUtil;
import com.thejoshwa.ultrasonic.androidapp.util.Util;
import com.thejoshwa.ultrasonic.androidapp.util.CacheCleaner;

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

/**
 * @author Sindre Mehus
 * @version $Id$
 */
public class DownloadFile {

    private static final String TAG = DownloadFile.class.getSimpleName();
    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 CancellableTask downloadTask;
    private boolean save;
    private boolean failed;
    private int bitRate;
	private volatile boolean isPlaying = false;
	private volatile boolean saveWhenDone = false;
	private volatile boolean completeWhenDone = 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 = Util.getMaxBitRate(context);
        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;
    }

    /**
     * Returns the effective bit rate.
     */
    public int getBitRate() {
		if (!partialFile.exists()) {
			bitRate = Util.getMaxBitRate(context);
		}
		
        if (bitRate > 0) {
            return bitRate;
        }
        
        return song.getBitRate() == null ? 160 : song.getBitRate();
    }

    public synchronized void download() {
        FileUtil.createDirectoryForParent(saveFile);
        failed = false;
        
		if (!partialFile.exists()) {
			bitRate = Util.getMaxBitRate(context);
		}
		
        downloadTask = new DownloadTask();
        downloadTask.start();
    }

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

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

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

        return saveFile;
    }

    public File getPartialFile() {
        return partialFile;
    }

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

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

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

    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 failed;
    }

    public void delete() {
        cancelDownload();
        Util.delete(partialFile);
        Util.delete(completeFile);
        Util.delete(saveFile);
        mediaStoreService.deleteFromMediaStore(this);
    }

    public void unpin() {
        if (saveFile.exists()) {
            saveFile.renameTo(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);
				saveWhenDone = false;
			} else if(completeWhenDone && !isPlaying) {
				if (save) {
					Util.renameFile(partialFile, saveFile);
                    mediaStoreService.saveInMediaStore(DownloadFile.this);
				} else {
					Util.renameFile(partialFile, completeFile);
				}
				
				completeWhenDone = false;
			}
		} catch(IOException ex) {
			Log.w(TAG, "Failed to rename file " + completeFile + " to " + saveFile);
		}
		
		this.isPlaying = isPlaying;
	}

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

    private class DownloadTask extends CancellableTask {

        @Override
        public void execute() {

            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();
                    Log.i(TAG, "Acquired wake lock " + wakeLock);
                }
				
				wifiLock = Util.createWifiLock(context, toString());
				wifiLock.acquire();

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

                MusicService musicService = MusicServiceFactory.getMusicService(context);

				// Some devices seem to throw error on partial file which doesn't exist
				boolean compare;
				
				Integer duration = song.getDuration();
				long fileLength = 0;
				
				if (!partialFile.exists()) {
					fileLength = partialFile.length();
				}
				
				try {
					compare = (bitRate == 0) || (duration == null || duration == 0) || (fileLength == 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);
					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");
					}

					downloadAndSaveCoverArt(musicService);
				}

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

            } catch (Exception x) {
                Util.close(out);
                Util.delete(completeFile);
                Util.delete(saveFile);
                if (!isCancelled()) {
                    failed = 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();
				}
                new CacheCleaner(context, DownloadServiceImpl.getInstance()).cleanSpace();
				if(DownloadServiceImpl.getInstance() != null) {
					((DownloadServiceImpl)DownloadServiceImpl.getInstance()).checkDownloads();
				}
            }
        }

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

        private void downloadAndSaveCoverArt(MusicService musicService) throws Exception {
            try {
                if (song.getCoverArt() != null) {
                	int size = Util.getMinDisplayMetric(context);
                    musicService.getCoverArt(context, song, size, true, true, 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() {
                @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();

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

                long now = System.currentTimeMillis();
                if (now - lastLog > 3000L) {  // Only every so often.
                    Log.i(TAG, "Downloaded " + Util.formatBytes(count) + " of " + song);
                    lastLog = now;
                }
            }
            return count;
        }
    }
}