using Gtk;
using Gdk;
using Pango;
using System;
using System.Text;

namespace MonoDevelop.Projects.Gui.Completion
{
	public class ListWindow: Gtk.Window
	{
		VScrollbar scrollbar;
		ListWidget list;
		IListDataProvider provider;
		Widget footer;
		VBox vbox;
		
		StringBuilder word;
		int curPos;
		
		[Flags]
		public enum KeyAction { Process=1, Ignore=2, CloseWindow=4, Complete=8 } 

		public ListWindow (): base (Gtk.WindowType.Popup)
		{
			vbox = new VBox ();
			
			HBox box = new HBox ();
			list = new ListWidget (this);
			list.SelectionChanged += new EventHandler (OnSelectionChanged);
			list.ScrollEvent += new ScrollEventHandler (OnScrolled);
			box.PackStart (list, true, true, 0);
			this.BorderWidth = 1;
			
			scrollbar = new VScrollbar (null);
			scrollbar.ValueChanged += new EventHandler (OnScrollChanged); 
			box.PackStart (scrollbar, false, false, 0);
			
			vbox.PackStart (box, true, true, 0);
			Add (vbox);
			
			this.TypeHint = WindowTypeHint.Menu;
		}
		
		public new void Show ()
		{
			this.ShowAll ();
			Reset ();
		}
		
		public void ShowFooter (Widget w)
		{
			HideFooter ();
			vbox.PackStart (w, false, false, 0);
			footer = w;
		}
		
		public void HideFooter ()
		{
			if (footer != null) {
				vbox.Remove (footer);
				footer = null;
			}
		}
		
		public void Reset ()
		{
			word = new StringBuilder ();
			curPos = 0;
			list.Reset ();
			if (provider == null)
				return;

			if (list.VisibleRows >= provider.ItemCount) {
				this.scrollbar.Hide();
			}
			else {
				scrollbar.Adjustment.Lower = 0;
				scrollbar.Adjustment.Upper = provider.ItemCount - list.VisibleRows;
				scrollbar.Adjustment.PageIncrement = list.VisibleRows - 1;
				scrollbar.Adjustment.StepIncrement = 1;
			}

			this.Resize(this.list.WidthRequest, this.list.HeightRequest);
		}
		
		public IListDataProvider DataProvider
		{
			get { return provider; }
			set { provider = value; }
		}
		
		public string CompleteWord
		{
			get { 
				if (list.Selection != -1)
					return provider.GetCompletionText (list.Selection);
				else
					return null;
			}
		}
		
		public int Selection {
			get { return list.Selection; }
		}
		
		public string PartialWord
		{
			get { return word.ToString (); }
			set
			{
				string newword = value;
				if (newword.Trim ().Length == 0)
					return;
				
				word = new StringBuilder (newword);
				curPos = newword.Length;
				UpdateWordSelection ();
			}
		}
		
		public bool IsUniqueMatch
		{
			get
			{
				int pos = list.Selection + 1;
				if (provider.ItemCount > pos && provider.GetCompletionText (pos).ToLower ().StartsWith (PartialWord.ToLower ()) || !(provider.GetCompletionText (list.Selection).ToLower ().StartsWith (PartialWord.ToLower ())))
					return false;
				
				return true;	
			}
		}
		
		protected ListWidget List
		{
			get { return list; }
		}
		
		public KeyAction ProcessKey (Gdk.Key key, Gdk.ModifierType modifier)
		{
			switch (key)
			{
				case Gdk.Key.Up:
					list.Selection --;
					return KeyAction.Ignore;
					
				case Gdk.Key.Down:
					list.Selection ++;
					return KeyAction.Ignore;
					
				case Gdk.Key.Page_Up:
					list.Selection -= list.VisibleRows - 1;
					return KeyAction.Ignore;
					
				case Gdk.Key.Page_Down:
					list.Selection += list.VisibleRows - 1;
					return KeyAction.Ignore;
					
				case Gdk.Key.Left:
					//if (curPos == 0) return KeyAction.CloseWindow | KeyAction.Process;
					//curPos--;
					return KeyAction.Process;
					
				case Gdk.Key.BackSpace:
					if (curPos == 0) return KeyAction.CloseWindow | KeyAction.Process;
					curPos--;
					word.Remove (curPos, 1);
					UpdateWordSelection ();
					return KeyAction.Process;
					
				case Gdk.Key.Right:
					//if (curPos == word.Length) return KeyAction.CloseWindow | KeyAction.Process;
					//curPos++;
					return KeyAction.Process;
					
				case Gdk.Key.Tab:
				case Gdk.Key.Return:
				case Gdk.Key.ISO_Enter:
				case Gdk.Key.Key_3270_Enter:
				case Gdk.Key.KP_Enter:
					return KeyAction.Complete | KeyAction.Ignore | KeyAction.CloseWindow;
					
				case Gdk.Key.Escape:
					return KeyAction.CloseWindow | KeyAction.Ignore;
					
				case Gdk.Key.Home:
				case Gdk.Key.End:
					return KeyAction.CloseWindow | KeyAction.Process;
					
				case Gdk.Key.Control_L:
				case Gdk.Key.Control_R:
				case Gdk.Key.Alt_L:
				case Gdk.Key.Alt_R:
				case Gdk.Key.Shift_L:
				case Gdk.Key.Shift_R:
				case Gdk.Key.ISO_Level3_Shift:	// AltGr
					return KeyAction.Process;
			}
			
			char c = (char)(uint)key;
			
			if (System.Char.IsLetterOrDigit (c) || c == '_') {
				word.Insert (curPos++, c);
				UpdateWordSelection ();
				return KeyAction.Process;
			}
			else if ((System.Char.IsPunctuation (c) || c == ' ') && !list.SelectionDisabled) {
				return KeyAction.Complete | KeyAction.Process | KeyAction.CloseWindow;
			}
			
			return KeyAction.CloseWindow | KeyAction.Process;
		}
		
		void UpdateWordSelection ()
		{
			SelectEntry (word.ToString ());
		}
		
		public void SelectEntry (string s)
		{
			int max = (provider == null ? 0 : provider.ItemCount);
			
			int bestMatch = -1;
			for (int n=0; n<max; n++) 
			{
				string txt = provider.GetCompletionText (n);
				if (txt.StartsWith (s)) {
					list.Selection = n;
					return;
				}
				else if (bestMatch == -1 && txt.ToLower().StartsWith (s.ToLower()))
					bestMatch = n;
			}
			
			if (bestMatch != -1) {
				list.Selection = bestMatch;
				return;
			}
			
			list.SelectionDisabled = true;
		}
		
		void OnScrollChanged (object o, EventArgs args)
		{
			list.Page = (int) scrollbar.Value;
		}

		void OnScrolled (object o, ScrollEventArgs args)
		{
			if (args.Event.Direction == Gdk.ScrollDirection.Up)
				scrollbar.Value --;
			else if (args.Event.Direction == Gdk.ScrollDirection.Down)
				scrollbar.Value ++;
		}
		
		void OnSelectionChanged (object o, EventArgs args)
		{
			scrollbar.Value = list.Page;
			OnSelectionChanged ();
		}
		
		protected virtual void OnSelectionChanged ()
		{
		}
		
		protected override bool OnExposeEvent (Gdk.EventExpose args)
		{
			base.OnExposeEvent (args);
			
			int winWidth, winHeight;
			this.GetSize (out winWidth, out winHeight);
			this.GdkWindow.DrawRectangle (this.Style.ForegroundGC (StateType.Insensitive), false, 0, 0, winWidth-1, winHeight-1);
			return false;
		}		
	}

	public class ListWidget: Gtk.DrawingArea
	{
		int margin = 0;
		int padding = 4;
		int listWidth = 300;
		
		Pango.Layout layout;
		ListWindow win;
		int selection = 0;
		int page = 0;
		int visibleRows = -1;
		int rowHeight;
		bool buttonPressed;
		bool disableSelection;

		public event EventHandler SelectionChanged;
				
		public ListWidget (ListWindow win)
		{
			this.win = win;
			this.Events = EventMask.ButtonPressMask | EventMask.ButtonReleaseMask | EventMask.PointerMotionMask; 
		}
		
		public void Reset ()
		{
			if (win.DataProvider == null) {
				selection = -1;
				return;
			}
			
			if (win.DataProvider.ItemCount == 0)
				selection = -1;
			else
				selection = 0;

			page = 0;
			disableSelection = false;
			UpdateStyle ();
			QueueDraw ();
			if (SelectionChanged != null) SelectionChanged (this, EventArgs.Empty);
		}
		
		public int Selection
		{
			get {
				return selection;
			}
			
			set {
				if (value < 0)
					value = 0;
				if (value >= win.DataProvider.ItemCount)
					value = win.DataProvider.ItemCount - 1;
					
				if (value != selection) 
				{
					selection = value;
						
					if (selection < page)
						page = selection;
					else if (selection >= page + VisibleRows) {
						page = selection - VisibleRows + 1;
						if (page < 0) page = 0;
					}
					
					if (SelectionChanged != null) SelectionChanged (this, EventArgs.Empty);
				}
				
				if (disableSelection)
					disableSelection = false;

				this.QueueDraw ();
			}
		}
		
		public bool SelectionDisabled
		{
			get { return disableSelection; }
			
			set {
				disableSelection = value; 
				this.QueueDraw ();
			}
		}
		
		public int Page
		{
			get { 
				return page; 
			}
			
			set {
				page = value;
				this.QueueDraw ();
			}
		}
		
		protected override bool OnButtonPressEvent (EventButton e)
		{
			Selection = GetRowByPosition ((int) e.Y);
			buttonPressed = true;
			return base.OnButtonPressEvent (e);
		}
		
		protected override bool OnButtonReleaseEvent (EventButton e)
		{
			buttonPressed = false;
			return base.OnButtonReleaseEvent (e);
		}
		
		protected override bool OnMotionNotifyEvent (EventMotion e)
		{
			if (!buttonPressed)
				return base.OnMotionNotifyEvent (e);
			
			int winWidth, winHeight;
			this.GdkWindow.GetSize (out winWidth, out winHeight);
			
	/*		int ypos = (int) e.Y;
			if (ypos < 0) {
			}
			else if (ypos >= winHeight) {
			}
			else
	*/			Selection = GetRowByPosition ((int) e.Y);
			
			return true;
		}

		protected override bool OnExposeEvent (Gdk.EventExpose args)
		{
			base.OnExposeEvent (args);
			DrawList ();
	  		return true;
		}

		void DrawList ()
		{
			int winWidth, winHeight;
			this.GdkWindow.GetSize (out winWidth, out winHeight);
			
			int ypos = margin;
			int lineWidth = winWidth - margin*2;
			int xpos = margin + padding;
				
			int n = 0;
			while (ypos < winHeight - margin && (page + n) < win.DataProvider.ItemCount)
			{
				layout.SetText (win.DataProvider.GetText (page + n));
				Gdk.Pixbuf icon = win.DataProvider.GetIcon (page + n);
				
				int wi, he, typos, iypos;
				layout.GetPixelSize (out wi, out he);
				typos = he < rowHeight ? ypos + (rowHeight - he) / 2 : ypos;
				iypos = icon.Height < rowHeight ? ypos + (rowHeight - icon.Height) / 2 : ypos;
				
				if (page + n == selection) {
					if (!disableSelection) {
						this.GdkWindow.DrawRectangle (this.Style.BaseGC (StateType.Selected), true, margin, ypos, lineWidth, he + padding);
						this.GdkWindow.DrawLayout (this.Style.TextGC (StateType.Selected), xpos + icon.Width + 2, typos, layout);
					}
					else {
						this.GdkWindow.DrawRectangle (this.Style.BaseGC (StateType.Selected), false, margin, ypos, lineWidth, he + padding);
						this.GdkWindow.DrawLayout (this.Style.TextGC (StateType.Normal), xpos + icon.Width + 2, typos, layout);
					}
				}
				else
					this.GdkWindow.DrawLayout (this.Style.TextGC (StateType.Normal), xpos + icon.Width + 2, typos, layout);
					
				this.GdkWindow.DrawPixbuf (this.Style.ForegroundGC (StateType.Normal), icon, 0, 0, xpos, iypos, icon.Width, icon.Height, Gdk.RgbDither.None, 0, 0);
				
				ypos += rowHeight;
				n++;
			}
		}
		
		int GetRowByPosition (int ypos)
		{
			if (visibleRows == -1) CalcVisibleRows ();
			return page + (ypos-margin) / rowHeight;
		}
		
		public Gdk.Rectangle GetRowArea (int row)
		{
			row -= page;
			int winWidth, winHeight;
			this.GdkWindow.GetSize (out winWidth, out winHeight);
			
			return new Gdk.Rectangle (margin, margin + rowHeight * row, winWidth, rowHeight);
		}
		
		public int VisibleRows
		{
			get {
				if (visibleRows == -1) CalcVisibleRows ();
				return visibleRows;
			}
		}
		
		void CalcVisibleRows ()
		{
			int winHeight = 200;
			int lvWidth, lvHeight;
			int rowWidth;
			
			this.GetSizeRequest (out lvWidth, out lvHeight);

			layout.GetPixelSize (out rowWidth, out rowHeight);
			rowHeight += padding;
			visibleRows = (winHeight + padding - margin * 2) / rowHeight;
			
			int newHeight;

			if (this.win.DataProvider.ItemCount > this.visibleRows)
				newHeight = (rowHeight * visibleRows) + margin * 2;
			else
				newHeight = (rowHeight * this.win.DataProvider.ItemCount) + margin * 2;
			
			if (lvWidth != listWidth || lvHeight != newHeight)
				this.SetSizeRequest (listWidth, newHeight);
		} 

		protected override void OnRealized ()
		{
			base.OnRealized ();
			UpdateStyle ();
		}
		
		void UpdateStyle ()
		{
			this.GdkWindow.Background = this.Style.Base (StateType.Normal);
			layout = new Pango.Layout (this.PangoContext);
			layout.Wrap = Pango.WrapMode.Char;
			
			FontDescription des = this.Style.FontDescription.Copy();
			layout.FontDescription = des;
			CalcVisibleRows ();
		}
	}

	public interface IListDataProvider
	{
		int ItemCount { get; }
		string GetText (int n);
		string GetCompletionText (int n);
		Gdk.Pixbuf GetIcon (int n);
	}
}

