/*******************************************************************************
 * Copyright (c) 2010 Tasktop Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.commons.identity;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.mylyn.commons.identity.Account;
import org.eclipse.mylyn.commons.identity.IIdentity;
import org.eclipse.mylyn.commons.identity.IProfile;
import org.eclipse.mylyn.commons.identity.IProfileImage;
import org.eclipse.mylyn.commons.identity.spi.Profile;
import org.eclipse.mylyn.commons.identity.spi.ProfileImage;

/**
 * @author Steffen Pingel
 */
public class Identity implements IIdentity {

	private static abstract class FutureJob<T> extends Job implements Future<T> {

		private boolean cancelled;

		private final AtomicReference<Throwable> futureException = new AtomicReference<Throwable>();

		private final AtomicReference<T> futureResult = new AtomicReference<T>();

		private final CountDownLatch resultLatch = new CountDownLatch(1);

		public FutureJob(String name) {
			super(name);
		}

		public boolean cancel(boolean mayInterruptIfRunning) {
			this.cancelled = true;
			return this.cancel();
		}

		public T get() throws InterruptedException, ExecutionException {
			resultLatch.await();
			return getFutureResult();
		}

		public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
			if (!resultLatch.await(timeout, unit)) {
				throw new TimeoutException();
			}
			return getFutureResult();
		}

		public boolean isCancelled() {
			return this.cancelled;
		}

		public boolean isDone() {
			return getResult() != null;
		}

		private T getFutureResult() throws ExecutionException {
			Throwable t = futureException.get();
			if (t != null) {
				throw new ExecutionException(t);
			}
			return futureResult.get();
		}

		protected void done() {
			if (resultLatch.getCount() > 0) {
				error(new RuntimeException());
			}
			resultLatch.countDown();
		}

		protected IStatus error(Throwable t) {
			futureException.set(t);
			resultLatch.countDown();
			if (t instanceof OperationCanceledException) {
				return Status.CANCEL_STATUS;
			}
			return Status.OK_STATUS;
		}

		protected IStatus success(T result) {
			futureResult.set(result);
			resultLatch.countDown();
			return Status.OK_STATUS;
		}

	}

	private static final class FutureResult<T> implements Future<T> {

		private final T result;

		private FutureResult(T result) {
			this.result = result;
		}

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

		public T get() throws InterruptedException, ExecutionException {
			return result;
		}

		public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
			return result;
		}

		public boolean isCancelled() {
			return false;
		}

		public boolean isDone() {
			return true;
		}
	}

	private final Set<Account> accounts;

	private final UUID id;

	private List<ProfileImage> images;

	private final List<PropertyChangeListener> listeners;

	private final IdentityModel model;

	private Profile profile;

	private boolean refreshProfile;

	public Identity(IdentityModel model) {
		this.model = model;
		this.id = UUID.randomUUID();
		this.accounts = new CopyOnWriteArraySet<Account>();
		this.listeners = new CopyOnWriteArrayList<PropertyChangeListener>();
	}

	public void addAccount(Account account) {
		accounts.add(account);
		refreshProfile = true;
	}

	public void addPropertyChangeListener(PropertyChangeListener listener) {
		listeners.add(listener);
	}

	public Account[] getAccounts() {
		return accounts.toArray(new Account[accounts.size()]);
	}

	public Account getAccountById(String id) {
		if (id == null) {
			return null;
		}
		for (Account account : accounts) {
			if (id.equals(account.getId())) {
				return account;
			}
		}
		return null;
	}

	public Account getAccountByKind(String kind) {
		if (kind == null) {
			return null;
		}
		for (Account account : accounts) {
			if (kind.equals(account.getKind())) {
				return account;
			}
		}
		return null;
	}

	public String[] getAliases() {
		Set<String> aliases = new HashSet<String>(accounts.size());
		for (Account account : accounts) {
			aliases.add(account.getId());
		}
		return aliases.toArray(new String[aliases.size()]);
	}

	public UUID getId() {
		return id;
	}

	public boolean is(Account account) {
		return accounts.contains(account);
	}

	public boolean is(String id) {
		return getAccountById(id) != null;
	}

	public void removeAccount(Account account) {
		accounts.remove(account);
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		listeners.remove(listener);
	}

	public synchronized Future<IProfileImage> requestImage(final int preferredWidth, final int preferredHeight) {
		if (images != null) {
			for (final ProfileImage image : images) {
				if (image.getWidth() == preferredWidth && image.getHeight() == preferredHeight) {
					return new FutureResult<IProfileImage>(image);
				}
			}
		}
		FutureJob<IProfileImage> job = new FutureJob<IProfileImage>("Retrieving Image") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					ProfileImage image = model.getImage(Identity.this, preferredWidth, preferredHeight, monitor);
					if (image != null) {
						addImage(image);
					}
					return success(image);
				} catch (Throwable t) {
					return error(t);
				} finally {
					done();
				}
			}
		};
		job.schedule();
		return job;
	}

	public Future<IProfile> requestProfile() {
		if (profile != null && !refreshProfile) {
			return new FutureResult<IProfile>(profile);
		}

		refreshProfile = false;
		FutureJob<IProfile> job = new FutureJob<IProfile>("Retrieving Profile") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				try {
					Profile profile = new Profile(Identity.this);
					model.updateProfile(profile, monitor);
					setProfile(profile);
					return success(profile);
				} catch (Throwable t) {
					return error(t);
				} finally {
					done();
				}
			}
		};
		job.schedule();
		return job;
	}

	private void firePropertyChangeEvent(String propertyName, Object oldValue, Object newValue) {
		PropertyChangeEvent event = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
		for (PropertyChangeListener listener : listeners) {
			listener.propertyChange(event);
		}
	}

	protected synchronized void addImage(ProfileImage image) {
		if (images == null) {
			images = new ArrayList<ProfileImage>();
		}
		images.add(image);
		firePropertyChangeEvent("image", null, image); //$NON-NLS-1$
	}

	protected synchronized void removeImage(ProfileImage image) {
		if (images != null) {
			images.remove(image);
		}
	}

	protected void setProfile(Profile profile) {
		this.profile = profile;
	}

}
