Editor/ChitChat/CustomWidgets/ListControlView.cs
using Sandbox;
using Editor;
using System;
using System.Threading.Tasks;
using System.Linq;
using System.Collections.Generic;

namespace ChitChat.Editor;

public class ListControlView : ControlWidget
{
	public Action<SerializedProperty> onItemSelected;

	/// <summary>
	/// Invokes when an item has been removed. Gives index of the removed item.
	/// </summary>
	public Action<int> onItemRemoved;
	public Action<int> onItemMoved;
	public Action<int> onItemRightClicked;

	public delegate ControlWidget CreateItemUI(SerializedProperty prop);
	private CreateItemUI _onCreateUIItem = null;

	public override bool SupportsMultiEdit => true;

	public int SelectedIndex { get; set; } = -1;
	
	private bool _hasAddButton = false;

	internal SerializedCollection Collection;

	private readonly Layout _content;
	private IconButton _addButton;
	private Layout _addButtonLayout;

	private bool _preventRebuild = false;
	private bool _movedItem = false;
	private int? _buildHash;
	private object _buildValue;

	protected override int ValueHash => Collection is null ? base.ValueHash : HashCode.Combine(base.ValueHash, Collection.Count(), _movedItem);

	public ListControlView(SerializedProperty property, CreateItemUI onCreateItemUI = null, bool hasAddButton = true)
		: this(property, GetCollection(property), onCreateItemUI, hasAddButton)
	{
	}

	private static SerializedCollection GetCollection(SerializedProperty property)
	{
		if (property == null)
			return null;

		if (!property.TryGetAsObject(out var so) || so is not SerializedCollection sc)
		{
			Log.Error("Couldn't get serialized property as serialized collection!");
			return null;
		}

		return sc;
	}

	public ListControlView(SerializedProperty property, SerializedCollection sc, CreateItemUI createItemUI = null, bool hasAddButton = true)
		: base(property)
	{
		Layout = Layout.Column();
		Layout.Spacing = 2;

		_hasAddButton = hasAddButton;
		_onCreateUIItem = createItemUI;

		if (sc == null && !property.IsMultipleValues) return;

		if (sc != null)
		{
			Collection = sc;
			Collection.OnEntryAdded = Rebuild;
			Collection.OnEntryRemoved = Rebuild;
		}

		_buildValue = SerializedProperty?.GetValue<object>();

		_content = Layout.Column();

		Layout.Add(_content);

		Rebuild();
	}

	public void SelectItem(int index)
	{
		if (index < Collection.Count() && index >= 0)
			OnItemClicked(index);
	}

	internal ControlWidget CreateUIItem(SerializedProperty prop)
	{
		if (prop == null) return null;

		if (_onCreateUIItem == null)
			return Create(prop);
		else
			return _onCreateUIItem?.Invoke(prop);
	}

	private void RefreshCollection()
	{
		var value = SerializedProperty?.GetValue<object>();

		if (_buildValue == value)
			return;

		_buildValue = value;

		// Collection has changed, need to get the new one
		var sc = GetCollection(SerializedProperty);

		if (sc != null)
		{
			Collection = sc;
			Collection.OnEntryAdded = Rebuild;
			Collection.OnEntryRemoved = Rebuild;
		}
	}

	protected override void OnValueChanged()
	{
		RefreshCollection();
		Rebuild();
	}

	public void Rebuild()
	{
		if (_preventRebuild) return;

		if (Collection != null)
		{
			var hash = ValueHash;
			if (_buildHash.HasValue && hash == _buildHash.Value) return;
			_buildHash = hash;
		}

		using var _ = SuspendUpdates.For(this);

		_content.Clear(true);
		_content.Margin = 0;

		var column = Layout.Column();

		if (SerializedProperty.IsMultipleValues)
		{
			RebuildMultiple(column);
		}
		else
		{
			RebuildSingle(column);
		}

		// bottom row
		if (!IsControlDisabled && _hasAddButton)
		{
			_addButtonLayout = Layout.Row();
			_addButtonLayout.Margin = new Sandbox.UI.Margin(Theme.ControlHeight + 2, 0, 0, 0);
			_addButton = new ListControlAddButton(this);
			_addButton.MouseClick = AddEntry;
			_addButtonLayout.Add(_addButton);
			_addButtonLayout.AddStretchCell(1);
			column.Add(_addButtonLayout);
		}

		_content.Add(column);
	}

	private void OnItemClicked(int index)
	{
		SelectedIndex = index;
		onItemSelected?.Invoke(Collection.Skip(index).First());
	}

	private void RebuildSingle(Layout column)
	{
		if (Collection is null) return;
		int index = 0;
		foreach (var itemProp in Collection)
		{
			ListItemWidget item = new ListItemWidget(this, itemProp, index);
			item.onItemClicked += OnItemClicked;
			column.Add(item);
			column.AddSeparator(8, Color.Transparent);
			index++;
		}
	}

	private void RebuildMultiple(Layout column)
	{
		var minCount = GetMultipleMin();
		if (minCount == int.MaxValue || minCount == 0) return;
		for (int i = 0; i < minCount; i++)
		{
			MultiSerializedObject mo = new();
			foreach (var listProp in SerializedProperty.MultipleProperties)
			{
				var collection = GetCollection(listProp);
				if (collection == null) continue;
				var property = collection.ElementAt(i);
				if (property != null)
				{
					mo.Add(property.Parent);
				}
			}
			mo.Rebuild();
			var so = (SerializedObject)mo;
			var itemProp = so.GetProperty(i.ToString());

			ListItemWidget item = new ListItemWidget(this, itemProp, i);
			item.onItemClicked += OnItemClicked;
			column.Add(item);
			column.AddSeparator(8, Color.Transparent);
		}
	}

	private void AddEntry()
	{
		if (Collection != null)
		{
			Collection.Add(null);
		}
		else
		{
			var minCount = GetMultipleMin();
			var maxCount = GetMultipleMax();
			var props = SerializedProperty.MultipleProperties.ToList();
			foreach (var prop in props)
			{
				var collection = GetCollection(prop);
				if (collection is null) continue;
				if ( minCount == maxCount || collection.Count() == minCount)
				{
					if (collection.Count() == 0 && minCount != 0) continue;
					collection?.Add(null);
				}
			}
			Rebuild();
		}
	}

	internal void InsertEntry(int index, object obj)
	{
		if (Collection != null)
		{
			Collection.Add(index + 1, obj);
		}
		else
		{
			foreach (var prop in SerializedProperty.MultipleProperties)
			{
				var collection = GetCollection(prop);
				if (index < collection.Count())
				{
					Collection.Add(index + 1, obj);
				}
			}
			Rebuild();
		}
	}

	private void RemoveEntry(int index)
	{
		if (Collection != null)
		{
			Collection.RemoveAt(index);
		}
		else
		{
			foreach (var prop in SerializedProperty.MultipleProperties)
			{
				var collection = GetCollection(prop);
				if (index < collection.Count())
				{
					collection.RemoveAt(index);
				}
			}
			Rebuild();
		}

		onItemRemoved?.Invoke(index);
	}

	private void DuplicateEntry(int index)
	{
		if (Collection != null)
		{
			var sourceProperty = Collection.Skip(index).First();
			var sourceObj = sourceProperty.GetValue<object>();
			var sourceJson = Json.ToNode(sourceObj);

			Collection.Add(index + 1, Json.FromNode(sourceJson, sourceProperty.PropertyType));
		}
		else
		{
			foreach (var prop in SerializedProperty.MultipleProperties)
			{
				var collection = GetCollection(prop);
				if (index < collection.Count())
				{
					var sourceProperty = collection.ElementAt(index);
					var sourceObj = sourceProperty.GetValue<object>();
					var sourceJson = Json.ToNode(sourceObj);
					collection.Add( index + 1, Json.FromNode(sourceJson, sourceProperty.PropertyType));
				}
			}
			Rebuild();
		}
	}

	private void MoveEntry(int index, int delta)
	{
		var movingIndex = index + delta;
		if (movingIndex < 0 || movingIndex > GetMultipleMin() - 1) return;

		_preventRebuild = true;

		if (Collection != null)
		{
			Move(Collection, index, delta);
		}
		else
		{
			foreach (var prop in SerializedProperty.MultipleProperties)
			{
				var collection = GetCollection(prop);
				if (collection is null ) continue;
				Move(collection, index, delta);
			}
		}

		_preventRebuild = false;
		_movedItem = !_movedItem;
		onItemMoved?.Invoke(movingIndex);
		Rebuild();
	}

	private void Move(SerializedCollection collection, int index, int delta)
	{
		List<object> list = new();
		var movingIndex = index + delta;
		
		foreach (var item in collection)
		{
			list.Add(item.GetValue<object>());
		}

		var prop = list.ElementAt(movingIndex);
		list.RemoveAt(movingIndex);
		list.Insert(index, prop);

		while (collection.Count() > 0)
		{
			collection.RemoveAt(0);
		}

		foreach (var item in list)
		{
			collection.Add(item);
		}
	}

	private int GetMultipleMin()
	{
		var minCount = int.MaxValue;
		foreach (var entry in SerializedProperty.MultipleProperties)
		{
			var collection = GetCollection(entry);
			if (collection == null) continue;
			var count = collection.Count();
			if (count < minCount) minCount = count;
		}
		if (minCount == int.MaxValue) return 0;
		return minCount;
	}

	private int GetMultipleMax()
	{
		var maxCount = int.MinValue;
		foreach (var entry in SerializedProperty.MultipleProperties)
		{
			var collection = GetCollection(entry);
			if (collection is null) continue;
			var count = collection.Count();
			if (count > maxCount) maxCount = count;
		}
		if (maxCount == int.MinValue) return 0;
		return maxCount;
	}

	private object GetDragData(int index)
	{
		if (Collection != null)
		{
			return index;
		}
		else
		{
			var firstProp = SerializedProperty.MultipleProperties.FirstOrDefault();
			if (firstProp == null) return null;
			var collection = GetCollection(firstProp);
			if (index < collection.Count())
			{
				return index;
			}
		}
		return null;
	}

	protected override void OnPaint()
	{
		Paint.Antialiasing = true;

		Paint.ClearPen();
		Paint.SetBrush( Theme.TextControl.Darken( 0.6f ) );
	}

	private class ListItemWidget : Widget
	{
		public Action<int> onItemClicked;

		private Drag _dragData;
		private DragHandle _dragHandle;
		private ControlWidget _controlWidget;
		private bool _draggingAbove = false;
		private bool _draggingBelow = false;

		private ListControlView _listWidget;
		private int _index = -1;

		public ListItemWidget(ListControlView parent, SerializedProperty property, int index) : base(parent)
		{
			_listWidget = parent;
			_index = index;
			ContentMargins = new Sandbox.UI.Margin(0, 0, 15, 0);
			Layout = Layout.Row();
			Layout.Margin = new Sandbox.UI.Margin(0, 2);
			Layout.Spacing = 2;
			ReadOnly = parent.ReadOnly;
			Enabled = parent.Enabled;
			Cursor = CursorShape.Finger;

			ToolTip = $"Element {_index}";

			_controlWidget = _listWidget.CreateUIItem(property);
			_controlWidget.ReadOnly = ReadOnly;
			_controlWidget.Enabled = Enabled;
			_controlWidget.MouseTracking = true;

			if (_controlWidget.IsControlDisabled)
			{
				Layout.Add(_controlWidget);
			}
			else
			{
				IsDraggable = !_controlWidget.IsControlDisabled;
				_dragHandle = new DragHandle(this) 
				{ 
					IconSize = 13, 
					Foreground = Theme.TextControl, 
					Background = Color.Transparent, 
					FixedWidth = Theme.ControlHeight, 
					FixedHeight = Theme.ControlHeight
				};
				IconButton removeButton = new IconButton("clear", () => parent.RemoveEntry(index)) 
				{ 
					ToolTip = "Remove", 
					Background = Theme.ControlBackground, 
					FixedWidth = Theme.ControlHeight, 
					FixedHeight = Theme.ControlHeight 
				};

				Layout.Add(_dragHandle);
				Layout.Add(_controlWidget);
				Layout.Add(removeButton);

				_dragHandle.MouseRightClick += () =>
				{
					ContextMenu menu = new ContextMenu(this);

					menu.AddOption("Remove", "clear", () => parent.RemoveEntry(index));
					menu.AddOption("Duplicate", "content_copy", () => parent.DuplicateEntry(index));

					menu.OpenAtCursor();
				};
			}

			AcceptDrops = true;
		}

		protected override void OnMouseClick(MouseEvent e)
		{
			base.OnMouseClick(e);

			if (e.LeftMouseButton)
			{
				onItemClicked?.Invoke(_index);
			}
		}

		protected override void OnMouseRightClick(MouseEvent e)
		{
			base.OnMouseRightClick(e);

			if (_controlWidget.IsUnderMouse)
				_listWidget.onItemRightClicked?.Invoke(_index);
		}

		protected override void OnPaint()
		{
			if (Enabled)
			{
				Rect rect = LocalRect;

				if (Paint.HasPressed)
				{
					Paint.ClearPen();
					Paint.SetBrush(Color.Blue);
					Paint.DrawRect(in rect, 3);
				}
				else if(Paint.HasMouseOver)
				{
					Paint.ClearPen();
					Paint.SetBrush(new Color(0.37f, 0.37f, 0.37f));
					Paint.DrawRect(in rect, 3);
				}
				
				if (_draggingAbove)
				{
					Paint.SetPen(Theme.SelectedBackground, 2f, PenStyle.Solid);
					Paint.DrawLine(LocalRect.TopLeft, LocalRect.TopRight);
					_draggingAbove = false;
				}
				else if (_draggingBelow)
				{
					Paint.SetPen(Theme.SelectedBackground, 2f, PenStyle.Solid);
					Paint.DrawLine(LocalRect.BottomLeft, LocalRect.BottomRight);
					_draggingBelow = false;
				}
			}
		}

		public override void OnDragHover(DragEvent ev)
		{
			if (!TryDragOperation(ev, out var dragDelta))
			{
				_draggingAbove = false;
				_draggingBelow = false;
				return;
			}

			_draggingAbove = dragDelta > 0;
			_draggingBelow = dragDelta < 0;
		}

		public override void OnDragDrop(DragEvent ev)
		{
			if(ev.Action == DropAction.Ignore)
				return;

			ev.Action = DropAction.Ignore;

			if (!TryDragOperation(ev, out var delta)) return;

			_listWidget.MoveEntry(_index, delta);
		}

		private bool TryDragOperation(DragEvent ev, out int delta)
		{
			delta = 0;
			var obj = ev.Data.Object;

			if (obj == null) return false;

			if (obj is int otherIndex)
			{
				if (_index == -1 || otherIndex == -1)
					return false;
				else if (_index == otherIndex)
				{
					_dragHandle.IsDraggable = true;
					return false;
				}

				delta = otherIndex - _index;
				return true;
			}

			return false;
		}

		private class DragHandle : IconButton
		{
			private ListItemWidget _item;

			public DragHandle(ListItemWidget item) : base("drag_handle")
			{
				_item = item;

				IsDraggable = _item.IsDraggable;
			}

			protected override void OnDragStart()
			{
				IsDraggable = false;
				_item._dragData = new Drag(this);
				_item._dragData.Data.Object = _item._listWidget.GetDragData(_item._index);
				_item._dragData.Execute();
			}
		}
	}

	private class ListControlAddButton : IconButton
	{
		ListControlView ParentList;
		public ListControlAddButton(ListControlView parentList) : base("add")
		{
			ParentList = parentList;
			Background = Theme.ControlBackground;
			ToolTip = "Add Element";
			FixedWidth = Theme.ControlHeight;
			FixedHeight = Theme.ControlHeight;
			AcceptDrops = true;
		}

		public override void OnDragDrop(DragEvent ev)
		{
			base.OnDragDrop(ev);

			var dataObj = ev.Data.Object;
			var parentType = ParentList.Collection.ValueType;

			// Allow dragging objects onto the add button
			if (dataObj is object[] dataArray)
			{
				foreach (var obj in dataArray)
				{
					var objType = obj.GetType();
					if (objType != parentType)
					{
						if ( obj is GameObject gameObject && parentType.IsAssignableTo(typeof(Component)))
						{
							var comp = gameObject.Components.Get(parentType);
							if (comp is not null)
							{
								ParentList.Collection.Add(comp);
							}
						}
						continue;
					}
					ParentList.Collection.Add(obj);
				}
			}
			else if (dataObj?.GetType() == parentType)
			{
				ParentList.Collection.Add(dataObj);
			}
			else if (ev.Data.Assets.Count > 0)
			{
				DropAssets(ev);
			}
		}

		private async void DropAssets(DragEvent ev)
		{
			var parentType = ParentList.Collection.ValueType;

			// Special case for SoundFile
			if (parentType == typeof(SoundFile))
			{
				await DropSoundFiles(ev);
				return;
			}

			foreach (var dataAsset in ev.Data.Assets)
			{
				if (dataAsset is null) continue;
				var asset = await dataAsset.GetAssetAsync();
				if (asset is null) continue;
				var resource = asset.LoadResource();
				if (resource is null) continue;
				ParentList.Collection.Add(resource);
			}
		}

		private async Task DropSoundFiles(DragEvent ev)
		{
			foreach (var dataAsset in ev.Data.Assets)
			{
				if (dataAsset is null) continue;
				if (dataAsset.AssetPath.EndsWith(".sound")) continue;
				if (!dataAsset.IsInstalled)
				{
					await dataAsset.GetAssetAsync();
				}
				var sound = SoundFile.Load(dataAsset.AssetPath);
				if (sound is null) continue;
				ParentList.Collection.Add(sound);
			}
		}
	}
}