package com.github.sarxos.webcam;

import static com.github.sarxos.webcam.WebcamExceptionHandler.handle;

import java.awt.image.BufferedImage;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

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

import com.github.sarxos.webcam.ds.cgt.WebcamGetImageTask;


/**
 * The goal of webcam updater class is to update image in parallel, so all calls to fetch image
 * invoked on webcam instance will be non-blocking (will return immediately).
 * 
 * @author Bartosz Firyn (sarxos)
 */
public class WebcamUpdater implements Runnable {

	/**
	 * Implementation of this interface is responsible for calculating the delay between 2 image
	 * fetching, when the non-blocking (asynchronous) access to the webcam is enabled.
	 */
	public static interface DelayCalculator {

		/**
		 * Calculates delay before the next image will be fetched from the webcam. Must return
		 * number greater or equal 0.
		 * 
		 * @param snapshotDuration - duration of taking the last image
		 * @param deviceFps - current FPS obtained from the device, or -1 if the driver doesn't
		 *            support it
		 * @return interval (in millis)
		 */
		long calculateDelay(long snapshotDuration, double deviceFps);
	}

	/**
	 * Default impl of DelayCalculator, based on TARGET_FPS. Returns 0 delay for snapshotDuration
	 * &gt; 20 millis.
	 */
	public static class DefaultDelayCalculator implements DelayCalculator {

		@Override
		public long calculateDelay(long snapshotDuration, double deviceFps) {
			// Calculate delay required to achieve target FPS.
			// In some cases it can be less than 0
			// because camera is not able to serve images as fast as
			// we would like to. In such case just run with no delay,
			// so maximum FPS will be the one supported
			// by camera device in the moment.

			long delay = Math.max((1000 / TARGET_FPS) - snapshotDuration, 0);
			return delay;
		}
	}

	/**
	 * Thread factory for executors used within updater class.
	 * 
	 * @author Bartosz Firyn (sarxos)
	 */
	private static final class UpdaterThreadFactory implements ThreadFactory {

		private static final AtomicInteger number = new AtomicInteger(0);

		@Override
		public Thread newThread(Runnable r) {
			Thread t = new Thread(r, String.format("webcam-updater-thread-%d", number.incrementAndGet()));
			t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
			t.setDaemon(true);
			return t;
		}

	}

	/**
	 * Logger.
	 */
	private static final Logger LOG = LoggerFactory.getLogger(WebcamUpdater.class);

	/**
	 * Target FPS.
	 */
	private static final int TARGET_FPS = 50;

	private static final UpdaterThreadFactory THREAD_FACTORY = new UpdaterThreadFactory();

	/**
	 * Executor service.
	 */
	private ScheduledExecutorService executor = null;

	/**
	 * Cached image.
	 */
	private final AtomicReference<BufferedImage> image = new AtomicReference<BufferedImage>();

	/**
	 * Webcam to which this updater is attached.
	 */
	private Webcam webcam = null;

	/**
	 * Current FPS rate.
	 */
	private volatile double fps = 0;

	/**
	 * Is updater running.
	 */
	private AtomicBoolean running = new AtomicBoolean(false);

	private volatile boolean imageNew = false;

	/**
	 * DelayCalculator implementation.
	 */
	private final DelayCalculator delayCalculator;

	/**
	 * Construct new webcam updater using DefaultDelayCalculator.
	 * 
	 * @param webcam the webcam to which updater shall be attached
	 */
	protected WebcamUpdater(Webcam webcam) {
		this(webcam, new DefaultDelayCalculator());
	}

	/**
	 * Construct new webcam updater.
	 * 
	 * @param webcam the webcam to which updater shall be attached
	 * @param delayCalculator implementation
	 */
	public WebcamUpdater(Webcam webcam, DelayCalculator delayCalculator) {
		this.webcam = webcam;
		if (delayCalculator == null) {
			this.delayCalculator = new DefaultDelayCalculator();
		} else {
			this.delayCalculator = delayCalculator;
		}
	}

	/**
	 * Start updater.
	 */
	public void start() {

		if (running.compareAndSet(false, true)) {

			image.set(new WebcamGetImageTask(Webcam.getDriver(), webcam.getDevice()).getImage());

			executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY);
			executor.execute(this);

			LOG.debug("Webcam updater has been started");
		} else {
			LOG.debug("Webcam updater is already started");
		}
	}

	/**
	 * Stop updater.
	 */
	public void stop() {
		if (running.compareAndSet(true, false)) {

			executor.shutdown();
			while (!executor.isTerminated()) {
				try {
					executor.awaitTermination(100, TimeUnit.MILLISECONDS);
				} catch (InterruptedException e) {
					return;
				}
			}

			LOG.debug("Webcam updater has been stopped");
		} else {
			LOG.debug("Webcam updater is already stopped");
		}
	}

	@Override
	public void run() {

		if (!running.get()) {
			return;
		}

		try {
			tick();
		} catch (Throwable t) {
			handle(t);
		}
	}

	private void tick() {

		if (!webcam.isOpen()) {
			return;
		}

		// Calculate time required to fetch 1 picture.

		WebcamDriver driver = Webcam.getDriver();
		WebcamDevice device = webcam.getDevice();

		assert driver != null;
		assert device != null;

		boolean imageOk = false;
		long t1 = System.currentTimeMillis();
		try {
			image.set(webcam.transform(new WebcamGetImageTask(driver, device).getImage()));
			imageNew = true;
			imageOk = true;
		} catch (WebcamException e) {
			handle(e);
		}
		long t2 = System.currentTimeMillis();

		double deviceFps = -1;
		if (device instanceof WebcamDevice.FPSSource) {
			deviceFps = ((WebcamDevice.FPSSource) device).getFPS();
		}

		long duration = t2 - t1;
		long delay = delayCalculator.calculateDelay(duration, deviceFps);

		long delta = duration + 1; // +1 to avoid division by zero
		if (deviceFps >= 0) {
			fps = deviceFps;
		} else {
			fps = (4 * fps + 1000 / delta) / 5;
		}

		// reschedule task

		if (webcam.isOpen()) {
			try {
				executor.schedule(this, delay, TimeUnit.MILLISECONDS);
			} catch (RejectedExecutionException e) {
				LOG.trace("Webcam update has been rejected", e);
			}
		}

		// notify webcam listeners about the new image available

		if (imageOk) {
			webcam.notifyWebcamImageAcquired(image.get());
		}
	}

	/**
	 * Return currently available image. This method will return immediately while it was been
	 * called after camera has been open. In case when there are parallel threads running and there
	 * is a possibility to call this method in the opening time, or before camera has been open at
	 * all, this method will block until webcam return first image. Maximum blocking time will be 10
	 * seconds, after this time method will return null.
	 * 
	 * @return Image stored in cache
	 */
	public BufferedImage getImage() {

		int i = 0;
		while (image.get() == null) {

			// Just in case if another thread starts calling this method before
			// updater has been properly started. This will loop while image is
			// not available.

			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}

			// Return null if more than 10 seconds passed (timeout).

			if (i++ > 100) {
				LOG.error("Image has not been found for more than 10 seconds");
				return null;
			}
		}

		imageNew = false;

		return image.get();
	}

	protected boolean isImageNew() {
		return imageNew;
	}

	/**
	 * Return current FPS number. It is calculated in real-time on the base of how often camera
	 * serve new image.
	 * 
	 * @return FPS number
	 */
	public double getFPS() {
		return fps;
	}
}
