/*
 * PS3 Media Server, for streaming any medias to your PS3.
 * Copyright (C) 2008  A.Brochard
 *
 * This program 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; version 2
 * of the License only.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package net.pms.network;

import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import net.pms.configuration.RendererConfiguration;
import net.pms.io.OutputParams;
import net.pms.io.ProcessWrapperImpl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.jna.Platform;

/**
 * Network speed tester class. This can be used in an asynchronous way, as it returns Future objects.
 * 
 * Future<Integer> speed = SpeedStats.getInstance().getSpeedInMBits(addr);
 * 
 *  @see Future
 * 
 * @author zsombor <gzsombor@gmail.com>
 *
 */
public class SpeedStats {
	private static SpeedStats instance = new SpeedStats();
	private static ExecutorService executor = Executors.newCachedThreadPool();
	public static SpeedStats getInstance() {
		return instance;
	}

	private static final Logger logger = LoggerFactory.getLogger(RendererConfiguration.class);

	private final Map<String, Future<Integer>> speedStats = new HashMap<String, Future<Integer>>();

	/**
	 * Return the network throughput for the given IP address in MBits. It is calculated in the background, and cached, 
	 * so only a reference is given to the result, which can be retrieved with calling get() method on it.
	 * @param addr
	 * @return  The network throughput
	 */
	public Future<Integer> getSpeedInMBits(InetAddress addr, String rendererName) {
		synchronized(speedStats) { 
			Future<Integer> value = speedStats.get(addr.getHostAddress());
			if (value != null) {
				return value;
			}
			value = executor.submit(new MeasureSpeed(addr, rendererName));
			speedStats.put(addr.getHostAddress(), value);
			return value;
		}
	}

	class MeasureSpeed implements Callable<Integer> {
		InetAddress addr;
		String rendererName;

		public MeasureSpeed(InetAddress addr, String rendererName) {
			this.addr = addr;
			this.rendererName = rendererName != null ? rendererName : "Unknown";
		}

		@Override
		public Integer call() throws Exception {
			try {
				return doCall();
			} catch (Exception e) {
				logger.warn("error during measuring network throughput : "+e.getMessage(), e);
				throw e;
			}
		}

		private Integer doCall() throws Exception {
			String ip = addr.getHostAddress();
			logger.info("Checking ip:" + ip + " for " + rendererName);
			// calling canonical host name at the first time is slow, so we call it in a separate thread
			String hostname = addr.getCanonicalHostName();
			synchronized(speedStats) {
				Future<Integer> otherTask = speedStats.get(hostname);
				if (otherTask != null) {
					// wait a little bit
					try {
						// probably we are waiting for ourself, to finishing the work ...  
						Integer value = otherTask.get(100, TimeUnit.MILLISECONDS);
						// if the other task already calculated, the speed, we get the result, 
						// unless we will do it now 
						if (value != null) {
							return value;
						}
					} catch (TimeoutException e) {
						logger.trace("we couldn't get the value based on canonical name");
					}
				}
			}

			
			if (!ip.equals(hostname)) {
				logger.info("Renderer " + rendererName + " found on this address: " + hostname + " (" + ip + ")");
			} else {
				logger.info("Renderer " + rendererName + " found on this address: " + ip);
			}

			// let's get that speed
			OutputParams op = new OutputParams(null);
			op.log = true;
			op.maxBufferSize = 1;
			String count = Platform.isWindows() ? "-n" : "-c";
			String size = Platform.isWindows() ? "-l" : "-s";
			final ProcessWrapperImpl pw = new ProcessWrapperImpl(new String[] { "ping", count, "3", size, "64000", addr.getHostAddress() }, op,
					true, false);
			Runnable r = new Runnable() {
				public void run() {
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
					}
					pw.stopProcess();
				}
			};
			Thread failsafe = new Thread(r);
			failsafe.start();
			pw.run();
			List<String> ls = pw.getOtherResults();
			int time = 0;
			int c = 0;
			for (String line : ls) {
				int msPos = line.indexOf("ms");
				try {
					if (msPos > -1) {
						String timeString = line.substring(line.lastIndexOf("=", msPos) + 1, msPos).trim();
						time += Double.parseDouble(timeString);
						c++;
					}
				} catch (Exception e) {
					// no big deal
				}
			}
			if (c > 0) {
				time = (int) (time / c);
			}

			if (time > 0) {
				int speedInMbits = (int) (1024 / time);
				logger.info("Address " + addr + " has an estimated network speed of: " + speedInMbits + " Mb/s");
				synchronized(speedStats) {
					CompletedFuture<Integer> result = new CompletedFuture<Integer>(speedInMbits);
					// change the statistics with a computed future values
					speedStats.put(ip, result);
					speedStats.put(hostname, result);
				}
				return speedInMbits;
			}
			return -1;
		}
	}

	static class CompletedFuture<X> implements Future<X> {
		X value;
		
		public CompletedFuture(X value) {
			this.value = value;
		}

		@Override
		public boolean cancel(boolean mayInterruptIfRunning) {
			return false;
		}

		@Override
		public boolean isCancelled() {
			return false;
		}

		@Override
		public boolean isDone() {
			return true;
		}

		@Override
		public X get() throws InterruptedException, ExecutionException {
			return value;
		}

		@Override
		public X get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
			return value;
		}
	}
}
