//
// AddinDatabase.cs
//
// Author:
//   Lluis Sanchez Gual
//
// Copyright (C) 2007 Novell, Inc (http://www.novell.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//


using System;
using System.Threading;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Xml;
using System.Reflection;
using Mono.Addins.Description;

namespace Mono.Addins.Database
{
	class AddinDatabase
	{
		const string VersionTag = "000";

		ArrayList addinSetupInfos;
		internal static bool RunningSetupProcess;
		bool fatalDatabseError;
		Hashtable cachedAddinSetupInfos = new Hashtable ();
		AddinScanResult currentScanResult;
		AddinHostIndex hostIndex;
		FileDatabase fileDatabase;
		string addinDbDir;
		DatabaseConfiguration config = null;
		AddinRegistry registry;
		
		public AddinDatabase (AddinRegistry registry)
		{
			this.registry = registry;
			addinDbDir = Path.Combine (registry.RegistryPath, "addin-db-" + VersionTag);
			fileDatabase = new FileDatabase (AddinDbDir);
		}
		
		string AddinDbDir {
			get { return addinDbDir; }
		}
		
		public string AddinCachePath {
			get { return Path.Combine (AddinDbDir, "addin-data"); }
		}
		
		public string AddinFolderCachePath {
			get { return Path.Combine (AddinDbDir, "addin-dir-data"); }
		}
		
		public string AddinPrivateDataPath {
			get { return Path.Combine (AddinDbDir, "addin-priv-data"); }
		}
		
		string HostIndexFile {
			get { return Path.Combine (AddinDbDir, "host-index"); }
		}
		
		string ConfigFile {
			get { return Path.Combine (AddinDbDir, "config.xml"); }
		}
		
		internal bool IsGlobalRegistry {
			get {
				return registry.RegistryPath == AddinRegistry.GlobalRegistryPath;
			}
		}
		
		public void Clear ()
		{
			if (Directory.Exists (AddinCachePath))
				Directory.Delete (AddinCachePath, true);
			if (Directory.Exists (AddinFolderCachePath))
				Directory.Delete (AddinFolderCachePath, true);
		}
		
		public ExtensionNodeSet FindNodeSet (string addinId, string id)
		{
			return FindNodeSet (addinId, id, new Hashtable ());
		}
		
		ExtensionNodeSet FindNodeSet (string addinId, string id, Hashtable visited)
		{
			if (visited.Contains (addinId))
				return null;
			visited.Add (addinId, addinId);
			Addin addin = GetInstalledAddin (addinId, true, false);
			if (addin == null) {
				foreach (Addin root in GetAddinRoots ()) {
					if (root.Id == addinId) {
						addin = root;
						break;
					}
				}
				if (addin == null)
					return null;
			}
			AddinDescription desc = addin.Description;
			if (desc == null)
				return null;
			foreach (ExtensionNodeSet nset in desc.ExtensionNodeSets)
				if (nset.Id == id)
					return nset;
			
			// Not found in the add-in. Look on add-ins on which it depends
			
			foreach (Dependency dep in desc.MainModule.Dependencies) {
				AddinDependency adep = dep as AddinDependency;
				if (adep == null) continue;
				
				string aid = Addin.GetFullId (desc.Namespace, adep.AddinId, adep.Version);
				ExtensionNodeSet nset = FindNodeSet (aid, id, visited);
				if (nset != null)
					return nset;
			}
			return null;
		}
		
		public AddinDescription GetDescription (string id)
		{
			InternalCheck ();
			IDisposable dblock = fileDatabase.LockRead ();
			try {
				string path = GetDescriptionPath (id);
				AddinDescription desc = AddinDescription.ReadBinary (fileDatabase, path);
				if (desc != null)
					desc.OwnerDatabase = this;
				return desc;
			}
			catch (FileNotFoundException) {
				throw new InvalidOperationException ("Add-in not found: " + id);
			} finally {
				dblock.Dispose ();
			}
		}
		
		public ArrayList GetInstalledAddins ()
		{
			if (addinSetupInfos != null)
				return addinSetupInfos;
			
			InternalCheck ();
			using (fileDatabase.LockRead ()) {
				return InternalGetInstalledAddins ();
			}
		}
		
		public ArrayList GetAddinRoots ()
		{
			ArrayList list = new ArrayList ();
			foreach (string file in fileDatabase.GetDirectoryFiles (AddinCachePath, "*.mroot")) {
				list.Add (new Addin (this, file));
			}
			return list;
		}
		
		ArrayList InternalGetInstalledAddins ()
		{
			if (addinSetupInfos != null)
				return addinSetupInfos;

			addinSetupInfos = new ArrayList ();
			
			foreach (string file in fileDatabase.GetDirectoryFiles (AddinCachePath, "*.maddin")) {
				addinSetupInfos.Add (new Addin (this, file));
			}
			return addinSetupInfos;
		}
		
		public Addin GetInstalledAddin (string id)
		{
			return GetInstalledAddin (id, false, false);
		}
		
		public Addin GetInstalledAddin (string id, bool exactVersionMatch)
		{
			return GetInstalledAddin (id, exactVersionMatch, false);
		}
		
		public Addin GetInstalledAddin (string id, bool exactVersionMatch, bool enabledOnly)
		{
			Addin sinfo = null;
			object ob = cachedAddinSetupInfos [id];
			if (ob != null) {
				sinfo = ob as Addin;
				if (sinfo != null) {
					if (!enabledOnly || sinfo.Enabled)
						return sinfo;
					if (exactVersionMatch)
						return null;
				}
				else if (enabledOnly)
					// Ignore the 'not installed' flag when disabled add-ins are allowed
					return null;
			}
		
			InternalCheck ();
			
			using (fileDatabase.LockRead ())
			{
				string path = GetDescriptionPath (id);
				if (sinfo == null && fileDatabase.Exists (path)) {
					sinfo = new Addin (this, path);
					cachedAddinSetupInfos [id] = sinfo;
					if (!enabledOnly || sinfo.Enabled)
						return sinfo;
					if (exactVersionMatch) {
						// Cache lookups with negative result
						cachedAddinSetupInfos [id] = this;
						return null;
					}
				}
				
				// Exact version not found. Look for a compatible version
				if (!exactVersionMatch) {
					sinfo = null;
					string version, name, bestVersion = null;
					Addin.GetIdParts (id, out name, out version);
					
					// FIXME: Not very efficient, will load all descriptions
					foreach (Addin ia in InternalGetInstalledAddins ()) 
					{
						if (Addin.GetIdName (ia.Id) == name && 
						    (!enabledOnly || ia.Enabled) &&
						    (version.Length == 0 || ia.SupportsVersion (version)) && 
						    (bestVersion == null || Addin.CompareVersions (bestVersion, ia.Version) > 0)) 
						{
							bestVersion = ia.Version;
							sinfo = ia;
						}
					}
					if (sinfo != null) {
						cachedAddinSetupInfos [id] = sinfo;
						return sinfo;
					}
				}
				
				// Cache lookups with negative result
				// Ignore the 'not installed' flag when disabled add-ins are allowed
				if (enabledOnly)
					cachedAddinSetupInfos [id] = this;
				return null;
			}
		}
		
		public Addin GetInstalledAddin (string id, string version)
		{
			foreach (Addin ia in GetInstalledAddins ()) {
				if ((id == null || ia.Id == id) && (version == null || ia.Version == version))
					return ia;
			}
			return null;
		}
		
		public void Shutdown ()
		{
			addinSetupInfos = null;
		}
		
		public Addin GetAddinForHostAssembly (string assemblyLocation)
		{
			InternalCheck ();
			Addin ainfo = null;
			
			object ob = cachedAddinSetupInfos [assemblyLocation];
			if (ob != null) {
				ainfo = ob as Addin;
				if (ainfo != null)
					return ainfo;
				else
					return null;
			}

			AddinHostIndex index = GetAddinHostIndex ();
			string addin, addinFile;
			if (index.GetAddinForAssembly (assemblyLocation, out addin, out addinFile)) {
				ainfo = new Addin (this, addin, addinFile);
				cachedAddinSetupInfos [assemblyLocation] = ainfo;
			}
			
			return ainfo;
		}
		
		
		public bool IsAddinEnabled (string id)
		{
			Addin ainfo = GetInstalledAddin (id);
			if (ainfo != null)
				return ainfo.Enabled;
			else
				return false;
		}
		
		internal bool IsAddinEnabled (string id, bool exactVersionMatch)
		{
			if (!exactVersionMatch)
				return IsAddinEnabled (id);
			Addin ainfo = GetInstalledAddin (id, exactVersionMatch, false);
			if (ainfo == null)
				return false;
			return Configuration.IsEnabled (id, ainfo.AddinInfo.EnabledByDefault);
		}
		
		public void EnableAddin (string id)
		{
			EnableAddin (id, true);
		}
		
		internal void EnableAddin (string id, bool exactVersionMatch)
		{
			Addin ainfo = GetInstalledAddin (id, exactVersionMatch, false);
			if (ainfo == null)
				// It may be an add-in root
				return;

			if (IsAddinEnabled (id))
				return;
			
			// Enable required add-ins
			
			foreach (Dependency dep in ainfo.AddinInfo.Dependencies) {
				if (dep is AddinDependency) {
					AddinDependency adep = dep as AddinDependency;
					string adepid = Addin.GetFullId (ainfo.AddinInfo.Namespace, adep.AddinId, adep.Version);
					EnableAddin (adepid, false);
				}
			}

			Configuration.SetStatus (id, true, ainfo.AddinInfo.EnabledByDefault);
			SaveConfiguration ();

			if (AddinManager.IsInitialized && AddinManager.Registry.RegistryPath == registry.RegistryPath)
				AddinManager.SessionService.ActivateAddin (id);
		}
		
		public void DisableAddin (string id)
		{
			Addin ai = GetInstalledAddin (id, true);
			if (ai == null)
				throw new InvalidOperationException ("Add-in '" + id + "' not installed.");

			if (!IsAddinEnabled (id))
				return;
			
			Configuration.SetStatus (id, false, ai.AddinInfo.EnabledByDefault);
			SaveConfiguration ();
			
			// Disable all add-ins which depend on it
			
			try {
				string idName = Addin.GetIdName (id);
				
				foreach (Addin ainfo in GetInstalledAddins ()) {
					foreach (Dependency dep in ainfo.AddinInfo.Dependencies) {
						AddinDependency adep = dep as AddinDependency;
						if (adep == null)
							continue;
						
						string adepid = Addin.GetFullId (ainfo.AddinInfo.Namespace, adep.AddinId, null);
						if (adepid != idName)
							continue;
						
						// The add-in that has been disabled, might be a requeriment of this one, or maybe not
						// if there is an older version available. Check it now.
						
						adepid = Addin.GetFullId (ainfo.AddinInfo.Namespace, adep.AddinId, adep.Version);
						Addin adepinfo = GetInstalledAddin (adepid, false, true);
						
						if (adepinfo == null) {
							DisableAddin (ainfo.Id);
							break;
						}
					}
				}
			}
			catch {
				// If something goes wrong, enable the add-in again
				Configuration.SetStatus (id, true, ai.AddinInfo.EnabledByDefault);
				SaveConfiguration ();
				throw;
			}

			if (AddinManager.IsInitialized && AddinManager.Registry.RegistryPath == registry.RegistryPath)
				AddinManager.SessionService.UnloadAddin (id);
		}		

		internal string GetDescriptionPath (string id)
		{
			return Path.Combine (AddinCachePath, id + ".maddin");
		}
		
		void InternalCheck ()
		{
			// If the database is broken, don't try to regenerate it at every check.
			if (fatalDatabseError)
				return;

			bool update = false;
			using (fileDatabase.LockRead ()) {
				if (!Directory.Exists (AddinCachePath)) {
					update = true;
				}
			}
			if (update)
				Update (null);
		}
		
		void GenerateAddinExtensionMapsInternal (IProgressStatus monitor, ArrayList addinsToUpdate, ArrayList removedAddins)
		{
			AddinUpdateData updateData = new AddinUpdateData (this);
			
			// Clear cached data
			cachedAddinSetupInfos.Clear ();
			
			// Collect all information
			
			Hashtable addinHash = new Hashtable ();
			
			if (monitor.LogLevel > 1)
				monitor.Log ("Generating add-in extension maps");
			
			Hashtable changedAddins = null;
			ArrayList descriptionsToSave = new ArrayList ();
			ArrayList files = new ArrayList ();
			
			bool partialGeneration = addinsToUpdate != null;
			
			// Get the files to be updated
			
			if (partialGeneration) {
				changedAddins = new Hashtable ();
				foreach (string s in addinsToUpdate) {
					changedAddins [s] = s;
					string mp = GetDescriptionPath (s);
					if (fileDatabase.Exists (mp))
						files.Add (mp);
					else
						files.AddRange (fileDatabase.GetObjectSharedFiles (this.AddinCachePath, s, ".mroot"));
				}
				foreach (string s in removedAddins)
					changedAddins [s] = s;
			}
			else {
				files.AddRange (fileDatabase.GetDirectoryFiles (AddinCachePath, "*.maddin"));
				files.AddRange (fileDatabase.GetDirectoryFiles (AddinCachePath, "*.mroot"));
			}
			
			// Load the descriptions.
			foreach (string file in files) {
			
				AddinDescription conf;
				if (!ReadAddinDescription (monitor, file, out conf)) {
					SafeDelete (monitor, file);
					continue;
				}

				// If the original file does not exist, the description can be deleted
				if (!File.Exists (conf.AddinFile)) {
					SafeDelete (monitor, file);
					continue;
				}
				
				// Remove old data from the description. If changedAddins==null, removes all data.
				// Otherwise, removes data only from the addins in the table.
				
				conf.UnmergeExternalData (changedAddins);
				descriptionsToSave.Add (conf);
				
				// Register extension points and node sets from root add-ins
				if (conf.IsRoot) {
					foreach (ExtensionPoint ep in conf.ExtensionPoints)
						updateData.RegisterAddinRootExtensionPoint (conf, ep);
					foreach (ExtensionNodeSet ns in conf.ExtensionNodeSets)
						updateData.RegisterAddinRootNodeSet (conf, ns);
				}
				else
					addinHash [conf.AddinId] = conf;
			}
			
			foreach (AddinDescription conf in addinHash.Values) {
				CollectExtensionData (conf, updateData);
			}
			
			updateData.ResolveExtensions (monitor, addinHash);
			
			// Update the extension points defined by this add-in			
			foreach (ExtensionPoint ep in updateData.GetUnresolvedExtensionPoints ()) {
				AddinDescription am = (AddinDescription) addinHash [ep.RootAddin];
				ExtensionPoint amep = am.ExtensionPoints [ep.Path];
				if (amep != null) {
					amep.MergeWith (am.AddinId, ep);
					amep.RootAddin = ep.RootAddin;
				}
			}

			// Now update the node sets
			foreach (ExtensionPoint ep in updateData.GetUnresolvedExtensionSets ()) {
				AddinDescription am = (AddinDescription) addinHash [ep.RootAddin];
				ExtensionNodeSet nset = am.ExtensionNodeSets [ep.Path];
				if (nset != null)
					nset.MergeWith (am.AddinId, ep.NodeSet);
			}
			
			// Save the maps
			foreach (AddinDescription conf in descriptionsToSave)
				conf.SaveBinary (fileDatabase);
			
			if (monitor.LogLevel > 1) {
				monitor.Log ("Addin relation map generated.");
				monitor.Log ("  Addins Updated: " + descriptionsToSave.Count);
				monitor.Log ("  Extension points: " + updateData.RelExtensionPoints);
				monitor.Log ("  Extensions: " + updateData.RelExtensions);
				monitor.Log ("  Extension nodes: " + updateData.RelExtensionNodes);
				monitor.Log ("  Node sets: " + updateData.RelNodeSetTypes);
			}
		}
		
		// Collects extension data in a hash table. The key is the path, the value is a list
		// of add-ins ids that extend that path
		
		void CollectExtensionData (AddinDescription conf, AddinUpdateData updateData)
		{
			foreach (ExtensionNodeSet nset in conf.ExtensionNodeSets) {
				try {
					updateData.RegisterNodeSet (conf, nset);
					updateData.RelNodeSetTypes++;
				} catch (Exception ex) {
					throw new InvalidOperationException ("Error reading node set: " + nset.Id, ex);
				}
			}
			
			foreach (ExtensionPoint ep in conf.ExtensionPoints) {
				try {
					updateData.RegisterExtensionPoint (conf, ep);
					updateData.RelExtensionPoints++;
				} catch (Exception ex) {
					throw new InvalidOperationException ("Error reading extension point: " + ep.Path, ex);
				}
			}
			
			foreach (ModuleDescription module in conf.AllModules) {
				foreach (Extension ext in module.Extensions) {
					updateData.RelExtensions++;
					updateData.RegisterExtension (conf, module, ext);
					AddChildExtensions (conf, module, updateData, ext.Path, ext.ExtensionNodes, false);
				}
			}
		}
		
		void AddChildExtensions (AddinDescription conf, ModuleDescription module, AddinUpdateData updateData, string path, ExtensionNodeDescriptionCollection nodes, bool conditionChildren)
		{
			// Don't register conditions as extension nodes.
			if (!conditionChildren)
				updateData.RegisterExtension (conf, module, path);
			
			foreach (ExtensionNodeDescription node in nodes) {
				if (node.NodeName == "ComplexCondition")
					continue;
				updateData.RelExtensionNodes++;
				string id = node.GetAttribute ("id");
				if (id.Length != 0)
					AddChildExtensions (conf, module, updateData, path + "/" + id, node.ChildNodes, node.NodeName == "Condition");
			}
		}

		internal void ResetCachedData ()
		{
			addinSetupInfos = null;
			hostIndex = null;
			cachedAddinSetupInfos.Clear ();
		}
		
		
		public bool AddinDependsOn (string id1, string id2)
		{
			Addin addin1 = GetInstalledAddin (id1, false);
			
			// We can assumbe that if the add-in is not returned here, it may be a root addin.
			if (addin1 == null)
				return false;

			id2 = Addin.GetIdName (id2);
			foreach (Dependency dep in addin1.AddinInfo.Dependencies) {
				AddinDependency adep = dep as AddinDependency;
				if (adep == null)
					continue;
				string depid = Addin.GetFullId (addin1.AddinInfo.Namespace, adep.AddinId, null);
				if (depid == id2)
					return true;
				else if (AddinDependsOn (depid, id2))
					return true;
			}
			return false;
		}
		
		public void Repair (IProgressStatus monitor)
		{
			using (fileDatabase.LockWrite ()) {
				try {
					if (Directory.Exists (AddinCachePath))
						Directory.Delete (AddinCachePath, true);
					if (Directory.Exists (AddinFolderCachePath))
						Directory.Delete (AddinFolderCachePath, true);
					if (File.Exists (HostIndexFile))
						File.Delete (HostIndexFile);
				}
				catch (Exception ex) {
					monitor.ReportError ("The add-in registry could not be rebuilt. It may be due to lack of write permissions to the directory: " + AddinDbDir, ex);
				}
			}
			Update (monitor);
		}
		
		public void Update (IProgressStatus monitor)
		{
			if (monitor == null)
				monitor = new ConsoleProgressStatus (false);

			if (RunningSetupProcess)
				return;
			
			fatalDatabseError = false;
			
			DateTime tim = DateTime.Now;
			
			Hashtable installed = new Hashtable ();
			bool changesFound = CheckFolders (monitor);
			
			if (monitor.IsCanceled)
				return;
			
			if (monitor.LogLevel > 1)
				monitor.Log ("Folders checked (" + (int) (DateTime.Now - tim).TotalMilliseconds + " ms)");
			
			if (changesFound) {
				// Something has changed, the add-ins need to be re-scanned, but it has
				// to be done in an external process
				
				using (fileDatabase.LockRead ()) {
					foreach (Addin ainfo in InternalGetInstalledAddins ()) {
						installed [ainfo.Id] = ainfo.Id;
					}
				}
				
				RunScannerProcess (monitor);
			
				ResetCachedData ();
			}
			
			if (fatalDatabseError)
				monitor.ReportError ("The add-in database could not be updated. It may be due to file corruption. Try running the setup repair utility", null);
			
			// Update the currently loaded add-ins
			if (changesFound && AddinManager.IsInitialized && AddinManager.Registry.RegistryPath == registry.RegistryPath) {
				Hashtable newInstalled = new Hashtable ();
				foreach (Addin ainfo in GetInstalledAddins ()) {
					newInstalled [ainfo.Id] = ainfo.Id;
				}
				
				foreach (string aid in installed.Keys) {
					if (!newInstalled.Contains (aid))
						AddinManager.SessionService.UnloadAddin (aid);
				}
				
				foreach (string aid in newInstalled.Keys) {
					if (!installed.Contains (aid)) {
						AddinManager.SessionService.ActivateAddin (aid);
					}
				}
			}
		}
		
		void RunScannerProcess (IProgressStatus monitor)
		{
			IProgressStatus scanMonitor = monitor;
			ArrayList pparams = new ArrayList ();
			pparams.Add (null); // scan folder
			
			bool retry = false;
			do {
				try {
					if (monitor.LogLevel > 1)
						monitor.Log ("Looking for addins");
					SetupProcess.ExecuteCommand (scanMonitor, registry.RegistryPath, AddinManager.StartupDirectory, "scan", (string[]) pparams.ToArray (typeof(string)));
					retry = false;
				}
				catch (Exception ex) {
					ProcessFailedException pex = ex as ProcessFailedException;
					if (pex != null) {
						// Get the last logged operation.
						if (pex.LastLog.StartsWith ("scan:")) {
							// It crashed while scanning a file. Add the file to the ignore list and try again.
							string file = pex.LastLog.Substring (5);
							pparams.Add (file);
							monitor.ReportWarning ("Could not scan file: " + file);
							retry = true;
							continue;
						}
					}
					fatalDatabseError = true;
					// If the process has crashed, try to do a new scan, this time using verbose log,
					// to give the user more information about the origin of the crash.
					if (pex != null && !retry) {
						monitor.ReportError ("Add-in scan operation failed. The Mono runtime may have encountered an error while trying to load an assembly.", null);
						if (monitor.LogLevel <= 1) {
							// Re-scan again using verbose log, to make it easy to find the origin of the error.
							retry = true;
							scanMonitor = new ConsoleProgressStatus (true);
						}
					} else
						retry = false;
					
					if (!retry) {
						monitor.ReportError ("Add-in scan operation failed", (ex is ProcessFailedException ? null : ex));
						monitor.Cancel ();
						return;
					}
				}
			}
			while (retry);
		}
		
		bool DatabaseInfrastructureCheck (IProgressStatus monitor)
		{
			// Do some sanity check, to make sure the basic database infrastructure can be created
			
			bool hasChanges = false;
			
			try {
			
				if (!Directory.Exists (AddinCachePath)) {
					Directory.CreateDirectory (AddinCachePath);
					hasChanges = true;
				}
			
				if (!Directory.Exists (AddinFolderCachePath)) {
					Directory.CreateDirectory (AddinFolderCachePath);
					hasChanges = true;
				}
			
				// Make sure we can write in those folders

				Util.CheckWrittableFloder (AddinCachePath);
				Util.CheckWrittableFloder (AddinFolderCachePath);
				
				fatalDatabseError = false;
			}
			catch (Exception ex) {
				monitor.ReportError ("Add-in cache directory could not be created", ex);
				fatalDatabseError = true;
				monitor.Cancel ();
			}
			return hasChanges;
		}
		
		
		internal bool CheckFolders (IProgressStatus monitor)
		{
			using (fileDatabase.LockRead ()) {
				AddinScanResult scanResult = new AddinScanResult ();
				scanResult.CheckOnly = true;
				InternalScanFolders (monitor, scanResult);
				return scanResult.ChangesFound;
			}
		}
		
		internal void ScanFolders (IProgressStatus monitor, string folderToScan, StringCollection filesToIgnore)
		{
			AddinScanResult res = new AddinScanResult ();
			res.FilesToIgnore = filesToIgnore;
			ScanFolders (monitor, res);
		}
		
		internal void ScanFolders (IProgressStatus monitor, AddinScanResult scanResult)
		{
			IDisposable checkLock = null;
			
			if (scanResult.CheckOnly)
				checkLock = fileDatabase.LockRead ();
			else {
				// All changes are done in a transaction, which won't be committed until
				// all files have been updated.
				
				if (!fileDatabase.BeginTransaction ()) {
					// The database is already being updated. Can't do anything for now.
					return;
				}
			}
			
			EventInfo einfo = typeof(AppDomain).GetEvent ("ReflectionOnlyAssemblyResolve");
			ResolveEventHandler resolver = new ResolveEventHandler (OnResolveAddinAssembly);
			
			try
			{
				// Perform the add-in scan
				
				if (!scanResult.CheckOnly) {
					AppDomain.CurrentDomain.AssemblyResolve += resolver;
					if (einfo != null) einfo.AddEventHandler (AppDomain.CurrentDomain, resolver);
				}
				
				InternalScanFolders (monitor, scanResult);
				
				if (!scanResult.CheckOnly)
					fileDatabase.CommitTransaction ();
			}
			catch {
				if (!scanResult.CheckOnly)
					fileDatabase.RollbackTransaction ();
				throw;
			}
			finally {
				currentScanResult = null;
				
				if (scanResult.CheckOnly)
					checkLock.Dispose ();
				else {
					AppDomain.CurrentDomain.AssemblyResolve -= resolver;
					if (einfo != null) einfo.RemoveEventHandler (AppDomain.CurrentDomain, resolver);
				}
			}
		}
		
		void InternalScanFolders (IProgressStatus monitor, AddinScanResult scanResult)
		{
			DateTime tim = DateTime.Now;
			
			DatabaseInfrastructureCheck (monitor);
			if (monitor.IsCanceled)
				return;
			
			try {
				scanResult.HostIndex = GetAddinHostIndex ();
			}
			catch (Exception ex) {
				if (scanResult.CheckOnly) {
					scanResult.ChangesFound = true;
					return;
				}
				monitor.ReportError ("Add-in root index is corrupt. The add-in database will be regenerated.", ex);
				scanResult.RegenerateAllData = true;
			}
			
			AddinScanner scanner = new AddinScanner (this);
			
			// Check if any of the previously scanned folders has been deleted
			
			foreach (string file in Directory.GetFiles (AddinFolderCachePath, "*.data")) {
				AddinScanFolderInfo folderInfo;
				bool res = ReadFolderInfo (monitor, file, out folderInfo);
				if (!res || !Directory.Exists (folderInfo.Folder)) {
					if (res) {
						// Folder has been deleted. Remove the add-ins it had.
						scanner.UpdateDeletedAddins (monitor, folderInfo, scanResult);
					}
					else {
						// Folder info file corrupt. Regenerate all.
						scanResult.ChangesFound = true;
						scanResult.RegenerateRelationData = true;
					}
					
					if (!scanResult.CheckOnly)
						SafeDelete (monitor, file);
					else
						return;
				}
			}
			
			// Look for changes in the add-in folders
			
			foreach (string dir in registry.AddinDirectories) {
				if (dir == registry.DefaultAddinsFolder)
					scanner.ScanFolderRec (monitor, dir, scanResult);
				else
					scanner.ScanFolder (monitor, dir, scanResult);
				if (scanResult.CheckOnly) {
					if (scanResult.ChangesFound || monitor.IsCanceled)
						return;
				}
			}
			
			if (scanResult.CheckOnly)
				return;
			
			// Scan the files which have been modified
			
			currentScanResult = scanResult;

			foreach (FileToScan file in scanResult.FilesToScan)
				scanner.ScanFile (monitor, file.File, file.AddinScanFolderInfo, scanResult);

			// Save folder info
			
			foreach (AddinScanFolderInfo finfo in scanResult.ModifiedFolderInfos)
				SaveFolderInfo (monitor, finfo);

			if (monitor.LogLevel > 1)
				monitor.Log ("Folders scan completed (" + (int) (DateTime.Now - tim).TotalMilliseconds + " ms)");

			SaveAddinHostIndex ();
			ResetCachedData ();
			
			if (!scanResult.ChangesFound) {
				if (monitor.LogLevel > 1)
					monitor.Log ("No changes found");
				return;
			}
			
			tim = DateTime.Now;
			try {
				if (scanResult.RegenerateRelationData)
					scanResult.AddinsToUpdateRelations = null;
				
				GenerateAddinExtensionMapsInternal (monitor, scanResult.AddinsToUpdateRelations, scanResult.RemovedAddins);
			}
			catch (Exception ex) {
				fatalDatabseError = true;
				monitor.ReportError ("The add-in database could not be updated. It may be due to file corruption. Try running the setup repair utility", ex);
			}
			
			if (monitor.LogLevel > 1)
				monitor.Log ("Add-in relations analyzed (" + (int) (DateTime.Now - tim).TotalMilliseconds + " ms)");
			
			SaveAddinHostIndex ();
		}
		
		public void ParseAddin (IProgressStatus progressStatus, string file, string outFile, bool inProcess)
		{
			if (!inProcess) {
				SetupProcess.ExecuteCommand (progressStatus, registry.RegistryPath, AddinManager.StartupDirectory, "get-desc", Path.GetFullPath (file), outFile);
				return;
			}
			
			using (fileDatabase.LockRead ())
			{
				// First of all, check if the file belongs to a registered add-in
				AddinScanFolderInfo finfo;
				if (GetFolderInfoForPath (progressStatus, Path.GetDirectoryName (file), out finfo) && finfo != null) {
					AddinFileInfo afi = finfo.GetAddinFileInfo (file);
					if (afi != null && afi.AddinId != null) {
						AddinDescription adesc;
						if (afi.IsRoot)
							GetHostDescription (progressStatus, afi.AddinId, file, out adesc);
						else
							GetAddinDescription (progressStatus, afi.AddinId, out adesc);
						if (adesc != null)
							adesc.Save (outFile);
						return;
					}
				}
				
				
				AddinScanner scanner = new AddinScanner (this);
				
				SingleFileAssemblyResolver res = new SingleFileAssemblyResolver (progressStatus, registry, scanner);
				ResolveEventHandler resolver = new ResolveEventHandler (res.Resolve);

				EventInfo einfo = typeof(AppDomain).GetEvent ("ReflectionOnlyAssemblyResolve");
				
				try {
					AppDomain.CurrentDomain.AssemblyResolve += resolver;
					if (einfo != null) einfo.AddEventHandler (AppDomain.CurrentDomain, resolver);
				
					AddinDescription desc = scanner.ScanSingleFile (progressStatus, file, new AddinScanResult ());
					if (desc != null)
						desc.Save (outFile);
				}
				finally {
					AppDomain.CurrentDomain.AssemblyResolve -= resolver;
					if (einfo != null) einfo.RemoveEventHandler (AppDomain.CurrentDomain, resolver);
				}
			}
		}
		
		Assembly OnResolveAddinAssembly (object s, ResolveEventArgs args)
		{
			string file = currentScanResult.GetAssemblyLocation (args.Name);
			if (file != null)
				return Util.LoadAssemblyForReflection (file);
			else
				return null;
		}
		
		public string GetFolderConfigFile (string path)
		{
			path = Path.GetFullPath (path);
			
			string s = path.Replace ("_", "__");
			s = s.Replace (Path.DirectorySeparatorChar, '_');
			s = s.Replace (Path.AltDirectorySeparatorChar, '_');
			s = s.Replace (Path.VolumeSeparatorChar, '_');
			
			return Path.Combine (AddinFolderCachePath, s + ".data");
		}
		
		internal void UninstallAddin (IProgressStatus monitor, string addinId, AddinScanResult scanResult)
		{
			scanResult.AddRemovedAddin (addinId);
			string file = GetDescriptionPath (addinId);
			DeleteAddin (monitor, file, scanResult);
		}
		
		internal void UninstallRootAddin (IProgressStatus monitor, string addinId, string addinFile, AddinScanResult scanResult)
		{
			string file = fileDatabase.GetSharedObjectFile (AddinCachePath, addinId, ".mroot", addinFile);
			DeleteAddin (monitor, file, scanResult);
		}
		
		void DeleteAddin (IProgressStatus monitor, string file, AddinScanResult scanResult)
		{
			if (!fileDatabase.Exists (file))
				return;
			
			// Add-in already existed. The dependencies of the old add-in need to be re-analized

			AddinDescription desc;
			if (ReadAddinDescription (monitor, file, out desc)) {
				Util.AddDependencies (desc, scanResult);
				if (desc.IsRoot)
					scanResult.HostIndex.RemoveHostData (desc.AddinId, desc.AddinFile);
			} else
				// If we can't get information about the old assembly, just regenerate all relation data
				scanResult.RegenerateRelationData = true;

			SafeDelete (monitor, file);
			SafeDeleteDir (monitor, Path.Combine (AddinPrivateDataPath, Path.GetFileNameWithoutExtension (file)));
		}
		
		public bool GetHostDescription (IProgressStatus monitor, string addinId, string fileName, out AddinDescription description)
		{
			try {
				description = AddinDescription.ReadHostBinary (fileDatabase, AddinCachePath, addinId, fileName);
				if (description != null)
					description.OwnerDatabase = this;
				return true;
			}
			catch (Exception ex) {
				if (monitor == null)
					throw;
				description = null;
				monitor.ReportError ("Could not read folder info file", ex);
				return false;
			}
		}
		
		public bool GetAddinDescription (IProgressStatus monitor, string addinId, out AddinDescription description)
		{
			string file = GetDescriptionPath (addinId);
			return ReadAddinDescription (monitor, file, out description);
		}
		
		public bool ReadAddinDescription (IProgressStatus monitor, string file, out AddinDescription description)
		{
			try {
				description = AddinDescription.ReadBinary (fileDatabase, file);
				if (description != null)
					description.OwnerDatabase = this;
				return true;
			}
			catch (Exception ex) {
				if (monitor == null)
					throw;
				description = null;
				monitor.ReportError ("Could not read folder info file", ex);
				return false;
			}
		}
		
		public bool SaveDescription (IProgressStatus monitor, AddinDescription desc, string replaceFileName)
		{
			try {
				if (replaceFileName != null)
					desc.SaveBinary (fileDatabase, replaceFileName);
				else if (desc.IsRoot)
					desc.SaveHostBinary (fileDatabase, AddinCachePath);
				else
					desc.SaveBinary (fileDatabase, GetDescriptionPath (desc.AddinId));
				return true;
			}
			catch (Exception ex) {
				monitor.ReportError ("Add-in info file could not be saved", ex);
				return false;
			}
		}
		
		public bool AddinDescriptionExists (string addinId)
		{
			return fileDatabase.Exists (GetDescriptionPath (addinId));
		}
		
		public bool HostDescriptionExists (string addinId, string sourceFile)
		{
			return fileDatabase.SharedObjectExists (AddinCachePath, addinId, ".mroot", sourceFile);
		}
		
		public bool ReadFolderInfo (IProgressStatus monitor, string file, out AddinScanFolderInfo folderInfo)
		{
			try {
				folderInfo = AddinScanFolderInfo.Read (fileDatabase, file);
				return true;
			}
			catch (Exception ex) {
				folderInfo = null;
				monitor.ReportError ("Could not read folder info file", ex);
				return false;
			}
		}
		
		public bool GetFolderInfoForPath (IProgressStatus monitor, string path, out AddinScanFolderInfo folderInfo)
		{
			try {
				folderInfo = AddinScanFolderInfo.Read (fileDatabase, AddinFolderCachePath, path);
				return true;
			}
			catch (Exception ex) {
				folderInfo = null;
				monitor.ReportError ("Could not read folder info file", ex);
				return false;
			}
		}

		public bool SaveFolderInfo (IProgressStatus monitor, AddinScanFolderInfo folderInfo)
		{
			try {
				folderInfo.Write (fileDatabase, AddinFolderCachePath);
				return true;
			}
			catch (Exception ex) {
				monitor.ReportError ("Could not write folder info file", ex);
				return false;
			}
		}
		
		public bool DeleteFolderInfo (IProgressStatus monitor, AddinScanFolderInfo folderInfo)
		{
			return SafeDelete (monitor, folderInfo.FileName);
		}
		
		public bool SafeDelete (IProgressStatus monitor, string file)
		{
			try {
				fileDatabase.Delete (file);
				return true;
			}
			catch (Exception ex) {
				if (monitor.LogLevel > 1) {
					monitor.Log ("Could not delete file: " + file);
					monitor.Log (ex.ToString ());
				}
				return false;
			}
		}
		
		public bool SafeDeleteDir (IProgressStatus monitor, string dir)
		{
			try {
				fileDatabase.DeleteDir (dir);
				return true;
			}
			catch (Exception ex) {
				if (monitor.LogLevel > 1) {
					monitor.Log ("Could not delete directory: " + dir);
					monitor.Log (ex.ToString ());
				}
				return false;
			}
		}
		
		AddinHostIndex GetAddinHostIndex ()
		{
			if (hostIndex != null)
				return hostIndex;
			
			using (fileDatabase.LockRead ()) {
				if (fileDatabase.Exists (HostIndexFile))
					hostIndex = AddinHostIndex.Read (fileDatabase, HostIndexFile);
				else
					hostIndex = new AddinHostIndex ();
			}
			return hostIndex;
		}
		
		void SaveAddinHostIndex ()
		{
			if (hostIndex != null)
				hostIndex.Write (fileDatabase, HostIndexFile);
		}
		
		internal string GetUniqueAddinId (string file, string oldId, string ns, string version)
		{
			string baseId = "__" + Path.GetFileNameWithoutExtension (file);

			if (Path.GetExtension (baseId) == ".addin")
				baseId = Path.GetFileNameWithoutExtension (baseId);
			
			string name = baseId;
			string id = Addin.GetFullId (ns, name, version);
			
			// If the old Id is already an automatically generated one, reuse it
			if (oldId != null && oldId.StartsWith (id))
				return name;
			
			int n = 1;
			while (fileDatabase.Exists (GetDescriptionPath (id))) {
				name = baseId + "_" + n;
				id = Addin.GetFullId (ns, name, version);
				n++;
			}
			return name;
		}
		
		public void ResetConfiguration ()
		{
			if (File.Exists (ConfigFile))
				File.Delete (ConfigFile);
		}
		
		DatabaseConfiguration Configuration {
			get {
				if (config == null) {
					using (fileDatabase.LockRead ()) {
						if (fileDatabase.Exists (ConfigFile))
							config = DatabaseConfiguration.Read (ConfigFile);
						else
							config = new DatabaseConfiguration ();
					}
				}
				return config;
			}
		}
		
		void SaveConfiguration ()
		{
			if (config != null) {
				using (fileDatabase.LockWrite ()) {
					config.Write (ConfigFile);
				}
			}
		}
	}
	
	class SingleFileAssemblyResolver
	{
		AddinScanResult scanResult;
		AddinScanner scanner;
		AddinRegistry registry;
		IProgressStatus progressStatus;
		
		public SingleFileAssemblyResolver (IProgressStatus progressStatus, AddinRegistry registry, AddinScanner scanner)
		{
			this.scanner = scanner;
			this.registry = registry;
			this.progressStatus = progressStatus;
		}
		
		public Assembly Resolve (object s, ResolveEventArgs args)
		{
			if (scanResult == null) {
				scanResult = new AddinScanResult ();
				scanResult.LocateAssembliesOnly = true;
			
				foreach (string dir in registry.AddinDirectories)
					scanner.ScanFolder (progressStatus, dir, scanResult);
			}
		
			string afile = scanResult.GetAssemblyLocation (args.Name);
			if (afile != null)
				return Util.LoadAssemblyForReflection (afile);
			else
				return null;
		}
	}
}


