/*******************************************************************************
 * Copyright (c) 2011 itemis AG (http://www.itemis.eu) and others.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.eclipse.xtext.xbase.resource;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.WrappedException;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.xtext.common.types.JvmMember;
import org.eclipse.xtext.common.types.JvmType;
import org.eclipse.xtext.common.types.TypesPackage;
import org.eclipse.xtext.common.types.impl.JvmDeclaredTypeImplCustom;
import org.eclipse.xtext.common.types.xtext.JvmMemberInitializableResource;
import org.eclipse.xtext.diagnostics.DiagnosticMessage;
import org.eclipse.xtext.diagnostics.ExceptionDiagnostic;
import org.eclipse.xtext.diagnostics.Severity;
import org.eclipse.xtext.linking.lazy.LazyURIEncoder;
import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.parser.IParseResult;
import org.eclipse.xtext.resource.CompilerPhases;
import org.eclipse.xtext.resource.DerivedStateAwareResource;
import org.eclipse.xtext.resource.IBatchLinkableResource;
import org.eclipse.xtext.resource.ISynchronizable;
import org.eclipse.xtext.util.CancelIndicator;
import org.eclipse.xtext.util.Triple;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;

import com.google.inject.Inject;

/**
 * A specialized EMF resource that is capable of resolving proxies in batch mode.
 * That is, on {@link #getEObject(String)}, the {@link BatchLinkingService} is used
 * to resolve a chunk of proxies. 
 * 
 * @author Sebastian Zarnekow - Linking assumptions
 */
public class BatchLinkableResource extends DerivedStateAwareResource implements ISynchronizable<BatchLinkableResource>, IBatchLinkableResource, JvmMemberInitializableResource {
	
	private static final Logger log = Logger.getLogger(BatchLinkableResource.class);
	
	@Inject
	private BatchLinkingService batchLinkingService;
	
	@Inject
	private CompilerPhases compilerPhases;
	
	private Set<Runnable> jvmMemberInitializers = null;
	
	private boolean hasJvmMemberInitializers = false;
	
	private boolean isInitializingJvmMembers = false;
	
	/**
	 * Returns the lock of the owning {@link ResourceSet}, if it exposes such a lock.
	 * Otherwise this resource itself is used as the lock context.
	 */
	/* @NonNull */
	@Override
	public Object getLock() {
		ResourceSet resourceSet = getResourceSet();
		if (resourceSet instanceof ISynchronizable<?>) {
			return ((ISynchronizable<?>) resourceSet).getLock();
		}
		return this;
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * @since 2.4
	 */
	/* @Nullable */
	@Override
	public <Result> Result execute(/* @NonNull */ IUnitOfWork<Result, ? super BatchLinkableResource> unit) throws Exception {
		synchronized (getLock()) {
			return unit.exec(this);
		}
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * Delegates to the {@link BatchLinkingService} if the requested reference is 
	 * {@link BatchLinkingService#isBatchLinkable(EReference) linkeable in batch mode}.
	 * 
	 * Implementation detail: This specialization of {@link #getEObject(String) getEObject}
	 * synchronizes on the {@link #getLock() lock} which is exposed by the synchronizable
	 * resource rather than on the resource directly. This guards against reentrant resolution
	 * from different threads that could block each other.
	 * 
	 * Usually one would want to lock only in the {@link BatchLinkingService} but we could
	 * have intermixed {@link LazyURIEncoder#isCrossLinkFragment(org.eclipse.emf.ecore.resource.Resource, String)
	 * lazy cross reference} and vanilla EMF cross references which again could lead to a
	 * dead lock.
	 */
	@Override
	public EObject getEObject(String uriFragment) {
		synchronized (getLock()) {
			try {
				if (getEncoder().isCrossLinkFragment(this, uriFragment) && !isLoadedFromStorage()) {
					if (!getUnresolvableURIFragments().contains(uriFragment)) {
						Triple<EObject, EReference, INode> triple = getEncoder().decode(this, uriFragment);
						if (batchLinkingService.isBatchLinkable(triple.getSecond())) {
							if (compilerPhases.isIndexing(this))
								log.error("Don't resolve expressions during indexing!", new IllegalStateException("Resource URI : "+getURI()+", fragment : "+uriFragment));
							return batchLinkingService.resolveBatched(triple.getFirst(), triple.getSecond(), uriFragment);
						}
						return getEObject(uriFragment, triple);
					} else {
						return null;
					}
				}
				return super.getEObject(uriFragment);
			} catch (RuntimeException e) {
				operationCanceledManager.propagateAsErrorIfCancelException(e);
				getErrors().add(new ExceptionDiagnostic(e));
				log.error("resolution of uriFragment '" + uriFragment + "' failed.", e);
				// wrapped because the javaDoc of this method states that WrappedExceptions are thrown
				// logged because EcoreUtil.resolve will ignore any exceptions.
				throw new WrappedException(e);
			}
		}
	}
	
	/**
	 * {@inheritDoc}
	 * 
	 * <p>Implementation detail: Overridden to use the shared {@link #getLock() lock}.</p> 
	 */
	@SuppressWarnings("sync-override")
	@Override
	public EList<EObject> getContents() {
		synchronized (getLock()) {
			if (isLoaded && !isLoading && !isInitializing && !isUpdating && !fullyInitialized && !isLoadedFromStorage()) {
				try {
					eSetDeliver(false);
					installDerivedState(false);
				} finally {
					eSetDeliver(true);
				}
			}
			return doGetContents();
		}
	}
	
	/**
	 * Delegates to the BatchLinkingService to resolve all references. The linking service
	 * is responsible to lock the resource or resource set. 
	 */
	@Override
	public void resolveLazyCrossReferences(CancelIndicator monitor) {
		IParseResult parseResult = getParseResult();
		if (parseResult != null) {
			batchLinkingService.resolveBatched(parseResult.getRootASTElement(), monitor);
		}
		operationCanceledManager.checkCanceled(monitor);
		super.resolveLazyCrossReferences(monitor);
	}
	
	@Override
	protected EObject handleCyclicResolution(Triple<EObject, EReference, INode> triple) throws AssertionError {
		if (!isValidationDisabled()) {
			EObject context = triple.getFirst();
			if (context.eClass() == TypesPackage.Literals.JVM_PARAMETERIZED_TYPE_REFERENCE) {
				// here we may end up with cyclic resolution requests in rare situations, e.g. for input types
				// like :
				/*
				 * package p;
				 * class C extends p.C.Bogus {}
				 */
				return null;
			}
			DiagnosticMessage message = new DiagnosticMessage("Cyclic linking detected : " + getReferences(triple, resolving), Severity.ERROR, "cyclic-resolution");
			List<Diagnostic> list = getDiagnosticList(message);
			Diagnostic diagnostic = createDiagnostic(triple, message);
			if (!list.contains(diagnostic))
				list.add(diagnostic);
		}
		return null;
	}

	@Override
	public void linkBatched(CancelIndicator monitor) {
		resolveLazyCrossReferences(monitor);
	}
	
	// Lazy initialization
	
	/**
	 * Executes any {@link Runnable}s added through {@link #addJvmMemberInitializer(Runnable)}
	 * 
	 * @since 2.8
	 * @noreference This method is not intended to be referenced by clients.
	 */
	@Override
	public void ensureJvmMembersInitialized() {
		if (jvmMemberInitializers == null)
			return;
		Set<Runnable> localRunnables = null;
		synchronized(this) {
			localRunnables = jvmMemberInitializers;
			jvmMemberInitializers = null;
			hasJvmMemberInitializers = false;
		}
		if (localRunnables == null)
			return;
		boolean wasDeliver = eDeliver();
		LinkedHashSet<Triple<EObject, EReference, INode>> before = resolving;
		try {
			eSetDeliver(false);
			if (!before.isEmpty()) {
				resolving = new LinkedHashSet<Triple<EObject, EReference, INode>>();
			}
			if (isInitializingJvmMembers) {
				throw new IllegalStateException("Reentrant attempt to initialize JvmMembers");
			}
			try {
				isInitializingJvmMembers = true;
				for (Runnable runnable : localRunnables) {
					runnable.run();
				}
			} finally {
				isInitializingJvmMembers = false;
			}
		} catch (Exception e) {
			log.error(e.getMessage(), e);
		} finally {
			if (!before.isEmpty()) {
				resolving = before;
			}
			eSetDeliver(wasDeliver);
		}
	}
	
	@Override
	public boolean isInitializingJvmMembers() {
		return isInitializingJvmMembers;
	}
	
	@Override
	protected void doDiscardDerivedState() {
		this.jvmMemberInitializers = null;
		this.hasJvmMemberInitializers = false;
		super.doDiscardDerivedState();
	}
	
	/**
	 * Register runnables to be executed when the {@link JvmMember members} of {@link JvmType types}
	 * in this resource are initialized.
	 * 
	 * @since 2.8
	 * @noreference This method is not intended to be referenced by clients.
	 */
	@Override
	public void addJvmMemberInitializer(Runnable runnable) {
		if (isInitializingJvmMembers) {
			throw new IllegalStateException("Cannot enqueue runnables during JvmMemberInitialization");
		}
		if (this.jvmMemberInitializers == null) {
			this.jvmMemberInitializers = new LinkedHashSet<Runnable>();
			this.hasJvmMemberInitializers = true;
			for (EObject obj : getContents()) {
				if (obj instanceof JvmDeclaredTypeImplCustom) {
					JvmDeclaredTypeImplCustom type = (JvmDeclaredTypeImplCustom) obj;
					markPendingInitialization(type);
				}
			}
		}
		this.jvmMemberInitializers.add(runnable);
	}
	
	/**
	 * Recursively traverse the types in this resource to mark them as lazy initialized types. 
	 */
	private void markPendingInitialization(JvmDeclaredTypeImplCustom type) {
		type.setPendingInitialization(true);
		for (JvmMember member : type.basicGetMembers()) {
			if (member instanceof JvmDeclaredTypeImplCustom) {
				markPendingInitialization((JvmDeclaredTypeImplCustom) member);
			}
		}
	}
	
	@Override
	public boolean hasJvmMemberInitializers() {
		return hasJvmMemberInitializers;
	}
	
}