using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing.Drawing2D;

namespace JtkManagerClient.Controls {
    public partial class GraphView : UserControl {
        public GraphView() {
            InitializeComponent();
        }

        Color axisColor = Color.Black;
        [DefaultValue(typeof(Color), "Black")]
        public Color AxisColor {
            get => axisColor; set { axisColor = value; Invalidate(); }
        }

        Color gridColor = Color.Gray;
        [DefaultValue(typeof(Color), "Gray")]
        public Color GridColor {
            get => gridColor; set { gridColor = value; Invalidate(); }
        }
        
        bool gridLines = true;
        [DefaultValue(true)]
        public bool GridLines {
            get => gridLines; set { gridLines = value; Invalidate(); }
        }

        Dictionary<Color, (double x, double y, string text)[]> data;
        public Dictionary<Color, (double x, double y, string text)[]> Data {
            get => data; set { data = value; Invalidate(); }
        }

        Padding outerAxisPadding = new Padding(30, 20, 20, 20);
        [DefaultValue(typeof(Padding), "30, 20, 20, 20")]
        public Padding OuterAxisPadding {
            get => outerAxisPadding; set { outerAxisPadding = value; Invalidate(); }
        }
        
        Padding innerAxisPadding = Padding.Empty;
        [DefaultValue(typeof(Padding), "0, 0, 0, 0")]
        public Padding InnerAxisPadding {
            get => innerAxisPadding; set { innerAxisPadding = value; Invalidate(); }
        }

        double? xAxisMin = null;
        [DefaultValue(null)]
        public double? XAxisMin {
            get => xAxisMin; set { xAxisMin = value; Invalidate(); }
        }

        double? xAxisMax = null;
        [DefaultValue(null)]
        public double? XAxisMax {
            get => xAxisMax; set { xAxisMax = value; Invalidate(); }
        }

        double? xAxisUnit = null;
        [DefaultValue(null)]
        public double? XAxisUnit {
            get => xAxisUnit; set { xAxisUnit = value; Invalidate(); }
        }

        double? yAxisMin = null;
        [DefaultValue(null)]
        public double? YAxisMin {
            get => yAxisMin; set { yAxisMin = value; Invalidate(); }
        }

        double? yAxisMax = null;
        [DefaultValue(null)]
        public double? YAxisMax {
            get => yAxisMax; set { yAxisMax = value; Invalidate(); }
        }

        double? yAxisUnit = null;
        [DefaultValue(null)]
        public double? YAxisUnit {
            get => yAxisUnit; set { yAxisUnit = value; Invalidate(); }
        }

        public Func<double, string> XAxisConverter;
        public Func<double, string> YAxisConverter;

        protected override void OnMouseMove(MouseEventArgs e) {
            base.OnMouseMove(e);
            Invalidate();
        }

        protected override void OnResize(EventArgs e) {
            base.OnResize(e);
            Invalidate();
        }

        protected override void OnPaint(PaintEventArgs e) {
            base.OnPaint(e);

            var g = e.Graphics;
            g.SmoothingMode = SmoothingMode.AntiAlias;

            // Layout
            var scale = g.DpiX / 96f;
            var outerRect = new RectangleF(
                x: scale * (Padding.Left + OuterAxisPadding.Left),
                y: scale * (Padding.Top + OuterAxisPadding.Top),
                width: Width - scale * (Padding.Right + OuterAxisPadding.Right + (Padding.Left + OuterAxisPadding.Left)),
                height: Height - scale * (Padding.Bottom + OuterAxisPadding.Bottom + (Padding.Top + OuterAxisPadding.Top)));
            var innerRect = new RectangleF(
                x: scale * (Padding.Left + OuterAxisPadding.Left + InnerAxisPadding.Left),
                y: scale * (Padding.Top + OuterAxisPadding.Top + InnerAxisPadding.Top),
                width: Width - scale * (Padding.Right + OuterAxisPadding.Right + InnerAxisPadding.Right + (Padding.Left + OuterAxisPadding.Left + InnerAxisPadding.Left)),
                height: Height - scale * (Padding.Bottom + OuterAxisPadding.Bottom + InnerAxisPadding.Bottom + (Padding.Top + OuterAxisPadding.Top + InnerAxisPadding.Top)));

            // Brushes and pens
            var axisPen = new Pen(AxisColor);
            var axisBrush = new SolidBrush(AxisColor);
            var gridPen = new Pen(GridColor);
            var gridBrush = new SolidBrush(GridColor);
            
            // Draw Design Mode Border
            if (DesignMode) {
                g.DrawLines(
                    pen: new Pen(Brushes.Black, 1f) { DashStyle = DashStyle.Dash },
                    points: new Point[] {
                        new Point(0, 0),
                        new Point(Width-1, 0),
                        new Point(Width-1, Height-1),
                        new Point(0, Height-1),
                        new Point(0, 0)
                    }
                );
            }

            // Draw Axis Lines
            g.DrawLines(axisPen, new PointF[] {
                new Point((int)outerRect.Left, (int)outerRect.Top),
                new Point((int)outerRect.Left, (int)outerRect.Bottom),
                new Point((int)outerRect.Right, (int)outerRect.Bottom)
            });
            
            if (data?.Count > 0) {
                double xAxisMin = 0;
                double xAxisMax = 0;
                double xAxisUnit = 0;
                double yAxisMin = 0;
                double yAxisMax = 0;
                double yAxisUnit = 0;
                double? xMin = null;
                double? xMax = null;
                double? yMin = null;
                double? yMax = null;
                foreach (var pair in data) {
                    var xValues = from item in pair.Value select item.x;
                    var xValuesMax = xValues.Max();
                    var xValuesMin = xValues.Min();
                    if (!xMin.HasValue || xValuesMin < xMin.Value) {
                        xMin = xValuesMin;
                    }
                    if (!xMax.HasValue || xValuesMax > xMax.Value) {
                        xMax = xValuesMax;
                    }

                    var yValues = from item in pair.Value select item.y;
                    var yValuesMin = yValues.Min();
                    var yValuesMax = yValues.Max();
                    if (!yMin.HasValue || yValuesMin < yMin.Value) {
                        yMin = yValuesMin;
                    }
                    if (!yMax.HasValue || yValuesMax > yMax.Value) {
                        yMax = yValuesMax;
                    }
                }
                var xCount = (int)(Width / scale / 40);
                xAxisMin = XAxisMin.HasValue ? XAxisMin.Value : xMin.Value;
                xAxisMax = XAxisMax.HasValue ? XAxisMax.Value : xMax.Value;
                xAxisUnit = XAxisUnit.HasValue ? XAxisUnit.Value : Math.Ceiling((xAxisMax - xAxisMin) / xCount);
                var yCount = (int)(Height / scale / 20) - 2;
                yAxisMin = YAxisMin.HasValue ? YAxisMin.Value : yMin.Value;
                yAxisMax = YAxisMax.HasValue ? YAxisMax.Value : yMax.Value;
                yAxisUnit = YAxisUnit.HasValue ? YAxisUnit.Value : (yAxisMax - yAxisMin) / yCount;
                if (!YAxisUnit.HasValue) {
                    if (!YAxisMin.HasValue) {
                        yAxisMin -= YAxisMax.HasValue ? yAxisUnit : yAxisUnit / 2f;
                    }
                    if (!YAxisMax.HasValue) {
                        yAxisMax += YAxisMin.HasValue ? yAxisUnit : yAxisUnit / 2f;
                    }
                }

                // Draw Grid Lines
                try {
                    var drawCount = 0;
                    for (var x = xAxisMin; x <= xAxisMax/* * 1.000001*/; x += xAxisUnit) {
                        if (drawCount++ > 1000) break;
                        var left = innerRect.Left + (x - xAxisMin) / (xAxisMax - xAxisMin) * innerRect.Width;
                        if (left != outerRect.Left && GridLines == true) {
                            g.DrawLine(gridPen, (int)left, (int)outerRect.Top, (int)left, (int)outerRect.Bottom);
                        }
                        var text = XAxisConverter?.Invoke(x) ?? x.ToString("N0");
                        var textSize = g.MeasureString(text, Font);
                        g.DrawString(text, Font, axisBrush, (float)(left - textSize.Width / 2), outerRect.Bottom);

                    }
                    for (var y = yAxisMin; y <= yAxisMax/* * 1.000001*/; y += yAxisUnit) {
                        if (drawCount++ > 1000) break;
                        var top = innerRect.Bottom - (y - yAxisMin) / (yAxisMax - yAxisMin) * innerRect.Height;
                        if (top != outerRect.Bottom && GridLines == true) {
                            g.DrawLine(gridPen, (int)outerRect.Left, (int)top, (int)outerRect.Right, (int)top);
                        }
                        var text = YAxisConverter?.Invoke(y) ?? y.ToString("N0");
                        var textSize = g.MeasureString(text, Font);
                        g.DrawString(text, Font, axisBrush, outerRect.Left - textSize.Width, (float)(top - textSize.Height / 2));
                    }
                } catch {

                }

                // Draw Data
                var mousePoint = PointToClient(MousePosition);
                (PointF drawingPoint, string text, Color color, float lengthToMouse)? nearestItem = null;
                foreach (var pair in data) {
                    if (pair.Value.Length < 2) {
                        continue;
                    }
                    var color = pair.Key;
                    var xOrderedDrawingItems = from item in pair.Value orderby item.x select (point: new PointF((float)(innerRect.Left + (item.x - xAxisMin) / (xAxisMax - xAxisMin) * innerRect.Width),(float)(innerRect.Bottom - (item.y - yAxisMin) / (yAxisMax - yAxisMin) * innerRect.Height)), item.text);
                    g.DrawLines(new Pen(color) { Width = 1.5f * scale }, xOrderedDrawingItems.Select(item => item.point).ToArray());
                    var brush = new SolidBrush(color);
                    var brush2 = new SolidBrush(BackColor);
                    foreach (var item in xOrderedDrawingItems) {
                        g.FillEllipse(brush, item.point.X - 3f * scale, item.point.Y - 3f * scale, 6f * scale, 6f * scale);
                        g.FillEllipse(brush2, item.point.X - 1.5f * scale, item.point.Y - 1.5f * scale, 3f * scale, 3f * scale);
                        var lengthToMouse = (float)Math.Sqrt(Math.Pow(mousePoint.X - item.point.X, 2) + Math.Pow(mousePoint.Y - item.point.Y, 2));
                        if (!nearestItem.HasValue || lengthToMouse < nearestItem.Value.lengthToMouse) {
                            nearestItem = (item.point, item.text, color, lengthToMouse);
                        }
                    }
                }
                if (nearestItem.HasValue && innerRect.Contains(mousePoint)) {
                    var color = nearestItem.Value.color;
                    var point = nearestItem.Value.drawingPoint;
                    var text = nearestItem.Value.text;
                    //g.DrawLine(new Pen(color), point.X, outerRect.Top, point.X, outerRect.Bottom);
                    var textSize = g.MeasureString(text, Font);
                    var textRect = new RectangleF(point, textSize);
                    if (point.X + textSize.Width > Width) {
                        textRect = new RectangleF(textRect.X - textSize.Width, textRect.Y, textSize.Width, textSize.Height);
                    }
                    if (point.Y + textSize.Height > Height) {
                        textRect = new RectangleF(textRect.X, textRect.Y - textSize.Height, textSize.Width, textSize.Height);
                    }
                    g.FillRectangle(new SolidBrush(color), RectangleF.Inflate(textRect, 1.5f*scale, 1.5f*scale));
                    g.FillRectangle(Brushes.White, textRect);
                    g.DrawString(text, Font, new SolidBrush(color), textRect.Location);
                }
            }
        }
    }
}
