#region Copyright & License Information
/*
 * Copyright 2007-2022 The OpenRA Developers (see AUTHORS)
 * This file is part of OpenRA, which is free software. It is made
 * available to you under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version. For more
 * information, see COPYING.
 */
#endregion

using System;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Widgets;

namespace OpenRA.Mods.Common.Widgets
{
	public interface ILayout
	{
		void AdjustChild(Widget w);
		void AdjustChildren();
	}

	public enum ScrollPanelAlign
	{
		Bottom,
		Top
	}

	public enum ScrollBar
	{
		Left,
		Right,
		Hidden
	}

	public class ScrollPanelWidget : Widget
	{
		readonly Ruleset modRules;
		public int ScrollbarWidth = 24;
		public int BorderWidth = 1;
		public int TopBottomSpacing = 2;
		public int ItemSpacing = 0;
		public int ButtonDepth = ChromeMetrics.Get<int>("ButtonDepth");
		public string ClickSound = ChromeMetrics.Get<string>("ClickSound");
		public string ClickDisabledSound = ChromeMetrics.Get<string>("ClickDisabledSound");
		public string Background = "scrollpanel-bg";
		public string ScrollBarBackground = "scrollpanel-bg";
		public string Button = "scrollpanel-button";
		public string Decorations = "scrollpanel-decorations";
		public readonly string DecorationScrollUp = "up";
		public readonly string DecorationScrollDown = "down";
		readonly CachedTransform<(bool Disabled, bool Pressed, bool Hover, bool Focused, bool Highlighted), Sprite> getUpArrowImage;
		readonly CachedTransform<(bool Disabled, bool Pressed, bool Hover, bool Focused, bool Highlighted), Sprite> getDownArrowImage;
		public int ContentHeight;
		public ILayout Layout;
		public int MinimumThumbSize = 10;
		public ScrollPanelAlign Align = ScrollPanelAlign.Top;
		public ScrollBar ScrollBar = ScrollBar.Right;
		public bool CollapseHiddenChildren;

		// Fraction of the remaining scroll-delta to move in 40ms
		public float SmoothScrollSpeed = 0.333f;

		protected bool upPressed;
		protected bool downPressed;
		protected bool upDisabled;
		protected bool downDisabled;
		protected bool thumbPressed;
		protected Rectangle upButtonRect;
		protected Rectangle downButtonRect;
		protected Rectangle backgroundRect;
		protected Rectangle scrollbarRect;
		protected Rectangle thumbRect;

		// The target value is the list offset we're trying to reach
		float targetListOffset;

		// The current value is the actual list offset at the moment
		float currentListOffset;

		// The Game.Runtime value when UpdateSmoothScrolling was last called
		// Used for calculating the per-frame smooth-scrolling delta
		long lastSmoothScrollTime = 0;

		// Setting "smooth" to true will only update the target list offset.
		// Setting "smooth" to false will also set the current list offset,
		// i.e. it will scroll immediately.
		//
		// For example, scrolling with the mouse wheel will use smooth
		// scrolling to give a nice visual effect that makes it easier
		// for the user to follow. Dragging the scrollbar's thumb, however,
		// will scroll to the desired position immediately.
		protected void SetListOffset(float value, bool smooth)
		{
			targetListOffset = value;
			if (!smooth)
			{
				var oldListOffset = currentListOffset;
				currentListOffset = value;

				// Update mouseover
				if (oldListOffset != currentListOffset)
					Ui.ResetTooltips();
			}
		}

		[ObjectCreator.UseCtor]
		public ScrollPanelWidget(ModData modData)
		{
			modRules = modData.DefaultRules;

			Layout = new ListLayout(this);

			getUpArrowImage = WidgetUtils.GetCachedStatefulImage(Decorations, DecorationScrollUp);
			getDownArrowImage = WidgetUtils.GetCachedStatefulImage(Decorations, DecorationScrollDown);
		}

		public override void RemoveChildren()
		{
			ContentHeight = 0;
			base.RemoveChildren();
			Scroll(0);
		}

		public override void AddChild(Widget child)
		{
			// Initial setup of margins/height
			Layout.AdjustChild(child);
			base.AddChild(child);
		}

		public override void RemoveChild(Widget child)
		{
			base.RemoveChild(child);
			Layout.AdjustChildren();
			Scroll(0);
		}

		public void ReplaceChild(Widget oldChild, Widget newChild)
		{
			oldChild.Removed();
			newChild.Parent = this;
			Children[Children.IndexOf(oldChild)] = newChild;
			Layout.AdjustChildren();
			Scroll(0);
		}

		public override void DrawOuter()
		{
			if (!IsVisible())
				return;

			UpdateSmoothScrolling();

			var rb = RenderBounds;
			var scrollbarHeight = rb.Height - 2 * ScrollbarWidth;

			// Scroll thumb is only visible if the content does not fit within the panel bounds
			var thumbHeight = 0;
			var thumbOrigin = rb.Y + ScrollbarWidth;
			if (ContentHeight > rb.Height)
			{
				thumbHeight = Math.Max(MinimumThumbSize, scrollbarHeight * rb.Height / ContentHeight);
				thumbOrigin += (int)((scrollbarHeight - thumbHeight) * currentListOffset / (rb.Height - ContentHeight));
			}

			switch (ScrollBar)
			{
				case ScrollBar.Left:
					backgroundRect = new Rectangle(rb.X + ScrollbarWidth, rb.Y, rb.Width + 1, rb.Height);
					upButtonRect = new Rectangle(rb.X, rb.Y, ScrollbarWidth, ScrollbarWidth);
					downButtonRect = new Rectangle(rb.X, rb.Bottom - ScrollbarWidth, ScrollbarWidth, ScrollbarWidth);
					scrollbarRect = new Rectangle(rb.X, rb.Y + ScrollbarWidth - 1, ScrollbarWidth, scrollbarHeight + 2);
					thumbRect = new Rectangle(rb.X, thumbOrigin, ScrollbarWidth, thumbHeight);
					break;
				case ScrollBar.Right:
					backgroundRect = new Rectangle(rb.X, rb.Y, rb.Width - ScrollbarWidth + 1, rb.Height);
					upButtonRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Y, ScrollbarWidth, ScrollbarWidth);
					downButtonRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Bottom - ScrollbarWidth, ScrollbarWidth, ScrollbarWidth);
					scrollbarRect = new Rectangle(rb.Right - ScrollbarWidth, rb.Y + ScrollbarWidth - 1, ScrollbarWidth, scrollbarHeight + 2);
					thumbRect = new Rectangle(rb.Right - ScrollbarWidth, thumbOrigin, ScrollbarWidth, thumbHeight);
					break;
				case ScrollBar.Hidden:
					backgroundRect = new Rectangle(rb.X, rb.Y, rb.Width + 1, rb.Height);
					break;
				default:
					throw new ArgumentOutOfRangeException();
			}

			WidgetUtils.DrawPanel(Background, backgroundRect);

			if (ScrollBar != ScrollBar.Hidden)
			{
				var upHover = Ui.MouseOverWidget == this && upButtonRect.Contains(Viewport.LastMousePos);
				upDisabled = thumbHeight == 0 || currentListOffset >= 0;

				var downHover = Ui.MouseOverWidget == this && downButtonRect.Contains(Viewport.LastMousePos);
				downDisabled = thumbHeight == 0 || currentListOffset <= Bounds.Height - ContentHeight;

				var thumbHover = Ui.MouseOverWidget == this && thumbRect.Contains(Viewport.LastMousePos);
				WidgetUtils.DrawPanel(ScrollBarBackground, scrollbarRect);
				ButtonWidget.DrawBackground(Button, upButtonRect, upDisabled, upPressed, upHover, false);
				ButtonWidget.DrawBackground(Button, downButtonRect, downDisabled, downPressed, downHover, false);

				if (thumbHeight > 0)
					ButtonWidget.DrawBackground(Button, thumbRect, false, HasMouseFocus && thumbHover, thumbHover, false);

				var upOffset = !upPressed || upDisabled ? 4 : 4 + ButtonDepth;
				var downOffset = !downPressed || downDisabled ? 4 : 4 + ButtonDepth;

				var upArrowImage = getUpArrowImage.Update((upDisabled, upPressed, upHover, false, false));
				WidgetUtils.DrawSprite(upArrowImage,
					new float2(upButtonRect.Left + upOffset, upButtonRect.Top + upOffset));

				var downArrowImage = getDownArrowImage.Update((downDisabled, downPressed, downHover, false, false));
				WidgetUtils.DrawSprite(downArrowImage,
					new float2(downButtonRect.Left + downOffset, downButtonRect.Top + downOffset));
			}

			var drawBounds = backgroundRect.InflateBy(-BorderWidth, -BorderWidth, -BorderWidth, -BorderWidth);
			Game.Renderer.EnableScissor(drawBounds);

			// ChildOrigin enumerates the widget tree, so only evaluate it once
			var co = ChildOrigin;
			drawBounds.X -= co.X;
			drawBounds.Y -= co.Y;

			foreach (var child in Children)
				if (child.Bounds.IntersectsWith(drawBounds))
					child.DrawOuter();

			Game.Renderer.DisableScissor();
		}

		public override int2 ChildOrigin => RenderOrigin + new int2(ScrollBar == ScrollBar.Left ? ScrollbarWidth : 0, (int)currentListOffset);

		public override bool EventBoundsContains(int2 location)
		{
			return EventBounds.Contains(location);
		}

		void Scroll(int amount, bool smooth = false)
		{
			var newTarget = targetListOffset + amount * Game.Settings.Game.UIScrollSpeed;
			newTarget = Math.Min(0, Math.Max(Bounds.Height - ContentHeight, newTarget));

			SetListOffset(newTarget, smooth);
		}

		public void ScrollToBottom(bool smooth = false)
		{
			var value = Align == ScrollPanelAlign.Top ?
				Math.Min(0, Bounds.Height - ContentHeight) :
				Bounds.Height - ContentHeight;

			SetListOffset(value, smooth);
		}

		public void ScrollToTop(bool smooth = false)
		{
			var value = Align == ScrollPanelAlign.Top ? 0 :
				Math.Max(0, Bounds.Height - ContentHeight);

			SetListOffset(value, smooth);
		}

		public bool ScrolledToBottom => targetListOffset == Math.Min(0, Bounds.Height - ContentHeight) || ContentHeight <= Bounds.Height;

		void ScrollToItem(Widget item, bool smooth = false)
		{
			// Scroll the item to be visible
			float? newOffset = null;
			if (item.Bounds.Top + currentListOffset < 0)
				newOffset = ItemSpacing - item.Bounds.Top;

			if (item.Bounds.Bottom + currentListOffset > RenderBounds.Height)
				newOffset = RenderBounds.Height - item.Bounds.Bottom - ItemSpacing;

			if (newOffset.HasValue)
				SetListOffset(newOffset.Value, smooth);
		}

		public void ScrollToItem(string itemKey, bool smooth = false)
		{
			var item = Children.FirstOrDefault(c =>
			{
				return c is ScrollItemWidget si && si.ItemKey == itemKey;
			});

			if (item != null)
				ScrollToItem(item, smooth);
		}

		public void ScrollToSelectedItem()
		{
			var item = Children.FirstOrDefault(c =>
			{
				return c is ScrollItemWidget si && si.IsSelected();
			});

			if (item != null)
				ScrollToItem(item);
		}

		void UpdateSmoothScrolling()
		{
			if (lastSmoothScrollTime == 0)
			{
				lastSmoothScrollTime = Game.RunTime;
				return;
			}

			var offsetDiff = targetListOffset - currentListOffset;
			var absOffsetDiff = Math.Abs(offsetDiff);
			if (absOffsetDiff > 1f)
			{
				var dt = Game.RunTime - lastSmoothScrollTime;
				currentListOffset += offsetDiff * SmoothScrollSpeed.Clamp(0.1f, 1.0f) * dt / 40;

				Ui.ResetTooltips();
			}
			else
				SetListOffset(targetListOffset, false);

			lastSmoothScrollTime = Game.RunTime;
		}

		public override void Tick()
		{
			if (upPressed)
				Scroll(1);

			if (downPressed)
				Scroll(-1);
		}

		public override bool YieldMouseFocus(MouseInput mi)
		{
			upPressed = downPressed = thumbPressed = false;
			return base.YieldMouseFocus(mi);
		}

		int2 lastMouseLocation;

		public override bool HandleMouseInput(MouseInput mi)
		{
			if (mi.Event == MouseInputEvent.Scroll)
			{
				Scroll(mi.Delta.Y, true);
				return true;
			}

			if (mi.Button != MouseButton.Left)
				return false;

			if (mi.Event == MouseInputEvent.Down && !TakeMouseFocus(mi))
				return false;

			if (!HasMouseFocus)
				return false;

			if (HasMouseFocus && mi.Event == MouseInputEvent.Up)
				return YieldMouseFocus(mi);

			if (thumbPressed && mi.Event == MouseInputEvent.Move)
			{
				var rb = RenderBounds;
				var scrollbarHeight = rb.Height - 2 * ScrollbarWidth;
				var thumbHeight = ContentHeight == 0 ? 0 : Math.Max(MinimumThumbSize, (int)(scrollbarHeight * Math.Min(rb.Height * 1f / ContentHeight, 1f)));
				var oldOffset = currentListOffset;

				var newOffset = currentListOffset + ((int)((lastMouseLocation.Y - mi.Location.Y) * (ContentHeight - rb.Height) * 1f / (scrollbarHeight - thumbHeight)));
				newOffset = Math.Min(0, Math.Max(rb.Height - ContentHeight, newOffset));
				SetListOffset(newOffset, false);

				if (oldOffset != newOffset)
					lastMouseLocation = mi.Location;
			}
			else
			{
				upPressed = upButtonRect.Contains(mi.Location);
				downPressed = downButtonRect.Contains(mi.Location);
				thumbPressed = thumbRect.Contains(mi.Location);
				if (thumbPressed)
					lastMouseLocation = mi.Location;

				if (mi.Event == MouseInputEvent.Down)
				{
					if (thumbPressed || (upPressed && !upDisabled) || (downPressed && !downDisabled))
						Game.Sound.PlayNotification(modRules, null, "Sounds", ClickSound, null);
					else if ((upPressed && upDisabled) || (downPressed && downDisabled))
						Game.Sound.PlayNotification(modRules, null, "Sounds", ClickDisabledSound, null);
				}
			}

			return upPressed || downPressed || thumbPressed;
		}

		IObservableCollection collection;
		Func<object, Widget> makeWidget;
		Func<Widget, object, bool> widgetItemEquals;
		bool autoScroll;

		public void Unbind()
		{
			Bind(null, null, null, false);
		}

		public void Bind(IObservableCollection c, Func<object, Widget> makeWidget, Func<Widget, object, bool> widgetItemEquals, bool autoScroll)
		{
			this.autoScroll = autoScroll;

			Game.RunAfterTick(() =>
			{
				if (collection != null)
				{
					collection.OnAdd -= BindingAdd;
					collection.OnRemove -= BindingRemove;
					collection.OnRemoveAt -= BindingRemoveAt;
					collection.OnSet -= BindingSet;
					collection.OnRefresh -= BindingRefresh;
				}

				this.makeWidget = makeWidget;
				this.widgetItemEquals = widgetItemEquals;

				RemoveChildren();
				collection = c;

				if (c != null)
				{
					foreach (var item in c.ObservedItems)
						BindingAddImpl(item);

					c.OnAdd += BindingAdd;
					c.OnRemove += BindingRemove;
					c.OnRemoveAt += BindingRemoveAt;
					c.OnSet += BindingSet;
					c.OnRefresh += BindingRefresh;
				}
			});
		}

		void BindingAdd(IObservableCollection col, object item)
		{
			Game.RunAfterTick(() =>
			{
				if (collection != col)
					return;

				BindingAddImpl(item);
			});
		}

		void BindingAddImpl(object item)
		{
			if (makeWidget == null)
				return;

			var widget = makeWidget(item);
			var scrollToBottom = autoScroll && ScrolledToBottom;

			AddChild(widget);

			if (scrollToBottom)
				ScrollToBottom();
		}

		void BindingRemove(IObservableCollection col, object item)
		{
			Game.RunAfterTick(() =>
			{
				if (collection != col)
					return;

				var widget = Children.FirstOrDefault(w => widgetItemEquals(w, item));
				if (widget != null)
					RemoveChild(widget);
			});
		}

		void BindingRemoveAt(IObservableCollection col, int index)
		{
			Game.RunAfterTick(() =>
			{
				if (collection != col)
					return;

				if (index < 0 || index >= Children.Count)
					return;

				RemoveChild(Children[index]);
			});
		}

		void BindingSet(IObservableCollection col, object oldItem, object newItem)
		{
			Game.RunAfterTick(() =>
			{
				if (collection != col)
					return;

				var newWidget = makeWidget(newItem);
				newWidget.Parent = this;

				var i = Children.FindIndex(w => widgetItemEquals(w, oldItem));
				if (i >= 0)
				{
					var oldWidget = Children[i];
					oldWidget.Removed();
					Children[i] = newWidget;
					Layout.AdjustChildren();
				}
				else
					AddChild(newWidget);
			});
		}

		void BindingRefresh(IObservableCollection col)
		{
			Game.RunAfterTick(() =>
			{
				if (collection != col)
					return;

				RemoveChildren();
				foreach (var item in collection.ObservedItems)
					BindingAddImpl(item);
			});
		}
	}
}
