/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.core.internal.registry;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.internal.runtime.InternalPlatform;
import org.eclipse.core.internal.runtime.Policy;
import org.eclipse.core.runtime.*;

/**
 * Failure reporting strategy:
 * - when reading an identified element (bundles, extension points and extensions),
 * catch any IOExceptions and rethrow them wrapped into a InvalidRegistryCacheException 
 * that describes where the error happened
 * - IOExceptions while reading non-identified elements (configuration elements and properties)
 * are just propagated to their callers
 * - the public entry points will catch any exceptions to ensure they are logged and return a valid
 * value instead (for instance, a null reference or an empty array)  
 */
public class RegistryCacheReader {

	MultiStatus problems = null;
	// objectTable will be an array list of objects. The objects will be things
	// like a plugin descriptor, extension point, etc. The integer
	// index value will be used in the cache to allow cross-references in the
	// cached registry.
	protected List objectTable = null;
	private boolean lazilyLoadExtensions;
	private boolean flushableExtensions = true;
	// indicates we failed load configuration elements lazily
	private boolean failed;
	protected File cacheFile;

	public static final byte REGISTRY_CACHE_VERSION = 7;
	public static final byte NULL = 0;
	public static final byte OBJECT = 1;
	public static final byte INDEX = 2;

	public RegistryCacheReader(File cacheFile, MultiStatus problems, boolean lazilyLoadExtensions, boolean flushable) {
		super();
		this.cacheFile = cacheFile;
		this.problems = problems;
		this.lazilyLoadExtensions = lazilyLoadExtensions;
		this.flushableExtensions = flushable;
		objectTable = new ArrayList();
	}

	public RegistryCacheReader(File cacheFile, MultiStatus problems) {
		this(cacheFile, problems, false, true);
	}

	private int addToObjectTable(Object object) {
		objectTable.add(object);
		// return the index of the object just added (i.e. size - 1)
		return (objectTable.size() - 1);
	}

	private void debug(String msg) {
		System.out.println("RegistryCacheReader: " + msg); //$NON-NLS-1$
	}

	private boolean readHeaderInformation(DataInputStream in, long expectedTimestamp) throws InvalidRegistryCacheException {
		try {
			if (in.readInt() != REGISTRY_CACHE_VERSION)
				return false;
			long installStamp = in.readLong();
			long registryStamp = in.readLong();
			String osStamp = in.readUTF();
			String windowsStamp = in.readUTF();
			String localeStamp = in.readUTF();
			InternalPlatform info = InternalPlatform.getDefault();
			return ((expectedTimestamp == 0 || expectedTimestamp == registryStamp) && (installStamp == info.getStateTimeStamp()) && (osStamp.equals(info.getOS())) && (windowsStamp.equals(info.getWS())) && (localeStamp.equals(info.getNL())));
		} catch (IOException e) {
			throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "HeaderInformation"), e); //$NON-NLS-1$//$NON-NLS-2$
		}
	}

	private void skipConfigurationElement(RegistryModelObject parent, DataInputStream in) throws IOException {
		readCachedString(in, false); //read name
		skipString(in); //skip value

		int length = in.readInt();
		for (int i = 0; i < length; i++) {
			skipConfigurationProperty(in);
		}
		length = in.readInt();
		for (int i = 0; i < length; i++) {
			skipConfigurationElement(null, in);
		}
	}

	private ConfigurationElement readConfigurationElement(RegistryModelObject parent, DataInputStream in) throws IOException {
		ConfigurationElement result = new ConfigurationElement();
		result.setParent(parent);
		result.setName(readCachedString(in, false));
		result.setValue(readString(in, false));

		int length = in.readInt();
		ConfigurationProperty[] properties = new ConfigurationProperty[length];
		for (int i = 0; i < length; i++) {
			properties[i] = readConfigurationProperty(in);
		}
		result.setProperties(properties);

		length = in.readInt();
		IConfigurationElement[] elements = new ConfigurationElement[length];
		for (int i = 0; i < length; i++) {
			elements[i] = readConfigurationElement(result, in);
		}
		result.setChildren(elements);
		return result;
	}

	private void skipConfigurationProperty(DataInputStream in) throws IOException {
		readCachedString(in, false); //Read the name
		skipString(in); // skip the value
	}

	private ConfigurationProperty readConfigurationProperty(DataInputStream in) throws IOException {
		String name = readCachedString(in, false);
		ConfigurationProperty result = new ConfigurationProperty();
		result.setName(name);
		result.setValue(readString(in, false));
		return result;
	}

	private Extension readExtension(DataInputStream in) throws InvalidRegistryCacheException {
		Extension result = null;
		try {
			result = (Extension) readIndex(in);
			if (result != null)
				return result;
			result = flushableExtensions ? new FlushableExtension() : new Extension();
			addToObjectTable(result);
			result.setSimpleIdentifier(readString(in, false));
			result.setParent(readBundleModel(in));
			result.setName(readString(in, false));
			result.setExtensionPointIdentifier(readCachedString(in, false));
			result.setSubElements(readSubElements(result, in));
			return result;
		} catch (IOException e) {
			String extensionId = null;
			if (result != null && result.getParent() != null)
				extensionId = result.getParentIdentifier() + "." + result.getSimpleIdentifier(); //$NON-NLS-1$
			throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "extension:  " + extensionId), e); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private ExtensionPoint readExtensionPoint(Namespace bundle, DataInputStream in) throws InvalidRegistryCacheException {
		ExtensionPoint result = null;
		try {
			result = (ExtensionPoint) readIndex(in);
			if (result != null)
				return result;
			result = new ExtensionPoint();
			addToObjectTable(result);
			result.setParent(bundle);
			result.setSimpleIdentifier(readString(in, true));
			result.setName(readString(in, false));
			result.setSchema(readString(in, false));

			// Now do the extensions.
			int length = in.readInt();
			IExtension[] extensions = new Extension[length];
			for (int i = 0; i < length; i++)
				extensions[i] = readExtension(in);
			result.setExtensions(extensions);
			return result;
		} catch (IOException e) {
			String extensionPointId = null;
			if (result != null && result.getParent() != null)
				extensionPointId = result.getUniqueIdentifier();
			throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "extension point: " + extensionPointId), e); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}

	private Namespace readBundleModel(DataInputStream in) throws InvalidRegistryCacheException {
		Namespace result = null;
		try {
			result = (Namespace) readIndex(in);
			if (result != null)
				return result;
			result = new Namespace();
			addToObjectTable(result);
			result.setUniqueIdentifier(readCachedString(in, true));
			result.setBundle(InternalPlatform.getDefault().getBundleContext().getBundle(in.readLong()));
			result.setParent(readRegistry(in));
			result.setHostIdentifier(readCachedString(in, false));

			// now do extension points
			int length = in.readInt();
			IExtensionPoint[] extensionPoints = new ExtensionPoint[length];
			for (int i = 0; i < length; i++)
				extensionPoints[i] = readExtensionPoint(result, in);
			result.setExtensionPoints(extensionPoints);

			// and then extensions
			length = in.readInt();
			IExtension[] extensions = flushableExtensions ? new FlushableExtension[length] : new Extension[length];
			for (int i = 0; i < length; i++)
				extensions[i] = readExtension(in);
			result.setExtensions(extensions);
			return result;
		} catch (IOException e) {
			String bundleId = (result == null || result.getUniqueIdentifier() == null) ? "<not available>" : result.getUniqueIdentifier(); //$NON-NLS-1$
			throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "plugin: " + bundleId), e); //$NON-NLS-1$//$NON-NLS-2$
		}
	}

	private ExtensionRegistry readCache(DataInputStream in, long expectedTimestamps) throws InvalidRegistryCacheException {
		if (!readHeaderInformation(in, expectedTimestamps)) {
			if (InternalPlatform.DEBUG_REGISTRY)
				debug("Cache header information out of date - ignoring cache"); //$NON-NLS-1$
			return null;
		}
		return readRegistry(in);
	}

	private ExtensionRegistry readRegistry(DataInputStream in) throws InvalidRegistryCacheException {
		try {
			ExtensionRegistry result = (ExtensionRegistry) readIndex(in);
			if (result != null)
				return result;

			result = new ExtensionRegistry();
			if (lazilyLoadExtensions)
				result.setCacheReader(this);
			addToObjectTable(result);
			// if there are no plugins in the registry, return null instead of
			// an empty registry?
			int length = in.readInt();
			if (length == 0)
				return null;
			for (int i = 0; i < length; i++)
				result.basicAdd(readBundleModel(in), false);
			if (lazilyLoadExtensions)
				result.setCacheReader(this);
			return result;
		} catch (IOException e) {
			throw new InvalidRegistryCacheException(Policy.bind("meta.regCacheIOExceptionReading", "ExtensionRegistry"), e); //$NON-NLS-1$//$NON-NLS-2$
		}
	}

	private ConfigurationElement[] readSubElements(Extension parent, DataInputStream in) throws IOException {
		int type = in.readByte();
		if (type == NULL)
			return null;

		//Here type is OBJECT
		// the first field is extension sub-elements data offset
		int offset = in.readInt();

		if (lazilyLoadExtensions) {
			Extension extension = parent;
			extension.setSubElementsCacheOffset(offset);
			skipBasicSubElements(parent, in);
			extension.setFullyLoaded(false);
			return null;
		}
		return readBasicSubElements(parent, in);
	}

	private void skipBasicSubElements(Extension parent, DataInputStream in) throws IOException {
		int length = in.readInt();
		for (int i = 0; i < length; i++) {
			skipConfigurationElement(parent, in);
		}
	}

	private ConfigurationElement[] readBasicSubElements(Extension parent, DataInputStream in) throws IOException {
		// read the number of sub elements to load
		int length = in.readInt();

		ConfigurationElement[] result = new ConfigurationElement[length];
		for (int i = 0; i < length; i++) {
			result[i] = readConfigurationElement(parent, in);
		}
		return result;
	}

	private String readString(DataInputStream in, boolean intern) throws IOException {
		byte type = in.readByte();
		if (type == NULL)
			return null;
		if (intern)
			return in.readUTF().intern();
		else
			return in.readUTF();
	}

	private void skipString(DataInputStream in) throws IOException {
		byte type = in.readByte();
		if (type == NULL)
			return;
		int utfLength = in.readUnsignedShort();
		byte bytearr[] = new byte[utfLength];
		in.readFully(bytearr, 0, utfLength);
	}

	private String readCachedString(DataInputStream in, boolean intern) throws IOException {
		byte type = in.readByte();
		if (type == NULL)
			return null;

		if (type == INDEX)
			return (String) objectTable.get(in.readInt());

		String stringRead = null;
		if (intern)
			stringRead = in.readUTF().intern();
		else
			stringRead = in.readUTF();
		addToObjectTable(stringRead);
		return stringRead;
	}

	private Object readIndex(DataInputStream in) throws IOException {
		byte type = in.readByte();
		return type == INDEX ? objectTable.get(in.readInt()) : null;
	}

	private DataInputStream openCacheFile() throws IOException {
		return new DataInputStream(new BufferedInputStream(new FileInputStream(cacheFile), 2048));
	}

	/**
	 * Lazily loads an extension model's sub-elements.
	 */
	public final ConfigurationElement[] loadConfigurationElements(Extension parent, int offset) {
		DataInputStream in = null;
		try {
			in = openCacheFile();
			in.skipBytes(offset);
			in.readInt(); // skip the offset itself
			return readBasicSubElements(parent, in);
		} catch (IOException e) {
			Throwable exception = InternalPlatform.DEBUG_REGISTRY ? e : null;
			String message = Policy.bind("meta.unableToReadCache"); //$NON-NLS-1$
			InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, message, exception));
		} catch (OutOfMemoryError oome) {
			// catch any OutOfMemoryErrors that may have been caused by corrupted data
			logError(oome);
		} catch (RuntimeException re) {
			// catch any ArrayIndexOutOfBounds/NullPointer/NegativeArraySize/... exceptions that may have been caused by corrupted data
			logError(re);
		} finally {
			try {
				if (in != null)
					in.close();
			} catch (IOException e) {
				// ignore
			}
		}
		// we only get here when we have problems
		failed = true;
		return new ConfigurationElement[0];
	}

	boolean hasFailed() {
		return failed;
	}

	private void logError(Throwable t) {
		// log general message
		String message = Policy.bind("meta.registryCacheReadProblems"); //$NON-NLS-1$						
		InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, message, null));
		// log actual error			
		Throwable exceptionToLog = InternalPlatform.DEBUG_REGISTRY ? t : null;
		InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, t.toString(), exceptionToLog));
	}

	public final ExtensionRegistry loadCache() {
		return this.loadCache(0);
	}

	/*
	 * If expectedTimestamp != 0, check it against the registry timestamp if the header. 
	 */
	public final ExtensionRegistry loadCache(long expectedTimestamp) {
		DataInputStream in = null;
		try {
			in = openCacheFile();
		} catch (IOException e) {
			Throwable exception = InternalPlatform.DEBUG_REGISTRY ? e : null;
			String message = Policy.bind("meta.unableToReadCache"); //$NON-NLS-1$
			InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, message, exception));
			return null;
		}
		try {
			return readCache(in, expectedTimestamp);
		} catch (InvalidRegistryCacheException e) {
			Throwable exception = InternalPlatform.DEBUG_REGISTRY ? e.getCause() : null;
			InternalPlatform.getDefault().log(new Status(IStatus.WARNING, Platform.PI_RUNTIME, 0, e.getMessage(), exception));
		} catch (OutOfMemoryError oome) {
			// catch any OutOfMemoryErrors that may have been caused by corrupted data
			logError(oome);
		} catch (RuntimeException re) {
			// catch any ArrayIndexOutOfBounds/NullPointer/NegativeArraySize/... exceptions that may have been caused by corrupted data
			logError(re);
		} finally {
			try {
				if (in != null)
					in.close();
			} catch (IOException e) {
				// ignore
			}
		}
		return null;
	}

	public class InvalidRegistryCacheException extends Exception {
		Throwable cause = null;

		public InvalidRegistryCacheException(String msg, Throwable cause) {
			super(msg);
			this.cause = cause;
		}

		public InvalidRegistryCacheException(String string) {
			super(string);
		}

		public Throwable getCause() {
			return cause;
		}
	}
}