/*
 * $Id$
 *
 * Copyright (C) 2004 Todd Berman <tberman@off.net>
 * Copyright (C) 2004 Jeroen Zwartepoorte <jeroen@xs4all.nl>
 * Copyright (C) 2005 John Luke <john.luke@gmail.com>
 *
 * based on work by:
 * Copyright (C) 2002 Gustavo Giráldez <gustavo.giraldez@gmx.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
using System;
using System.Collections;
using Gtk;

namespace Gdl
{
	public class DockMaster
	{
		private Hashtable dockObjects = new Hashtable ();
		private ArrayList toplevelDocks = new ArrayList ();
		private DockObject controller = null;
		private int dockNumber = 1;

		// for naming nameless manual objects
		private int number = 1;
		private string defaultTitle;

		private Gdk.GC rootXorGC;
		private bool rectDrawn;
		private Dock rectOwner;

		private DockRequest request;

		// hashes to quickly calculate the overall locked status:
		// if size(unlocked_items) == 0 then locked = 1
		// else if size(locked_items) == 0 then locked = 0
		// else locked = -1
		private Hashtable lockedItems = new Hashtable ();
		private Hashtable unlockedItems = new Hashtable ();

		public event EventHandler LayoutChanged;
		internal event EventHandler NotifyLocked;

		public DockMaster ()
		{
		}

		public string DefaultTitle {
			get {
				return defaultTitle;
			}
			set {
				defaultTitle = value;
			}
		}

		public int DockNumber {
			get {
				return dockNumber;
			}
			set {
				dockNumber = value;
			}
		}

		public ICollection DockObjects {
			get {
				return dockObjects.Values;
			}
		}

		// 1  = all the dock items bound to the master are locked
		// 0  = all the dock items bound to the master are unlocked
		// -1 = inconsistent
		public int Locked {
			get {
				return ComputeLocked ();
			}
			set {
				if (value >= 0)
					LockUnlock (value > 0);
				EmitNotifyLocked ();
			}
		}

		public ArrayList TopLevelDocks {
			get {
				return toplevelDocks;
			}
		}

		protected void ForeachLockUnlock (DockItem item, bool locked)
		{
			item.Locked = locked;
			if (item.IsCompound) {
				foreach (Widget w in item.Children) {
					DockItem i = w as DockItem;
					if (i != null)
						ForeachLockUnlock (i, locked);
				}
			}
		}

		public void LockUnlock (bool locked)
		{
			foreach (Dock dock in toplevelDocks) {
				if (dock.Root != null && dock.Root is DockItem)
					ForeachLockUnlock ((DockItem)dock.Root, locked);
			}

			// FIXME: not sure about which list to foreach here
			// just to be sure hidden items are set too
			foreach (Widget w in toplevelDocks) {
				DockItem i = w as DockItem;
				if (i != null)
					ForeachLockUnlock (i, locked);
			}
		}

		public void Add (DockObject obj)
		{
			if (obj == null)
				return;

			if (!obj.IsAutomatic) {
				/* create a name for the object if it doesn't have one */
				if (obj.Name == null)
					obj.Name = "__dock_" + number++;

				/* add the object to our hash list */
				if (dockObjects.Contains (obj.Name))
					Console.WriteLine ("Unable to add object, name \"{0}\" taken", obj.Name);
				else
					dockObjects.Add (obj.Name, obj);
			}

			if (obj is Dock) {
				/* if this is the first toplevel we are adding, name it controller */
				if (toplevelDocks.Count == 0)
					controller = obj;

				/* add dock to the toplevel list */
				if (((Dock)obj).Floating)
					toplevelDocks.Insert (0, obj);
				else
					toplevelDocks.Add (obj);

				/* we are interested in the dock request this toplevel
				 * receives to update the layout */
				obj.Docked += new DockedHandler (OnItemDocked);
			} else if (obj is DockItem) {
				DockItem item = obj as DockItem;

				/* we need to connect the item's events */
				item.Detached += new DetachedHandler (OnItemDetached);
				item.Docked += new DockedHandler (OnItemDocked);
				item.DockItemDragBegin += new DockItemDragBeginHandler (OnDragBegin);
				item.DockItemMotion += new DockItemMotionHandler (OnDragMotion);
				item.DockItemDragEnd += new DockItemDragEndHandler (OnDragEnd);

				/* register to "locked" notification if the item has a grip,
				 * and add the item to the corresponding hash */
				item.PropertyChanged += new PropertyChangedHandler (OnItemPropertyChanged);

				/* post a layout_changed emission if the item is not automatic
				 * (since it should be added to the items model) */
				if (!item.IsAutomatic) {
					EmitLayoutChangedEvent ();
				}
			}
		}

		public void Remove (DockObject obj)
		{
			if (obj == null)
				return;

			// remove from locked/unlocked hashes and property change if that's the case
			if (obj is DockItem && ((DockItem)obj).HasGrip) {
				int locked = Locked;
				if (lockedItems.Contains (obj)) {
					lockedItems.Remove (obj);
					if (Locked != locked)
						EmitNotifyLocked ();
				}
				if (unlockedItems.Contains (obj)) {
					unlockedItems.Remove (obj);
					if (Locked != locked)
						EmitNotifyLocked ();
				}
			}

			if (obj is Dock) {
				toplevelDocks.Remove (obj);
				obj.Docked -= new DockedHandler (OnItemDocked);

				if (obj == controller) {
					DockObject newController = null;

					// now find some other non-automatic toplevel to use as a
					// new controller.  start from the last dock, since it's
					// probably a non-floating and manual
					ArrayList reversed = toplevelDocks;
					reversed.Reverse ();

					foreach (DockObject item in reversed) {
						if (!item.IsAutomatic) {
							newController = item;
							break;
						}
					}

					if (newController != null) {
						controller = newController;
					} else {
						// no controller, no master
						controller = null;
					}
				}
			}

			// disconnect the signals
			if (obj is DockItem) {
				DockItem item = obj as DockItem;
				item.Detached -= new DetachedHandler (OnItemDetached);
				item.Docked -= new DockedHandler (OnItemDocked);
				item.DockItemDragBegin -= new DockItemDragBeginHandler (OnDragBegin);
				item.DockItemMotion -= new DockItemMotionHandler (OnDragMotion);
				item.DockItemDragEnd -= new DockItemDragEndHandler (OnDragEnd);
				item.PropertyChanged -= new PropertyChangedHandler (OnItemPropertyChanged);
			}

			// remove the object from the hash if it is there
			if (obj.Name != null && dockObjects.Contains (obj.Name))
				dockObjects.Remove (obj.Name);

			/* post a layout_changed emission if the item is not automatic
			 * (since it should be removed from the items model) */
			if (!obj.IsAutomatic)
				EmitLayoutChangedEvent ();
		}

		public DockObject GetObject (string name)
		{
			if (name == null)
				return null;
			return (DockObject)dockObjects[name];
		}

		public DockObject Controller {
			get { return controller; }
			set {
				if (value != null) {
					if (value.IsAutomatic)
						Console.WriteLine ("New controller is automatic, only manual dock objects should be named controller");
					// check that the controller is in the toplevel list
					if (!toplevelDocks.Contains (value))
						Add (value);
					controller = value;
				} else {
					// no controller, no master
					controller = null;
				}
			}
		}

		internal void EmitLayoutChangedEvent ()
		{
			if (LayoutChanged != null)
				LayoutChanged (this, EventArgs.Empty);
		}

		private void OnItemDetached (object o, DetachedArgs args)
		{
			DockItem obj = o as DockItem;
			if (!obj.InReflow && !obj.IsAutomatic)
				EmitLayoutChangedEvent ();
		}

		private void OnItemDocked (object o, DockedArgs args)
		{
			DockItem requestor = args.Requestor as DockItem;

			// here we are in fact interested in the requestor, since it's
			// assumed that object will not change its visibility... for the
			// requestor, however, could mean that it's being shown
			if (!requestor.InReflow && !requestor.IsAutomatic)
				EmitLayoutChangedEvent ();
		}

		private void OnItemPropertyChanged (object o, string name)
		{
			DockItem item = o as DockItem;
			int locked = ComputeLocked ();
			bool item_locked = item.Locked;

			if (item_locked) {
				if (unlockedItems.ContainsKey (item))
					unlockedItems.Remove (item);
				if (!lockedItems.ContainsKey (item))
					lockedItems.Add (item, 1);
			}
			else {
				if (lockedItems.ContainsKey (item))
					lockedItems.Remove (item);
				if (!unlockedItems.ContainsKey (item))
					unlockedItems.Add (item, 1);
			}

			if (ComputeLocked () != locked)
				EmitNotifyLocked ();
		}

		private int ComputeLocked ()
		{
			if (unlockedItems.Count == 0)
				return 1;
			else if (lockedItems.Count == 0)
				return 0;
			else
				return -1;
		}

		private void OnDragBegin (DockItem item)
		{
			/* Set the target to itself so it won't go floating with just a click. */
			request = new DockRequest ();
			request.Applicant = item;
			request.Target = item;
			request.Position = DockPlacement.Floating;
			request.Extra = IntPtr.Zero;

			rectDrawn = false;
			rectOwner = null;
		}

		private void OnDragEnd (DockItem item, bool cancelled)
		{
			if (item != request.Applicant)  {
				Console.WriteLine ("Dragged item is not the same as the one we started with");
				return;
			}

			/* Erase previously drawn rectangle */
			if (rectDrawn)
				XorRect ();

			/* cancel conditions */
			if (cancelled || request.Applicant == request.Target)
				return;

			// dock object to the requested position
			request.Target.Dock (request.Applicant,
					     request.Position,
					     request.Extra);

			EmitLayoutChangedEvent ();
		}

		private void OnDragMotion (DockItem item, int rootX, int rootY)
		{
			Dock dock = null;
			int winX, winY;
			int x, y;
			bool mayDock = false;
			DockRequest myRequest = new DockRequest (request);

			if (item != request.Applicant)  {
				Console.WriteLine ("Dragged item is not the same as the one we started with");
				return;
			}

			/* first look under the pointer */
			Gdk.Window window = Gdk.Window.AtPointer (out winX, out winY);
			if (window != null && window.UserData != IntPtr.Zero) {
				/* ok, now get the widget who owns that window and see if we can
				   get to a Dock by walking up the hierarchy */
				Widget widget = GLib.Object.GetObject (window.UserData, false) as Widget;
				while (widget != null && (!(widget is Dock) ||
				       (widget is DockObject && ((DockObject)widget).Master != this)))
						widget = widget.Parent;

				if (widget != null) {
					int winW, winH, depth;

					/* verify that the pointer is still in that dock
					   (the user could have moved it) */
					widget.GdkWindow.GetGeometry (out winX, out winY,
								      out winW, out winH,
								      out depth);
					widget.GdkWindow.GetOrigin (out winX, out winY);
					if (rootX >= winX && rootX < winX + winW &&
					    rootY >= winY && rootY < winY + winH)
						dock = widget as Dock;
				}
			}

			if (dock != null) {
				/* translate root coordinates into dock object coordinates
				   (i.e. widget coordinates) */
				dock.GdkWindow.GetOrigin (out winX, out winY);
				x = rootX - winX;
				y = rootY - winY;
				mayDock = dock.OnDockRequest (x, y, ref myRequest);
			} else {
				/* try to dock the item in all the docks in the ring in turn */
				foreach (Dock topDock in toplevelDocks) {
					if (topDock.GdkWindow == null)
						Console.WriteLine ("Dock has no GdkWindow: {0}, {1}", topDock.Name, topDock);
					/* translate root coordinates into dock object
					   coordinates (i.e. widget coordinates) */
					topDock.GdkWindow.GetOrigin (out winX, out winY);
					x = rootX - winX;
					y = rootY - winY;
					mayDock = topDock.OnDockRequest (x, y, ref myRequest);
					if (mayDock)
						break;
				}
			}

			if (!mayDock) {
				dock = null;

				myRequest.Target = Dock.GetTopLevel (item);
				myRequest.Position = DockPlacement.Floating;
				Requisition preferredSize = item.PreferredSize;
				myRequest.Width = preferredSize.Width;
				myRequest.Height = preferredSize.Height;
				myRequest.X = rootX - item.DragOffX;
				myRequest.Y = rootY - item.DragOffY;

				Gdk.Rectangle rect = new Gdk.Rectangle (myRequest.X,
									myRequest.Y,
									myRequest.Width,
									myRequest.Height);

				// setup extra docking information
				myRequest.Extra = rect;
			}

			if (!(myRequest.X == request.X &&
			      myRequest.Y == request.Y &&
			      myRequest.Width == request.Width &&
			      myRequest.Height == request.Height &&
			      dock == rectOwner)) {

				/* erase the previous rectangle */
				if (rectDrawn)
					XorRect ();
			}

			// set the new values
			request = myRequest;
			rectOwner = dock;

			/* draw the previous rectangle */
			if (!rectDrawn)
				XorRect ();
		}

		private void XorRect ()
		{
			rectDrawn = !rectDrawn;

			if (rectOwner != null) {
				Gdk.Rectangle rect = new Gdk.Rectangle (request.X,
								        request.Y,
								        request.Width,
								        request.Height);
				rectOwner.XorRect (rect);
				return;
			}

			Gdk.Window window = Gdk.Global.DefaultRootWindow;

			if (rootXorGC == null) {
				Gdk.GCValues values = new Gdk.GCValues ();
				values.Function = Gdk.Function.Invert;
				values.SubwindowMode = Gdk.SubwindowMode.IncludeInferiors;

				rootXorGC = new Gdk.GC (window);
				rootXorGC.SetValues (values, Gdk.GCValuesMask.Function |
						     Gdk.GCValuesMask.Subwindow);
			}

			rootXorGC.SetLineAttributes (1, Gdk.LineStyle.OnOffDash,
						     Gdk.CapStyle.NotLast,
						     Gdk.JoinStyle.Bevel);

			rootXorGC.SetDashes (1, new sbyte[] {1, 1}, 2);

			window.DrawRectangle (rootXorGC, false, request.X, request.Y,
					      request.Width, request.Height);

			rootXorGC.SetDashes (0, new sbyte[] {1, 1}, 2);

			window.DrawRectangle (rootXorGC, false, request.X + 1,
					      request.Y + 1, request.Width - 2,
					      request.Height - 2);
		}

		void EmitNotifyLocked ()
		{
			if (NotifyLocked != null)
				NotifyLocked (this, EventArgs.Empty);
		}
	}
}
