#region ================== Copyright (c) 2007 Pascal vd Heiden

/*
 * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
 * This program is released under GNU General Public License
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 */

#endregion

#region ================== Namespaces

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Text;
using CodeImp.DoomBuilder.Properties;
using System.IO;
using CodeImp.DoomBuilder.IO;
using System.Collections;
using System.Reflection;
using System.Windows.Forms;

#endregion

namespace CodeImp.DoomBuilder.Actions
{
	public class ActionManager
	{
		#region ================== Constants

		private const string ACTIONS_RESOURCE = "Actions.cfg";

		#endregion

		#region ================== Variables

		// Actions
		private Dictionary<string, Action> actions;
		
		// Categories
		private SortedDictionary<string, string> categories;
		
		// Keys state
		private int modifiers;
		private List<int> pressedkeys;
		
		// Begun actions
		private List<Action> activeactions;
		
		// Exclusive invokation
		private bool exclusiverequested;
		
		// Disposing
		private bool isdisposed = false;

		#endregion

		#region ================== Properties

		internal SortedDictionary<string, string> Categories { get { return categories; } }
		internal Action this[string action] { get { if(actions.ContainsKey(action)) return actions[action]; else throw new ArgumentException("There is no such action \"" + action + "\""); } }
		public bool IsDisposed { get { return isdisposed; } }
		internal bool ExclusiveRequested { get { return exclusiverequested; } }
		
		#endregion

		#region ================== Constructor / Disposer

		// Constructor
		internal ActionManager()
		{
			// Initialize
			General.WriteLogLine("Starting action manager...");
			actions = new Dictionary<string, Action>();
			pressedkeys = new List<int>();
			activeactions = new List<Action>();
			categories = new SortedDictionary<string, string>();
			
			// Load all actions in this assembly
			LoadActions(General.ThisAssembly);
			
			// We have no destructor
			GC.SuppressFinalize(this);
		}

		// Disposer
		internal void Dispose()
		{
			// Not already disposed?
			if(!isdisposed)
			{
				// Clean up
				
				// Done
				isdisposed = true;
			}
		}

		#endregion

		#region ================== Actions

		// This loads all actions from an assembly
		internal void LoadActions(Assembly asm)
		{
			Stream actionsdata;
			StreamReader actionsreader;
			Configuration cfg;
			string cat, name, title, desc, shortname;
			bool amouse, akeys, ascroll, debugonly, noshift, repeat;
			string[] resnames;
			AssemblyName asmname = asm.GetName();

			// Find a resource named Actions.cfg
			resnames = asm.GetManifestResourceNames();
			foreach(string rn in resnames)
			{
				// Found one?
				if(rn.EndsWith(ACTIONS_RESOURCE, StringComparison.InvariantCultureIgnoreCase))
				{
					// Get a stream from the resource
					actionsdata = asm.GetManifestResourceStream(rn);
					actionsreader = new StreamReader(actionsdata, Encoding.ASCII);

					// Load configuration from stream
					cfg = new Configuration();
					cfg.InputConfiguration(actionsreader.ReadToEnd());
					if(cfg.ErrorResult != 0)
					{
						string errordesc = "Error in Actions configuration on line " + cfg.ErrorLine + ": " + cfg.ErrorDescription;
						General.CancelAutoMapLoad();
						General.WriteLogLine("ERROR: Unable to read Actions configuration from assembly " + Path.GetFileName(asm.Location) + "!");
						General.WriteLogLine(errordesc);
						General.ShowErrorMessage("Unable to read Actions configuration from assembly " + Path.GetFileName(asm.Location) + "!\n" + errordesc, MessageBoxButtons.OK);
					}
					else
					{
						// Read the categories structure
						IDictionary cats = cfg.ReadSetting("categories", new Hashtable());
						foreach(DictionaryEntry c in cats)
						{
							// Make the category if not already added
							if(!categories.ContainsKey(c.Key.ToString()))
								categories.Add(c.Key.ToString(), c.Value.ToString());
						}

						// Go for all objects in the configuration
						foreach(DictionaryEntry a in cfg.Root)
						{
							// Get action properties
							shortname = a.Key.ToString();
							name = asmname.Name.ToLowerInvariant() + "_" + shortname;
							debugonly = cfg.ReadSetting(a.Key + ".debugonly", false);

							// Not the categories structure?
							if(shortname.ToLowerInvariant() != "categories")
							{
								// Check if action should be included
								if(General.DebugBuild || !debugonly)
								{
									// Create an action
									CreateAction(cfg, name, shortname);
								}
							}
						}
					}
					
					// Done with the resource
					actionsreader.Dispose();
					actionsdata.Dispose();
				}
			}
		}

		// This manually creates an action
		private void CreateAction(Configuration cfg, string name, string shortname)
		{
			// Action does not exist yet?
			if(!actions.ContainsKey(name))
			{
				// Read the key from configuration
				int key = General.Settings.ReadSetting("shortcuts." + name, -1);

				// Create an action
				actions.Add(name, new Action(cfg, name, shortname, key));
			}
			else
			{
				// Action already exists!
				General.WriteLogLine("WARNING: Action '" + name + "' already exists. Action names must be unique!");
			}
		}

		// This binds all methods marked with this attribute
		internal void BindMethods(Type type)
		{
			// Bind static methods
			BindMethods(null, type);
		}

		// This binds all methods marked with this attribute
		internal void BindMethods(object obj)
		{
			// Bind instance methods
			BindMethods(obj, obj.GetType());
		}

		// This binds all methods marked with this attribute
		private void BindMethods(object obj, Type type)
		{
			MethodInfo[] methods;
			ActionAttribute[] attrs;
			ActionDelegate del;
			string actionname;

			if(obj == null)
				General.WriteLogLine("Binding static action methods for class " + type.Name + "...");
			else
				General.WriteLogLine("Binding action methods for " + type.Name + " object...");

			// Go for all methods on obj
			methods = type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
			foreach(MethodInfo m in methods)
			{
				// Check if the method has this attribute
				attrs = (ActionAttribute[])m.GetCustomAttributes(typeof(BeginActionAttribute), true);

				// Go for all attributes
				foreach(ActionAttribute a in attrs)
				{
					// Create a delegate for this method
					del = (ActionDelegate)Delegate.CreateDelegate(typeof(ActionDelegate), obj, m);

					// Make proper name
					actionname = a.GetFullActionName(type.Assembly);

					// Bind method to action
					if(Exists(actionname))
						actions[actionname].BindBegin(del);
					else
						throw new ArgumentException("Could not bind " + m.ReflectedType.Name + "." + m.Name + " to action \"" + actionname + "\", that action does not exist! Refer to, or edit Actions.cfg for all available application actions.");
				}
				
				// Check if the method has this attribute
				attrs = (ActionAttribute[])m.GetCustomAttributes(typeof(EndActionAttribute), true);

				// Go for all attributes
				foreach(ActionAttribute a in attrs)
				{
					// Create a delegate for this method
					del = (ActionDelegate)Delegate.CreateDelegate(typeof(ActionDelegate), obj, m);

					// Make proper name
					actionname = a.GetFullActionName(type.Assembly);

					// Bind method to action
					if(Exists(actionname))
						actions[actionname].BindEnd(del);
					else
						throw new ArgumentException("Could not bind " + m.ReflectedType.Name + "." + m.Name + " to action \"" + actionname + "\", that action does not exist! Refer to, or edit Actions.cfg for all available application actions.");
				}
			}
		}

		// This binds a delegate manually
		internal void BindBeginDelegate(Assembly asm, ActionDelegate d, BeginActionAttribute a)
		{
			string actionname;

			// Make proper name
			actionname = a.GetFullActionName(asm);

			// Bind delegate to action
			if(Exists(actionname))
				actions[actionname].BindBegin(d);
			else
				General.WriteLogLine("WARNING: Could not bind delegate for " + d.Method.Name + " to action \"" + a.ActionName + "\" (" + actionname + "), that action does not exist! Refer to, or edit Actions.cfg for all available application actions.");
		}

		// This binds a delegate manually
		internal void BindEndDelegate(Assembly asm, ActionDelegate d, EndActionAttribute a)
		{
			string actionname;

			// Make proper name
			actionname = a.GetFullActionName(asm);

			// Bind delegate to action
			if(Exists(actionname))
				actions[actionname].BindEnd(d);
			else
				General.WriteLogLine("WARNING: Could not bind delegate for " + d.Method.Name + " to action \"" + a.ActionName + "\" (" + actionname + "), that action does not exist! Refer to, or edit Actions.cfg for all available application actions.");
		}

		// This unbinds all methods marked with this attribute
		internal void UnbindMethods(Type type)
		{
			// Unbind static methods
			UnbindMethods(null, type);
		}

		// This unbinds all methods marked with this attribute
		internal void UnbindMethods(object obj)
		{
			// Unbind instance methods
			UnbindMethods(obj, obj.GetType());
		}

		// This unbinds all methods marked with this attribute
		private void UnbindMethods(object obj, Type type)
		{
			MethodInfo[] methods;
			ActionAttribute[] attrs;
			ActionDelegate del;
			string actionname;

			if(obj == null)
				General.WriteLogLine("Unbinding static action methods for class " + type.Name + "...");
			else
				General.WriteLogLine("Unbinding action methods for " + type.Name + " object...");

			// Go for all methods on obj
			methods = type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
			foreach(MethodInfo m in methods)
			{
				// Check if the method has this attribute
				attrs = (ActionAttribute[])m.GetCustomAttributes(typeof(BeginActionAttribute), true);

				// Go for all attributes
				foreach(ActionAttribute a in attrs)
				{
					// Create a delegate for this method
					del = (ActionDelegate)Delegate.CreateDelegate(typeof(ActionDelegate), obj, m);

					// Make proper name
					actionname = a.GetFullActionName(type.Assembly);

					// Unbind method from action
					actions[actionname].UnbindBegin(del);
				}
				
				// Check if the method has this attribute
				attrs = (ActionAttribute[])m.GetCustomAttributes(typeof(EndActionAttribute), true);

				// Go for all attributes
				foreach(ActionAttribute a in attrs)
				{
					// Create a delegate for this method
					del = (ActionDelegate)Delegate.CreateDelegate(typeof(ActionDelegate), obj, m);

					// Make proper name
					actionname = a.GetFullActionName(type.Assembly);

					// Unbind method from action
					actions[actionname].UnbindEnd(del);
				}
			}
		}

		// This unbinds a delegate manually
		internal void UnbindBeginDelegate(Assembly asm, ActionDelegate d, BeginActionAttribute a)
		{
			string actionname;

			// Make proper name
			actionname = a.GetFullActionName(asm);

			// Unbind delegate to action
			actions[actionname].UnbindBegin(d);
		}

		// This unbinds a delegate manually
		internal void UnbindEndDelegate(Assembly asm, ActionDelegate d, EndActionAttribute a)
		{
			string actionname;

			// Make proper name
			actionname = a.GetFullActionName(asm);

			// Unbind delegate to action
			actions[actionname].UnbindEnd(d);
		}
		
		// This checks if a given action exists
		public bool Exists(string action)
		{
			return actions.ContainsKey(action);
		}

		// This returns a list of all actions
		internal Action[] GetAllActions()
		{
			Action[] list = new Action[actions.Count];
			actions.Values.CopyTo(list, 0);
			return list;
		}

		// This returns the specified action
		public Action GetActionByName(string fullname)
		{
			return actions[fullname];
		}
		
		// This saves the control settings
		internal void SaveSettings()
		{
			// Go for all actions
			foreach(KeyValuePair<string, Action> a in actions)
			{
				// Write to configuration
				General.Settings.WriteSetting("shortcuts." + a.Key, a.Value.ShortcutKey);
			}
		}

		// This invokes the Begin and End of the given action
		public bool InvokeAction(string actionname)
		{
			if(Exists(actionname))
			{
				actions[actionname].Invoke();
				return true;
			}
			else
			{
				return false;
			}
		}
		
		#endregion

		#region ================== Shortcut Keys
		
		// This applies default keys if they are not already in use
		internal void ApplyDefaultShortcutKeys()
		{
			// Find actions that have no key set
			foreach(KeyValuePair<string, Action> a in actions)
			{
				// Key set?
				if(a.Value.ShortcutKey == -1)
				{
					// Check if the default key is not already used
					bool keyused = false;
					foreach(KeyValuePair<string, Action> d in actions)
					{
						// Check if the keys are the same
						// Note that I use the mask of the source action to check if they match any combination
						if((d.Value.ShortcutKey & a.Value.ShortcutMask) == (a.Value.DefaultShortcutKey & a.Value.ShortcutMask))
						{
							// No party.
							keyused = true;
							break;
						}
					}
					
					// Party?
					if(!keyused)
					{
						// Apply the default key
						a.Value.SetShortcutKey(a.Value.DefaultShortcutKey);
					}
					else
					{
						// No party.
						a.Value.SetShortcutKey(0);
					}
				}
			}
		}
		
		// This checks if a given action is active
		public bool CheckActionActive(Assembly asm, string actionname)
		{
			if(asm == null) asm = General.ThisAssembly;
			
			// Find active action
			string fullname = asm.GetName().Name.ToLowerInvariant() + "_" + actionname;
			foreach(Action a in activeactions)
			{
				if(a.Name == fullname) return true;
			}
			
			// No such active action
			return false;
		}
		
		// Removes all shortcut keys
		internal void RemoveShortcutKeys()
		{
			// Clear all keys
			foreach(KeyValuePair<string, Action> a in actions)
				a.Value.SetShortcutKey(0);
		}
		
		// This notifies a key has been pressed
		internal void KeyPressed(int key)
		{
			int strippedkey = key & ~((int)Keys.Alt | (int)Keys.Shift | (int)Keys.Control);
			if((strippedkey == (int)Keys.ShiftKey) || (strippedkey == (int)Keys.ControlKey)) key = strippedkey;
			bool repeat = pressedkeys.Contains(strippedkey);
			
			// Update pressed keys
			if(!repeat) pressedkeys.Add(strippedkey);
			
			// Add action to active list
			Action[] acts = GetActionsByKey(key);
			foreach(Action a in acts) if(!activeactions.Contains(a)) activeactions.Add(a);

			// Invoke actions
			BeginActionByKey(key, repeat);
		}

		// This notifies a key has been released
		internal void KeyReleased(int key)
		{
			int strippedkey = key & ~((int)Keys.Alt | (int)Keys.Shift | (int)Keys.Control);
			List<Action> keepactions = new List<Action>();
			
			// Update pressed keys
			if(pressedkeys.Contains(strippedkey)) pressedkeys.Remove(strippedkey);

			// End actions that no longer match
			EndActiveActions();
		}

		// This releases all pressed keys
		internal void ReleaseAllKeys()
		{
			// Clear pressed keys
			pressedkeys.Clear();

			// End actions
			EndActiveActions();
		}
		
		// This updates the modifiers
		internal void UpdateModifiers(int mods)
		{
			// Update modifiers
			modifiers = mods;

			// End actions that no longer match
			EndActiveActions();
		}
		
		// This will call the associated actions for a keypress
		internal void BeginActionByKey(int key, bool repeated)
		{
			// Get all actions for which a begin is bound
			List<Action> boundactions = new List<Action>(actions.Count);
			foreach(KeyValuePair<string, Action> a in actions)
				if(a.Value.BeginBound) boundactions.Add(a.Value);
			
			// Go for all actions
			foreach(Action a in boundactions)
			{
				// This action is associated with this key?
				if(a.KeyMatches(key))
				{
					// Allowed to repeat?
					if(a.Repeat || !repeated)
					{
						// Invoke action
						a.Begin();
					}
					else
					{
						//General.WriteLogLine("Action \"" + a.Value.Name + "\" failed because it does not support repeating activation!");
					}
				}
			}
		}

		// This will end active actions for which the pressed keys do not match
		private void EndActiveActions()
		{
			bool listchanged;
			
			do
			{
				// Go for all active actions
				listchanged = false;
				for(int i = 0; i < activeactions.Count; i++)
				{
					Action a = activeactions[i];
					
					// Go for all pressed keys
					bool stillactive = false;
					foreach(int k in pressedkeys)
					{
						if((k == (int)Keys.ShiftKey) || (k == (int)Keys.ControlKey))
							stillactive |= a.KeyMatches(k);
						else
							stillactive |= a.KeyMatches(k | modifiers);
					}

					// End the action if no longer matches any of the keys
					if(!stillactive)
					{
						activeactions.RemoveAt(i);
						listchanged = true;
						a.End();
						break;
					}
				}
			}
			while(listchanged);
		}
		
		// This returns all action names for a given key
		public string[] GetActionNamesByKey(int key)
		{
			List<string> actionnames = new List<string>();
			
			// Go for all actions
			foreach(KeyValuePair<string, Action> a in actions)
			{
				// This action is associated with this key?
				if(a.Value.KeyMatches(key))
				{
					// List short name
					actionnames.Add(a.Value.ShortName);
				}
			}

			// Return result;
			return actionnames.ToArray();
		}

		// This returns all action names for a given key
		public Action[] GetActionsByKey(int key)
		{
			List<Action> actionnames = new List<Action>();

			// Go for all actions
			foreach(KeyValuePair<string, Action> a in actions)
			{
				// This action is associated with this key?
				if(a.Value.KeyMatches(key))
				{
					// List short name
					actionnames.Add(a.Value);
				}
			}

			// Return result;
			return actionnames.ToArray();
		}
		
		#endregion

		#region ================== Exclusive Invokation

		// This resets the exclusive request
		internal void ResetExclusiveRequest()
		{
			exclusiverequested = false;
		}
		
		/// <summary>
		/// This asks for exclusive invokation of the current BeginAction or EndAction.
		/// Returns true when successull, false when denied (already given to another caller)
		/// </summary>
		public bool RequestExclusiveInvokation()
		{
			if(exclusiverequested)
			{
				// Already given out
				return false;
			}
			else
			{
				// Success
				exclusiverequested = true;
				return true;
			}
		}
		
		#endregion
	}
}