// 
// WindowBackend.cs
//  
// Author:
//       Carlos Alberto Cortez <calberto.cortez@gmail.com>
//       Luís Reis <luiscubal@gmail.com>
// 
// Copyright (c) 2011 Carlos Alberto Cortez
// Copyright (c) 2012 Luís Reis
// 
// 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using SW = System.Windows;

using Xwt.Backends;

namespace Xwt.WPFBackend
{
	public class WindowBackend : WindowFrameBackend, IWindowBackend, IPopupWindowBackend, IUtilityWindowBackend
	{
		protected Grid rootPanel;
		public System.Windows.Controls.Menu mainMenu;
		MenuBackend mainMenuBackend;
		FrameworkElement widget;
		DockPanel contentBox;
		WindowStyle defaultWindowStyle = WindowStyle.SingleBorderWindow;

		public int Id { get; set; }

		public WindowBackend ()
		{
			base.Window = new WpfWindow ();
			Window.UseLayoutRounding = true;
			rootPanel = CreateMainGrid ();
			contentBox = new DockPanel ();

			Window.Content = rootPanel;
			Grid.SetColumn (contentBox, 0);
			Grid.SetRow (contentBox, 1);
			rootPanel.Children.Add (contentBox);
		}

		new WpfWindow Window
		{
			get { return (WpfWindow)base.Window; }
		}

		bool initialized;

		public override void Initialize ()
		{
			if (!initialized) {
				base.Initialize();
				Window.Frontend = (Window)Frontend;
				if (Frontend is UtilityWindow)
					defaultWindowStyle = WindowStyle.ToolWindow;
				Decorated = true;
				initialized = true;
			}
		}

		public void Initialize(IWindowFrameEventSink sink, PopupWindow.PopupType type)
		{
			if (!initialized) {
				base.Initialize();
				Window.Frontend = (Window)Frontend;
				defaultWindowStyle = WindowStyle.ToolWindow;
				Decorated = false;
				initialized = true;
			}
		}

		// A Grid with a single column, and two rows (menu and child control).
		static Grid CreateMainGrid ()
		{
			var grid = new Grid ();

			grid.ColumnDefinitions.Add (new ColumnDefinition ());
			
			var menuRow = new RowDefinition () { Height = GridLength.Auto }; // Only take the menu requested space.
			var contentRow = new RowDefinition (); // Take all the remaining space (default).

			grid.RowDefinitions.Add (menuRow);
			grid.RowDefinitions.Add (contentRow);

			return grid;
		}

		public override bool Decorated
		{
			get { return base.Decorated; }
			set
			{
				base.Decorated = value;
				if (value)
					Window.WindowStyle = defaultWindowStyle;
				Window.AllowsTransparency = Window.WindowStyle == WindowStyle.None && BackgroundColor.Alpha < 1.0;
			}
		}

		public override bool HasMenu {
			get { return mainMenu != null; }
		}

		public override Rectangle Bounds
		{
			get
			{
				return Window.ClientBounds;
			}
			set
			{
				Window.ClientBounds = value;
				Context.InvokeUserCode (delegate
				{
					EventSink.OnBoundsChanged (Bounds);
				});
			}
		}

		public void SetChild (IWidgetBackend child)
		{
			if (widget != null) {
				contentBox.Children.Remove (widget);
				widget.SizeChanged -= ChildSizeChanged;
			}
			widget = ((IWpfWidgetBackend)child).Widget;
			contentBox.Children.Add (widget);

			// This event is subscribed to ensure that the content of the
			// widget is reallocated when the widget gets a new size. This
			// is not a problem when setting the child before showing the
			// window, but it may be a problem if the window is already visible.
			widget.SizeChanged += ChildSizeChanged;

			if (child != null)
				UpdateChildPlacement (child);
		}

		public virtual void UpdateChildPlacement (IWidgetBackend childBackend)
		{
			WidgetBackend.SetChildPlacement (childBackend);
		}

		void ChildSizeChanged (object o, SizeChangedEventArgs args)
		{
			((Window)Frontend).Content.Surface.Reallocate ();
		}

		public void SetMainMenu (IMenuBackend menu)
		{
			if (mainMenu != null) {
				mainMenuBackend.ParentWindow = null;
				rootPanel.Children.Remove (mainMenu);
			}
		
			if (menu == null) {
				mainMenu = null;
				mainMenuBackend = null;
				return;
			}

			var menuBackend = (MenuBackend)menu;

			var m = new System.Windows.Controls.Menu ();
			foreach (var item in menuBackend.Items)
				m.Items.Add (item.Item);

			Grid.SetColumn (m, 0);
			Grid.SetRow (m, 0);
			rootPanel.Children.Add (m);

			mainMenu = m;
			mainMenuBackend = menuBackend;
			mainMenuBackend.ParentWindow = this;
		}

		public void SetPadding (double left, double top, double right, double bottom)
		{
			contentBox.Margin = new Thickness (left, top, right, bottom);
		}

		public virtual void SetMinSize (Size s)
		{
			Window.SetMinSize (s);
		}

		public virtual void GetMetrics (out Size minSize, out Size decorationSize)
		{
			minSize = decorationSize = Size.Zero;
			if (mainMenu != null) {
				mainMenu.InvalidateMeasure ();
				mainMenu.Measure (new System.Windows.Size (double.PositiveInfinity, double.PositiveInfinity));
				var h = mainMenu.DesiredSize.Height;
				decorationSize.Height = h;
			}
		}

		protected override void OnResizeModeChanged ()
		{
			Window.ResetBorderSize ();
		}

		public Xwt.Drawing.Color BackgroundColor {
			get { return Window.Background.ToXwtColor (); }
			set {
				Window.Background = ResPool.GetSolidBrush (value);
				Window.AllowsTransparency = Window.WindowStyle == WindowStyle.None && value.Alpha < 1.0;
			}
		}
	}

	class WpfWindow : System.Windows.Window
	{
		public Window Frontend;

		bool borderCalculated;
		WidgetSpacing frameBorder;
		Size minSizeRequested;
		double initialX, initialY;

		public WpfWindow ()
		{
			// We initially use WidthAndHeight mode since we need to calculate the size
			// of the window borders
			SizeToContent = System.Windows.SizeToContent.WidthAndHeight;
		}

		public void ResetBorderSize ()
		{
			// Called when the size of the border may have changed
			if (borderCalculated) {
				var r = ClientBounds;
				initialX = Left + frameBorder.Left;
				initialY = Top + frameBorder.Top;
				borderCalculated = false;
				ClientBounds = r;
			}
		}

		public void SetMinSize (Size size)
		{
			if (borderCalculated) {
				if (size.Width != -1)
					MinWidth = size.Width + frameBorder.HorizontalSpacing;
				if (size.Height != -1)
					MinHeight = size.Height + frameBorder.VerticalSpacing;
			}
			else
				minSizeRequested = size;
		}

		public Rectangle ClientBounds
		{
			get
			{
				var c = (FrameworkElement)Content;
				var w = double.IsNaN (c.Width) ? c.ActualWidth : c.Width;
				var h = double.IsNaN (c.Height) ? c.ActualHeight : c.Height;
				if (PresentationSource.FromVisual (c) == null)
					return new Rectangle (initialX, initialY, w, h);
				else {
					var p = c.PointToScreenDpiAware (new SW.Point (0, 0));
					return new Rectangle (p.X, p.Y, w, h);
				}
			}
			set
			{
				// Don't use WindowFrameBackend.ToNonClientRect to calculate the client area because that method is not reliable (see comment in ToNonClientRect).
				// Instead, we use our own border size calculation method, which is:
				// 1) Set the Width and Height of the widget to the desired client rect, and set SizeToContent property to WidthAndHeight
				// 2) The window will resize itself to fit the content
				// 3) When the size of the window is set (OnRenderSizeChanged event), calculate the border by comparing the screen position of
				//    the root content with the screen position of the window.

				if (borderCalculated) {
					// Border size already calculated. Just do the math.
					Left = value.Left - frameBorder.Left;
					Top = value.Top - frameBorder.Top;
					Width = value.Width + frameBorder.HorizontalSpacing;
					Height = value.Height + frameBorder.VerticalSpacing;
				}
				else {
					// store the required size and position and enable SizeToContent mode. When the window size is set, we'll calculate the border size.
					var c = (FrameworkElement)Content;
					initialX = value.Left;
					initialY = value.Top;
					c.Width = value.Width;
					c.Height = value.Height;
					SizeToContent = System.Windows.SizeToContent.WidthAndHeight;
				}
			}
		}

		protected override System.Windows.Size ArrangeOverride (System.Windows.Size arrangeBounds)
		{
			var s = base.ArrangeOverride (arrangeBounds);
			if (Frontend.Content != null)
				Frontend.Content.Surface.Reallocate ();
			return s;
		}

		protected override void OnRenderSizeChanged (SizeChangedInfo sizeInfo)
		{
			// Once the physical size of the window has been set we can calculate
			// the size of the borders, which will be used for further client/non client
			// area coordinate conversions
			CalcBorderSize (sizeInfo.NewSize.Width, sizeInfo.NewSize.Height);
			base.OnRenderSizeChanged (sizeInfo);
		}

		void CalcBorderSize (double windowWidth, double windowHeight)
		{
			if (borderCalculated)
				return;

			var c = (FrameworkElement)Content;
			var p = c.PointToScreenDpiAware (new SW.Point (0, 0));
			var left = p.X - Left;
			var top = p.Y - Top;
			frameBorder = new WidgetSpacing (left, top, windowWidth - c.ActualWidth - left, windowHeight - c.ActualHeight - top);
			borderCalculated = true;
			Left = initialX - left;
			Top = initialY - top;
			SetMinSize (minSizeRequested);

			// Border size calculation done and we can go back to Manual resize mode.
			// From now on, the content has to adapt to the size of the window.
			SizeToContent = System.Windows.SizeToContent.Manual;
			c.Width = double.NaN;
			c.Height = double.NaN;
		}
	}
}
