//
// GuiBuilderProject.cs
//
// Author:
//   Lluis Sanchez Gual
//
// Copyright (C) 2006 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 System.Collections.Specialized;
using System.CodeDom.Compiler;

using MonoDevelop.Core;
using MonoDevelop.Projects;
using MonoDevelop.Ide;
using ICSharpCode.NRefactory.TypeSystem;
using MonoDevelop.Ide.TypeSystem;
using System.Linq;

namespace MonoDevelop.GtkCore.GuiBuilder
{
	public class GuiBuilderProject
	{
		internal object MemoryProbe = Counters.GuiProjectsInMemory.CreateMemoryProbe ();
		
		List<GuiBuilderWindow> formInfos;
		Stetic.Project gproject;
		DotNetProject project;
		string fileName;
		bool hasError;
		bool needsUpdate = true;
		
		FileSystemWatcher watcher;
		DateTime lastSaveTime;
		object fileSaveLock = new object ();
		bool disposed;
		bool librariesUpdated;
		
		public event WindowEventHandler WindowAdded;
		public event WindowEventHandler WindowRemoved;
		public event EventHandler Reloaded;
		public event EventHandler Unloaded;
		public event EventHandler Changed;

		public GuiBuilderProject (DotNetProject project, string fileName)
		{
			this.fileName = fileName;
			this.project = project;
			Counters.GuiProjectsLoaded++;
			GuiBuilderService.NotifyGuiProjectLoaded ();
		}
		
		void Load ()
		{
			if (gproject != null || disposed || fileName == null)
				return;
			
			gproject = GuiBuilderService.SteticApp.CreateProject ();
			formInfos = new List<GuiBuilderWindow> ();
			
			if (!System.IO.File.Exists (fileName)) {
				// Regenerate the gtk-gui folder if the stetic project
				// doesn't exist.
				GtkDesignInfo.FromProject (project).UpdateGtkFolder ();
			}

			try {
				gproject.Load (fileName);
			} catch (Exception ex) {
				MessageService.ShowException (ex, GettextCatalog.GetString ("The GUI designer project file '{0}' could not be loaded.", fileName));
				hasError = true;
			}

			Counters.SteticProjectsLoaded++;
			gproject.ResourceProvider = GtkDesignInfo.FromProject (project).ResourceProvider;
			gproject.WidgetAdded += OnAddWidget;
			gproject.WidgetRemoved += OnRemoveWidget;
			gproject.ActionGroupsChanged += OnGroupsChanged;
			project.FileAddedToProject += OnFileAdded;
			project.FileRemovedFromProject += OnFileRemoved;
			project.ReferenceAddedToProject += OnReferenceAdded;
			project.ReferenceRemovedFromProject += OnReferenceRemoved;
			
			foreach (Stetic.WidgetInfo ob in gproject.Widgets)
				RegisterWindow (ob, false);
				
			// Monitor changes in the file
			lastSaveTime = System.IO.File.GetLastWriteTime (fileName);
			watcher = new FileSystemWatcher ();
			if (System.IO.File.Exists (fileName)) {
				watcher.Path = Path.GetDirectoryName (fileName);
				watcher.Filter = Path.GetFileName (fileName);
				watcher.Changed += (FileSystemEventHandler) DispatchService.GuiDispatch (new FileSystemEventHandler (OnSteticFileChanged));
				watcher.EnableRaisingEvents = true;
			}
		}	
	
		void Unload ()
		{
			if (gproject == null)
				return;

			Counters.SteticProjectsLoaded--;
			
			if (Unloaded != null)
				Unloaded (this, EventArgs.Empty);
			if (formInfos != null) {
				foreach (GuiBuilderWindow win in formInfos)
					win.Dispose ();
				formInfos = null;
			}
			if (gproject != null) {
				gproject.WidgetAdded -= OnAddWidget;
				gproject.WidgetRemoved -= OnRemoveWidget;
				gproject.ActionGroupsChanged -= OnGroupsChanged;
				gproject.Dispose ();
				gproject = null;
			}
			if (project != null) {
				project.FileAddedToProject -= OnFileAdded;
				project.FileRemovedFromProject -= OnFileRemoved;
				project.ReferenceAddedToProject -= OnReferenceAdded;
				project.ReferenceRemovedFromProject -= OnReferenceRemoved;
			}
			needsUpdate = true;
			hasError = false;
			librariesUpdated = false;
			if (watcher != null) {
				watcher.Dispose ();
				watcher = null;
			}
			NotifyChanged ();
		}
		
		void OnSteticFileChanged (object s, FileSystemEventArgs args)
		{
			lock (fileSaveLock) {
				if (lastSaveTime == System.IO.File.GetLastWriteTime (fileName))
					return;
			}
			
			if (GuiBuilderService.HasOpenDesigners (project, true)) {
				if (MessageService.AskQuestion (GettextCatalog.GetString ("The project '{0}' has been modified by an external application. Do you want to reload it?", project.Name), GettextCatalog.GetString ("Unsaved changes in the open GTK designers will be lost."), AlertButton.Cancel, AlertButton.Reload) != AlertButton.Reload)
					return;
			}
			if (!disposed)
				Reload ();
		}
		
		public void Reload ()
		{
			if (disposed)
				return;
			Unload ();
			if (Reloaded != null)
				Reloaded (this, EventArgs.Empty);
			NotifyChanged ();
		}
		
		public bool HasError {
			get { return hasError; }
		}

		public bool IsEmpty {
			get {
				// If the project is not loaded, assume not empty
				return gproject != null && Windows != null && Windows.Count == 0; 
			}
		}
		
		public void SaveAll (bool saveMdProject)
		{
			if (gproject != null)
				gproject.SetAllWidgetsModified ();
			SaveProject (saveMdProject);
		}

		public void SaveWindow (bool saveMdProject, string modifiedWindow)
		{
			if (gproject != null)
				gproject.SetWidgetModified (modifiedWindow);
			SaveProject (saveMdProject);
		}

		public void SaveProject (bool saveMdProject)
		{
			if (disposed)
				return;

			if (gproject != null && !hasError) {
				lock (fileSaveLock) {
					gproject.Save (fileName);
					lastSaveTime = System.IO.File.GetLastWriteTime (fileName);
				}
			}
				
			if (GtkDesignInfo.FromProject (project).UpdateGtkFolder () && saveMdProject)
				IdeApp.ProjectOperations.Save (project);
		}
		
		public string File {
			get { return fileName; }
		}
		
		public Stetic.Project SteticProject {
			get {
				Load ();
				return gproject;
			}
		}
		
		public ICollection<GuiBuilderWindow> Windows {
			get {
				Load ();
				return formInfos; 
			}
		}
		
		public DotNetProject Project {
			get { return project; }
		}
		
		public void Dispose ()
		{
			if (disposed)
				return;
			GuiBuilderService.NotifyGuiProjectUnloaded ();
			Counters.GuiProjectsLoaded--;
			disposed = true;
			if (watcher != null)
				watcher.Dispose ();
			Unload ();
		}
		
		public Stetic.WidgetInfo AddNewComponent (Stetic.ComponentType type, string name)
		{
			Stetic.WidgetInfo c = SteticProject.AddNewComponent (type, name);
			RegisterWindow (c, true);
			return c;
		}
		
		public Stetic.WidgetInfo AddNewComponent (XmlElement element)
		{
			Stetic.WidgetInfo c = SteticProject.AddNewComponent (element);
			// Register the window now, don't wait for the WidgetAdded event since
			// it may take some time, and the GuiBuilderWindow object is needed
			// just after this call
			RegisterWindow (c, true);
			return c;
		}
	
		void RegisterWindow (Stetic.WidgetInfo widget, bool notify)
		{
			if (formInfos != null) {
				foreach (GuiBuilderWindow w in formInfos)
					if (w.RootWidget == widget)
						return;
			
				GuiBuilderWindow win = new GuiBuilderWindow (this, gproject, widget);
				formInfos.Add (win);
			
				if (notify) {
					if (WindowAdded != null)
						WindowAdded (this, new WindowEventArgs (win));
					NotifyChanged ();
				}
			}
		}
	
		void UnregisterWindow (GuiBuilderWindow win)
		{
			if (!formInfos.Contains (win))
				return;

			formInfos.Remove (win);

			if (WindowRemoved != null)
				WindowRemoved (this, new WindowEventArgs (win));

			win.Dispose ();
			NotifyChanged ();
		}
		
		public void Remove (GuiBuilderWindow win)
		{
			gproject.RemoveComponent (win.RootWidget);
			UnregisterWindow (win);
		}
	
		public void RemoveActionGroup (Stetic.ActionGroupInfo group)
		{
			gproject.RemoveActionGroup (group);
		}
	
		void OnAddWidget (object s, Stetic.WidgetInfoEventArgs args)
		{
			if (!disposed)
				RegisterWindow (args.WidgetInfo, true);
		}
		
		void OnRemoveWidget (object s, Stetic.WidgetInfoEventArgs args)
		{
			if (disposed || Windows == null)
				return;
			foreach (GuiBuilderWindow form in Windows) {
				if (form.RootWidget.Name == args.WidgetInfo.Name) {
					UnregisterWindow (form);
					break;
				}
			}
		}
		
		void OnFileAdded (object sender, ProjectFileEventArgs e)
		{
			foreach (ProjectFileEventInfo args in e) {
				var doc = TypeSystemService.ParseFile (args.Project, args.ProjectFile.Name);
				if (doc == null)
					continue;
	
				string dir = Path.Combine (Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData), "stetic"), "deleted-designs");
				if (!Directory.Exists (dir) || Directory.GetFiles (dir).Length == 0)
					continue;
	
				foreach (var t in doc.TopLevelTypeDefinitions) {
					string path = Path.Combine (dir, t.FullName + ".xml");
					if (!System.IO.File.Exists (path))
						continue;
					XmlDocument xmldoc = new XmlDocument ();
					xmldoc.Load (path);
					AddNewComponent (xmldoc.DocumentElement);
					System.IO.File.Delete (path);
				}
			}
		}

		void OnFileRemoved (object sender, ProjectFileEventArgs e)
		{
			ArrayList toDelete = new ArrayList ();

			foreach (ProjectFileEventInfo args in e) {
				var doc = TypeSystemService.ParseFile (args.Project, args.ProjectFile.Name);
				if (doc == null)
					continue;
	
				foreach (var t in doc.TopLevelTypeDefinitions) {
					GuiBuilderWindow win = GetWindowForClass (t.FullName);
					if (win != null)
						toDelete.Add (win);
				}
			}
			
			foreach (GuiBuilderWindow win in toDelete)
				Remove (win);
		}

		void OnGroupsChanged (object s, EventArgs a)
		{
			if (!disposed)
				NotifyChanged ();
		}

		void OnReferenceAdded (object ob, ProjectReferenceEventArgs args)
		{
			if (disposed || !librariesUpdated)
				return;
			string pref = GetReferenceLibraryPath (args.ProjectReference);
			if (pref != null) {
				gproject.AddWidgetLibrary (pref);
				SaveProject (false);
			}
		}
		
		void OnReferenceRemoved (object ob, ProjectReferenceEventArgs args)
		{
			if (disposed || !librariesUpdated)
				return;
			string pref = GetReferenceLibraryPath (args.ProjectReference);
			if (pref != null) {
				gproject.RemoveWidgetLibrary (pref);
				SaveProject (false);
			}
		}

		string GetReferenceLibraryPath (MonoDevelop.Projects.ProjectReference pref)
		{
			string path = null;
			
			if (pref.ReferenceType == ReferenceType.Project) {
				DotNetProject p = project.ParentSolution.FindProjectByName (pref.Reference) as DotNetProject;
				if (p != null)
					path = p.GetOutputFileName (IdeApp.Workspace.ActiveConfiguration);
			} else if (pref.ReferenceType == ReferenceType.Assembly) {
				path = pref.Reference;
			} else if (pref.ReferenceType == ReferenceType.Package) {
				path = pref.Reference;
			}
			if (path != null && GuiBuilderService.SteticApp.IsWidgetLibrary (path))
				return path;
			else
				return null;
		}
		
		public void ImportGladeFile ()
		{
			var dlg = new MonoDevelop.Components.SelectFileDialog (GettextCatalog.GetString ("Open Glade File"));
			dlg.AddFilter (GettextCatalog.GetString ("Glade files"), "*.glade");
			dlg.AddAllFilesFilter ();
			if (dlg.Run ()) {
				SteticProject.ImportGlade (dlg.SelectedFile);
				SaveAll (true);
			}
		}
		
		public GuiBuilderWindow GetWindowForClass (string className)
		{
			if (Windows != null) {
				foreach (GuiBuilderWindow form in Windows) {
					if (CodeBinder.GetObjectName (form.RootWidget) == className)
						return form;
				}
			}
			return null;
		}

		public GuiBuilderWindow GetWindowForFile (FilePath fileName)
		{
			if (Windows != null) {
				foreach (GuiBuilderWindow win in Windows) {
					if (fileName == win.SourceCodeFile)
						return win;
				}
			}
			return null;
		}
		
		public GuiBuilderWindow GetWindow (string name)
		{
			if (Windows != null) {
				foreach (GuiBuilderWindow win in Windows) {
					if (name == win.Name)
						return win;
				}
			}
			return null;
		}

		public Stetic.ActionGroupInfo GetActionGroupForFile (FilePath fileName)
		{
			foreach (Stetic.ActionGroupInfo group in SteticProject.ActionGroups) {
				if (fileName == GetSourceCodeFile (group, true))
					return group;
			}
			return null;
		}
		
		public Stetic.ActionGroupInfo GetActionGroup (string name)
		{
			return SteticProject.GetActionGroup (name);
		}

		public FilePath GetSourceCodeFile (Stetic.ProjectItemInfo obj)
		{
			return GetSourceCodeFile (obj, true);
		}

		public FilePath GetSourceCodeFile (Stetic.ProjectItemInfo obj, bool getUserClass)
		{
			var cls = GetClass (obj, getUserClass);
			if (cls != null)
				return cls.Region.FileName;
			return null;
		}
		
		IUnresolvedTypeDefinition GetClass (Stetic.ProjectItemInfo obj, bool getUserClass)
		{
			string name = CodeBinder.GetClassName (obj);
			return FindClass (name, getUserClass);
		}
		
		public IUnresolvedTypeDefinition FindClass (string className)
		{
			return FindClass (className, true);
		}
		
		public IUnresolvedTypeDefinition FindClass (string className, bool getUserClass)
		{
			FilePath gui_folder = GtkDesignInfo.FromProject (project).GtkGuiFolder;
			var ctx = GetParserContext ();
			if (ctx == null)
				return null;
			var classes = ctx.MainAssembly.GetAllTypeDefinitions ();
			if (classes == null)
				return null;
			foreach (var cls in classes) {
				if (cls.FullName == className) {
					if (getUserClass) {
						// Return this class only if it is declared outside the gtk-gui
						// folder. Generated partial classes will be ignored.
						foreach (var part in cls.Parts) {
							if (!string.IsNullOrEmpty (part.Region.FileName) && !((FilePath)cls.Region.FileName).IsChildPathOf (gui_folder)) {
								return part;
							}
						}
						continue;
					}
					if (getUserClass && !string.IsNullOrEmpty (cls.Region.FileName) && ((FilePath)cls.Region.FileName).IsChildPathOf (gui_folder))
						continue;
					return cls.Parts.First ();
				}
			}
			return null;
		}
		
		public ICompilation GetParserContext ()
		{
			var dom = TypeSystemService.GetCompilation (Project);
			if (dom != null && needsUpdate) {
				needsUpdate = false;
//				dom.ForceUpdate ();
			}
			return dom;
		}
		
		public WidgetParser WidgetParser {
			get {
				return new WidgetParser (GetParserContext ());
			}
		}

		public void UpdateLibraries ()
		{
			if (hasError || disposed || gproject == null)
				return;

			bool needsSave = false;
			librariesUpdated = true;
			
			string[] oldLibs = gproject.WidgetLibraries;
			
			ArrayList libs = new ArrayList ();
			string[] internalLibs;
			
			foreach (var pref in project.References) {
				string wref = GetReferenceLibraryPath (pref);
				if (wref != null)
					libs.Add (wref);
			}
			
			ReferenceManager refmgr = new ReferenceManager (project);
			string target_version = refmgr.TargetGtkVersion;
			refmgr.Dispose ();
			
			// Make sure the target gtk version is properly set
			if (gproject.TargetGtkVersion != target_version) {
				gproject.TargetGtkVersion = target_version;
				needsSave = true;
			}

			string outLib = project.GetOutputFileName (IdeApp.Workspace.ActiveConfiguration);
			if (!string.IsNullOrEmpty (outLib))
				internalLibs = new string [] { outLib };
			else
				internalLibs = new string [0];

			string[] newLibs = (string[]) libs.ToArray (typeof(string));
			
			// See if something has changed
			if (LibrariesChanged (oldLibs, internalLibs, newLibs)) {
				gproject.SetWidgetLibraries (newLibs, internalLibs);
				needsSave = true;
			} else {
				GuiBuilderService.SteticApp.UpdateWidgetLibraries (false);
			}
			
			if (needsSave)
				SaveAll (true);
		}
		
		bool LibrariesChanged (string[] oldLibs, string[] internalLibs, string[] newLibs)
		{
			if (oldLibs.Length == newLibs.Length + internalLibs.Length) {
				foreach (string s in newLibs) {
					if (!((IList)oldLibs).Contains (s))
						return true;
				}
				foreach (string s in internalLibs) {
					if (!((IList)oldLibs).Contains (s))
						return true;
				}
				return false;
			} else
				return true;
		}
		
		void NotifyChanged ()
		{
			if (Changed != null && !disposed)
				Changed (this, EventArgs.Empty);
		}

		public StringCollection GenerateFiles (string guiFolder)
		{
			StringCollection files = new StringCollection ();

			if (hasError)
				return files;

			IDotNetLanguageBinding binding = LanguageBindingService.GetBindingPerLanguageName (project.LanguageName) as IDotNetLanguageBinding;
			string path = Path.Combine (guiFolder, binding.GetFileName ("generated"));
			if (!System.IO.File.Exists (path)) {
				// Generate an empty build class
				CodeDomProvider provider = binding.GetCodeDomProvider ();
				if (provider == null)
					throw new UserException ("Code generation not supported for language: " + project.LanguageName);
				GuiBuilderService.SteticApp.GenerateProjectCode (path, "Stetic", provider, null);
			}
			files.Add (path);

			if (Windows != null) {
				foreach (GuiBuilderWindow win in Windows)
					files.Add (GuiBuilderService.GenerateSteticCodeStructure (project, win.RootWidget, true, false));
			}
					
			foreach (Stetic.ActionGroupInfo ag in SteticProject.ActionGroups)
				files.Add (GuiBuilderService.GenerateSteticCodeStructure (project, ag, true, false));

			return files;
		}
	}
	
	public delegate void WindowEventHandler (object s, WindowEventArgs args);
	
	public class WindowEventArgs: EventArgs
	{
		GuiBuilderWindow win;
		
		public WindowEventArgs (GuiBuilderWindow win)
		{
			this.win = win;
		}
		
		public GuiBuilderWindow Window {
			get { return win; }
		}
	}
}
