/*******************************************************************************
 * Copyright (c) 2002, 2016 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:
 *     Rational Software - Initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *     Anton Leherbauer (Wind River Systems)
 *     IBM Corporation - EFS support
 *     Marc-Andre Laperle (Ericsson)
 *******************************************************************************/
package org.eclipse.cdt.internal.core.model;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.ICLogConstants;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ICModelStatusConstants;
import org.eclipse.cdt.internal.core.util.CharArrayBuffer;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;

import com.ibm.icu.text.MessageFormat;

public class Util implements ICLogConstants {
	public static boolean VERBOSE_PARSER;
	public static boolean VERBOSE_SCANNER;
	public static boolean VERBOSE_MODEL;
	public static boolean VERBOSE_DELTA;
	public static boolean PARSER_EXCEPTIONS;

	public static String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$

	private Util() {
	}

	public static StringBuffer getContent(IFile file) throws IOException {
		InputStream stream = null;
		try {
			stream = new BufferedInputStream(file.getContents(true));
		} catch (CoreException e) {
			throw new IOException(e.getMessage());
		}
		try {
			char[] b = getInputStreamAsCharArray(stream, -1, null);
			return new StringBuffer(b.length).append(b);
		} finally {
			try {
				stream.close();
			} catch (IOException e) {
			}
		}
	}

	/**
	 * Returns the given input stream's contents as a character array. If a
	 * length is specified (ie. if length != -1), only length chars are
	 * returned. Otherwise all chars in the stream are returned. Closes the stream.
	 *
	 * @throws IOException
	 *             if a problem occured reading the stream.
	 */
	public static char[] getInputStreamAsCharArray(InputStream stream, int length, String encoding) throws IOException {
		final InputStreamReader reader = encoding == null ? new InputStreamReader(stream)
				: new InputStreamReader(stream, encoding);
		try {
			char[] contents;
			if (length == -1) {
				contents = new char[0];
				int contentsLength = 0;
				int charsRead = -1;
				do {
					int available = stream.available();
					// resize contents if needed
					if (contentsLength + available > contents.length) {
						System.arraycopy(contents, 0, contents = new char[contentsLength + available], 0,
								contentsLength);
					}
					// read as many chars as possible
					charsRead = reader.read(contents, contentsLength, available);
					if (charsRead > 0) {
						// remember length of contents
						contentsLength += charsRead;
					}
				} while (charsRead > 0);
				// resize contents if necessary
				if (contentsLength < contents.length) {
					System.arraycopy(contents, 0, contents = new char[contentsLength], 0, contentsLength);
				}
			} else {
				contents = new char[length];
				int len = 0;
				int readSize = 0;
				while ((readSize != -1) && (len != length)) {
					// See PR 1FMS89U
					// We record first the read size. In this case len is the
					// actual read size.
					len += readSize;
					readSize = reader.read(contents, len, length - len);
				}
				// See PR 1FMS89U
				// Now we need to resize in case the default encoding used more
				// than one byte for each
				// character
				if (len != length)
					System.arraycopy(contents, 0, (contents = new char[len]), 0, len);
			}
			return contents;
		} finally {
			reader.close();
		}
	}

	/**
	 * Returns the given file's contents as a character array.
	 */
	public static char[] getResourceContentsAsCharArray(IFile file) throws CModelException {
		try {
			return getResourceContentsAsCharArray(file, file.getCharset());
		} catch (CoreException exc) {
			throw new CModelException(exc, ICModelStatusConstants.CORE_EXCEPTION);
		}
	}

	public static char[] getResourceContentsAsCharArray(IFile file, String encoding) throws CModelException {
		InputStream stream = null;
		try {
			stream = new BufferedInputStream(file.getContents(true));
		} catch (CoreException e) {
			throw new CModelException(e, ICModelStatusConstants.ELEMENT_DOES_NOT_EXIST);
		}
		try {
			return Util.getInputStreamAsCharArray(stream, -1, encoding);
		} catch (IOException e) {
			throw new CModelException(e, ICModelStatusConstants.IO_EXCEPTION);
		} finally {
			try {
				stream.close();
			} catch (IOException e) {
			}
		}
	}

	/*
	 * Add a log entry
	 */
	public static void log(Throwable e, String message, LogConst logType) {
		IStatus status = new Status(IStatus.ERROR, CCorePlugin.PLUGIN_ID, message, e);
		Util.log(status, logType);
	}

	public static void log(IStatus status, LogConst logType) {
		if (logType.equals(ICLogConstants.PDE)) {
			CCorePlugin.getDefault().getLog().log(status);
		} else if (logType.equals(ICLogConstants.CDT)) {
			CCorePlugin.getDefault().cdtLog.log(status);
		}
	}

	public static void log(String message, LogConst logType) {
		IStatus status = new Status(IStatus.INFO, CCorePlugin.PLUGIN_ID, IStatus.INFO, message, null);
		Util.log(status, logType);
	}

	public static void debugLog(String message, DebugLogConstants client) {
		Util.debugLog(message, client, true);
	}

	public static void debugLog(String message, DebugLogConstants client, boolean addTimeStamp) {
		if (CCorePlugin.getDefault() == null)
			return;
		if (CCorePlugin.getDefault().isDebugging() && isActive(client)) {
			// Time stamp
			if (addTimeStamp)
				message = MessageFormat.format("[{0}] {1}", new Object[] { //$NON-NLS-1$
						Long.valueOf(System.currentTimeMillis()), message });
			while (message.length() > 100) {
				String partial = message.substring(0, 100);
				message = message.substring(100);
				System.out.println(partial + "\\"); //$NON-NLS-1$
			}
			if (message.endsWith("\n")) { //$NON-NLS-1$
				System.err.print(message);
			} else {
				System.out.println(message);
			}
		}
	}

	public static boolean isActive(DebugLogConstants client) {
		if (client.equals(DebugLogConstants.PARSER)) {
			return VERBOSE_PARSER;
		} else if (client.equals(DebugLogConstants.SCANNER)) {
			return VERBOSE_SCANNER;
		} else if (client.equals(DebugLogConstants.MODEL)) {
			return VERBOSE_MODEL;
		}
		return false;
	}

	public static void setDebugging(boolean value) {
		CCorePlugin.getDefault().setDebugging(value);
	}

	/**
	 * Combines two hash codes to make a new one.
	 */
	public static int combineHashCodes(int hashCode1, int hashCode2) {
		return hashCode1 * 17 + hashCode2;
	}

	/**
	 * Compares two arrays using equals() on the elements. Either or both
	 * arrays may be null. Returns true if both are null. Returns false if only
	 * one is null. If both are arrays, returns true iff they have the same
	 * length and all elements compare true with equals.
	 */
	public static boolean equalArraysOrNull(Object[] a, Object[] b) {
		if (a == b)
			return true;
		if (a == null || b == null)
			return false;
		int len = a.length;
		if (len != b.length)
			return false;
		for (int i = 0; i < len; ++i) {
			if (a[i] == null) {
				if (b[i] != null)
					return false;
			} else {
				if (!a[i].equals(b[i]))
					return false;
			}
		}
		return true;
	}

	/**
	 * Compares two arrays using equals() on the elements. Either or both
	 * arrays may be null. Returns true if both are null. Returns false if only
	 * one is null. If both are arrays, returns true iff they have the same
	 * length and all elements are equal.
	 */
	public static boolean equalArraysOrNull(int[] a, int[] b) {
		if (a == b)
			return true;
		if (a == null || b == null)
			return false;
		int len = a.length;
		if (len != b.length)
			return false;
		for (int i = 0; i < len; ++i) {
			if (a[i] != b[i])
				return false;
		}
		return true;
	}

	/**
	 * Compares two objects using equals(). Either or both array may be null.
	 * Returns true if both are null. Returns false if only one is null.
	 * Otherwise, return the result of comparing with equals().
	 */
	public static boolean equalOrNull(Object a, Object b) {
		if (a == b) {
			return true;
		}
		if (a == null || b == null) {
			return false;
		}
		return a.equals(b);
	}

	/**
	 * Normalizes the cariage returns in the given text.
	 * They are all changed  to use the given buffer's line separator.
	 */
	public static char[] normalizeCRs(char[] text, char[] buffer) {
		CharArrayBuffer result = new CharArrayBuffer();
		int lineStart = 0;
		int length = text.length;
		if (length == 0)
			return text;
		String lineSeparator = getLineSeparator(text, buffer);
		char nextChar = text[0];
		for (int i = 0; i < length; i++) {
			char currentChar = nextChar;
			nextChar = i < length - 1 ? text[i + 1] : ' ';
			switch (currentChar) {
			case '\n':
				int lineLength = i - lineStart;
				char[] line = new char[lineLength];
				System.arraycopy(text, lineStart, line, 0, lineLength);
				result.append(line);
				result.append(lineSeparator);
				lineStart = i + 1;
				break;
			case '\r':
				lineLength = i - lineStart;
				if (lineLength >= 0) {
					line = new char[lineLength];
					System.arraycopy(text, lineStart, line, 0, lineLength);
					result.append(line);
					result.append(lineSeparator);
					if (nextChar == '\n') {
						nextChar = ' ';
						lineStart = i + 2;
					} else {
						// when line separator are mixed in the same file
						// \r might not be followed by a \n. If not, we should increment
						// lineStart by one and not by two.
						lineStart = i + 1;
					}
				} else {
					// when line separator are mixed in the same file
					// we need to prevent NegativeArraySizeException
					lineStart = i + 1;
				}
				break;
			}
		}
		char[] lastLine;
		if (lineStart > 0) {
			int lastLineLength = length - lineStart;
			if (lastLineLength > 0) {
				lastLine = new char[lastLineLength];
				System.arraycopy(text, lineStart, lastLine, 0, lastLineLength);
				result.append(lastLine);
			}
			return result.getContents();
		}
		return text;
	}

	/**
	 * Normalizes the cariage returns in the given text.
	 * They are all changed  to use given buffer's line sepatator.
	 */
	public static String normalizeCRs(String text, String buffer) {
		return new String(normalizeCRs(text.toCharArray(), buffer.toCharArray()));
	}

	/**
	 * Returns the line separator used by the given buffer.
	 * Uses the given text if none found.
	 *
	 * @return </code>"\n"</code> or </code>"\r"</code> or  </code>"\r\n"</code>
	 */
	private static String getLineSeparator(char[] text, char[] buffer) {
		// search in this buffer's contents first
		String lineSeparator = findLineSeparator(buffer);
		if (lineSeparator == null) {
			// search in the given text
			lineSeparator = findLineSeparator(text);
			if (lineSeparator == null) {
				// default to system line separator
				return LINE_SEPARATOR;
			}
		}
		return lineSeparator;
	}

	/**
	 * Finds the first line separator used by the given text.
	 *
	 * @return </code>"\n"</code> or </code>"\r"</code> or  </code>"\r\n"</code>,
	 *			or <code>null</code> if none found
	 */
	public static String findLineSeparator(char[] text) {
		// find the first line separator
		int length = text.length;
		if (length > 0) {
			char nextChar = text[0];
			for (int i = 0; i < length; i++) {
				char currentChar = nextChar;
				nextChar = i < length - 1 ? text[i + 1] : ' ';
				switch (currentChar) {
				case '\n':
					return "\n"; //$NON-NLS-1$
				case '\r':
					return nextChar == '\n' ? "\r\n" : "\r"; //$NON-NLS-1$ //$NON-NLS-2$
				}
			}
		}
		// not found
		return null;
	}

	/**
	 * Returns value of line separator preference from the given preference node.
	 */
	private static String getLineSeparatorFromPreferences(Preferences node) {
		try {
			// be careful looking up for our node so not to create any nodes as side effect
			if (node.nodeExists(Platform.PI_RUNTIME))
				return node.node(Platform.PI_RUNTIME).get(Platform.PREF_LINE_SEPARATOR, null);
		} catch (BackingStoreException e) {
			// ignore
		}
		return null;
	}

	/**
	 * Returns the first line separator found in the given file.
	 *
	 * @param fileUri - URI if the file on the local file-system.
	 * @return the first line separator in the given file or {@code null} if none was read.
	 */
	public static String getLineSeparator(URI fileUri) {
		String value = null;
		InputStream input = null;
		try {
			java.io.File file = new java.io.File(fileUri);
			if (file.exists()) {
				input = new FileInputStream(file);
				value = getLineSeparator(input);
			}
		} catch (Exception e) {
			// ignore
		} finally {
			try {
				if (input != null)
					input.close();
			} catch (IOException e) {
				//ignore
			}
		}
		return value;
	}

	/**
	 * Returns the line separator for the given file. If line separator is not found in the file
	 * default value for the project or workspace is returned.
	 *
	 * @param file - the file to look for a line separator.
	 * @return the line separator for the given file. The method does not return {@code null}.
	 */
	public static String getLineSeparator(IFile file) {
		String value = null;
		InputStream input = null;
		try {
			input = file.getContents();
			value = getLineSeparator(input);
		} catch (CoreException e) {
			// ignore
		} finally {
			try {
				if (input != null)
					input.close();
			} catch (IOException e) {
				//ignore
			}
		}

		if (value == null)
			value = getDefaultLineSeparator(file.getProject());

		return value;
	}

	/**
	 * Returns default line separator for the given project. The returned value
	 * will be the first available value from the list below:
	 * <ol>
	 *   <li> Line separator defined in project preferences.
	 *   <li> Line separator defined in instance preferences.
	 *   <li> Line separator defined in default preferences.
	 *   <li> Operating system default line separator.
	 * </ol>
	 *
	 * @param project - the project. If {@code null} no project preferences are inquired.
	 * @return line separator for the given project. The method does not return {@code null}.
	 */
	public static String getDefaultLineSeparator(IProject project) {
		String value = null;
		Preferences rootNode = Platform.getPreferencesService().getRootNode();
		// if the file does not exist or has no content yet, try with project preferences
		if (project != null) {
			value = getLineSeparatorFromPreferences(rootNode.node(ProjectScope.SCOPE).node(project.getName()));
			if (value != null)
				return value;
		}
		value = getDefaultLineSeparator();
		return value;
	}

	/**
	 * Returns default line separator for the workspace. The returned value
	 * will be the first available value from the list below:
	 * <ol>
	 *   <li> Line separator defined in instance preferences.
	 *   <li> Line separator defined in default preferences.
	 *   <li> Operating system default line separator.
	 * </ol>
	 * @return line separator for the workspace. The method does not return {@code null}.
	 */
	public static String getDefaultLineSeparator() {
		Preferences rootNode = Platform.getPreferencesService().getRootNode();
		String value = null;

		// try with instance preferences
		value = getLineSeparatorFromPreferences(rootNode.node(InstanceScope.SCOPE));
		if (value != null)
			return value;
		// try with default preferences
		value = getLineSeparatorFromPreferences(rootNode.node(DefaultScope.SCOPE));
		if (value != null)
			return value;
		// if there is no preference set, fall back to OS default value
		return LINE_SEPARATOR;
	}

	/**
	 * Returns the first line separator used by the input stream.
	 *
	 * @param input - input stream to inspect.
	 * @return line separator for the given input stream or {@code null} if no line separator was read.
	 */
	private static String getLineSeparator(InputStream input) {
		try {
			int c = input.read();
			while (c != -1 && c != '\r' && c != '\n')
				c = input.read();
			if (c == '\n')
				return "\n"; //$NON-NLS-1$
			if (c == '\r') {
				int c2 = input.read();
				if (c2 == '\n')
					return "\r\n"; //$NON-NLS-1$
				return "\r"; //$NON-NLS-1$
			}
		} catch (IOException e) {
			// ignore
		}
		return null;
	}

	/**
	 * Return true if the file is not a directory and has length > 0
	 */
	public static boolean isNonZeroLengthFile(IPath path) {
		return isNonZeroLengthFile(URIUtil.toURI(path));
	}

	/**
	 * Return true if the file referred to by the URI is not a directory and has length > 0
	 */
	public static boolean isNonZeroLengthFile(URI uri) {
		try {
			IFileInfo file = EFS.getStore(uri).fetchInfo();
			if (file.getLength() == EFS.NONE) {
				return false;
			}
			return true;
		} catch (CoreException e) {
			// ignore
		}
		return false;
	}
}
