/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     Matt McCutchen (hashproduct+eclipse@gmail.com) - Bug 189304 [Sync Info] cvsignore lines should be split on whitespace
 *	   Sergey Prigogin (Google) - [464072] Refresh on Access ignored during text search
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.core.util;

import java.io.*;
import java.net.URI;
import java.util.*;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.internal.ccvs.core.*;
import org.eclipse.team.internal.ccvs.core.resources.CVSEntryLineTag;
import org.eclipse.team.internal.ccvs.core.syncinfo.*;

/*
 * This is a helper class that knows the format of the CVS metafiles. It
 * provides a bridge between the CVS metafile formats and location to the
 * Eclipse CVS client ResourceSyncInfo and FolderSyncInfo types.
 */
public class SyncFileWriter {

	// the famous CVS meta directory name
	public static final String CVS_DIRNAME = "CVS"; //$NON-NLS-1$

	// CVS meta files located in the CVS subdirectory
	public static final String REPOSITORY = "Repository"; //$NON-NLS-1$
	public static final String ROOT = "Root"; //$NON-NLS-1$
	public static final String STATIC = "Entries.Static";	 //$NON-NLS-1$
	public static final String TAG = "Tag";	 //$NON-NLS-1$
	public static final String ENTRIES = "Entries"; //$NON-NLS-1$
	//private static final String PERMISSIONS = "Permissions"; //$NON-NLS-1$
	public static final String ENTRIES_LOG="Entries.Log"; //$NON-NLS-1$
	public static final String NOTIFY = "Notify"; //$NON-NLS-1$
	public static final String BASE_DIRNAME = "Base"; //$NON-NLS-1$
	public static final String BASEREV = "Baserev"; //$NON-NLS-1$
	
	// the local workspace file that contains pattern for ignored resources
	public static final String IGNORE_FILE = ".cvsignore"; //$NON-NLS-1$

	// Some older CVS clients may of added a line to the entries file consisting
	// of only a 'D'. It is safe to ignore these entries.	
	private static final String FOLDER_TAG="D"; //$NON-NLS-1$
	
	// Command characters found in the Entries.log file
	private static final String ADD_TAG="A "; //$NON-NLS-1$
	private static final String REMOVE_TAG="R "; //$NON-NLS-1$	
	
	// key for saving the mod stamp for each written meta file
	public static final QualifiedName MODSTAMP_KEY = new QualifiedName("org.eclipse.team.cvs.core", "meta-file-modtime"); //$NON-NLS-1$ //$NON-NLS-2$
	
	/**
	 * Reads the CVS/Entries, CVS/Entries.log and CVS/Permissions files from the
	 * specified folder and returns ResourceSyncInfo instances for the data stored therein.
	 * If the folder does not have a CVS subdirectory then <code>null</code> is returned.
	 */
	public static byte[][] readAllResourceSync(IContainer parent) throws CVSException {
		IFolder cvsSubDir = getCVSSubdirectory(parent);
		
		if (!folderExists(cvsSubDir)){
			return null;
		}
		
		if (Policy.DEBUG_METAFILE_CHANGES) {
			System.out.println("Reading Entries file for " + parent.getFullPath()); //$NON-NLS-1$
		}

		// process Entries file contents
		String[] entries = readLines(cvsSubDir.getFile(ENTRIES));
		if (entries == null) return null;
		Map infos = new TreeMap();
		for (String line : entries) {
			if(!FOLDER_TAG.equals(line) && !"".equals(line)) { //$NON-NLS-1$
				try {
					ResourceSyncInfo info = new ResourceSyncInfo(line, null);
					infos.put(info.getName(), info);
				} catch (CVSException e) {
					// There was a problem parsing the entry line.
					// Log the problem and skip the entry
					CVSProviderPlugin.log(new CVSStatus(IStatus.ERROR, NLS.bind(CVSMessages.SyncFileWriter_0, new String[] { parent.getFullPath().toString() }), e));
				}			
			}
		}
		
		// process Entries.log file contents
		String[] entriesLog = readLines(cvsSubDir.getFile(ENTRIES_LOG));
		if (entriesLog != null) {
			for (String line : entriesLog) {
				if (line.startsWith(ADD_TAG)) {
					line = line.substring(ADD_TAG.length());
					ResourceSyncInfo info = new ResourceSyncInfo(line, null);
					infos.put(info.getName(), info);
				} else if (line.startsWith(REMOVE_TAG)) {
					line = line.substring(REMOVE_TAG.length());
					ResourceSyncInfo info = new ResourceSyncInfo(line, null);
					infos.remove(info.getName());
				}
			}
		}
		
		//return (ResourceSyncInfo[])infos.values().toArray(new ResourceSyncInfo[infos.size()]);
		byte[][] result = new byte[infos.size()][];
		int i = 0;
		for (Iterator iter = infos.values().iterator(); iter.hasNext();) {
			ResourceSyncInfo info = (ResourceSyncInfo) iter.next();
			result[i++] = info.getBytes();
		}
		return result;
	}
	
	private static boolean folderExists(IFolder cvsSubDir) throws CVSException {
		try {
			URI uri = cvsSubDir.getLocationURI();
			if (uri != null){
				IFileStore store = EFS.getStore(uri);
				if (store != null){
					return store.fetchInfo().exists();
				}
			}
		} catch (CoreException e) {
			throw CVSException.wrapException(e);
		} 
		return false;
	}

	public static void writeAllResourceSync(IContainer parent, byte[][] infos) throws CVSException {
		try {
			if (Policy.DEBUG_METAFILE_CHANGES) {
				System.out.println("Writing Entries file for folder " + parent.getFullPath()); //$NON-NLS-1$
			}
			IFolder cvsSubDir = createCVSSubdirectory(parent);

			// format file contents
			String[] entries = new String[infos.length];
			for (int i = 0; i < infos.length; i++) {
				byte[] info = infos[i];
				entries[i] = new String(info);
			}

			// write Entries
			writeLines(cvsSubDir.getFile(ENTRIES), entries);

			// delete Entries.log
			cvsSubDir.getFile(ENTRIES_LOG).delete(IResource.NONE, null);
		} catch(CoreException e) {
			throw CVSException.wrapException(e);
		}
	}
	/**
	 * Reads the CVS/Root, CVS/Repository, CVS/Tag, and CVS/Entries.static files from
	 * the specified folder and returns a FolderSyncInfo instance for the data stored therein.
	 * If the folder does not have a CVS subdirectory then <code>null</code> is returned.
	 */
	public static FolderSyncInfo readFolderSync(IContainer folder) throws CVSException {
		IFolder cvsSubDir = getCVSSubdirectory(folder);
		
		if (!folderExists(cvsSubDir)){
			return null;
		}

		if (Policy.DEBUG_METAFILE_CHANGES) {
			System.out.println("Reading Root/Repository files for " + folder.getFullPath()); //$NON-NLS-1$
		}
		
		// check to make sure the the cvs folder is hidden
		if (!cvsSubDir.isTeamPrivateMember() && cvsSubDir.exists()) {
			try {
				cvsSubDir.setTeamPrivateMember(true);
			} catch (CoreException e) {
				CVSProviderPlugin.log(e);
			}
		}
				
		// read CVS/Root
		String root = readFirstLine(cvsSubDir.getFile(ROOT));
		if (root == null) return null;
		
		// read CVS/Repository
		String repository = readFirstLine(cvsSubDir.getFile(REPOSITORY));
		if (repository == null) return null;
		
		// read CVS/Tag
		String tag = readFirstLine(cvsSubDir.getFile(TAG));
		if (Policy.DEBUG_METAFILE_CHANGES && tag != null) {
			System.out.println("Reading Tag file for " + folder.getFullPath()); //$NON-NLS-1$
		}
		CVSTag cvsTag = (tag != null) ? new CVSEntryLineTag(tag) : null;

		// read Entries.Static
		String staticDir = readFirstLine(cvsSubDir.getFile(STATIC));
		if (Policy.DEBUG_METAFILE_CHANGES && staticDir != null) {
			System.out.println("Reading Static file for " + folder.getFullPath()); //$NON-NLS-1$
		}
		boolean isStatic = (staticDir != null);
		
		// return folder sync
		return new FolderSyncInfo(repository, root, cvsTag, isStatic);		
	}
	
	/**
	 * Writes the CVS/Root, CVS/Repository, CVS/Tag, and CVS/Entries.static files to the
	 * specified folder using the data contained in the specified FolderSyncInfo instance.
	 */
	public static void writeFolderSync(IContainer folder, FolderSyncInfo info) throws CVSException {
		try {
			if (Policy.DEBUG_METAFILE_CHANGES) {
				System.out.println("Writing Root/Respository files for " + folder.getFullPath()); //$NON-NLS-1$
			}
			IFolder cvsSubDir = createCVSSubdirectory(folder);
	
			// write CVS/Root
			writeLines(cvsSubDir.getFile(ROOT), new String[] {info.getRoot()});
			
			// write CVS/Repository
			writeLines(cvsSubDir.getFile(REPOSITORY), new String[] {info.getRepository()});
			
			// write CVS/Tag
			IFile tagFile = cvsSubDir.getFile(TAG);
			if (info.getTag() != null) {
				if (Policy.DEBUG_METAFILE_CHANGES) {
					System.out.println("Writing Tag file for " + folder.getFullPath()); //$NON-NLS-1$
				}
				writeLines(tagFile, new String[] {info.getTag().toEntryLineFormat(false)});
			} else {
				if(tagFile.exists()) {
					if (Policy.DEBUG_METAFILE_CHANGES) {
						System.out.println("Deleting Tag file for " + folder.getFullPath()); //$NON-NLS-1$
					}
					tagFile.delete(IResource.NONE, null);
				}
			}
			
			// write CVS/Entries.Static
			IFile staticFile = cvsSubDir.getFile(STATIC);
			if(info.getIsStatic()) {
				// the existence of the file is all that matters
				if (Policy.DEBUG_METAFILE_CHANGES) {
					System.out.println("Writing Static file for " + folder.getFullPath()); //$NON-NLS-1$
				}
				writeLines(staticFile, new String[] {""}); //$NON-NLS-1$
			} else {
				if(staticFile.exists()) {
					if (Policy.DEBUG_METAFILE_CHANGES) {
						System.out.println("Deleting Static file for " + folder.getFullPath()); //$NON-NLS-1$
					}
					staticFile.delete(IResource.NONE, null);
				}
			}
		} catch(CoreException e) {
			throw CVSException.wrapException(e);
		}
	}

	/**
	 * Returns all .cvsignore entries for the specified folder.
	 */
	public static String[] readCVSIgnoreEntries(IContainer folder) throws CVSException {
		IFile ignoreFile = folder.getFile(new Path(IGNORE_FILE));
		if (ignoreFile != null) {
			String[] lines = readLines(ignoreFile);
			if (lines == null)
				return null;
			// Split each line on spaces and tabs.
			ArrayList/*<String>*/ entries = new ArrayList/*<String>*/();
			for (String line : lines) {
				int pos = 0;
				while (pos < line.length()) {
					if (line.charAt(pos) == ' ' || line.charAt(pos) == '\t')
						pos++;
					else {
						int start = pos;
						while (pos < line.length() && line.charAt(pos) != ' ' && line.charAt(pos) != '\t')
							pos++;
						entries.add(line.substring(start, pos));
					}
				}
			}
			return (String[]) entries.toArray(new String[entries.size()]);
		}
		return null;
	}
	
	/**
	 * Writes all entries to the specified folder's .cvsignore file, overwriting any
	 * previous edition of the file.
	 */
	public static void writeCVSIgnoreEntries(IContainer folder, String[] patterns) throws CVSException {
		IFile ignoreFile = folder.getFile(new Path(IGNORE_FILE));
		writeLines(ignoreFile, patterns);
	}	

	/**
	 * Delete folder sync is equivalent to removing the CVS subdir.
	 */
	public static void deleteFolderSync(IContainer folder) throws CVSException {		
		try {
			if (Policy.DEBUG_METAFILE_CHANGES) {
				System.out.println("Deleting CVS directory from " + folder.getFullPath()); //$NON-NLS-1$
			}
			getCVSSubdirectory(folder).delete(IResource.NONE, null);
		} catch(CoreException e) {
			throw CVSException.wrapException(e);
		}
	}

	/**
	 * Reads the CVS/Notify file from the specified folder and returns NotifyInfo instances 
	 * for the data stored therein. If the folder does not have a CVS subdirectory then <code>null</code> is returned.
	 */
	public static NotifyInfo[] readAllNotifyInfo(IContainer parent) throws CVSException {
		IFolder cvsSubDir = getCVSSubdirectory(parent);

		if (!folderExists(cvsSubDir)){
			return null;
		}
		
		// process Notify file contents
		String[] entries = readLines(cvsSubDir.getFile(NOTIFY));
		if (entries == null) return null;
		Map infos = new TreeMap();
		for (String line : entries) {
			if(!"".equals(line)) { //$NON-NLS-1$
				try {
					NotifyInfo info = new NotifyInfo(parent, line);
					infos.put(info.getName(), info);
				} catch (CVSException e) {
					// We couldn't parse the notify info
					// Log it and ignore
					CVSProviderPlugin.log(e);
				}			
			}
		}
		
		return (NotifyInfo[])infos.values().toArray(new NotifyInfo[infos.size()]);
	}
	
	/**
	 * Writes the CVS/Notify file to the specified folder using the data contained in the 
	 * specified NotifyInfo instances. A CVS subdirectory must already exist (an exception 
	 * is thrown if it doesn't).
	 */
	public static void writeAllNotifyInfo(IContainer parent, NotifyInfo[] infos) throws CVSException {
		// get the CVS directory
		IFolder cvsSubDir = getCVSSubdirectory(parent);
		// write lines will throw an exception if the CVS directory does not exist
		
		if (infos.length == 0) {
			// if there are no notify entries, delete the notify file
			try {
				IFile notifyFile = cvsSubDir.getFile(NOTIFY);
				if(notifyFile.exists()) {
					notifyFile.delete(IResource.NONE, null);
				}
			} catch (CoreException e) {
				throw CVSException.wrapException(e);
			}
		} else {
			// format file contents
			String[] entries = new String[infos.length];
			for (int i = 0; i < infos.length; i++) {
				NotifyInfo info = infos[i];
				entries[i] = info.getNotifyLine();
			}
	
			// write Notify entries
			writeLines(cvsSubDir.getFile(NOTIFY), entries);
		}
	}

	/**
	 * Reads the CVS/Baserev file from the specified folder and returns
	 * BaserevInfo instances for the data stored therein. If the folder does not
	 * have a CVS subdirectory then <code>null</code> is returned.
	 */
	public static BaserevInfo[] readAllBaserevInfo(IContainer parent) throws CVSException {
		IFolder cvsSubDir = getCVSSubdirectory(parent);
		
		if (!folderExists(cvsSubDir)){
			return null;
		}
		
		// process Notify file contents
		String[] entries = readLines(cvsSubDir.getFile(BASEREV));
		if (entries == null) return null;
		Map infos = new TreeMap();
		for (String line : entries) {
			if(!"".equals(line)) { //$NON-NLS-1$
				BaserevInfo info = new BaserevInfo(line);
				infos.put(info.getName(), info);
			}
		}

		return (BaserevInfo[])infos.values().toArray(new BaserevInfo[infos.size()]);
	}

	/**
	 * Writes the CVS/Baserev file to the specified folder using the data
	 * contained in the specified BaserevInfo instances. A CVS subdirectory must
	 * already exist (an exception is thrown if it doesn't).
	 */
	public static void writeAllBaserevInfo(IContainer parent, BaserevInfo[] infos) throws CVSException {
		// get the CVS directory
		IFolder cvsSubDir = getCVSSubdirectory(parent);
		// write lines will throw an exception if the CVS directory does not exist

		// format file contents
		String[] entries = new String[infos.length];
		for (int i = 0; i < infos.length; i++) {
			BaserevInfo info = infos[i];
			entries[i] = info.getEntryLine();
		}

		// write Notify entries
		writeLines(cvsSubDir.getFile(BASEREV), entries);
	}
				
	/**
	 * Returns the CVS subdirectory for this folder.
	 */
	private static IFolder getCVSSubdirectory(IContainer folder) {
		return folder.getFolder(new Path(CVS_DIRNAME));
	}
	
	/**
	 * Creates and makes team-private and returns a CVS subdirectory in this folder.
	 */
	private static IFolder createCVSSubdirectory(IContainer folder) throws CVSException {
		try {
			final IFolder cvsSubDir = getCVSSubdirectory(folder);
			if (! cvsSubDir.exists()) {
				// important to have both the folder creation and setting of team-private in the
				// same runnable so that the team-private flag is set before other delta listeners 
				// sees the CVS folder creation.
				ResourcesPlugin.getWorkspace().run((IWorkspaceRunnable) monitor -> {
					// Re-check existence in case this method was called without a resource rule
					if (! cvsSubDir.exists()) {
						if (existsInFileSystem(cvsSubDir)) {
							cvsSubDir.refreshLocal(IResource.DEPTH_INFINITE, null);
							cvsSubDir.setTeamPrivateMember(true);
						} else {
							cvsSubDir.create(IResource.TEAM_PRIVATE, true /*make local*/, null);
						}
					} else {
						if (!cvsSubDir.isTeamPrivateMember()) {
							cvsSubDir.setTeamPrivateMember(true);
						}
					}
				}, folder, 0, null);
			}
			return cvsSubDir;
		} catch (CoreException e) {
			throw CVSException.wrapException(e);
		}
	}

	protected static boolean existsInFileSystem(IFolder cvsSubDir) {
		URI uri = cvsSubDir.getLocationURI();
		if (uri != null) {
			try {
				IFileStore store = EFS.getStore(uri);
				if (store != null) {
					return store.fetchInfo().exists();
				}
			} catch (CoreException e) {
				CVSProviderPlugin.log(e);
			}
		}
		return false;
	}

	/*
	 * Reads the first line of the specified file.
	 * Returns null if the file does not exist, or the empty string if it is blank.
	 */
	private static String readFirstLine(IFile file) throws CVSException {
		try {
			InputStream in = getInputStream(file);
			if (in != null) {
				try (BufferedReader reader = new BufferedReader(new InputStreamReader(in), 512)) {
					String line = reader.readLine();
					if (line == null) return ""; //$NON-NLS-1$
					return line;
				}
			}
			return null;
		} catch (IOException e) {
			throw CVSException.wrapException(e);
		} catch (CoreException e) {
			// If the IFile doesn't exist or the underlying File doesn't exist,
			// just return null to indicate the absence of the file
			switch (e.getStatus().getCode()) {
			case IResourceStatus.RESOURCE_NOT_FOUND:
			case IResourceStatus.NOT_FOUND_LOCAL:
			case IResourceStatus.FAILED_READ_LOCAL:
				return null;
			default:
				throw CVSException.wrapException(e);
			}
		}
	}

	private static InputStream getInputStream(IFile file) throws CoreException, FileNotFoundException {
		if (file.exists()) {
			return file.getContents(true);
		}
		
		URI uri = file.getLocationURI();
		if (uri != null) {
			IFileStore store = EFS.getStore(uri);
			if (store != null) {
				return store.openInputStream(EFS.NONE, null);
			}
		}
		
		IPath location = file.getLocation();
		if (location != null) {
			File ioFile = location.toFile();
			if (ioFile != null && ioFile.exists()) {
				return new FileInputStream(ioFile);
			}
		}

		return null;
	}
	
	/*
	 * Reads all lines of the specified file.
	 * Returns null if the file does not exist.
	 */
	private static String[] readLines(IFile file) throws CVSException {
		try {
			InputStream in = getInputStream(file);
			if (in != null) {
				BufferedReader reader = new BufferedReader(new InputStreamReader(in), 512);
				List fileContentStore = new ArrayList();
				try {
					String line;
					while ((line = reader.readLine()) != null) {
						fileContentStore.add(line);
					}
					return (String[]) fileContentStore.toArray(new String[fileContentStore.size()]);
				} finally {
					reader.close();
				}
			}
			return null;
		} catch (IOException e) {
			throw CVSException.wrapException(e);
		} catch (CoreException e) {
			// If the IFile doesn't exist or the underlying File doesn't exist,
			// just return null to indicate the absence of the file
			switch (e.getStatus().getCode()) {
			case IResourceStatus.RESOURCE_NOT_FOUND:
			case IResourceStatus.NOT_FOUND_LOCAL:
			case IResourceStatus.FAILED_READ_LOCAL:
				return null;
			default:
				throw CVSException.wrapException(e);
			}
		}
	}
	
	/*
	 * Writes all lines to the specified file, using linefeed terminators for
	 * compatibility with other CVS clients.
	 */
	private static void writeLines(final IFile file, final String[] contents) throws CVSException {
		try {
			// The creation of sync files has to be in a runnable in order for the resulting delta
			// to include the MODSTAMP value. If not in a runnable then create/setContents
			// will trigger a delta and the SyncFileWriter change listener won't know that the delta
			// was a result of our own creation.
			ResourcesPlugin.getWorkspace().run((IWorkspaceRunnable) monitor -> {
				try {
					ByteArrayOutputStream os = new ByteArrayOutputStream();
					writeLinesToStreamAndClose(os, contents);
					if(!file.exists()) {
						file.create(new ByteArrayInputStream(os.toByteArray()), IResource.FORCE /*don't keep history but do force*/, null);
					} else {
						file.setContents(new ByteArrayInputStream(os.toByteArray()), IResource.FORCE /*don't keep history but do force*/, null);
					}			
					file.setSessionProperty(MODSTAMP_KEY, Long.valueOf(file.getModificationStamp()));
				} catch(CVSException e) {
					throw new CoreException(e.getStatus());
				}
			}, ResourcesPlugin.getWorkspace().getRuleFactory().createRule(file), 0, null);
		} catch (CoreException e) {
			throw CVSException.wrapException(e);
		}
	}
	
	private static void writeLinesToStreamAndClose(OutputStream os, String[] contents) throws CVSException {
		byte[] lineEnd = getLineDelimiter();
		try {
			try {
				for (String content : contents) {
					os.write(content.getBytes());
					os.write(lineEnd);
				}
			} finally {
				os.close();
			}
		} catch (IOException e) {
			throw CVSException.wrapException(e);
		}
	}
	
	/**
	 * Method writeFileToBaseDirectory.
	 * 
	 * @param file
	 * @param info
	 */
	public static void writeFileToBaseDirectory(IFile file, IProgressMonitor monitor) throws CVSException {
		monitor = Policy.monitorFor(monitor);
		monitor.beginTask(null, 100);
		try {
			IFolder baseFolder = getBaseDirectory(file);
			if (!baseFolder.exists()) {
				baseFolder.create(false /* force */, true /* local */, Policy.subMonitorFor(monitor, 10));
			}
			IFile target = baseFolder.getFile(new Path(null, file.getName()));
			if (target.exists()) {
				// XXX Should ensure that we haven't already copied it
				// XXX write the revision to the CVS/Baserev file
				setReadOnly(target, false);
				target.delete(true, Policy.subMonitorFor(monitor, 10));
			}
			// Copy the file so the timestamp is maintained
			file.copy(target.getFullPath(), true /* force */, Policy.subMonitorFor(monitor, 80));
		} catch (CoreException e) {
			throw CVSException.wrapException(e);
		} finally {
			monitor.done();
		}
	}
	/**
	 * Method restoreFileFromBaseDirectory.
	 * @param file
	 * @param info
	 * @param monitor
	 */
	public static void restoreFileFromBaseDirectory(IFile file, IProgressMonitor monitor) throws CVSException {
		monitor = Policy.monitorFor(monitor);
		monitor.beginTask(null, 100);
		try {
			IFolder baseFolder = getBaseDirectory(file);
			IFile source = baseFolder.getFile(new Path(null, file.getName()));
			if (!source.exists()) {
				IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.SyncFileWriter_baseNotAvailable, new String[] { file.getFullPath().toString() }), file);
				throw new CVSException(status); 
			}
			if (file.exists()) {
				file.delete(false /* force */, true /* keep history */, Policy.subMonitorFor(monitor, 10));
			}
			// Make the source writtable to avoid problems on some file systems (bug 109308)
			setReadOnly(source, false);
			// Copy the file so the timestamp is maintained
			source.move(file.getFullPath(), false /* force */, true /* keep history */,Policy.subMonitorFor(monitor, 100));
		} catch (CoreException e) {
			throw CVSException.wrapException(e);
		} finally {
			monitor.done();
		}
	}

	private static void setReadOnly(IFile source, boolean readOnly) {
		ResourceAttributes attrs = source.getResourceAttributes();
		if (attrs != null && attrs.isReadOnly() != readOnly) {
			attrs.setReadOnly(readOnly);
			try {
				source.setResourceAttributes(attrs);
			} catch (CoreException e) {
				// Just log the failure since the move may succeed anyway
				CVSProviderPlugin.log(e);
			}
		}
	}
	
	/**
	 * Method deleteFileFromBaseDirectory.
	 * @param file
	 * @param monitor
	 */
	public static void deleteFileFromBaseDirectory(IFile file, IProgressMonitor monitor) throws CVSException {
		monitor = Policy.monitorFor(monitor);
		monitor.beginTask(null, 100);
		try {
			IFolder baseFolder = getBaseDirectory(file);
			IFile source = baseFolder.getFile(new Path(null, file.getName()));
			if (source.exists()) {
				setReadOnly(source, false);
				source.delete(false, false, Policy.subMonitorFor(monitor, 100));
			}
		} catch (CoreException e) {
			throw CVSException.wrapException(e);
		} finally {
			monitor.done();
		}
	}

	private static IFolder getBaseDirectory(IFile file) {
		IContainer cvsFolder = getCVSSubdirectory(file.getParent());
		IFolder baseFolder = cvsFolder.getFolder(new Path(BASE_DIRNAME));
		return baseFolder;
	}
	
	/**
	 * Return a handle to the CVS/Template file for the given folder
	 * @param folder
	 * @return IFile
	 * @throws CVSException
	 */
	public static IFile getTemplateFile(IContainer folder) throws CVSException {
		IFolder cvsFolder = createCVSSubdirectory(folder);
		return cvsFolder.getFile("Template"); //$NON-NLS-1$
	}
	
	/**
	 * Method isEdited.
	 * @param resource
	 * @return boolean
	 */
	public static boolean isEdited(IFile file) {
		IFolder baseFolder = getBaseDirectory(file);
		IFile baseFile = baseFolder.getFile(file.getName());
		return baseFile.exists();
	}
	
	private static byte[] getLineDelimiter() {
		if (CVSProviderPlugin.getPlugin().isUsePlatformLineend()) {
			String property = System.getProperty("line.separator"); //$NON-NLS-1$
			if (property != null) return property.getBytes();
		}
		return new byte[] { 0x0A }; 
	}

}
