using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using PerformanceMonitorLite.Services;

namespace PerformanceMonitorLite.Helpers;

/// <summary>
/// Adds mouse-hover tooltips to a ScottPlot chart with multiple scatter series.
/// Shows the series name, value, and timestamp in a popup that follows the mouse.
/// </summary>
internal sealed class ChartHoverHelper
{
    private readonly ScottPlot.WPF.WpfPlot _chart;
    private readonly List<(ScottPlot.Plottables.Scatter Scatter, string Label)> _scatters = new();
    private readonly List<(ScottPlot.Plottables.BarPlot BarPlot, string Label)> _barPlots = new();
    private readonly Popup _popup;
    private readonly TextBlock _text;
    private string _unit;
    private DateTime _lastUpdate;

    public ChartHoverHelper(ScottPlot.WPF.WpfPlot chart, string unit)
    {
        _chart = chart;
        _unit = unit;

        _text = new TextBlock
        {
            Foreground = new SolidColorBrush(Color.FromRgb(0xE0, 0xE0, 0xE0)),
            FontSize = 13
        };

        _popup = new Popup
        {
            PlacementTarget = chart,
            Placement = PlacementMode.Relative,
            IsHitTestVisible = false,
            AllowsTransparency = true,
            Child = new Border
            {
                Background = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)),
                BorderBrush = new SolidColorBrush(Color.FromRgb(0x55, 0x55, 0x55)),
                BorderThickness = new Thickness(1),
                CornerRadius = new CornerRadius(3),
                Padding = new Thickness(8, 4, 8, 4),
                Child = _text
            }
        };

        chart.MouseMove += OnMouseMove;
        chart.MouseLeave += OnMouseLeave;

        /* Tab switching can leave the popup wedged: WPF unloads the parent TabItem
           without firing MouseLeave, so IsOpen stays true with a stale anchor.
           When the chart becomes visible again, OnMouseMove sets IsOpen = true
           but it is already true, so the popup never re-anchors and never shows.
           Force-close on every visibility/load transition so the next mouse move
           re-opens cleanly. */
        chart.IsVisibleChanged += OnChartVisibilityChanged;
        chart.Unloaded += OnChartUnloaded;
        chart.Loaded += OnChartLoaded;
    }

    public string Unit { get => _unit; set => _unit = value; }

    public void Dispose()
    {
        _chart.MouseMove -= OnMouseMove;
        _chart.MouseLeave -= OnMouseLeave;
        _chart.IsVisibleChanged -= OnChartVisibilityChanged;
        _chart.Unloaded -= OnChartUnloaded;
        _chart.Loaded -= OnChartLoaded;
        _popup.IsOpen = false;
        _scatters.Clear();
        _barPlots.Clear();
    }

    private void OnChartVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e) =>
        _popup.IsOpen = false;

    private void OnChartUnloaded(object sender, RoutedEventArgs e) =>
        _popup.IsOpen = false;

    private void OnChartLoaded(object sender, RoutedEventArgs e) =>
        _popup.IsOpen = false;

    public void Clear()
    {
        _scatters.Clear();
        _barPlots.Clear();
    }

    public void Add(ScottPlot.Plottables.Scatter scatter, string label) =>
        _scatters.Add((scatter, label));

    public void Add(ScottPlot.Plottables.BarPlot barPlot, string label) =>
        _barPlots.Add((barPlot, label));

    /// <summary>
    /// Returns the nearest series label and data-point time for the given mouse position,
    /// or null if no series is close enough.
    /// </summary>
    public (string Label, DateTime Time)? GetNearestSeries(Point mousePos)
    {
        if (_scatters.Count == 0 && _barPlots.Count == 0) return null;
        var dpi = VisualTreeHelper.GetDpi(_chart);
        var pixel = new ScottPlot.Pixel(
            (float)(mousePos.X * dpi.DpiScaleX),
            (float)(mousePos.Y * dpi.DpiScaleY));
        var mouseCoords = _chart.Plot.GetCoordinates(pixel);

        double bestDistance = double.MaxValue;
        ScottPlot.DataPoint bestPoint = default;
        string bestLabel = "";

        foreach (var (scatter, label) in _scatters)
        {
            var nearest = scatter.Data.GetNearest(mouseCoords, _chart.Plot.LastRender);
            if (!nearest.IsReal) continue;
            var nearestPixel = _chart.Plot.GetPixel(
                new ScottPlot.Coordinates(nearest.X, nearest.Y));
            double dx = nearestPixel.X - pixel.X;
            double dy = nearestPixel.Y - pixel.Y;
            double dist = dx * dx + dy * dy;
            if (dist < bestDistance)
            {
                bestDistance = dist;
                bestPoint = nearest;
                bestLabel = label;
            }
        }

        FindNearestBar(pixel, ref bestDistance, ref bestPoint, ref bestLabel);

        if (bestPoint.IsReal && bestDistance < 2500) // ~50px radius
            return (bestLabel, DateTime.FromOADate(bestPoint.X));
        return null;
    }

    private void FindNearestBar(ScottPlot.Pixel pixel, ref double bestDistance,
        ref ScottPlot.DataPoint bestPoint, ref string bestLabel)
    {
        foreach (var (barPlot, label) in _barPlots)
        {
            foreach (var bar in barPlot.Bars)
            {
                var topPixel = _chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Position, bar.Value));
                double halfWidthPx = Math.Abs(
                    _chart.Plot.GetPixel(new ScottPlot.Coordinates(bar.Position + bar.Size / 2, bar.Value)).X
                    - topPixel.X);
                double dx = Math.Abs(topPixel.X - pixel.X);
                if (dx > halfWidthPx + 4) continue;
                double dy = Math.Abs(topPixel.Y - pixel.Y);
                double dist = dx * dx + dy * dy;
                if (dist < bestDistance)
                {
                    bestDistance = dist;
                    double segmentHeight = bar.Value - bar.ValueBase;
                    bestPoint = new ScottPlot.DataPoint(new ScottPlot.Coordinates(bar.Position, segmentHeight), 0);
                    bestLabel = label;
                }
            }
        }
    }

    private void OnMouseMove(object sender, MouseEventArgs e)
    {
        if (_scatters.Count == 0 && _barPlots.Count == 0) return;
        var now = DateTime.UtcNow;
        if ((now - _lastUpdate).TotalMilliseconds < 50) return;
        _lastUpdate = now;

        var pos = e.GetPosition(_chart);
        var dpi = VisualTreeHelper.GetDpi(_chart);
        var pixel = new ScottPlot.Pixel(
            (float)(pos.X * dpi.DpiScaleX),
            (float)(pos.Y * dpi.DpiScaleY));
        var mouseCoords = _chart.Plot.GetCoordinates(pixel);

        double bestDistance = double.MaxValue;
        ScottPlot.DataPoint bestPoint = default;
        string bestLabel = "";

        foreach (var (scatter, label) in _scatters)
        {
            var nearest = scatter.Data.GetNearest(mouseCoords, _chart.Plot.LastRender);
            if (nearest.IsReal)
            {
                var nearestPixel = _chart.Plot.GetPixel(
                    new ScottPlot.Coordinates(nearest.X, nearest.Y));
                double dx = nearestPixel.X - pixel.X;
                double dy = nearestPixel.Y - pixel.Y;
                double dist = dx * dx + dy * dy;
                if (dist < bestDistance)
                {
                    bestDistance = dist;
                    bestPoint = nearest;
                    bestLabel = label;
                }
            }
        }

        FindNearestBar(pixel, ref bestDistance, ref bestPoint, ref bestLabel);

        if (bestPoint.IsReal && bestDistance < 2500) // ~50px radius
        {
            var time = ServerTimeHelper.ConvertForDisplay(DateTime.FromOADate(bestPoint.X), ServerTimeHelper.CurrentDisplayMode);
            string valueFormatted = (bestPoint.Y == Math.Floor(bestPoint.Y))
                ? bestPoint.Y.ToString("N0")
                : bestPoint.Y.ToString("N1");
            _text.Text = $"{bestLabel}\n{valueFormatted} {_unit}\n{time:HH:mm:ss}";
            _popup.HorizontalOffset = pos.X + 15;
            _popup.VerticalOffset = pos.Y + 15;
            /* Toggle if already open so WPF re-evaluates the placement target.
               Without this, a popup that was IsOpen = true when its TabItem was
               unloaded stays "open" with a stale anchor and never appears on
               return — the assignment below is a no-op. */
            if (_popup.IsOpen) _popup.IsOpen = false;
            _popup.IsOpen = true;
        }
        else
        {
            _popup.IsOpen = false;
        }
    }

    private void OnMouseLeave(object sender, MouseEventArgs e)
    {
        _popup.IsOpen = false;
    }
}
