using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using System.Linq;
using Radzen.Blazor.Rendering;
using System.Threading.Tasks;
using System.Net.Mime;
using Microsoft.AspNetCore.Components.Rendering;

namespace Radzen.Blazor
{
    /// <summary>
    /// Base class of <see cref="RadzenChart" /> series.
    /// </summary>
    /// <typeparam name="TItem">The type of the series data.</typeparam>
    [UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2026, Justification = TrimMessages.DataTypePreserved)]
    [UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2087, Justification = TrimMessages.DataTypePreserved)]
    public abstract class CartesianSeries<
        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] TItem> : RadzenChartComponentBase, IChartSeries, IChartValueAxisSeries, IDisposable
    {
        /// <summary>
        /// Cache for the value returned by <see cref="Category"/> when that value is only dependent on
        /// <see cref="CategoryProperty"/>.
        /// </summary>
        Func<TItem, double>? categoryPropertyCache;

        /// <summary>
        /// Returns the parent <see cref="RadzenChart"/> instance or throws an <see cref="InvalidOperationException"/> if not present.
        /// </summary>
        /// <returns>The parent <see cref="RadzenChart"/>.</returns>
        /// <exception cref="InvalidOperationException">Thrown when the parent chart is not set.</exception>
        protected RadzenChart RequireChart()
        {
            return Chart ?? throw new InvalidOperationException($"{GetType().Name} requires a parent RadzenChart.");
        }

        /// <summary>
        /// Creates a getter function that returns a value from the specified category scale for the specified data item.
        /// </summary>
        /// <param name="scale">The scale.</param>
        internal Func<TItem, double> Category(ScaleBase scale)
        {
            if (categoryPropertyCache != null)
            {
                return categoryPropertyCache;
            }

            if (IsNumeric(CategoryProperty))
            {
                categoryPropertyCache = PropertyAccess.Getter<TItem, double>(CategoryProperty!);
                return categoryPropertyCache;
            }

            if (IsDate(CategoryProperty))
            {
                var category = PropertyAccess.Getter<TItem, DateTime>(CategoryProperty!);
                categoryPropertyCache = (item) => category(item).Ticks;
                return categoryPropertyCache;
            }

            if (scale is OrdinalScale ordinal)
            {
                if (String.IsNullOrEmpty(CategoryProperty))
                {
                    return (item) => Items.IndexOf(item);
                }

                var ordinalCategory = PropertyAccess.Getter<TItem, object>(CategoryProperty);

                return (item) => ordinal.Data?.IndexOf(ordinalCategory(item)) ?? -1;
            }

            return (item) => Items.IndexOf(item);
        }

        /// <summary>
        /// Helper function. Used internally.
        /// </summary>
        protected Func<TItem, double> ComposeCategory(ScaleBase scale)
        {
            ArgumentNullException.ThrowIfNull(scale);

            return scale.Compose(Category(scale));
        }

        /// <summary>
        /// Helper function. Used internally.
        /// </summary>
        protected Func<TItem, double> ComposeValue(ScaleBase scale)
        {
            ArgumentNullException.ThrowIfNull(scale);

            return scale.Compose(Value);
        }

        /// <summary>
        /// Returns true if the ValueProperty type is nullable.
        /// </summary>
        protected bool IsValueNullable()
        {
            if (string.IsNullOrEmpty(ValueProperty))
            {
                return false;
            }

            var propertyType = PropertyAccess.GetPropertyType(typeof(TItem), ValueProperty);
            return propertyType != null && Nullable.GetUnderlyingType(propertyType) != null;
        }

        /// <summary>
        /// Creates a function that returns the raw object value of the ValueProperty (before conversion to double).
        /// Used to detect null values in nullable numeric properties.
        /// </summary>
        protected Func<TItem, object?> GetRawValueGetter()
        {
            return PropertyAccess.Getter<TItem, object?>(ValueProperty!);
        }

        /// <summary>
        /// Determines whether the property with the specified name is <see cref="DateTime" />
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns><c>true</c> if the specified property name is date; otherwise, <c>false</c>.</returns>
        /// <exception cref="ArgumentException">Property {propertyName} does not exist</exception>
        protected bool IsDate(string? propertyName)
        {
            if (String.IsNullOrEmpty(propertyName))
            {
                return false;
            }

            var property = PropertyAccess.GetPropertyType(typeof(TItem), propertyName);

            if (property == null)
            {
                throw new ArgumentException($"Property {propertyName} does not exist");
            }

            if(PropertyAccess.IsDateOnly(property))
            {
                return false;
            }
            return PropertyAccess.IsDate(property);
        }

        /// <summary>
        /// Determines whether the property with the specified name is numeric.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns><c>true</c> if the specified property name is numeric; otherwise, <c>false</c>.</returns>
        /// <exception cref="ArgumentException">Property {propertyName} does not exist</exception>
        protected bool IsNumeric(string? propertyName)
        {
            if (String.IsNullOrEmpty(propertyName))
            {
                return false;
            }

            var property = PropertyAccess.GetPropertyType(typeof(TItem), propertyName);

            if (property == null)
            {
                throw new ArgumentException($"Property {propertyName} does not exist");
            }

            return PropertyAccess.IsNumeric(property);
        }

        /// <summary>
        /// Gets or sets the name of the value axis this series is bound to.
        /// When null or empty, the series uses the primary (default) axis.
        /// </summary>
        [Parameter]
        public string? ValueAxisName { get; set; }

        /// <inheritdoc />
        [Parameter]
        public string Title { get; set; } = null!;

        /// <summary>
        /// Gets or sets the child content.
        /// </summary>
        /// <value>The child content.</value>
        [Parameter]
        public RenderFragment? ChildContent { get; set; }

        /// <summary>
        /// Gets or sets the tooltip template.
        /// </summary>
        /// <value>The tooltip template.</value>
        [Parameter]
        public RenderFragment<TItem>? TooltipTemplate { get; set; }

        /// <summary>
        /// Gets the list of overlays.
        /// </summary>
        /// <value>The Overlays list.</value>
        public IList<IChartSeriesOverlay> Overlays { get; } = new List<IChartSeriesOverlay>();

        /// <summary>
        /// Gets the coordinate system of the series.
        /// </summary>
        /// <value>Coordinate system enum value.</value>
        public virtual CoordinateSystem CoordinateSystem => CoordinateSystem.Cartesian;

        /// <summary>
        /// The name of the property of <typeparamref name="TItem" /> that provides the X axis (a.k.a. category axis) values.
        /// </summary>
        [Parameter]
        public string? CategoryProperty { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="CartesianSeries{TItem}"/> is visible.
        /// Invisible series do not appear in the legend and cannot be shown by the user.
        /// Use the <c>Visible</c> property to programatically show or hide a series.
        /// </summary>
        /// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
        [Parameter]
        public bool Visible { get; set; } = true;

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="CartesianSeries{TItem}"/> is hidden.
        /// Hidden series are initially invisible and the user can show them by clicking on their label in the legend.
        /// Use the <c>Hidden</c> property to hide certain series from your users but still allow them to see them.
        /// </summary>
        /// <value><c>true</c> if hidden; otherwise, <c>false</c>.</value>
        [Parameter]
        public bool Hidden { get; set; }

        bool IsVisible { get; set; } = true;

        bool IChartSeries.Visible
        {
            get
            {
                return IsVisible;
            }
        }

        /// <inheritdoc />
        public bool ShowInLegend { get => Visible; }

        /// <summary>
        /// The name of the property of <typeparamref name="TItem" /> that provides the Y axis (a.k.a. value axis) values.
        /// </summary>
        [Parameter]
        public string? ValueProperty { get; set; }

        /// <inheritdoc />
        [Parameter]
        public int RenderingOrder { get; set; }

        /// <summary>
        /// Creates a getter function that returns a value from the specified data item. Uses <see cref="ValueProperty" />.
        /// </summary>
        /// <value>The value.</value>
        /// <exception cref="ArgumentException">ValueProperty should not be empty</exception>
        internal Func<TItem, double> Value
        {
            get
            {
                if (String.IsNullOrEmpty(ValueProperty))
                {
                    throw new ArgumentException("ValueProperty should not be empty");
                }

                return PropertyAccess.Getter<TItem, double>(ValueProperty);
            }
        }

        /// <summary>
        /// Gets or sets the data of the series. The data is enumerated and its items are displayed by the series.
        /// </summary>
        /// <value>The data.</value>
        [Parameter]
        public IEnumerable<TItem>? Data { get; set; }

        /// <summary>
        /// Stores <see cref="Data" /> as an IList of <typeparamref name="TItem"/>.
        /// </summary>
        /// <value>The items.</value>
        protected IList<TItem> Items { get; set; } = new List<TItem>();

        /// <inheritdoc />
        public RadzenMarkers Markers { get; set; } = new RadzenMarkers();

        /// <inheritdoc />
        public virtual MarkerType MarkerType
        {
            get
            {
                var markerType = MarkerType.None;

                if (Markers != null)
                {
                    markerType = Markers.MarkerType;
                }

                if (markerType == MarkerType.Auto && Chart != null)
                {
                    var index = Chart.Series.IndexOf(this);

                    var types = new[] { MarkerType.Circle, MarkerType.Triangle, MarkerType.Square, MarkerType.Diamond };

                    markerType = types[index % types.Length];
                }

                return markerType;
            }
        }

        /// <summary>
        /// Returns the category values
        /// </summary>
        protected virtual IList<object> GetCategories()
        {
            if (String.IsNullOrEmpty(CategoryProperty))
            {
                return Items.Select((item, index) => (object)index).ToList();
            }

            return Items.Select(PropertyAccess.Getter<TItem, object>(CategoryProperty)).ToList();
        }

        /// <inheritdoc />
        public virtual ScaleBase TransformCategoryScale(ScaleBase scale)
        {
            ArgumentNullException.ThrowIfNull(scale);

            if (Items == null)
            {
                return scale;
            }

            if (Items.Any())
            {
                scale.Input.MergeWidth(ScaleRange.From(Items, Category(scale)));
            }

            if (IsNumeric(CategoryProperty))
            {
                return scale;
            }

            if (IsDate(CategoryProperty))
            {
                return new DateScale
                {
                    Input = scale.Input,
                    Output = scale.Output
                };
            }

            var data = GetCategories();

            if (scale is OrdinalScale ordinal && ordinal.Data != null)
            {
                foreach (var item in ordinal.Data)
                {
                    if (!data.Contains(item))
                    {
                        var index = ordinal.Data.IndexOf(item);
                        if (index <= data.Count)
                        {
                            data.Insert(index, item);
                        }
                        else
                        {
                            data.Add(item);
                        }
                    }
                }
            }

            return new OrdinalScale
            {
                Data = data,
                Input = scale.Input,
                Output = scale.Output,
            };
        }

        /// <inheritdoc />
        public virtual ScaleBase TransformValueScale(ScaleBase scale)
        {
            ArgumentNullException.ThrowIfNull(scale);

            if (Items != null)
            {
                IEnumerable<TItem> items = Items;

                if (IsValueNullable())
                {
                    var rawGetter = GetRawValueGetter();
                    items = items.Where(item => rawGetter(item) != null);
                }

                if (items.Any())
                {
                    scale.Input.MergeWidth(ScaleRange.From(items, Value));
                }
            }

            return scale;
        }

        /// <inheritdoc />
        public abstract RenderFragment Render(ScaleBase categoryScale, ScaleBase valueScale);

        /// <inheritdoc />
        public RenderFragment RenderOverlays(ScaleBase categoryScale, ScaleBase valueScale)
        {
            return new RenderFragment(builder =>
            {
                builder.OpenRegion(0);
                foreach (var overlay in Overlays)
                {
                    if (overlay.Visible && !overlay.RenderOnTop)
                    {
                        builder.AddContent(1, overlay.Render(categoryScale, valueScale));
                    }
                }
                builder.CloseRegion();
            });
        }

        /// <inheritdoc />
        public RenderFragment RenderTopOverlays(ScaleBase categoryScale, ScaleBase valueScale)
        {
            return new RenderFragment(builder =>
            {
                builder.OpenRegion(0);
                foreach (var overlay in Overlays)
                {
                    if (overlay.Visible && overlay.RenderOnTop)
                    {
                        builder.AddContent(1, overlay.Render(categoryScale, valueScale));
                    }
                }
                builder.CloseRegion();
            });
        }

        /// <inheritdoc />
        public abstract string Color { get; }

        /// <summary>
        /// Specifies whether the chart highlights the hovered data point of this series with an active point
        /// dot. Range-style series override this to <c>false</c> - a single dot cannot represent their value range.
        /// </summary>
        public virtual bool ShowActivePoint => true;

        /// <inheritdoc />
        public override async Task SetParametersAsync(ParameterView parameters)
        {
            var shouldRefresh = parameters.DidParameterChange(nameof(Data), Data);
            var visibleChanged = parameters.DidParameterChange(nameof(Visible), Visible);
            var hiddenChanged = parameters.DidParameterChange(nameof(Hidden), Hidden);
            var categoryChanged = parameters.DidParameterChange(nameof(CategoryProperty), CategoryProperty);

            await base.SetParametersAsync(parameters);

            if (hiddenChanged)
            {
                IsVisible = !Hidden;
                shouldRefresh = true;
            }

            if (visibleChanged)
            {
                IsVisible = Visible;
                shouldRefresh = true;
            }

            if (categoryChanged || shouldRefresh)
            {
                categoryPropertyCache = null;
            }

            if (Data != null && Data.Count() != Items.Count)
            {
                shouldRefresh = true;
            }

            if (shouldRefresh)
            {
                if (Data != null)
                {
                    if (Data is IList<TItem> list)
                    {
                        Items = list;
                    }
                    else
                    {
                        Items = Data.ToList();
                    }

                    if (!string.IsNullOrEmpty(CategoryProperty) && (IsDate(CategoryProperty) || IsNumeric(CategoryProperty)))
                    {
                        Items = Items.AsQueryable().OrderBy(CategoryProperty).ToList();
                    }
                }

                if (Chart != null)
                {
                    await Chart.Refresh(false);
                }
            }
        }

        /// <inheritdoc />
        protected override void Initialize()
        {
            Chart?.AddSeries(this);
        }

        /// <inheritdoc />
        public virtual bool Contains(double x, double y, double tolerance)
        {
            return false;
        }

        /// <inheritdoc />
        public virtual double MeasureLegend()
        {
            return TextMeasurer.TextWidth(GetTitle());
        }

        /// <inheritdoc />
        public virtual IEnumerable<double> MeasureLegendItems()
        {
            return new[] { TextMeasurer.TextWidth(GetTitle()) };
        }

        /// <summary>
        /// Determines if the provided point is inside the provided polygon.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="polygon">The polygon.</param>
        /// <returns><c>true</c> if the polygon contains the point, <c>false</c> otherwise.</returns>
        protected bool InsidePolygon(Point point, Point[] polygon)
        {
            ArgumentNullException.ThrowIfNull(point);
            ArgumentNullException.ThrowIfNull(polygon);

            var minX = polygon[0].X;
            var maxX = polygon[0].X;
            var minY = polygon[0].Y;
            var maxY = polygon[0].Y;

            for (var i = 1; i < polygon.Length; i++)
            {
                var current = polygon[i];
                minX = Math.Min(current.X, minX);
                maxX = Math.Max(current.X, maxX);
                minY = Math.Min(current.Y, minY);
                maxY = Math.Max(current.Y, maxY);
            }

            if (point.X < minX || point.X > maxX || point.Y < minY || point.Y > maxY)
            {
                return false;
            }

            bool inside = false;

            for (int i = 0, j = polygon.Length - 1; i < polygon.Length; j = i++)
            {
                if ((polygon[i].Y > point.Y) != (polygon[j].Y > point.Y) &&
                     point.X < (polygon[j].X - polygon[i].X) * (point.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X)
                {
                    inside = !inside;
                }
            }

            return inside;
        }

        /// <inheritdoc />
        public virtual RenderFragment RenderTooltip(object data)
        {
            var chart = RequireChart();
            var item = (TItem)data;

            return builder =>
            {
                if (chart.Tooltip.Shared)
                {
                    var category = !string.IsNullOrEmpty(CategoryProperty) ? PropertyAccess.GetValue(item, CategoryProperty) : null;
                    if (category != null)
                    {
                        builder.OpenComponent<ChartSharedTooltip>(0);
                        builder.AddAttribute(1, nameof(ChartSharedTooltip.Class), TooltipClass(item));
                        builder.AddAttribute(2, nameof(ChartSharedTooltip.Title), TooltipTitle(item));
                        builder.AddAttribute(3, nameof(ChartSharedTooltip.ChildContent), RenderSharedTooltipItems(category));
                        builder.CloseComponent();
                    }
                }
                else
                {
                    builder.OpenComponent<ChartTooltip>(0);
                    builder.AddAttribute(1, nameof(ChartTooltip.ChildContent), TooltipTemplate?.Invoke(item));
                    builder.AddAttribute(2, nameof(ChartTooltip.Title), TooltipTitle(item));
                    builder.AddAttribute(3, nameof(ChartTooltip.Label), TooltipLabel(item));
                    builder.AddAttribute(4, nameof(ChartTooltip.Value), TooltipValue(item));
                    builder.AddAttribute(5, nameof(ChartTooltip.Class), TooltipClass(item));
                    builder.AddAttribute(6, nameof(ChartTooltip.Style), TooltipStyle(item));
                    builder.CloseComponent();
                }
            };
        }

        private RenderFragment RenderSharedTooltipItems(object category)
        {
            var chart = RequireChart();

            return builder =>
            {
                var visibleSeries = chart.Series.Where(s => s.Visible).ToList();

                foreach (var series in visibleSeries)
                {
                    builder.AddContent(1, series.RenderSharedTooltipItem(category));
                }
            };
        }

        /// <inheritdoc />
        public virtual RenderFragment RenderSharedTooltipItem(object category)
        {
            return builder =>
            {
                var item = Items.FirstOrDefault(i => !string.IsNullOrEmpty(CategoryProperty) && object.Equals(PropertyAccess.GetValue(i, CategoryProperty), category));

                if (item != null)
                {
                    builder.OpenComponent<ChartSharedTooltipItem>(0);
                    builder.AddAttribute(1, nameof(ChartSharedTooltipItem.Value), TooltipValue(item));
                    builder.AddAttribute(2, nameof(ChartSharedTooltipItem.ChildContent), TooltipTemplate?.Invoke(item));
                    builder.AddAttribute(3, nameof(ChartSharedTooltipItem.LegendItem), RenderLegendItem(false));
                    builder.CloseComponent();
                }
            };
        }

        /// <inheritdoc />
        public Point GetTooltipPosition(object data)
        {
            var item = (TItem)data;
            var x = TooltipX(item);
            var y = TooltipY(item);

            return new Point { X = x, Y = y };
        }

        /// <summary>
        /// Gets the tooltip inline style.
        /// </summary>
        /// <param name="item">The item.</param>
        protected virtual string TooltipStyle(TItem item)
        {
            return Chart?.Tooltip?.Style ?? string.Empty;
        }

        /// <summary>
        /// Gets the tooltip CSS class.
        /// </summary>
        /// <param name="item">The item.</param>
        protected virtual string TooltipClass(TItem item)
        {
            var chart = Chart;
            if (chart == null)
            {
                return "rz-series-tooltip";
            }

            return $"rz-series-{chart.Series.IndexOf(this)}-tooltip";
        }

        /// <inheritdoc />
        public virtual RenderFragment RenderLegendItem()
        {
            return RenderLegendItem(true);
        }

        /// <summary>
        /// Renders the legend item for this series.
        /// </summary>
        protected virtual RenderFragment RenderLegendItem(bool clickable)
        {
            var chart = RequireChart();
            var index = chart.Series.IndexOf(this);
            var style = new List<string>();

            if (IsVisible == false)
            {
                style.Add("text-decoration: line-through");
            }

            return builder =>
            {
                builder.OpenComponent<LegendItem>(0);
                builder.AddAttribute(1, nameof(LegendItem.Index), index);
                builder.AddAttribute(2, nameof(LegendItem.Color), Color);
                builder.AddAttribute(3, nameof(LegendItem.MarkerType), MarkerType);
                builder.AddAttribute(4, nameof(LegendItem.Style), string.Join(";", style));
                builder.AddAttribute(5, nameof(LegendItem.MarkerSize), MarkerSize);
                builder.AddAttribute(6, nameof(LegendItem.Text), GetTitle());
                builder.AddAttribute(7, nameof(LegendItem.Click), EventCallback.Factory.Create(this, OnLegendItemClick));
                builder.AddAttribute(8, nameof(LegendItem.Clickable), clickable);
                builder.AddAttribute(9, nameof(LegendItem.ShowLine), ShowLineInLegend);
                builder.CloseComponent();
            };
        }

        /// <summary>
        /// Specifies whether the legend swatch displays a line indicator instead of a filled marker. Overridden by line series.
        /// </summary>
        protected internal virtual bool ShowLineInLegend => false;

        /// <inheritdoc />
        public double MarkerSize
        {
            get
            {
                double markerSize = 5;

                if (Markers != null)
                {
                    markerSize = Markers.Size;
                }

                return markerSize;
            }
        }

        /// <inheritdoc />
        public double GetMedian()
        {
            var values = Items.Select(Value).OrderBy(e => e).ToList();
            if (values.Count == 0)
            {
                return 0;
            }

            return values[values.Count / 2];
        }

        /// <inheritdoc />
        public double GetMean()
        {
            return Items.Any() ? Items.Select(Value).Average() : double.NaN;
        }

        /// <inheritdoc />
        public double GetMode()
        {
            if (!Items.Any())
            {
                return double.NaN;
            }

            return Items
                .GroupBy(item => Value(item))
                .Select(g => new { Value = g.Key, Count = g.Count() })
                .OrderByDescending(e => e.Count)
                .First()
                .Value;
        }

        /// <summary>
        /// https://en.wikipedia.org/wiki/Simple_linear_regression#Fitting_the_regression_line
        /// </summary>
        public (double a, double b) GetTrend()
        {
            double a = double.NaN, b = double.NaN;

            var chart = Chart;
            if (chart == null)
            {
                return (a, b);
            }

            if (Items.Any())
            {
                Func<TItem, double> X;
                Func<TItem, double> Y;
                var vs = chart.GetValueScale(ValueAxisName);
                if (chart.ShouldInvertAxes())
                {
                    var categoryAccessor = Category(vs);
                    X = e => chart.CategoryScale.Scale(Value(e));
                    Y = e => vs.Scale(categoryAccessor(e));
                }
                else
                {
                    var categoryAccessor = Category(chart.CategoryScale);
                    X = e => chart.CategoryScale.Scale(categoryAccessor(e));
                    Y = e => vs.Scale(Value(e));
                }

                var data = Items.ToList();
                var avgX = data.Select(e => X(e)).Average();
                var avgY = data.Select(e => Y(e)).Average();
                var sumXY = data.Sum(e => (X(e) - avgX) * (Y(e) - avgY));
                if (chart.ShouldInvertAxes())
                {
                    var sumYSq = data.Sum(e => (Y(e) - avgY) * (Y(e) - avgY));
                    b = sumXY / sumYSq;
                    a = avgX - b * avgY;
                }
                else
                {
                    var sumXSq = data.Sum(e => (X(e) - avgX) * (X(e) - avgX));
                    b = sumXY / sumXSq;
                    a = avgY - b * avgX;
                }
            }

            return (a, b);
        }

        /// <inheritdoc />
        public IList<Point> GetScaledDataPoints()
        {
            var result = new List<Point>();
            var chart = Chart;
            if (chart == null || Items == null || !Items.Any())
            {
                return result;
            }

            var vs = chart.GetValueScale(ValueAxisName);
            if (chart.ShouldInvertAxes())
            {
                var categoryAccessor = Category(vs);
                foreach (var item in Items)
                {
                    var px = chart.CategoryScale.Scale(Value(item));
                    var py = vs.Scale(categoryAccessor(item));
                    result.Add(new Point { X = px, Y = py });
                }
            }
            else
            {
                var categoryAccessor = Category(chart.CategoryScale);
                foreach (var item in Items)
                {
                    var px = chart.CategoryScale.Scale(categoryAccessor(item));
                    var py = vs.Scale(Value(item));
                    result.Add(new Point { X = px, Y = py });
                }
            }

            return result;
        }

        private async Task OnLegendItemClick()
        {
            IsVisible = !IsVisible;

            var chart = Chart;

            if (chart?.LegendClick.HasDelegate == true)
            {
                var args = new LegendClickEventArgs
                {
                    Data = this.Data,
                    Title = GetTitle(),
                    IsVisible = IsVisible,
                };

                await chart.LegendClick.InvokeAsync(args);

                IsVisible = args.IsVisible;
            }

            if (chart != null)
            {
                await chart.Refresh();
            }
        }

        /// <inheritdoc />
        public string GetTitle()
        {
            var chart = Chart;
            if (string.IsNullOrEmpty(Title))
            {
                var index = chart?.Series.IndexOf(this) ?? 0;
                return $"Series {index + 1}";
            }

            return Title;
        }

        /// <summary>
        /// Gets the label of the tooltip displayed for this item.
        /// </summary>
        /// <param name="item">The item.</param>
        protected virtual string TooltipLabel(TItem item)
        {
            return GetTitle();
        }

        /// <summary>
        /// Gets the title of the tooltip displayed for this item.
        /// </summary>
        /// <param name="item">The item.</param>
        protected virtual string TooltipTitle(TItem item)
        {
            var chart = RequireChart();
            var category = Category(chart.CategoryScale);
            return chart.CategoryAxis.Format(chart.CategoryScale, chart.CategoryScale.Value(category(item)));
        }

        /// <summary>
        /// Gets the value of the tooltip displayed for this item.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>System.String.</returns>
        protected virtual string TooltipValue(TItem item)
        {
            var chart = RequireChart();
            var valueAxis = chart.GetValueAxis(ValueAxisName);
            var valueScale = chart.GetValueScale(ValueAxisName);
            return valueAxis.Format(valueScale, valueScale.Value(Value(item)));
        }

        /// <summary>
        /// Gets the X coordinate of the tooltip of the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        internal virtual double TooltipX(TItem item)
        {
            var chart = RequireChart();
            var category = Category(chart.CategoryScale);
            return chart.CategoryScale.Scale(category(item), true);
        }

        /// <summary>
        /// Gets the Y coordinate of the tooltip of the specified item.
        /// </summary>
        /// <param name="item">The item.</param>
        internal virtual double TooltipY(TItem item)
        {
            var chart = RequireChart();
            return chart.GetValueScale(ValueAxisName).Scale(Value(item), true);
        }

        /// <inheritdoc />
        public virtual (object, Point) DataAt(double x, double y)
        {
            if (Items.Any())
            {
                var retObject = Items.Select(item =>
                {
                    var distance = Math.Abs(TooltipX(item) - x);
                    return new { Item = item, Distance = distance };
                }).Aggregate((a, b) => a.Distance < b.Distance ? a : b).Item;

                return (retObject!,
                    new Point() { X = TooltipX(retObject), Y = TooltipY(retObject)});
            }

            return (default!, new Point());
        }

        /// <inheritdoc />
        public virtual IEnumerable<ChartDataLabel> GetDataLabels(double offsetX, double offsetY)
        {
            var chart = RequireChart();
            var list = new List<ChartDataLabel>();

            foreach (var d in Items)
            {
                var value = Value(d);
                list.Add(new ChartDataLabel
                {
                    Position = new Point { X = TooltipX(d) + offsetX, Y = TooltipY(d) + offsetY },
                    Anchor = new Point { X = TooltipX(d), Y = TooltipY(d) },
                    Value = value,
                    TextAnchor = "middle",
                    Text = chart.GetValueAxis(ValueAxisName).Format(chart.GetValueScale(ValueAxisName), value)
                });
            }

            return list;
        }

        /// <inheritdoc />
        public virtual IEnumerable<ChartDataLabel> GetDataLabels(double offsetX, double offsetY, DataLabelPosition position)
        {
            if (position == DataLabelPosition.Auto)
            {
                return GetDataLabels(offsetX, offsetY);
            }

            var chart = RequireChart();
            var list = new List<ChartDataLabel>();
            const double gap = 16;

            foreach (var d in Items)
            {
                var anchorX = TooltipX(d);
                var anchorY = TooltipY(d);
                var y = position switch
                {
                    DataLabelPosition.Top => anchorY - gap,
                    DataLabelPosition.Bottom => anchorY + gap,
                    _ => anchorY,
                };
                var value = Value(d);

                list.Add(new ChartDataLabel
                {
                    Position = new Point { X = anchorX + offsetX, Y = y + offsetY },
                    Anchor = new Point { X = anchorX, Y = anchorY },
                    Value = value,
                    TextAnchor = "middle",
                    Text = chart.GetValueAxis(ValueAxisName).Format(chart.GetValueScale(ValueAxisName), value)
                });
            }

            return list;
        }

        /// <summary>
        /// Returns a color from the specified list of colors. Rotates colors.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="colors">The colors.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <param name="colorRange">The color range value.</param>
        /// <param name="value">The value of the item.</param>
        protected string? PickColor(int index, IEnumerable<string>? colors, string? defaultValue = null, IList<SeriesColorRange>? colorRange = null, double value = 0.0)
        {
            if (colorRange != null)
            {
                var result = colorRange.Where(r => r.Min <= value && r.Max >= value).FirstOrDefault<SeriesColorRange>();
                return result?.Color ?? defaultValue;
            }
            else
            {
                if (colors == null || !colors.Any())
                {
                    return defaultValue;
                }

                return colors.ElementAt(index % colors.Count());
            }
        }
        /// <inheritdoc />
        public void Dispose()
        {
            Chart?.RemoveSeries(this);
        }

        /// <inheritdoc />
        public async Task InvokeClick(EventCallback<SeriesClickEventArgs> handler, object data)
        {
            var chart = RequireChart();
            var category = Category(chart.CategoryScale);
            var dataItem = (TItem)data;

            await handler.InvokeAsync(new SeriesClickEventArgs
            {
                Data = data,
                Title = GetTitle(),
                Category = !string.IsNullOrEmpty(CategoryProperty) ? PropertyAccess.GetValue(data, CategoryProperty) : null,
                Value = !string.IsNullOrEmpty(ValueProperty) ? PropertyAccess.GetValue(data, ValueProperty) : null,
                Point = new SeriesPoint
                {
                    Category = category(dataItem),
                    Value = Value(dataItem)
                }
            });
        }
    }
}