context: /*
      Copyright (C) 2014-2019 de4dot@gmail.com
  
      This file is part of dnSpy
  
      dnSpy 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 3 of the License, or
      (at your option) any later version.
  
      dnSpy 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 dnSpy.  If not, see <http://www.gnu.org/licenses/>.
  */
  
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Windows;
  using System.Windows.Controls.Primitives;
  using System.Windows.Input;
  using System.Windows.Media;
  using dnSpy.Contracts.Hex;
  using dnSpy.Contracts.Hex.Editor;
  using dnSpy.Contracts.Hex.Formatting;
  using dnSpy.Controls;
  using VSTA = Microsoft.VisualStudio.Text.Adornments;
  using VSTF = Microsoft.VisualStudio.Text.Formatting;
  
  namespace dnSpy.Hex.Editor {
  	sealed class HexPopupSpaceReservationAgent : HexSpaceReservationAgent {
  		bool IsVisible => !(popup.Child is null);
  		public override bool HasFocus => IsVisible && popup.IsKeyboardFocusWithin;
  		public override bool IsMouseOver => IsVisible && popup.IsMouseOver;
  		public override event EventHandler GotFocus;
  		public override event EventHandler LostFocus;
  
  		readonly HexSpaceReservationManager spaceReservationManager;
  		readonly WpfHexView wpfHexView;
  		readonly UIElement content;
  		readonly Popup popup;
  		VSTA.PopupStyles style;
  		HexLineSpan lineSpan;
  		double popupZoomLevel = double.NaN;
  
  		public HexPopupSpaceReservationAgent(HexSpaceReservationManager spaceReservationManager, WpfHexView wpfHexView, HexLineSpan lineSpan, VSTA.PopupStyles style, UIElement content) {
  			if (lineSpan.IsDefault)
  				throw new ArgumentException();
  			if ((style & (VSTA.PopupStyles.DismissOnMouseLeaveText | VSTA.PopupStyles.DismissOnMouseLeaveTextOrContent)) == (VSTA.PopupStyles.DismissOnMouseLeaveText | VSTA.PopupStyles.DismissOnMouseLeaveTextOrContent))
  				throw new ArgumentOutOfRangeException(nameof(style));
  			this.spaceReservationManager = spaceReservationManager ?? throw new ArgumentNullException(nameof(spaceReservationManager));
  			this.wpfHexView = wpfHexView;
  			this.lineSpan = lineSpan;
  			this.style = style;
  			this.content = content ?? throw new ArgumentNullException(nameof(content));
  			popup = new Popup {
  				PlacementTarget = wpfHexView.VisualElement,
  				Placement = PlacementMode.Relative,
  				Visibility = Visibility.Collapsed,
  				IsOpen = false,
  				AllowsTransparency = true,
  				UseLayoutRounding = true,
  				SnapsToDevicePixels = true,
  			};
  		}
  
  		void Content_GotFocus(object? sender, RoutedEventArgs e) => GotFocus?.Invoke(this, EventArgs.Empty);
  		void Content_LostFocus(object? sender, RoutedEventArgs e) => LostFocus?.Invoke(this, EventArgs.Empty);
  
  		internal void Update(HexLineSpan lineSpan, VSTA.PopupStyles style) {
  			if (lineSpan.IsDefault)
  				throw new ArgumentException();
  			if ((style & (VSTA.PopupStyles.DismissOnMouseLeaveText | VSTA.PopupStyles.DismissOnMouseLeaveTextOrContent)) == (VSTA.PopupStyles.DismissOnMouseLeaveText | VSTA.PopupStyles.DismissOnMouseLeaveTextOrContent))
  				throw new ArgumentOutOfRangeException(nameof(style));
  			this.lineSpan = lineSpan;
  			this.style = style;
  			wpfHexView.QueueSpaceReservationStackRefresh();
  		}
  
  		Rect? GetVisualSpanBounds() {
  			var lines = wpfHexView.HexViewLines.GetHexViewLinesIntersectingSpan(lineSpan.BufferSpan);
  			if (lines.Count == 0)
  				return null;
  			double left = double.PositiveInfinity, right = double.NegativeInfinity;
  			double top = double.PositiveInfinity, bottom = double.NegativeInfinity;
  			foreach (var line in lines) {
  				if (!line.IsVisible())
  					continue;
  				foreach (var textBounds in GetTextBounds(line)) {
  					left = Math.Min(left, textBounds.Left);
  					right = Math.Max(right, textBounds.Right);
  					top = Math.Min(top, textBounds.TextTop);
  					bottom = Math.Max(bottom, textBounds.TextBottom);
  				}
  			}
  			bool b = left != double.PositiveInfinity && right != double.NegativeInfinity &&
  				top != double.PositiveInfinity && bottom != double.NegativeInfinity;
  			if (!b)
  				return null;
  			if (left > right)
  				right = left;
  			if (top > bottom)
  				bottom = top;
  			return new Rect(left, top, right - left, bottom - top);
  		}
  
  		IList<VSTF.TextBounds> GetTextBounds(HexViewLine line) {
  			if (lineSpan.IsTextSpan) {
  				if (lineSpan.TextSpan!.Value.Length == 0) {
  					if (line.BufferSpan.Contains(lineSpan.BufferSpan)) {
  						var bounds = line.GetCharacterBounds(lineSpan.TextSpan.Value.Start);
  						// It's just a point, so use zero width
  						bounds = new VSTF.TextBounds(bounds.Leading, bounds.Top, 0, bounds.Height, bounds.TextTop, bounds.TextHeight);
  						return new VSTF.TextBounds[] { bounds };
  					}
  					return Array.Empty<VSTF.TextBounds>();
  				}
  				else
  					return line.GetNormalizedTextBounds(lineSpan);
  			}
  			else {
  				var fullSpan = lineSpan.BufferSpan;
  				if (fullSpan.Length == 0) {
  					if (line.BufferSpan.Contains(fullSpan))
  						return line.GetNormalizedTextBounds(fullSpan, lineSpan.SelectionFlags!.Value);
  					return Array.Empty<VSTF.TextBounds>();
  				}
  				else
  					return line.GetNormalizedTextBounds(lineSpan);
  			}
  		}
  
  		Size PopupSize {
  			get {
  				var maxSize = HexPopupHelper.GetMaxSize(wpfHexView);
  				content.Measure(maxSize);
  				return new Size(Math.Min(content.DesiredSize.Width, maxSize.Width), Math.Min(content.DesiredSize.Height, maxSize.Height));
  			}
  		}
  
  		Size ToScreenSize(Size size) => HexPopupHelper.TransformToDevice(wpfHexView, size);
  
  		Rect WpfHexViewRectToScreenRect(Rect wpfHexViewRect) {
  			wpfHexViewRect.X -= wpfHexView.ViewportLeft;
  			wpfHexViewRect.Y -= wpfHexView.ViewportTop;
  			return ToScreenRect(wpfHexViewRect);
  		}
  
  		Rect ToScreenRect(Rect wpfRect) => new Rect(ToScreenPoint(wpfRect.TopLeft), ToScreenPoint(wpfRect.BottomRight));
  		Point ToScreenPoint(Point point) => wpfHexView.VisualElement.PointToScreen(point);
  
  		public override Geometry? PositionAndDisplay(Geometry reservedSpace) {
  			var spanBoundsTmp = GetVisualSpanBounds();
  			if (spanBoundsTmp is null || spanBoundsTmp.Value.IsEmpty)
  				return null;
  			var spanBounds = WpfHexViewRectToScreenRect(spanBoundsTmp.Value);
  			var desiredSize = ToScreenSize(PopupSize);
  
  			var screen = new Screen(wpfHexView.VisualElement);
  			var screenRect = screen.IsValid ? screen.DisplayRect : SystemParameters.WorkArea;
  
  			Rect? popupRect = null;
  			if ((style & VSTA.PopupStyles.PositionClosest) != 0) {
  				foreach (var pos in GetValidPositions(screenRect, reservedSpace, desiredSize, spanBounds, style))
  					popupRect = GetClosest(spanBounds, popupRect, pos, style);
  			}
  			else {
  				foreach (var pos in GetValidPositions(screenRect, reservedSpace, desiredSize, spanBounds, style)) {
  					popupRect = pos;
  					break;
  				}
  			}
  
  			if (popupRect is null)
  				return null;
  			var viewRelativeRect = HexPopupHelper.TransformFromDevice(wpfHexView, popupRect.Value);
  
  			bool isOpen = popup.IsOpen;
  			if (!isOpen)
  				AddEvents();
  
  			if (popupZoomLevel != wpfHexView.ZoomLevel) {
  				// Must set IsOpen to false when setting a new scale transform
  				popup.IsOpen = false;
  				HexPopupHelper.SetScaleTransform(wpfHexView, popup);
  				popupZoomLevel = wpfHexView.ZoomLevel;
  			}
  			if (!isOpen) {
  				popup.Child = content;
  				popup.Visibility = Visibility.Visible;
  			}
  			popup.VerticalOffset = viewRelativeRect.Top;
  			popup.HorizontalOffset = viewRelativeRect.Left;
  			popup.IsOpen = true;
  
  			return new RectangleGeometry(popupRect.Value);
  		}
  
  		Rect GetClosest(Rect spanBounds, Rect? rect, Rect candidate, VSTA.PopupStyles style) {
  			if (rect is null)
  				return candidate;
  			double rectDist, candidateDist;
  			if ((style & VSTA.PopupStyles.PositionLeftOrRight) != 0) {
  				rectDist = GetHorizontalDistance(spanBounds, rect.Value);
  				candidateDist = GetHorizontalDistance(spanBounds, candidate);
  			}
  			else {
  				rectDist = GetVerticalDistance(spanBounds, rect.Value);
  				candidateDist = GetVerticalDistance(spanBounds, candidate);
  			}
  			return rectDist <= candidateDist ? rect.Value : candidate;
  		}
  
  		double GetHorizontalDistance(Rect spanBounds, Rect rect) {
  			double a = Math.Min(Math.Abs(rect.Left - spanBounds.Left), Math.Abs(rect.Right - spanBounds.Left));
  			double b = Math.Min(Math.Abs(rect.Left - spanBounds.Right), Math.Abs(rect.Right - spanBounds.Right));
  			return Math.Min(a, b);
  		}
  
  		double GetVerticalDistance(Rect spanBounds, Rect rect) {
  			double a = Math.Min(Math.Abs(rect.Top - spanBounds.Top), Math.Abs(rect.Bottom - spanBounds.Top));
  			double b = Math.Min(Math.Abs(rect.Top - spanBounds.Bottom), Math.Abs(rect.Bottom - spanBounds.Bottom));
  			return Math.Min(a, b);
  		}
  
  		bool OverlapsReservedSpace(Geometry reservedSpace, Rect rect) {
  			var rs = reservedSpace.Bounds;
  			return rs.Left < rect.Right && rs.Right > rect.Left && rs.Top < rect.Bottom && rs.Bottom > rect.Top;
  		}
  
  		IEnumerable<Rect> GetValidPositions(Rect screenRect, Geometry reservedSpace, Size desiredSize, Rect spanBounds, VSTA.PopupStyles style) {
  			var possiblePositions = new List<Rect>();
  			possiblePositions.AddRange(GetPositions(screenRect, reservedSpace, desiredSize, spanBounds, style));
  			possiblePositions.AddRange(GetPositions(screenRect, reservedSpace, desiredSize, spanBounds, style ^ VSTA.PopupStyles.PreferLeftOrTopPosition));
  			bool isLeftRight = (style & VSTA.PopupStyles.PositionLeftOrRight) != 0;
  
gt: 			foreach (var pos in possiblePositions) {