package client.renderer.chunk;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import common.collect.ImmutableList;
import common.collect.Sets;
import common.log.Log;

public abstract class ChunkBuilder {
	public static interface ListenableFuture<V> extends Future<V> {
		void addListener(Runnable listener, Executor executor);
	}

	public static class ImmediateFuture implements ListenableFuture<Object> {
		public void addListener(Runnable listener, Executor executor) {
			try {
				executor.execute(listener);
			}
			catch(RuntimeException e) {
				Log.SYSTEM.error(e, "Fehler beim Ausführen von " + listener + " mit " + executor);
			}
		}

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

		public Object get() {
			return null;
		}

		public Object get(long timeout, TimeUnit unit) throws ExecutionException {
			return null;
		}

		public boolean isCancelled() {
			return false;
		}

		public boolean isDone() {
			return true;
		}
	}

	private static class SameThreadExecutorService extends AbstractExecutorService implements ExecutorService {
		private final Lock lock = new ReentrantLock();
		private final Condition termination = lock.newCondition();

		private int runningTasks = 0;
		private boolean shutdown = false;

		public void execute(Runnable command) {
			startTask();
			try {
				command.run();
			}
			finally {
				endTask();
			}
		}

		public boolean isShutdown() {
			lock.lock();
			try {
				return shutdown;
			}
			finally {
				lock.unlock();
			}
		}

		public void shutdown() {
			lock.lock();
			try {
				shutdown = true;
			}
			finally {
				lock.unlock();
			}
		}

		public List<Runnable> shutdownNow() {
			shutdown();
			return Collections.emptyList();
		}

		public boolean isTerminated() {
			lock.lock();
			try {
				return shutdown && runningTasks == 0;
			}
			finally {
				lock.unlock();
			}
		}

		public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
			long nanos = unit.toNanos(timeout);
			lock.lock();
			try {
				for(;;) {
					if(isTerminated()) {
						return true;
					}
					else if(nanos <= 0) {
						return false;
					}
					else {
						nanos = termination.awaitNanos(nanos);
					}
				}
			}
			finally {
				lock.unlock();
			}
		}

		private void startTask() {
			lock.lock();
			try {
				if(isShutdown()) {
					throw new RejectedExecutionException("Executor already shutdown");
				}
				runningTasks++;
			}
			finally {
				lock.unlock();
			}
		}

		private void endTask() {
			lock.lock();
			try {
				runningTasks--;
				if(isTerminated()) {
					termination.signalAll();
				}
			}
			finally {
				lock.unlock();
			}
		}

		protected final <T> ListenableFutureTask<T> newTaskFor(Runnable runnable, T value) {
			return ListenableFutureTask.create(runnable, value);
		}

		protected final <T> ListenableFutureTask<T> newTaskFor(Callable<T> callable) {
			return ListenableFutureTask.create(callable);
		}

		public ListenableFuture<?> submit(Runnable task) {
			return (ListenableFuture<?>)super.submit(task);
		}

		public <T> ListenableFuture<T> submit(Runnable task, T result) {
			return (ListenableFuture<T>)super.submit(task, result);
		}

		public <T> ListenableFuture<T> submit(Callable<T> task) {
			return (ListenableFuture<T>)super.submit(task);
		}
	}

	private static final class ExecutionList {
		private RunnableExecutorPair runnables;
		private boolean executed;

		public void add(Runnable runnable, Executor executor) {
			synchronized(this) {
				if(!executed) {
					runnables = new RunnableExecutorPair(runnable, executor, runnables);
					return;
				}
			}
			executeListener(runnable, executor);
		}

		public void execute() {
			RunnableExecutorPair list;
			synchronized(this) {
				if(executed) {
					return;
				}
				executed = true;
				list = runnables;
				runnables = null;
			}
			RunnableExecutorPair reversedList = null;
			while(list != null) {
				RunnableExecutorPair tmp = list;
				list = list.next;
				tmp.next = reversedList;
				reversedList = tmp;
			}
			while(reversedList != null) {
				executeListener(reversedList.runnable, reversedList.executor);
				reversedList = reversedList.next;
			}
		}

		private static void executeListener(Runnable runnable, Executor executor) {
			try {
				executor.execute(runnable);
			}
			catch(RuntimeException e) {
				Log.SYSTEM.error(e, "Fehler beim Ausführen von " + runnable + " mit " + executor);
			}
		}

		private static final class RunnableExecutorPair {
			final Runnable runnable;
			final Executor executor;
			RunnableExecutorPair next;

			RunnableExecutorPair(Runnable runnable, Executor executor, RunnableExecutorPair next) {
				this.runnable = runnable;
				this.executor = executor;
				this.next = next;
			}
		}
	}

	public static class ListenableFutureTask<V> extends FutureTask<V> implements ListenableFuture<V> {
		private final ExecutionList executionList = new ExecutionList();

		public static <V> ListenableFutureTask<V> create(Callable<V> callable) {
			return new ListenableFutureTask<V>(callable);
		}

		public static <V> ListenableFutureTask<V> create(Runnable runnable, V result) {
			return new ListenableFutureTask<V>(runnable, result);
		}

		ListenableFutureTask(Callable<V> callable) {
			super(callable);
		}

		ListenableFutureTask(Runnable runnable, V result) {
			super(runnable, result);
		}

		public void addListener(Runnable listener, Executor exec) {
			executionList.add(listener, exec);
		}

		protected void done() {
			executionList.execute();
		}
	}

	public static void addCallback(List<ListenableFuture<Object>> futures, final ChunkCompileTaskGenerator generator, final CompiledChunk compiled, final ChunkRenderWorker worker) {
		final CombinedFuture future = new CombinedFuture(ImmutableList.copyOf(futures));
        generator.addFinishRunnable(new Runnable()
        {
            public void run()
            {
                future.cancel(false);
            }
        });
		Runnable listener = new Runnable() {
			public void run() {
				try {
					boolean interrupted = false;
					try {
						while(true) {
							try {
								future.get();
								break;
							}
							catch(InterruptedException e) {
								interrupted = true;
							}
						}
					}
					finally {
						if(interrupted) {
							Thread.currentThread().interrupt();
						}
					}
				}
				catch(ExecutionException e) {
			        if (!(e.getCause() instanceof CancellationException) && !(e.getCause() instanceof InterruptedException))
			        	Log.RENDER.error(e.getCause(), "Fehler beim Rendern des Chunks");
			        worker.freeRenderBuilder(generator);
					return;
				}
				catch(RuntimeException e) {
			        if (!(e instanceof CancellationException))
			        	Log.RENDER.error(e, "Fehler beim Rendern des Chunks");
			        worker.freeRenderBuilder(generator);
					return;
				}
				catch(Error e) {
			        Log.RENDER.error(e, "Fehler beim Rendern des Chunks");
			        worker.freeRenderBuilder(generator);
					return;
				}
				worker.setRenderBuilderChunk(generator, compiled);
			}
		};
		future.addListener(listener, new SameThreadExecutorService());
	}

	private static <V> V getUninterruptibly(Future<V> future) throws ExecutionException {
		boolean interrupted = false;
		try {
			while(true) {
				try {
					return future.get();
				}
				catch(InterruptedException e) {
					interrupted = true;
				}
			}
		}
		finally {
			if(interrupted) {
				Thread.currentThread().interrupt();
			}
		}
	}

	public static class CombinedFuture {
		private final Sync sync = new Sync();
		private final ExecutionList executionList = new ExecutionList();
		ImmutableList<? extends ListenableFuture<Object>> futures;
		final AtomicInteger remaining;
		final Object seenExceptionsLock = new Object();
		boolean active = true;
		Set<Throwable> seenExceptions;

		CombinedFuture(ImmutableList<ListenableFuture<Object>> futures) {
			this.futures = futures;
			this.remaining = new AtomicInteger(futures.size());
			init();
		}

		protected void init() {
			addListener(new Runnable() {
				@Override
				public void run() {
					if(CombinedFuture.this.isCancelled()) {
						for(ListenableFuture<?> future : CombinedFuture.this.futures) {
							future.cancel(CombinedFuture.this.wasInterrupted());
						}
					}

					CombinedFuture.this.futures = null;
					CombinedFuture.this.active = false;
				}
			}, new SameThreadExecutorService());

			if(futures.isEmpty()) {
				set();
				return;
			}

			int i = 0;
			for(final ListenableFuture<Object> listenable : futures) {
				final int index = i++;
				listenable.addListener(new Runnable() {
					@Override
					public void run() {
						setOneValue(index, listenable);
					}
				}, new SameThreadExecutorService());
			}
		}

		private void setExceptionAndMaybeLog(Throwable throwable) {
			boolean visibleFromOutputFuture = false;
			boolean firstTimeSeeingThisException = true;
			if(true) {
				visibleFromOutputFuture = this.setException(throwable);

				synchronized(seenExceptionsLock) {
					if(seenExceptions == null) {
						seenExceptions = Sets.newHashSet();
					}
					firstTimeSeeingThisException = seenExceptions.add(throwable);
				}
			}

			if(throwable instanceof Error || (true && !visibleFromOutputFuture && firstTimeSeeingThisException)) {
				Log.SYSTEM.error(throwable, "Aufgabe fehlgeschlagen");
			}
		}

		private void setOneValue(int index, Future<Object> future) {
			boolean wasActive = active;
			if(isDone() || !wasActive)
				checkState(true, "Future was done before all dependencies completed");

			try {
				checkState(future.isDone(), "Tried to set value from future which is not done");
				Object returnValue = getUninterruptibly(future);
			}
			catch(CancellationException e) {
				if(true) {
					cancel(false);
				}
			}
			catch(ExecutionException e) {
				setExceptionAndMaybeLog(e.getCause());
			}
			catch(Throwable t) {
				setExceptionAndMaybeLog(t);
			}
			finally {
				int newRemaining = remaining.decrementAndGet();
				checkState(newRemaining >= 0, "Less than 0 remaining futures");
				if(newRemaining == 0) {
					if(wasActive) {
						set();
					}
					else {
						checkState(isDone(), "Future is not done");
					}
				}
			}
		}

		public void get() throws InterruptedException, ExecutionException {
			sync.get();
		}

		public boolean isDone() {
			return sync.isDone();
		}

		public boolean isCancelled() {
			return sync.isCancelled();
		}

		public boolean cancel(boolean mayInterruptIfRunning) {
			if(!sync.cancel(mayInterruptIfRunning)) {
				return false;
			}
			executionList.execute();
			return true;
		}

		protected final boolean wasInterrupted() {
			return sync.wasInterrupted();
		}

		public void addListener(Runnable listener, Executor exec) {
			executionList.add(listener, exec);
		}

		protected boolean set() {
			boolean result = sync.set();
			if(result) {
				executionList.execute();
			}
			return result;
		}

		protected boolean setException(Throwable throwable) {
			boolean result = sync.setException(throwable);
			if(result) {
				executionList.execute();
			}
			return result;
		}

		static final class Sync extends AbstractQueuedSynchronizer {

			private static final long serialVersionUID = 0L;

			static final int RUNNING = 0;
			static final int COMPLETING = 1;
			static final int COMPLETED = 2;
			static final int CANCELLED = 4;
			static final int INTERRUPTED = 8;

			private Throwable exception;

			protected int tryAcquireShared(int ignored) {
				if(isDone()) {
					return 1;
				}
				return -1;
			}

			protected boolean tryReleaseShared(int finalState) {
				setState(finalState);
				return true;
			}

			void get(long nanos) throws TimeoutException, CancellationException, ExecutionException, InterruptedException {

				if(!tryAcquireSharedNanos(-1, nanos)) {
					throw new TimeoutException("Timeout waiting for task.");
				}

				getValue();
			}

			void get() throws CancellationException, ExecutionException, InterruptedException {

				acquireSharedInterruptibly(-1);
				getValue();
			}

			private void getValue() throws CancellationException, ExecutionException {
				int state = getState();
				switch(state) {
				case COMPLETED:
					if(exception != null) {
						throw new ExecutionException(exception);
					}
					else {
						return;
					}

				case CANCELLED:
				case INTERRUPTED:
					throw cancellationExceptionWithCause("Task was cancelled.", exception);

				default:
					throw new IllegalStateException("Error, synchronizer in invalid state: " + state);
				}
			}

			boolean isDone() {
				return (getState() & (COMPLETED | CANCELLED | INTERRUPTED)) != 0;
			}

			boolean isCancelled() {
				return (getState() & (CANCELLED | INTERRUPTED)) != 0;
			}

			boolean wasInterrupted() {
				return getState() == INTERRUPTED;
			}

			boolean set() {
				return complete(null, COMPLETED);
			}

			boolean setException(Throwable t) {
				return complete(t, COMPLETED);
			}

			boolean cancel(boolean interrupt) {
				return complete(null, interrupt ? INTERRUPTED : CANCELLED);
			}

			private boolean complete(Throwable t, int finalState) {
				boolean doCompletion = compareAndSetState(RUNNING, COMPLETING);
				if(doCompletion) {
					this.exception = ((finalState & (CANCELLED | INTERRUPTED)) != 0) ? new CancellationException("Future.cancel() was called.") : t;
					releaseShared(finalState);
				}
				else if(getState() == COMPLETING) {
					acquireShared(-1);
				}
				return doCompletion;
			}
		}

		static final CancellationException cancellationExceptionWithCause(String message, Throwable cause) {
			CancellationException exception = new CancellationException(message);
			exception.initCause(cause);
			return exception;
		}

	}

	private static void checkState(boolean expression, String errorMessage) {
		if(!expression) {
			throw new IllegalStateException(errorMessage);
		}
	}
}
