using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using C1.Silverlight.Chart;

namespace InvExplorer
{
    public class ChartCursor : Grid
    {
        // ** fields
        C1Chart _chart;
        DateTime _dtLast = DateTime.MinValue;
        Rectangle _rc;
        TranslateTransform _tt;
        Storyboard _sb;

        // ** ctor
        public ChartCursor()
        {
            // let the chart handle the mouse
            IsHitTestVisible = false;

            // cursor line
            _rc = new Rectangle();
            _rc.VerticalAlignment = VerticalAlignment.Stretch;
            _rc.HorizontalAlignment = HorizontalAlignment.Left;
            _rc.Fill = new SolidColorBrush(Colors.Gray);

            _rc.Width = 0.5;
            Children.Add(_rc);

            // translate transform to position this element over the chart
            _tt = new TranslateTransform();
            RenderTransform = _tt;

            // storyboard to update chart legend (sloooow)
            _sb = new Storyboard();
            _sb.Duration = new Duration(TimeSpan.FromMilliseconds(50));
            _sb.Completed += _sb_Completed;
        }

        // ** object model

        /// <summary>
        /// Gets or sets the C1Chart that owns this cursor.
        /// </summary>
        public C1Chart Chart
        {
            get { return (C1Chart)GetValue(ChartProperty); }
            set { SetValue(ChartProperty, value); }
        }
        /// <summary>
        /// Identifies the <see cref="Chart"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty ChartProperty =
            DependencyProperty.Register(
                "MinDate", typeof(C1Chart), typeof(ChartCursor),
                new PropertyMetadata(OnPropertyChanged));

        // ** event handlers
        void _chart_MouseLeave(object sender, MouseEventArgs e)
        {
            _dtLast = DateTime.MinValue;
            SetVisibility(Visibility.Collapsed);
        }
        void _chart_MouseMove(object sender, MouseEventArgs e)
        {
            // get position
            var pt = e.GetPosition(_chart);
            var ptData = _chart.View.PointToData(pt);

            // bad X? hide and leave
            if (double.IsNaN(ptData.X))
            {
                SetVisibility(Visibility.Collapsed);
                return;
            }

            // constrain the cursor to the earliest date
            var date = DateTime.MaxValue;

            // look for values at that date
            var visibility = Visibility.Collapsed;
            foreach (XYDataSeries ds in _chart.Data.Children)
            {
                // remove custom label if any
                ds.Tag = null;

                // find data point near the mouse
                int index = BinarySearch(ds.XValues, ptData.X);
                if (index > -1)
                {
                    // update label for this series
                    var value = ds.Values[index];
                    var fmt = _chart.View.AxisY.AnnoFormat;
                    if (fmt.IndexOf('#') < 0)
                    {
                        fmt = fmt.Replace('0', '2');
                    }
                    var text = value.ToString(fmt);
                    ds.Tag = string.Format("{0}:{1}", ds.Name, text);

                    // constrain cursor
                    var xvalue = ds.XValues[index];
                    var dt = DateTime.FromOADate(xvalue);
                    if (dt < date)
                    {
                        date = dt;
                        pt = _chart.View.PointFromData(new Point(xvalue, value));
                    }

                    // show cursor and values
                    visibility = Visibility.Visible;
                }
            }

            // update if the mouse actually moved
            if (_dtLast != date)
            {
                // save last date
                _dtLast = date;

                // update cursor position
                var rc = _chart.View.PlotRect;
                _rc.Height = rc.Height;
                _tt.Y = rc.Top;
                _tt.X = pt.X;

                // update visibility and legends
                SetVisibility(visibility);
            }
        }

        // ** implementation

        // handle property changes (setting the Chart property)
        static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var cc = d as ChartCursor;
            cc.OnPropertyChanged(e);
        }
        void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            if (e.Property == ChartProperty)
            {
                if (_chart != null)
                {
                    _chart.MouseLeave -= _chart_MouseLeave;
                    _chart.MouseMove -= _chart_MouseMove;
                }
                _chart = Chart;
                if (_chart != null)
                {
                    _chart.MouseLeave += _chart_MouseLeave;
                    _chart.MouseMove += _chart_MouseMove;

                    // make marker brush match chart grlidline brush
                    _rc.Fill = _chart.View.AxisY.MajorGridStroke;
                }
            }
        }

        // show/hide the cursor and update the legends
        void SetVisibility(Visibility visibility)
        {
            // update visibility
            if (visibility != this.Visibility)
            {
                // update cursor visibility
                this.Visibility = visibility;

                // restore legend (no values)
                if (visibility == Visibility.Collapsed)
                {
                    foreach (var ds in _chart.Data.Children)
                    {
                        ds.Tag = null;
                    }
                }
            }

            // update the legends
            _sb.Stop();
            _sb.Seek(TimeSpan.Zero);
            _sb.Begin();
        }
        void _sb_Completed(object sender, EventArgs e)
        {
            foreach (var ds in _chart.Data.Children)
            {
                var label = ds.Tag as string;
                ds.Label = string.IsNullOrEmpty(label) ? ds.Name : label;
            }
        }

        // finds a value along the X axis
        int BinarySearch(DoubleCollection values, double value)
        {
            // we need some values...
            if (values.Count <= 0)
            {
                return -1;
            }

            // initialize bounds
            var lo = 0;
            var hi = values.Count - 1;

            // handle values in descending  order
            if (values[hi] < values[lo])
            {
                // not in range
                if (value < values[hi]) return hi;
                if (value > values[lo]) return lo;

                // search
                while (hi - lo > 1)
                {
                    var mid = (lo + hi) / 2;
                    var x = values[mid];
                    if (x > value)
                    {
                        lo = mid;
                    }
                    else if (x < value)
                    {
                        hi = mid;
                    }
                    else
                    {
                        return mid;
                    }
                }
                return Math.Abs(values[hi] - value) < Math.Abs(values[lo] - value) ? hi : lo;
            }
            else // handle values in ascending order
            {
                // not in range
                if (value > values[hi]) return hi;
                if (value < values[lo]) return lo;

                // search
                while (hi - lo > 1)
                {
                    var mid = (lo + hi) / 2;
                    var x = values[mid];
                    if (x < value)
                    {
                        lo = mid;
                    }
                    else if (x > value)
                    {
                        hi = mid;
                    }
                    else
                    {
                        return mid;
                    }
                }
                return Math.Abs(values[hi] - value) < Math.Abs(values[lo] - value) ? hi : lo;
            }
        }
    }
}
