/*******************************************************************************
 * Copyright (c) 2000, 2018 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.core;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.*;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.resources.mapping.ResourceTraversal;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.*;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.diff.IThreeWayDiff;
import org.eclipse.team.core.subscribers.*;
import org.eclipse.team.core.synchronize.SyncInfo;
import org.eclipse.team.core.synchronize.SyncInfoSet;
import org.eclipse.team.core.variants.*;
import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot;
import org.eclipse.team.internal.ccvs.core.resources.EclipseSynchronizer;
import org.eclipse.team.internal.ccvs.core.syncinfo.*;
import org.eclipse.team.internal.ccvs.core.util.ResourceStateChangeListeners;
import org.eclipse.team.internal.core.subscribers.ActiveChangeSetManager;

/**
 * CVSWorkspaceSubscriber
 */
public class CVSWorkspaceSubscriber extends CVSSyncTreeSubscriber implements IResourceStateChangeListener {
	
	private CVSResourceVariantTree baseTree, remoteTree;

	// qualified name for remote sync info
	private static final String REMOTE_RESOURCE_KEY = "remote-resource-key"; //$NON-NLS-1$

	private boolean contentFetch;
	
	CVSWorkspaceSubscriber(QualifiedName id, String name) {
		super(id, name);
		
		// install sync info participant
		ResourceVariantByteStore baseSynchronizer = new CVSBaseResourceVariantTree();
		baseTree = new CVSResourceVariantTree(baseSynchronizer, null, getCacheFileContentsHint()) {
			public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException {
				// TODO Ensure that file contents are cached for modified local files
				try {
					monitor.beginTask(null, 100);
					return new IResource[0];
				} finally {
					monitor.done();
				}
			}
		};
		CVSDescendantResourceVariantByteStore remoteSynchronizer = new CVSDescendantResourceVariantByteStore(
				baseSynchronizer, 
				new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, REMOTE_RESOURCE_KEY)));
		remoteTree = new CVSResourceVariantTree(remoteSynchronizer, null, getCacheFileContentsHint()) {
			public boolean isCacheFileContentsHint() {
				return getCacheFileContentsHint();
			}
		};
		
		ResourceStateChangeListeners.getListener().addResourceStateChangeListener(this); 
	}

	/* 
	 * Return the list of projects shared with a CVS team provider.
	 * 
	 * [Issue : this will have to change when folders can be shared with
	 * a team provider instead of the current project restriction]
	 * (non-Javadoc)
	 * @see org.eclipse.team.core.sync.ISyncTreeSubscriber#roots()
	 */
	public IResource[] roots() {
		List result = new ArrayList();
		IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		for (int i = 0; i < projects.length; i++) {
			IProject project = projects[i];
			if(project.isOpen()) {
				RepositoryProvider provider = RepositoryProvider.getProvider(project, CVSProviderPlugin.getTypeId());
				if(provider != null) {
					result.add(project);
				}
			}
		}
		return (IProject[]) result.toArray(new IProject[result.size()]);
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.IResourceStateChangeListener#resourceSyncInfoChanged(org.eclipse.core.resources.IResource[])
	 */
	public void resourceSyncInfoChanged(IResource[] changedResources) {
		internalResourceSyncInfoChanged(changedResources, true); 
	}

	private void internalResourceSyncInfoChanged(IResource[] changedResources, boolean canModifyWorkspace) {
		getRemoteByteStore().handleResourceChanges(changedResources, canModifyWorkspace);	
		fireTeamResourceChange(SubscriberChangeEvent.asSyncChangedDeltas(this, changedResources));
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.IResourceStateChangeListener#externalSyncInfoChange(org.eclipse.core.resources.IResource[])
	 */
	public void externalSyncInfoChange(IResource[] changedResources) {
		internalResourceSyncInfoChanged(changedResources, false);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.IResourceStateChangeListener#resourceModified(org.eclipse.core.resources.IResource[])
	 */
	public void resourceModified(IResource[] changedResources) {
		// This is only ever called from a delta POST_CHANGE
		// which causes problems since the workspace tree is closed
		// for modification and we flush the sync info in resourceSyncInfoChanged
		
		// Since the listeners of the Subscriber will also listen to deltas
		// we don't need to propogate this.
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.IResourceStateChangeListener#projectConfigured(org.eclipse.core.resources.IProject)
	 */
	public void projectConfigured(IProject project) {
		SubscriberChangeEvent delta = new SubscriberChangeEvent(this, ISubscriberChangeEvent.ROOT_ADDED, project);
		fireTeamResourceChange(new SubscriberChangeEvent[] {delta});
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.IResourceStateChangeListener#projectDeconfigured(org.eclipse.core.resources.IProject)
	 */
	public void projectDeconfigured(IProject project) {
		try {
			getRemoteTree().flushVariants(project, IResource.DEPTH_INFINITE);
		} catch (TeamException e) {
			CVSProviderPlugin.log(e);
		}
		SubscriberChangeEvent delta = new SubscriberChangeEvent(this, ISubscriberChangeEvent.ROOT_REMOVED, project);
		fireTeamResourceChange(new SubscriberChangeEvent[] {delta});
	}

	public void setRemote(IResource resource, IResourceVariant remote, IProgressMonitor monitor) throws TeamException {
		// TODO: This exposes internal behavior to much
		IResource[] changedResources = 
			((CVSResourceVariantTree)getRemoteTree()).collectChanges(resource, remote, IResource.DEPTH_INFINITE, monitor);
		if (changedResources.length != 0) {
			fireTeamResourceChange(SubscriberChangeEvent.asSyncChangedDeltas(this, changedResources));
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.CVSSyncTreeSubscriber#getBaseSynchronizationCache()
	 */
	protected IResourceVariantTree getBaseTree() {
		return baseTree;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.core.CVSSyncTreeSubscriber#getRemoteSynchronizationCache()
	 */
	protected IResourceVariantTree getRemoteTree() {
		return remoteTree;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.core.subscribers.Subscriber#collectOutOfSync(org.eclipse.core.resources.IResource[], int, org.eclipse.team.core.synchronize.SyncInfoSet, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public void collectOutOfSync(IResource[] resources, int depth, final SyncInfoSet set, final IProgressMonitor monitor) {
		monitor.beginTask(null, IProgressMonitor.UNKNOWN);
		for (int i = 0; i < resources.length; i++) {
			IResource resource = resources[i];
			try {
				if (!isSupervised(resource)) {
					return;
				}
			} catch (TeamException e) {
				// fallthrough and try to collect sync info
				CVSProviderPlugin.log(e);
			}
			try {
				visit(resource, new IResourceVisitor() {
					public boolean visit(IResource innerResource) throws CoreException {
						try {
							Policy.checkCanceled(monitor);
							if (innerResource.getType() != IResource.FILE) {
								monitor.subTask(NLS.bind(CVSMessages.CVSWorkspaceSubscriber_1, new String[] { innerResource.getFullPath().toString() })); 
							}
							if (isOutOfSync(innerResource, monitor)) {
								SyncInfo info = getSyncInfo(innerResource);
								if (info != null && info.getKind() != 0) {
									set.add(info);
								}
							}
						} catch (TeamException e) {
							set.addError(new TeamStatus(
									IStatus.ERROR, CVSProviderPlugin.ID, ITeamStatus.RESOURCE_SYNC_INFO_ERROR,
									NLS.bind(CVSMessages.CVSWorkspaceSubscriber_2, new String[] { innerResource.getFullPath().toString(), e.getMessage() }), e, innerResource)); 
						}
						return true;
					}
				}, depth);
			} catch (CoreException e) {
				set.addError(new TeamStatus(
						IStatus.ERROR, CVSProviderPlugin.ID, ITeamStatus.SYNC_INFO_SET_ERROR,
						e.getMessage(), e, ResourcesPlugin.getWorkspace().getRoot()));
			}
		}
		monitor.done();
	}
	
	private void visit(IResource resource, IResourceVisitor visitor, int depth) throws CoreException {
		boolean keepGoing = visitor.visit(resource);
		if (keepGoing && depth != IResource.DEPTH_ZERO) {
			IResource[] members = members(resource);
			for (int i = 0; i < members.length; i++) {
				IResource member = members[i];
				visit(member, visitor, depth == IResource.DEPTH_ONE ? IResource.DEPTH_ZERO : IResource.DEPTH_INFINITE);
			}
		}
	}

	/* internal use only */ boolean isOutOfSync(IResource resource, IProgressMonitor monitor) throws TeamException {
		return (hasIncomingChange(resource) || hasOutgoingChange(resource, monitor));
	}
	
	private boolean hasIncomingChange(IResource resource) throws TeamException {
		return getRemoteByteStore().isVariantKnown(resource);
	}
	
	private boolean hasOutgoingChange(IResource resource, IProgressMonitor monitor) throws CVSException {
		if (resource.getType() == IResource.PROJECT || resource.getType() == IResource.ROOT) {
			// a project (or the workspace root) cannot have outgoing changes
			return false;
		}
		int state = EclipseSynchronizer.getInstance().getModificationState(resource.getParent());
		if (state == ICVSFile.CLEAN) {
			// if the parent is known to be clean then the resource must also be clean
			return false;
		}
		if (resource.getType() == IResource.FILE) {
			// A file is an outgoing change if it is modified
			ICVSFile file = CVSWorkspaceRoot.getCVSFileFor((IFile)resource);
			return file.isModified(monitor);
		} else {
			// A folder is an outgoing change if it is not a CVS folder and not ignored
			ICVSFolder folder = CVSWorkspaceRoot.getCVSFolderFor((IContainer)resource);
			return !folder.isCVSFolder() && !folder.isIgnored();
		}
	}
	
	/*
	 * TODO: Should not need to access this here
	 */
	private CVSDescendantResourceVariantByteStore getRemoteByteStore() {
		return (CVSDescendantResourceVariantByteStore)((CVSResourceVariantTree)getRemoteTree()).getByteStore();
	}

	/**
	 * Update the remote tree to the base
	 * @param folder
	 * @param recurse 
	 */
	public void updateRemote(CVSTeamProvider provider, ICVSFolder folder, boolean recurse, IProgressMonitor monitor) throws TeamException {
		try {
			monitor.beginTask(null, IProgressMonitor.UNKNOWN);
			IResource resource = folder.getIResource();
			if (resource != null) {
				ICVSResource tree = buildBaseTree(
						resource, 
						false, 
						Policy.subMonitorFor(monitor, 50));
				setRemote(resource, (IResourceVariant)tree, Policy.subMonitorFor(monitor, 50));
			}
		} finally {
			monitor.done();
		}
	}
	
	public ICVSRemoteResource buildBaseTree(IResource resource, boolean immutable, IProgressMonitor monitor) throws TeamException {
		try {
			monitor.beginTask(null, IProgressMonitor.UNKNOWN);
			return ((CVSResourceVariantTree)getBaseTree()).buildTree(null, resource, immutable, monitor);
		} finally {
			monitor.done();
		}
	}

	public ICVSRemoteResource buildRemoteTree(IResource resource, boolean immutable, IProgressMonitor monitor) throws TeamException {
		try {
			monitor.beginTask(null, IProgressMonitor.UNKNOWN);
			return ((CVSResourceVariantTree)getRemoteTree()).buildTree(null, resource, immutable, monitor);
		} finally {
			monitor.done();
		}
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.core.subscribers.Subscriber#getState(org.eclipse.core.resources.mapping.ResourceMapping, int, org.eclipse.core.runtime.IProgressMonitor)
	 */
	public int getState(ResourceMapping mapping, int stateMask, IProgressMonitor monitor) throws CoreException {
		if ((stateMask & IThreeWayDiff.INCOMING) == 0) {
			// If we're only interested in outgoing changes, used the cached modified state
			ResourceTraversal[] traversals = mapping.getTraversals(new SubscriberResourceMappingContext(this, false), monitor);
			if (hasLocalChanges(traversals, monitor)) {
				int state = IThreeWayDiff.OUTGOING;
				state |= getOutgoingKind(traversals, monitor);
				return state;
			} else {
				return 0;
			}
		}
		return super.getState(mapping, stateMask, monitor);
	}
	
	private int getOutgoingKind(ResourceTraversal[] traversals, IProgressMonitor monitor) throws CoreException {
		int kind = 0;
		for (int i = 0; i < traversals.length; i++) {
			ResourceTraversal traversal = traversals[i];
			IResource[] resources = traversal.getResources();
			for (int j = 0; j < resources.length; j++) {
				IResource resource = resources[j];
				IDiff node = getDiff(resource);
				if (node == null)
					return IDiff.CHANGE;
				int nextKind = node.getKind();
				if (kind == 0)
					kind = nextKind;
				if (nextKind != kind || nextKind == IDiff.CHANGE)
					return IDiff.CHANGE;
			}
		}
		return kind;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.team.core.subscribers.Subscriber#hasLocalChanges(org.eclipse.core.resources.mapping.ResourceTraversal[], org.eclipse.core.runtime.IProgressMonitor)
	 */
	public boolean hasLocalChanges(ResourceTraversal[] traversals, IProgressMonitor monitor) throws CoreException {
		monitor = Policy.monitorFor(monitor);
		for (int i = 0; i < traversals.length; i++) {
			ResourceTraversal traversal = traversals[i];
			IResource[] resources = traversal.getResources();
			switch (traversal.getDepth()) {
			case IResource.DEPTH_ZERO:
				for (int j = 0; j < resources.length; j++) {
					IResource resource = resources[j];
					if (isDirectlyDirty(resource, monitor)) {
						return true;
					}
				}
				break;
			case IResource.DEPTH_INFINITE:
				for (int j = 0; j < resources.length; j++) {
					IResource resource = resources[j];
					if (isDirty(resource, monitor)) {
						return true;
					}
				}
				break;
			case IResource.DEPTH_ONE:
				for (int j = 0; j < resources.length; j++) {
					IResource resource = resources[j];
					if (isDirectlyDirty(resource, monitor)) {
						return true;
					}
					IResource[] children = members(resource);
					for (int k = 0; k < children.length; k++) {
						IResource child = children[k];				
						if (isDirectlyDirty(child, monitor)) {
							return true;
						}
					}
				}
				break;
			}
		}
		return false;
	}
	
	private boolean isDirectlyDirty(IResource resource, IProgressMonitor monitor) throws CoreException {
		if (resource.getType() == IResource.FILE) {
			if (isDirty(resource, monitor))
				return true;
		} else {
			IDiff node = getDiff(resource);
			if (node != null 
					&& node instanceof IThreeWayDiff 
					&& ((IThreeWayDiff)node).getLocalChange() != null
					&& ((IThreeWayDiff)node).getLocalChange().getKind() != IDiff.NO_CHANGE)
				return true;
		}
		return false;
	}
	
	public boolean isDirty(final ICVSResource cvsResource, IProgressMonitor monitor) throws CVSException {
		if (cvsResource.exists())
			return !cvsResource.isIgnored() && cvsResource.isModified(monitor);
		return cvsResource.isManaged() && cvsResource.isModified(monitor);
	}
	
	public boolean isDirty(IResource resource, IProgressMonitor monitor) throws CVSException {
		try {
			ICVSResource cvsResource = CVSWorkspaceRoot.getCVSResourceFor(resource);
			return isDirty(cvsResource, monitor);
		} catch (CVSException e) {
			//if we get an error report it to the log but assume dirty.
			boolean accessible = resource.getProject().isAccessible();
			if (accessible) {
				throw e;
			}
			// Return dirty if the project is open and clean otherwise
			return accessible;
		}

	}

	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == ActiveChangeSetManager.class) {
			return adapter.cast(CVSProviderPlugin.getPlugin().getChangeSetManager());
		}
		return super.getAdapter(adapter);
	}

	public void refreshWithContentFetch(ResourceTraversal[] traversals, IProgressMonitor monitor) throws TeamException {
		try {
			contentFetch = true;
			refresh(traversals, monitor);
		} finally {
			contentFetch = false;
		}
	}
	
	protected boolean getCacheFileContentsHint() {
		return contentFetch;
	}
}
