/*******************************************************************************
 * Copyright (c) 2000, 2016 QNX Software Systems 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:
 *     QNX Software Systems - Initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.debug.internal.core.sourcelookup;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;

import org.eclipse.cdt.core.resources.FileStorage;
import org.eclipse.cdt.debug.core.CDebugCorePlugin;
import org.eclipse.cdt.debug.core.CDebugUtils;
import org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation;
import org.eclipse.cdt.debug.core.sourcelookup.IDirectorySourceLocation;
import org.eclipse.cdt.internal.core.resources.ResourceLookup;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.osgi.util.NLS;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 *
 * Locates source elements in a directory in the local
 * file system. Returns instances of <code>FileStorage</code>.
 *
 * @since Sep 23, 2002
 */
public class CDirectorySourceLocation implements IDirectorySourceLocation {
	private static final String ELEMENT_NAME = "cDirectorySourceLocation"; //$NON-NLS-1$
	private static final String ATTR_DIRECTORY = "directory"; //$NON-NLS-1$
	private static final String ATTR_ASSOCIATION = "association"; //$NON-NLS-1$
	private static final String ATTR_SEARCH_SUBFOLDERS = "searchSubfolders"; //$NON-NLS-1$

	/**
	 * The root directory of this source location
	 */
	private IPath fDirectory;
	/**
	 * The associated path of this source location.
	 */
	private IPath fAssociation;
	private boolean fSearchForDuplicateFiles;
	private boolean fSearchSubfolders;
	private File[] fFolders;

	/**
	 * Constructor for CDirectorySourceLocation.
	 */
	public CDirectorySourceLocation() {
	}

	/**
	 * Constructor for CDirectorySourceLocation.
	 */
	public CDirectorySourceLocation(IPath directory, IPath association, boolean searchSubfolders) {
		setDirectory(directory);
		setAssociation(association);
		setSearchSubfolders(searchSubfolders);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation#findSourceElement(String)
	 */
	@Override
	public Object findSourceElement(String name) throws CoreException {
		Object result = null;
		if (!isEmpty(name) && getDirectory() != null) {
			File file = new File(name);
			if (file.isAbsolute()) {
				result = findFileByAbsolutePath(name);
			} else {
				result = findFileByRelativePath(name);
			}
			if (result == null && getAssociation() != null) {
				IPath path = new Path(name);
				if (path.segmentCount() > 1 && getAssociation().isPrefixOf(path)) {
					path = getDirectory().append(path.removeFirstSegments(getAssociation().segmentCount()));
					result = findFileByAbsolutePath(path.toOSString());
				}
			}
		}
		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter.equals(ICSourceLocation.class))
			return (T) this;
		if (adapter.equals(CDirectorySourceLocation.class))
			return (T) this;
		if (adapter.equals(IPath.class))
			return (T) getDirectory();
		return null;
	}

	/**
	 * Sets the directory in which source elements will be searched for.
	 *
	 * @param directory a directory
	 */
	private void setDirectory(IPath directory) {
		fDirectory = directory;
	}

	/**
	 * Returns the root directory of this source location.
	 *
	 * @return directory
	 */
	@Override
	public IPath getDirectory() {
		return fDirectory;
	}

	public void getDirectory(IPath path) {
		fDirectory = path;
	}

	public void setAssociation(IPath association) {
		fAssociation = association;
	}

	@Override
	public IPath getAssociation() {
		return fAssociation;
	}

	@SuppressWarnings("unchecked")
	private Object findFileByAbsolutePath(String name) {
		File file = new File(name);
		if (!file.isAbsolute())
			return null;
		File[] folders = getFolders();
		if (folders != null) {
			LinkedList<Object> list = new LinkedList<>();
			for (int i = 0; i < folders.length; ++i) {
				Object result = findFileByAbsolutePath(folders[i], name);
				if (result instanceof List) {
					if (searchForDuplicateFiles()) {
						list.addAll((List<Object>) result);
					} else {
						return list.getFirst();
					}
				} else if (result != null) {
					if (searchForDuplicateFiles()) {
						list.add(result);
					} else {
						return result;
					}
				}
			}
			if (list.size() > 0)
				return (list.size() == 1) ? list.getFirst() : list;
		}
		return null;
	}

	private Object findFileByAbsolutePath(File folder, String name) {
		File file = new File(name);
		if (!file.isAbsolute())
			return null;
		IPath filePath = new Path(name);
		IPath path = new Path(folder.getAbsolutePath());
		IPath association = getAssociation();
		if (!isPrefix(path, filePath) || path.segmentCount() + 1 != filePath.segmentCount()) {
			if (association != null && isPrefix(association, filePath)
					&& association.segmentCount() + 1 == filePath.segmentCount()) {
				filePath = path.append(filePath.removeFirstSegments(association.segmentCount()));
			} else {
				return null;
			}
		}

		// Try for a file in another workspace project
		IFile[] wsFiles = ResourceLookup.findFilesForLocation(filePath);
		LinkedList<IFile> list = new LinkedList<>();
		for (int j = 0; j < wsFiles.length; ++j)
			if (wsFiles[j].exists()) {
				if (!searchForDuplicateFiles())
					return wsFiles[j];
				list.add(wsFiles[j]);
			}
		if (list.size() > 0)
			return (list.size() == 1) ? list.getFirst() : list;

		file = filePath.toFile();
		if (file.exists() && file.isFile()) {
			return createExternalFileStorage(filePath);
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private Object findFileByRelativePath(String fileName) {
		File[] folders = getFolders();
		if (folders != null) {
			LinkedList<Object> list = new LinkedList<>();
			for (int i = 0; i < folders.length; ++i) {
				Object result = findFileByRelativePath(folders[i], fileName);
				if (result instanceof List) {
					if (searchForDuplicateFiles()) {
						list.addAll((List<Object>) result);
					} else {
						return list.getFirst();
					}
				} else if (result != null) {
					if (searchForDuplicateFiles()) {
						list.add(result);
					} else {
						return result;
					}
				}
			}
			if (list.size() > 0)
				return (list.size() == 1) ? list.getFirst() : list;
		}
		return null;
	}

	private Object findFileByRelativePath(File folder, String fileName) {
		IPath path = new Path(folder.getAbsolutePath());
		path = path.append(fileName);
		File file = path.toFile();
		if (file.exists() && file.isFile()) {
			path = new Path(file.getAbsolutePath());
			IFile[] wsFiles = ResourceLookup.findFilesForLocation(path);
			LinkedList<IFile> list = new LinkedList<>();
			for (int j = 0; j < wsFiles.length; ++j)
				if (wsFiles[j].exists()) {
					if (!searchForDuplicateFiles())
						return wsFiles[j];
					list.add(wsFiles[j]);
				}
			if (list.size() > 0)
				return (list.size() == 1) ? list.getFirst() : list;
			return createExternalFileStorage(path);
		}
		return null;
	}

	private IStorage createExternalFileStorage(IPath path) {
		return new FileStorage(path);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation#getMemento()
	 */
	@Override
	public String getMemento() throws CoreException {
		Document document = null;
		Throwable ex = null;
		try {
			document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
			Element node = document.createElement(ELEMENT_NAME);
			document.appendChild(node);
			node.setAttribute(ATTR_DIRECTORY, getDirectory().toOSString());
			if (getAssociation() != null)
				node.setAttribute(ATTR_ASSOCIATION, getAssociation().toOSString());
			node.setAttribute(ATTR_SEARCH_SUBFOLDERS, String.valueOf(searchSubfolders()));
			return CDebugUtils.serializeDocument(document);
		} catch (ParserConfigurationException e) {
			ex = e;
		} catch (IOException e) {
			ex = e;
		} catch (TransformerException e) {
			ex = e;
		}
		abort(NLS.bind(InternalSourceLookupMessages.CDirectorySourceLocation_0, getDirectory().toOSString()), ex);
		// execution will not reach here
		return null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation#initializeFrom(java.lang.String)
	 */
	@Override
	public void initializeFrom(String memento) throws CoreException {
		Exception ex = null;
		try {
			Element root = null;
			DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			StringReader reader = new StringReader(memento);
			InputSource source = new InputSource(reader);
			root = parser.parse(source).getDocumentElement();

			String dir = root.getAttribute(ATTR_DIRECTORY);
			if (isEmpty(dir)) {
				abort(InternalSourceLookupMessages.CDirectorySourceLocation_1, null);
			} else {
				IPath path = new Path(dir);
				if (path.isValidPath(dir) && path.toFile().isDirectory() && path.toFile().exists()) {
					setDirectory(path);
				} else {
					abort(NLS.bind(InternalSourceLookupMessages.CDirectorySourceLocation_2, dir), null);
				}
			}
			dir = root.getAttribute(ATTR_ASSOCIATION);
			if (isEmpty(dir)) {
				setAssociation(null);
			} else {
				IPath path = new Path(dir);
				if (path.isValidPath(dir)) {
					setAssociation(path);
				} else {
					setAssociation(null);
				}
			}
			setSearchSubfolders(Boolean.valueOf(root.getAttribute(ATTR_SEARCH_SUBFOLDERS)).booleanValue());
			return;
		} catch (ParserConfigurationException e) {
			ex = e;
		} catch (SAXException e) {
			ex = e;
		} catch (IOException e) {
			ex = e;
		}
		abort(InternalSourceLookupMessages.CDirectorySourceLocation_3, ex);
	}

	/**
	 * Throws an internal error exception
	 */
	private void abort(String message, Throwable e) throws CoreException {
		IStatus s = new Status(IStatus.ERROR, CDebugCorePlugin.getUniqueIdentifier(), CDebugCorePlugin.INTERNAL_ERROR,
				message, e);
		throw new CoreException(s);
	}

	private boolean isEmpty(String string) {
		return string == null || string.length() == 0;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof IDirectorySourceLocation) {
			IPath dir = ((IDirectorySourceLocation) obj).getDirectory();
			IPath association = ((IDirectorySourceLocation) obj).getAssociation();
			if (dir == null)
				return false;
			boolean result = dir.equals(getDirectory());
			if (result) {
				if (association == null && getAssociation() == null)
					return true;
				if (association != null)
					return association.equals(getAssociation());
			}
		}
		return false;
	}

	private boolean isPrefix(IPath prefix, IPath path) {
		int segCount = prefix.segmentCount();
		if (segCount >= path.segmentCount())
			return false;
		String prefixString = prefix.toOSString();
		String pathString = path.removeLastSegments(path.segmentCount() - segCount).toOSString();
		return prefixString.equalsIgnoreCase(pathString);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation#setSearchForDuplicateFiles(boolean)
	 */
	@Override
	public void setSearchForDuplicateFiles(boolean search) {
		fSearchForDuplicateFiles = search;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation#searchForDuplicateFiles()
	 */
	@Override
	public boolean searchForDuplicateFiles() {
		return fSearchForDuplicateFiles;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.IDirectorySourceLocation#searchSubfolders()
	 */
	@Override
	public boolean searchSubfolders() {
		return fSearchSubfolders;
	}

	public void setSearchSubfolders(boolean search) {
		resetFolders();
		fSearchSubfolders = search;
	}

	protected File[] getFolders() {
		if (fFolders == null)
			initializeFolders();
		return fFolders;
	}

	protected void resetFolders() {
		fFolders = null;
	}

	private void initializeFolders() {
		if (getDirectory() != null) {
			ArrayList<File> list = new ArrayList<>();
			File root = getDirectory().toFile();
			list.add(root);
			if (searchSubfolders())
				list.addAll(getFileFolders(root));
			fFolders = list.toArray(new File[list.size()]);
		}
	}

	private List<File> getFileFolders(File file) {
		ArrayList<File> list = new ArrayList<>();
		File[] folders = file.listFiles(new FileFilter() {
			@Override
			public boolean accept(File pathname) {
				return pathname.isDirectory();
			}
		});
		list.addAll(Arrays.asList(folders));
		for (int i = 0; i < folders.length; ++i)
			list.addAll(getFileFolders(folders[i]));
		return list;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return (getDirectory() != null) ? getDirectory().toOSString() : ""; //$NON-NLS-1$
	}

	/* (non-Javadoc)
	 * @see org.eclipse.cdt.debug.core.sourcelookup.ICSourceLocation#dispose()
	 */
	@Override
	public void dispose() {
	}
}
