package net.pms.update;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Observable;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import net.pms.PMS;
import net.pms.util.UriRetriever;
import net.pms.util.UriRetrieverCallback;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Checks for and downloads new versions of PMS.
 * 
 * @author Tim Cox (mail@tcox.org)
 */
public class AutoUpdater extends Observable implements UriRetrieverCallback {
	private static final String TARGET_FILENAME = "new-version.exe";
	private static final Logger logger = LoggerFactory.getLogger(AutoUpdater.class);

	public static enum State {
		NOTHING_KNOWN, POLLING_SERVER, NO_UPDATE_AVAILABLE, UPDATE_AVAILABLE, DOWNLOAD_IN_PROGRESS, DOWNLOAD_FINISHED, EXECUTING_SETUP, ERROR
	}
	private final String serverUrl;
	private final UriRetriever uriRetriever = new UriRetriever();
	private final AutoUpdaterServerProperties serverProperties = new AutoUpdaterServerProperties();
	private final OperatingSystem operatingSystem = new OperatingSystem();
	private final Version currentVersion;
	private Executor executor = Executors.newSingleThreadExecutor();
	private State state = State.NOTHING_KNOWN;
	private Object stateLock = new Object();
	private Throwable errorStateCause;
	private int bytesDownloaded = -1;
	private int totalBytes = -1;
	private boolean downloadCancelled = false;

	public AutoUpdater(String updateServerUrl, String currentVersion) {
		this.serverUrl = updateServerUrl; // may be null if updating is disabled
		this.currentVersion = new Version(currentVersion);
	}

	public void pollServer() {
		if (serverUrl != null) { // don't poll if the server URL is null
			executor.execute(new Runnable() {
				public void run() {
					try {
						doPollServer();
					} catch (UpdateException e) {
						setErrorState(e);
					}
				}
			});
		}
	}

	private void doPollServer() throws UpdateException {
		assertNotInErrorState();
		String propertiesUrl = getPropertiesUrl();
		try {
			setState(State.POLLING_SERVER);
			byte[] propertiesAsData = uriRetriever.get(propertiesUrl);
			synchronized (stateLock) {
				serverProperties.loadFrom(propertiesAsData);
				setState(isUpdateAvailable() ? State.UPDATE_AVAILABLE : State.NO_UPDATE_AVAILABLE);
			}
		} catch (IOException e) {
			wrapException(propertiesUrl, "Cannot download properties", e);
		}
	}

	public void getUpdateFromNetwork() {
		executor.execute(new Runnable() {
			public void run() {
				try {
					doGetUpdateFromNetwork();
				} catch (UpdateException e) {
					setErrorState(e);
				}
			}
		});
	}

	public void runUpdateAndExit() {
		executor.execute(new Runnable() {
			public void run() {
				try {
					doRunUpdateAndExit();
				} catch (UpdateException e) {
					setErrorState(e);
				}
			}
		});
	}

	private void setErrorState(UpdateException e) {
		synchronized (stateLock) {
			setState(State.ERROR);
			errorStateCause = e;
		}
	}

	private void doGetUpdateFromNetwork() throws UpdateException {
		assertNotInErrorState();
		assertUpdateIsAvailable();

		setState(State.DOWNLOAD_IN_PROGRESS);
		downloadUpdate();
		setState(State.DOWNLOAD_FINISHED);
	}

	private void doRunUpdateAndExit() throws UpdateException {
		synchronized (stateLock) {
			if (state != State.DOWNLOAD_FINISHED) {
				throw new UpdateException("Must download before run");
			}
		}

		setState(State.EXECUTING_SETUP);
		launchExe();
		System.exit(0);
	}

	private void launchExe() throws UpdateException {
		try {
			File exe = new File(TARGET_FILENAME);
			if (!exe.exists()) {
				exe = new File(PMS.getConfiguration().getTempFolder(), TARGET_FILENAME);
			}
			Runtime.getRuntime().exec(exe.getAbsolutePath());
		} catch (IOException e) {
			wrapException(serverProperties.getDownloadUrl(), "Unable to run update. You may need to manually download it.", e);
		}
	}

	private void assertUpdateIsAvailable() throws UpdateException {
		synchronized (stateLock) {
			if (!serverProperties.isStateValid()) {
				throw new UpdateException("Server error. Try again later.");
			}

			if (!isUpdateAvailable()) {
				throw new UpdateException("Attempt to perform non-existent update");
			}
		}
	}

	private void assertNotInErrorState() throws UpdateException {
		synchronized (stateLock) {
			if (state == State.ERROR) {
				throw new UpdateException("Update system must be reset after an error.");
			}
		}
	}

	private synchronized void setState(State value) {
		synchronized (stateLock) {
			state = value;
			if (state == State.DOWNLOAD_FINISHED) {
				bytesDownloaded = totalBytes;
			} else if (state != State.DOWNLOAD_IN_PROGRESS) {
				bytesDownloaded = -1;
				totalBytes = -1;
			}
			if (state != State.ERROR) {
				errorStateCause = null;
			}
		}

		setChanged();
		notifyObservers();
	}

	public boolean isUpdateAvailable() {
		// TODO(tcox):  Make updates work on Linux and Mac
		return operatingSystem.isWindows() && serverProperties.getLatestVersion().isGreaterThan(currentVersion);
	}

	private void downloadUpdate() throws UpdateException {
		String downloadUrl = serverProperties.getDownloadUrl();
		try {
			byte[] download = uriRetriever.getWithCallback(downloadUrl, this);
			writeToDisk(download);
		} catch (IOException e) {
			wrapException(downloadUrl, "Cannot download update", e);
		}
	}

	private void writeToDisk(byte[] download) throws IOException {
		File target = new File(TARGET_FILENAME);
		InputStream downloadedFromNetwork = new ByteArrayInputStream(download);
		FileOutputStream fileOnDisk = null;
		try {
			try {
				fileOnDisk = new FileOutputStream(target);
				fileOnDisk.write("test".getBytes());
			} catch (Exception e) {
				// seems no rights
				target = new File(PMS.getConfiguration().getTempFolder(), TARGET_FILENAME);
			} finally {
				fileOnDisk.close();
			}
			fileOnDisk = new FileOutputStream(target);
			int bytesSaved = IOUtils.copy(downloadedFromNetwork, fileOnDisk);
			logger.info("Wrote " + bytesSaved + " bytes to " + target.getAbsolutePath());
		} finally {
			IOUtils.closeQuietly(downloadedFromNetwork);
			IOUtils.closeQuietly(fileOnDisk);
		}
	}

	private void wrapException(String downloadUrl, String message, Throwable cause) throws UpdateException {
		throw new UpdateException("Error: " + message, cause);
	}

	private String getPropertiesUrl() {
		return serverUrl + "?currentVersion=" + currentVersion + "&operatingSystem=" + operatingSystem + "&cacheBuster=" + Math.random();
	}

	@Override
	public void progressMade(String uri, int bytesDownloaded, int totalBytes) throws CancelDownloadException {
		synchronized (stateLock) {
			this.bytesDownloaded = bytesDownloaded;
			this.totalBytes = totalBytes;

			if (downloadCancelled) {
				setErrorState(new UpdateException("Download cancelled"));
				throw new CancelDownloadException();
			}
		}

		setChanged();
		notifyObservers();
	}

	public State getState() {
		synchronized (stateLock) {
			return state;
		}
	}

	public Throwable getErrorStateCause() {
		synchronized (stateLock) {
			return errorStateCause;
		}
	}

	public int getBytesDownloaded() {
		synchronized (stateLock) {
			return bytesDownloaded;
		}
	}

	public int getTotalBytes() {
		synchronized (stateLock) {
			return totalBytes;
		}
	}

	public void cancelDownload() {
		synchronized (stateLock) {
			downloadCancelled = true;
		}
	}

	public boolean isDownloadCancelled() {
		synchronized (stateLock) {
			return downloadCancelled;
		}
	}
}
