package net.oni2.httpfiledownloader;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashSet;

import net.oni2.ProxySettings;

/**
 * @author Christian Illy
 */
public class FileDownloader implements Runnable {
	/**
	 * @author Christian Illy
	 */
	public enum EState {
		/**
		 * Downloader initialized but not started
		 */
		INIT,
		/**
		 * Download running
		 */
		RUNNING,
		/**
		 * Download suspended
		 */
		PAUSED,
		/**
		 * Download interrupted
		 */
		INTERRUPTED,
		/**
		 * Download finished successfully
		 */
		FINISHED,
		/**
		 * Aborted because of an error
		 */
		ERROR
	};

	private HashSet<FileDownloadListener> listeners = new HashSet<FileDownloadListener>();
	private Thread t = null;
	private URL url = null;
	private File target = null;
	private int fileLength = Integer.MAX_VALUE;
	private int downloaded = 0;

	private EState state = EState.INIT;

	/**
	 * @param url
	 *            URL of file to download
	 * @param target
	 *            Path of target file to save to
	 * @throws IOException
	 *             If url could not be opened
	 */
	public FileDownloader(String url, File target) throws IOException {
		this.url = new URL(url);
		this.target = target;
	}

	/**
	 * @param listener
	 *            Listener to add
	 */
	public void addListener(FileDownloadListener listener) {
		listeners.add(listener);
	}

	/**
	 * @param listener
	 *            Listener to remove
	 */
	public void removeListener(FileDownloadListener listener) {
		listeners.remove(listener);
	}

	/**
	 * Start the download process
	 */
	public synchronized void start() {
		if (t == null) {
			state = EState.RUNNING;
			t = new Thread(this);
			t.start();
		}
	}

	/**
	 * @param suspend
	 *            Suspend or resume
	 */
	public synchronized void suspend(boolean suspend) {
		if ((state == EState.RUNNING) || (state == EState.PAUSED)) {
			if (suspend)
				state = EState.PAUSED;
			else
				state = EState.RUNNING;
			updateStatus(downloaded, fileLength);
		}
	}

	/**
	 * Stop (abort) download
	 */
	public synchronized void stop() {
		if (state != EState.FINISHED) {
			state = EState.INTERRUPTED;
			if (t != null) {
				try {
					t.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				t = null;
			}
			updateStatus(0, 1);
			if (target.exists())
				target.delete();
		}
	}

	/**
	 * @return current state
	 */
	public EState getState() {
		return state;
	}

	private synchronized void updateStatus(int current, int total) {
		downloaded = current;
		for (FileDownloadListener l : listeners) {
			l.statusUpdate(this, state, current, total);
		}
	}

	@Override
	public void run() {
		int downloaded = 0;
		String strLastModified = null;
		String strEtag = null;
		RandomAccessFile outFile = null;

		try {
			outFile = new RandomAccessFile(target, "rw");
		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
			state = EState.ERROR;
			updateStatus(downloaded, fileLength);
			return;
		}

		try {
			while (downloaded < fileLength) {
				switch (state) {
					case ERROR:
						updateStatus(downloaded, fileLength);
						return;
					case PAUSED:
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						break;
					case INTERRUPTED:
						return;
					case RUNNING:
						BufferedInputStream input = null;
						try {
							URLConnection connection = url
									.openConnection(ProxySettings.getInstance()
											.getProxy());
							connection.setRequestProperty("Cache-Control",
									"no-cache");
							if (downloaded == 0) {
								connection.connect();
								strLastModified = connection
										.getHeaderField("Last-Modified");
								strEtag = connection.getHeaderField("ETag");
								fileLength = connection.getContentLength();
							} else {
								connection.setRequestProperty("Range", "bytes="
										+ downloaded + "-");
								if (strEtag != null)
									connection.setRequestProperty("If-Range",
											strEtag);
								else
									connection.setRequestProperty("If-Range",
											strLastModified);
								connection.connect();
							}

							// Setup streams and buffers.
							input = new BufferedInputStream(
									connection.getInputStream(), 8192);
							if (downloaded > 0)
								outFile.seek(downloaded);
							byte data[] = new byte[1024];

							// Download file.
							int dataRead = 0;
							int i = 0;
							while (((dataRead = input.read(data, 0, 1024)) != -1)
									&& (state == EState.RUNNING)) {
								outFile.write(data, 0, dataRead);
								downloaded += dataRead;
								if (downloaded >= fileLength)
									break;

								i++;
								if ((i % 50) == 0)
									updateStatus(downloaded, fileLength);
							}
							input.close();
						} catch (IOException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
							try {
								if (input != null)
									input.close();
							} catch (IOException e2) {
								e2.printStackTrace();
							}
						}
						break;
					default:
						break;
				}
			}
		} finally {
			try {
				// Close streams.
				outFile.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		state = EState.FINISHED;
		updateStatus(downloaded, fileLength);
	}

	/**
	 * @return the target
	 */
	public File getTarget() {
		return target;
	}

	/**
	 * @return the downloaded size
	 */
	public int getDownloaded() {
		return downloaded;
	}

}
