/*******************************************************************************
 * Copyright (c) 2008, 2015 Wind River Systems, Inc. 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:
 *     Markus Schorn - initial API and implementation
 *     Karsten Thoms (itemis) - Bug 471103
 *******************************************************************************/
package org.eclipse.cdt.internal.core.pdom.indexer;

import java.io.File;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.net.URI;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.cdt.utils.UNCPathConverter;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;

/**
 * A cache for checking whether a file exists. The cache shall be used for a limited amount of time,
 * only (e.g. one indexer task). It uses as much memory as it needs. To protect against OutOfMemory
 * situations, a soft reference is used.
 * @since 5.0
 */
public final class FileExistsCache {
	private static final Content EMPTY_STRING_ARRAY = new Content(new String[0]);
	private static boolean BYPASS_CACHE = Boolean.getBoolean("CDT_INDEXER_BYPASS_FILE_EXISTS_CACHE"); //$NON-NLS-1$

	private static class Content {
		public Content(String[] names) {
			fNames = names;
			fIsFile = new BitSet(names.length * 2);
		}

		public String[] fNames;
		public BitSet fIsFile;
	}

	private Reference<Map<String, Content>> fCache;
	// Cache for recent results of isFile calls (bug 471103).
	private final Map<String, Boolean> fCacheIsFile = new HashMap<>();
	private final boolean fCaseInSensitive;

	public FileExistsCache(boolean caseInsensitive) {
		fCaseInSensitive = caseInsensitive;
		Map<String, Content> cache = new HashMap<>();
		// Before running out of memory the entire map will be thrown away.
		fCache = new SoftReference<>(cache);
	}

	public boolean isFile(String path) {
		// Fast return when path was already queried. The method is potentially called multiple times with
		// the same path on each return statement the returned value is stored in the cache (bug 471103).
		Boolean cachedResult = fCacheIsFile.get(path);
		if (!BYPASS_CACHE && cachedResult != null) {
			return cachedResult.booleanValue();
		}

		boolean result = isFileInternal(path);
		fCacheIsFile.put(path, result);
		return result;
	}

	private boolean isFileInternal(String path) {
		String parent;
		String name;
		File file = null;
		IFileStore parentStore = null;
		IFileStore fileStore = null;

		if (UNCPathConverter.isUNC(path)) {
			try {
				URI uri = UNCPathConverter.getInstance().toURI(path);
				fileStore = EFS.getStore(uri);
				if (BYPASS_CACHE) {
					return fileStore != null && !fileStore.fetchInfo().isDirectory();
				}
				parentStore = fileStore.getParent();
				if (parentStore == null) {
					parentStore = fileStore;
				}
				parent = parentStore.toURI().toString();
				name = fileStore.getName();
			} catch (CoreException e) {
				return false;
			}
		} else {
			file = new File(path);
			if (BYPASS_CACHE) {
				return file.isFile();
			}

			parent = file.getParent();
			if (parent == null)
				return false;

			name = file.getName();
		}
		if (fCaseInSensitive)
			name = name.toUpperCase();

		Content avail = getExistsCache().get(parent);
		if (avail == null) {
			String[] files = null;
			try {
				files = parentStore == null ? new File(parent).list() : parentStore.childNames(EFS.NONE, null);
			} catch (CoreException e) {
				// Ignore
			}
			if (files == null || files.length == 0) {
				avail = EMPTY_STRING_ARRAY;
			} else {
				if (fCaseInSensitive) {
					for (int i = 0; i < files.length; i++) {
						files[i] = files[i].toUpperCase();
					}
				}
				Arrays.sort(files);
				avail = new Content(files);
			}
			getExistsCache().put(parent, avail);
		}
		int idx = Arrays.binarySearch(avail.fNames, name);
		if (idx < 0)
			return false;
		idx *= 2;

		final BitSet isFileBitset = avail.fIsFile;
		if (isFileBitset.get(idx))
			return true;
		if (isFileBitset.get(idx + 1))
			return false;

		if ((file != null && file.isFile()) || (fileStore != null && !fileStore.fetchInfo().isDirectory())) {
			isFileBitset.set(idx);
			return true;
		}
		isFileBitset.set(idx + 1);
		return false;
	}

	private Map<String, Content> getExistsCache() {
		Map<String, Content> cache = fCache.get();
		if (cache == null) {
			cache = new HashMap<>();
			// Before running out of memory the entire map will be thrown away.
			fCache = new SoftReference<>(cache);
		}
		return cache;
	}
}
