//
// SetupService.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.Xml;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using ICSharpCode.SharpZipLib.Zip;
using Mono.Addins.Description;
using Mono.Addins.Setup.ProgressMonitoring;
using Microsoft.Win32;
using System.Diagnostics;
using Mono.PkgConfig;

namespace Mono.Addins.Setup
{
	public class SetupService
	{
		RepositoryRegistry repositories;
		string applicationNamespace;
		string installDirectory;
		AddinStore store;
		AddinSystemConfiguration config;
		
		AddinRegistry registry;
		
		public SetupService ()
		{
			if (AddinManager.IsInitialized)
				registry = AddinManager.Registry;
			else
				registry = AddinRegistry.GetGlobalRegistry ();
			
			repositories = new RepositoryRegistry (this);
			store = new AddinStore (this);
		}
		
		public SetupService (AddinRegistry registry)
		{
			this.registry = registry;
			repositories = new RepositoryRegistry (this);
			store = new AddinStore (this);
		}
		
		public AddinRegistry Registry {
			get { return registry; }
		}
		
		internal string RepositoryCachePath {
			get { return Path.Combine (registry.RegistryPath, "repository-cache"); }
		}
		
		string RootConfigFile {
			get { return Path.Combine (registry.RegistryPath, "addins-setup.config"); }
		}
		
		public string ApplicationNamespace {
			get { return applicationNamespace; }
			set { applicationNamespace = value; }
		}
		
		public string InstallDirectory {
			get {
				if (installDirectory != null && installDirectory.Length > 0)
					return installDirectory;
				else
					return registry.DefaultAddinsFolder;
			}
			set { installDirectory = value; }
		}
		
		public RepositoryRegistry Repositories {
			get { return repositories; }
		}
		
		internal AddinStore Store {
			get { return store; }
		}
		
		public bool ResolveDependencies (IProgressStatus statusMonitor, AddinRepositoryEntry[] addins, out PackageCollection resolved, out PackageCollection toUninstall, out DependencyCollection unresolved)
		{
			return store.ResolveDependencies (statusMonitor, addins, out resolved, out toUninstall, out unresolved);
		}
		
		public bool ResolveDependencies (IProgressStatus statusMonitor, PackageCollection packages, out PackageCollection toUninstall, out DependencyCollection unresolved)
		{
			return store.ResolveDependencies (statusMonitor, packages, out toUninstall, out unresolved);
		}
		
		public bool Install (IProgressStatus statusMonitor, params string[] files)
		{
			return store.Install (statusMonitor, files);
		}
		
		public bool Install (IProgressStatus statusMonitor, params AddinRepositoryEntry[] addins)
		{
			return store.Install (statusMonitor, addins);
		}
		
		public bool Install (IProgressStatus statusMonitor, PackageCollection packages)
		{
			return store.Install (statusMonitor, packages);
		}
		
		public void Uninstall (IProgressStatus statusMonitor, string id)
		{
			store.Uninstall (statusMonitor, id);
		}
		
		public static AddinHeader GetAddinHeader (Addin addin)
		{
			return AddinInfo.ReadFromDescription (addin.Description);
		}
		
		public Addin[] GetDependentAddins (string id, bool recursive)
		{
			return store.GetDependentAddins (id, recursive);
		}
		
		public void BuildPackage (IProgressStatus statusMonitor, string targetDirectory, params string[] filePaths)
		{
			foreach (string file in filePaths)
				BuildPackageInternal (statusMonitor, targetDirectory, file);
		}
		
		void BuildPackageInternal (IProgressStatus monitor, string targetDirectory, string filePath)
		{
			AddinDescription conf = registry.GetAddinDescription (monitor, filePath);
			if (conf == null) {
				monitor.ReportError ("Could not read add-in file: " + filePath, null);
				return;
			}
			
			string basePath = Path.GetDirectoryName (filePath);
			
			if (targetDirectory == null)
				targetDirectory = basePath;

			// Generate the file name
			
			string name;
			if (conf.LocalId.Length == 0)
				name = Path.GetFileNameWithoutExtension (filePath);
			else
				name = conf.LocalId;
			name = Addin.GetFullId (conf.Namespace, name, conf.Version);
			name = name.Replace (',','_').Replace (".__", ".");
			
			string outFilePath = Path.Combine (targetDirectory, name) + ".mpack";
			
			ZipOutputStream s = new ZipOutputStream (File.Create (outFilePath));
			s.SetLevel(5);
			
			// Generate a stripped down description of the add-in in a file, since the complete
			// description may be declared as assembly attributes
			
			XmlDocument doc = new XmlDocument ();
			doc.PreserveWhitespace = false;
			doc.LoadXml (conf.SaveToXml ().OuterXml);
			CleanDescription (doc.DocumentElement);
			MemoryStream ms = new MemoryStream ();
			XmlTextWriter tw = new XmlTextWriter (ms, System.Text.Encoding.UTF8);
			tw.Formatting = Formatting.Indented;
			doc.WriteTo (tw);
			tw.Flush ();
			byte[] data = ms.ToArray ();
			
			ZipEntry infoEntry = new ZipEntry ("addin.info");
			s.PutNextEntry (infoEntry);
			s.Write (data, 0, data.Length);
			
			// Now add the add-in files
			
			ArrayList list = new ArrayList ();
			if (!conf.AllFiles.Contains (Path.GetFileName (filePath)))
				list.Add (Path.GetFileName (filePath));
			foreach (string f in conf.AllFiles) {
				list.Add (f);
			}
			
			monitor.Log ("Creating package " + Path.GetFileName (outFilePath));
			
			foreach (string file in list) {
				string fp = Path.Combine (basePath, file);
				using (FileStream fs = File.OpenRead (fp)) {
					byte[] buffer = new byte [fs.Length];
					fs.Read (buffer, 0, buffer.Length);
					
					ZipEntry entry = new ZipEntry (file);
					s.PutNextEntry (entry);
					s.Write (buffer, 0, buffer.Length);
				}
			}
			
			s.Finish();
			s.Close();			
		}
		
		void CleanDescription (XmlElement parent)
		{
			ArrayList todelete = new ArrayList ();
			
			foreach (XmlNode nod in parent.ChildNodes) {
				XmlElement elem = nod as XmlElement;
				if (elem == null)
					continue;
				if (elem.LocalName == "Module")
					CleanDescription (elem);
				else if (elem.LocalName != "Dependencies" && elem.LocalName != "Runtime")
					todelete.Add (elem);
			}
			foreach (XmlElement e in todelete)
				parent.RemoveChild (e);
		}
		
		public void BuildRepository (IProgressStatus statusMonitor, string path)
		{
			string mainPath = Path.Combine (path, "main.mrep");
			ArrayList allAddins = new ArrayList ();
			
			Repository rootrep = (Repository) AddinStore.ReadObject (mainPath, typeof(Repository));
			if (rootrep == null) {
				rootrep = new Repository ();
			}
			
			IProgressMonitor monitor = ProgressStatusMonitor.GetProgressMonitor (statusMonitor);
			BuildRepository (monitor, rootrep, path, "root.mrep", allAddins);
			AddinStore.WriteObject (mainPath, rootrep);
			GenerateIndexPage (rootrep, allAddins, path);
			monitor.Log.WriteLine ("Updated main.mrep");
		}
		
		void BuildRepository (IProgressMonitor monitor, Repository rootrep, string rootPath, string relFilePath, ArrayList allAddins)
		{
			DateTime lastModified = DateTime.MinValue;
			
			string mainFile = Path.Combine (rootPath, relFilePath);
			string mainPath = Path.GetDirectoryName (mainFile);
			
			Repository mainrep = (Repository) AddinStore.ReadObject (mainFile, typeof(Repository));
			if (mainrep == null) {
				mainrep = new Repository ();
			}
			
			bool modified = false;
			
			monitor.Log.WriteLine ("Checking directory: " + mainPath);
			foreach (string file in Directory.GetFiles (mainPath, "*.mpack")) {
				string fname = Path.GetFileName (file);
				PackageRepositoryEntry entry = (PackageRepositoryEntry) mainrep.FindEntry (fname);
				if (entry == null) {
					entry = new PackageRepositoryEntry ();
					AddinPackage p = (AddinPackage) Package.FromFile (file);
					entry.Addin = (AddinInfo) p.Addin;
					entry.Url = fname;
					mainrep.AddEntry (entry);
					modified = true;
					monitor.Log.WriteLine ("Added addin: " + fname);
				}
				allAddins.Add (entry);
				
				DateTime date = File.GetLastWriteTime (file);
				if (date > lastModified)
					lastModified = date;
			}
			
			ArrayList toRemove = new ArrayList ();
			foreach (PackageRepositoryEntry entry in mainrep.Addins)
				if (!File.Exists (Path.Combine (mainPath, entry.Url)))
					toRemove.Add (entry);
					
			foreach (PackageRepositoryEntry entry in toRemove)
				mainrep.RemoveEntry (entry);
			
			if (modified || toRemove.Count > 0) {
				AddinStore.WriteObject (mainFile, mainrep);
				monitor.Log.WriteLine ("Updated " + relFilePath);
			}

			ReferenceRepositoryEntry repEntry = (ReferenceRepositoryEntry) rootrep.FindEntry (relFilePath);
			if (repEntry != null) {
				if (repEntry.LastModified < lastModified)
					repEntry.LastModified = lastModified;
			} else {
				repEntry = new ReferenceRepositoryEntry ();
				repEntry.LastModified = lastModified;
				repEntry.Url = relFilePath;
				rootrep.AddEntry (repEntry);
			}
			
			foreach (string dir in Directory.GetDirectories (mainPath)) {
				string based = dir.Substring (rootPath.Length + 1);
				BuildRepository (monitor, rootrep, rootPath, Path.Combine (based, "main.mrep"), allAddins);
			}
		}
		
		void GenerateIndexPage (Repository rep, ArrayList addins, string basePath)
		{
			StreamWriter sw = new StreamWriter (Path.Combine (basePath, "index.html"));
			sw.WriteLine ("<html><body>");
			sw.WriteLine ("<h1>Add-in Repository</h1>");
			if (rep.Name != null && rep.Name != "")
				sw.WriteLine ("<h2>" + rep.Name + "</h2>");
			sw.WriteLine ("<p>This is a list of add-ins available in this repository.</p>");
			sw.WriteLine ("<table border=1><thead><tr><th>Add-in</th><th>Version</th><th>Description</th></tr></thead>");
			
			foreach (PackageRepositoryEntry entry in addins) {
				sw.WriteLine ("<tr><td>" + entry.Addin.Name + "</td><td>" + entry.Addin.Version + "</td><td>" + entry.Addin.Description + "</td></tr>");
			}
			
			sw.WriteLine ("</table>");
			sw.WriteLine ("</body></html>");
			sw.Close ();
		}
		
		internal AddinSystemConfiguration Configuration {
			get {
				if (config == null) {
					config = (AddinSystemConfiguration) AddinStore.ReadObject (RootConfigFile, typeof(AddinSystemConfiguration));
					if (config == null)
						config = new AddinSystemConfiguration ();
				}
				return config;
			}
		}
		
		internal void SaveConfiguration ()
		{
			if (config != null) {
				AddinStore.WriteObject (RootConfigFile, config); 
			}
		}

		internal void ResetConfiguration ()
		{
			if (File.Exists (RootConfigFile))
				File.Delete (RootConfigFile);
			ResetAddinInfo ();
		}
				
		internal void ResetAddinInfo ()
		{
			if (Directory.Exists (RepositoryCachePath))
				Directory.Delete (RepositoryCachePath, true);
		}
		
		public static Application GetExtensibleApplication (string name)
		{
			return GetExtensibleApplication (name, null);
		}
		
		public static Application GetExtensibleApplication (string name, IEnumerable<string> searchPaths)
		{
			AddinsPcFileCache pcc = GetAddinsPcFileCache (searchPaths);
			PackageInfo pi = pcc.GetPackageInfoByName (name, searchPaths);
			if (pi != null)
				return new Application (pi);
			else
				return null;
		}
		
		public static Application[] GetExtensibleApplications ()
		{
			return GetExtensibleApplications (null);
		}
		
		public static Application[] GetExtensibleApplications (IEnumerable<string> searchPaths)
		{
			List<Application> list = new List<Application> ();
			
			AddinsPcFileCache pcc = GetAddinsPcFileCache (searchPaths);
			foreach (PackageInfo pinfo in pcc.GetPackages (searchPaths)) {
				if (pinfo.IsValidPackage)
					list.Add (new Application (pinfo));
			}
			return list.ToArray ();
		}
		
		static AddinsPcFileCache pcFileCache;
		
		static AddinsPcFileCache GetAddinsPcFileCache (IEnumerable<string> searchPaths)
		{
			if (pcFileCache == null) {
				pcFileCache = new AddinsPcFileCache ();
				if (searchPaths != null)
					pcFileCache.Update (searchPaths);
				else
					pcFileCache.Update ();
			}
			return pcFileCache;
		}
	}
	
	class AddinsPcFileCacheContext: IPcFileCacheContext
	{
		public bool IsCustomDataComplete (string pcfile, PackageInfo pkg)
		{
			return true;
		}
		
		public void StoreCustomData (Mono.PkgConfig.PcFile pcfile, PackageInfo pkg)
		{
		}

		public void ReportError (string message, System.Exception ex)
		{
			Console.WriteLine (message);
			Console.WriteLine (ex);
		}
	}
	
	class AddinsPcFileCache: PcFileCache
	{
		public AddinsPcFileCache (): base (new AddinsPcFileCacheContext ())
		{
		}
		
		protected override string CacheDirectory {
			get {
				string path = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
				path = Path.Combine (path, "mono.addins");
				return path;
			}
		}
		
		protected override void ParsePackageInfo (PcFile file, PackageInfo pinfo)
		{
			string rootPath = file.GetVariable ("MonoAddinsRoot");
			string regPath = file.GetVariable ("MonoAddinsRegistry");
			string testCmd = file.GetVariable ("MonoAddinsTestCommand");
			if (string.IsNullOrEmpty (rootPath) || string.IsNullOrEmpty (regPath))
				return;
			pinfo.SetData ("MonoAddinsRoot", rootPath);
			pinfo.SetData ("MonoAddinsRegistry", regPath);
			pinfo.SetData ("MonoAddinsTestCommand", testCmd);
		}
	}
	
	public class Application
	{
		AddinRegistry registry;
		string description;
		string name;
		string testCommand;
		string startupPath;
		string registryPath;
		
		internal Application (PackageInfo pinfo)
		{
			name = pinfo.Name;
			description = pinfo.Description;
			startupPath = pinfo.GetData ("MonoAddinsRoot");
			registryPath = pinfo.GetData ("MonoAddinsRegistry");
			testCommand = pinfo.GetData ("MonoAddinsTestCommand");
		}
		
		public AddinRegistry Registry {
			get {
				if (registry == null)
					registry = new AddinRegistry (RegistryPath, StartupPath);
				return registry;
			}
		}

		public string Description {
			get {
				return description;
			}
		}

		public string Name {
			get {
				return name;
			}
		}

		public string RegistryPath {
			get {
				return registryPath;
			}
		}

		public string StartupPath {
			get {
				return startupPath;
			}
		}

		public string TestCommand {
			get {
				return testCommand;
			}
		}
	}
}
