#region << Using Directives >>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using Volpe.Cafe.EspressoUI.Utils;
using Volpe.Cafe.Utils;
#endregion

namespace Volpe.Cafe.EspressoUI.Charts
{
    /// <summary>
    /// Provides the base class for drawing charts of various styles.
    /// </summary>
    [Serializable]
    partial class Chart : UserControl
    {

        #region /*** Events ***/

        /// <summary>
        /// Occurs after the chart's layout has been updated.
        /// </summary>
        public event EventHandler LayoutUpdated;
        /// <summary>
        /// Occurs when the chart's view needs to change as a result of a mouse input, a keyboard's arrow key press, or from the
        /// chart's context menu.
        /// </summary>
        public event ChangeViewEventHandler ChangeView;

        #endregion

        #region /*** Ctors ***/

        /// <summary>
        /// Initializes a new instance of the <see cref="Chart"/> class.
        /// </summary>
        public Chart()
        {
            base.SetStyle(ControlStyles.Selectable, true);
            //
            this.InitializeAnimationAndTrackingEffects();
            this.InitializeLayout();
            this.InitializeComponent();
            this.InitializeOther();
        }

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true, if managed resources should be disposed; false, otherwise.</param>
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }

        #endregion

        #region /*** Methods ***/

        #region /* Designer Generated Code */

        void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // Chart
            // 
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
            this.DoubleBuffered = true;
            this.Margin = new System.Windows.Forms.Padding(4);
            this.Name = "Chart";
            this.Size = new System.Drawing.Size(400, 400);
            this.ResumeLayout(false);

        }

        #endregion

        #region /* Layout Initialization and Updating */

        void InitializeLayout()
        {
            this.TitleHeight = MinTitleHeight;
            //
            this.Grid      = new GridInfo  (this);
            this.Legend    = new LegendInfo(this);
            this.ChartAxes = new AxisInfo[] { new AxisInfo(this, AxisType.Category ),
                                              new AxisInfo(this, AxisType.Primary  ),
                                              new AxisInfo(this, AxisType.Secondary) };
            this.Series    = new SeriesInfo(this);
            //
            // the layout of chart's axis will also need to be updated whenever the grid's or legend's layout is updated
            this.Grid  .LayoutUpdated += new EventHandler(this.GridOrLegend_LayoutUpdated);
            this.Legend.LayoutUpdated += new EventHandler(this.GridOrLegend_LayoutUpdated);
            this.Series.LayoutUpdated += new EventHandler(this.ChartElements_LayoutUpdated);
            foreach (AxisInfo axis in this.ChartAxes)
            {
                axis.LayoutUpdated += new EventHandler(this.ChartElements_LayoutUpdated);
            }
            //
            this.UpdateLayout(true);
        }
        void UpdateLayout(bool forceFontRefresh)
        {
            // update fonts
            this.RefreshFonts(forceFontRefresh);
            // update title
            this.UpdateTitleLayout();
            // update chart parts
            this.Grid  .SetDirty(false);
            this.Legend.SetDirty(false);
            foreach (AxisInfo axis in this.ChartAxes)
            {
                axis.SetDirty(false);
            }
            this.Series.SetDirty(false);
            this.Invalidate();
            // raise layout-updated event
            this.OnLayoutUpdated(EventArgs.Empty);
        }
        void RefreshFonts(bool forceFontRefresh)
        {
            float scaleFactor = Math.Max(1F, (Math.Min(this.Width, this.Height) * this._zoomFactor / 32F));
            if (forceFontRefresh || scaleFactor != this._scaleFactor)
            {
                // update scale
                this._scaleFactor    = scaleFactor;
                this._altScaleFactor = (float)Math.Sqrt(scaleFactor / 2F) * 2F;
                // dispose of old fonts
                if (this.TitleFont   != null) { this.TitleFont  .Dispose(); }
                if (this.ChartFontSm != null) { this.ChartFontSm.Dispose(); }
                if (this.ChartFontLg != null) { this.ChartFontLg.Dispose(); }
                // create new font
                float         sz = this._altScaleFactor * this.Font.Size * GlobalSettings.DPIScale;
                this.  TitleFont = new Font(this.Font.FontFamily, (float)Math.Round(sz * 2 * 5/3) / 8F, FontStyle.Bold);
                this.ChartFontLg = new Font(this.Font.FontFamily, (float)Math.Round(sz * 2 * 4/3) / 8F);
                this.ChartFontSm = new Font(this.Font.FontFamily, (float)Math.Round(sz * 2      ) / 8F);
            }
        }
        void UpdateTitleLayout()
        {
            using (Graphics g = this.CreateGraphics())
            {
                SizeF sz = g.MeasureString(this._title, this.TitleFont);
                this.TitleHeight = Math.Max(MinTitleHeight, sz.Height);
                float x = ((float)this.Width - sz.Width) / 2F;
                float y = (this.TitleHeight - sz.Height) / 2F;
                this.TitleLocation = new PointF(x, y);
            }
        }
        void GridOrLegend_LayoutUpdated(object sender, EventArgs e)
        {
            foreach (AxisInfo axis in this.ChartAxes)
            {
                axis.SetDirty(false);
            }
            this.Series.SetDirty(false);
            this.ChartElements_LayoutUpdated(sender, e);
        }
        void ChartElements_LayoutUpdated(object sender, EventArgs e)
        {
            this.OnLayoutUpdated(e);
        }

        #endregion

        #region /* Other Initialization */

        void InitializeAnimationAndTrackingEffects()
        {
            this.Animation     = new AnimationEffects     (this);
            this.MouseState    = new MouseTrackingState   (this);
            this.KeyboardState = new KeyboardTrackingState(this);
        }
        void InitializeOther()
        {
        }

        #endregion

        #region /* Methods for raising events */

        /// <summary>
        /// Raises the <see cref="Chart.LayoutUpdated"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected virtual void OnLayoutUpdated(EventArgs e)
        {
            this.LayoutUpdated?.Invoke(this, e);
        }
        /// <summary>
        /// Raises the <see cref="Chart.ChangeView"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected virtual void OnChangeView(ChangeViewEventArgs e)
        {
            this.ChangeView?.Invoke(this, e);
        }
        ChangeViewEventArgs OnChangeView(ChangeViewTrigger trigger, Orientation orientation, int direction)
        {
            ChangeViewEventArgs e = new ChangeViewEventArgs(trigger, orientation, direction);
            this.OnChangeView(e);
            return e;
        }

        #endregion

        #region /* Custom Painting */

        /// <summary>
        /// Raises the <see cref="Control.Paint"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            // animate and paint
            this.Animation.Paint(e.Graphics);
        }

        #endregion

        /// <summary>
        /// Raises the <see cref="Control.Resize"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected override void OnResize(EventArgs e)
        {
            this.UpdateLayout(false);
            base.OnResize(e);
        }
        /// <summary>
        /// Raises the <see cref="Control.FontChanged"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected override void OnFontChanged(EventArgs e)
        {
            // font change triggers complete re-init of layout
            this.UpdateLayout(true);
            this.Invalidate();
            //
            base.OnFontChanged(e);
        }

        #region /* Expanding/Collapsing */

        /// <summary>
        /// Raises the <see cref="Control.MouseDoubleClick"/> event.
        /// </summary>
        /// <param name="e">Event data to pass to the event.</param>
        protected override void OnMouseDoubleClick(MouseEventArgs e)
        {
            base.OnMouseDoubleClick(e);
            if (e.Button == MouseButtons.Left)
            {
                this.ToggleExpanded();
            }
        }

        /// <summary>
        /// Expands the chart to fit the contents of the parent.
        /// </summary>
        public virtual void Expand()
        {
            if (this.IsExpanded) { return; }

            // if auto-expanding to top-level, find top-level parent
            Control parent = this.Parent;
            if (this._autoExpandTopLevel)
            {
                parent = this.GetTopLevelParent(parent);
            }

            // this control may not have a border --
            // if we're expanding to top-level parent (most likely a windows form), things may not look nice and neat
            // thus, we're going to cache the control's current border and set it to "FixedSingle"
            // once collapsed, the border will be restored
            this._autoExpandCachedBorder = this.BorderStyle;

            // cache bounds and parent so we can restore them later
            this._autoExpandCachedBounds = this.Bounds;
            this._autoExpandCachedParent = this.Parent;

            // expand ...
            this.BeginUpdate();
            if (parent != this.Parent)
            {   // move control to top-level parent
                this.Parent.Controls.Remove(this);
                parent.Controls.Add(this);
            }
            this.SetBounds(0, 0, parent.Width, parent.Height, BoundsSpecified.All);
            this.BorderStyle = BorderStyle.FixedSingle;
            this.BringToFront();
            this.EndUpdate();
            //
            parent.Refresh();
            this._isExpanded = true;
        }
        /// <summary>
        /// Collapses the chart to its original size.
        /// </summary>
        public virtual void Collapse()
        {
            if (!this.IsExpanded) { return; }

            // if the chart was expanded to top-level parent, collapse back to original (cached) parent
            Control parent = this.Parent;
            if (this._autoExpandCachedParent != parent)
            {
                parent = this._autoExpandCachedParent;
            }

            this.BeginUpdate();
            Control topLevelParent = null;
            if (this._autoExpandCachedParent != this.Parent)
            {   // if the chart was expanded to a different parent control (such as top-level),
                // collapse back to original (cached) parent
                topLevelParent = this.Parent;
                this.Parent.Controls.Remove(this);
                this._autoExpandCachedParent.Controls.Add(this);
            }
            this.Bounds = this._autoExpandCachedBounds;
            this.BorderStyle = this._autoExpandCachedBorder;
            this.BringToFront();
            this.EndUpdate();
            //
            if (topLevelParent != null) { topLevelParent.Refresh(); }
            this._isExpanded = false;
        }

        void ToggleExpanded()
        {
            if (this._autoExpand && this.Parent != null)
            {
                if (this.IsExpanded) { this.Collapse(); }
                else { this.Expand(); }
            }
        }
        Control GetTopLevelParent(Control c)
        {
            if (c.Parent == null) { return c; }
            return this.GetTopLevelParent(c.Parent);
        }

        #endregion

        /// <summary>
        /// Adds a new <see cref="DataSeries"/> with the specified name to the chart.
        /// </summary>
        /// <param name="name">Specifies the name of the series displayed in the chart's legend.</param>
        public void AddDataSeries(string name)
        {
            this.AddDataSeries(name, AxisType.Primary, ChartStyle.Default);
        }
        /// <summary>
        /// Adds a new <see cref="DataSeries"/> with the specified name and attributes to the chart.
        /// </summary>
        /// <param name="name">Specifies the name of the series displayed in the chart's legend.</param>
        /// <param name="axis">Specifies whether the series uses primary or secondary values axis.</param>
        /// <param name="style">Specifies the display style of the series.</param>
        public void AddDataSeries(string name, AxisType axis, ChartStyle style)
        {
            this.AddDataSeries(name, axis, style, true, null, null);
        }
        /// <summary>
        /// Adds a new <see cref="DataSeries"/> with the specified name and attributes to the chart.
        /// </summary>
        /// <param name="name">Specifies the name of the series displayed in the chart's legend.</param>
        /// <param name="axis">Specifies whether the series uses primary or secondary values axis.</param>
        /// <param name="style">Specifies the display style of the series.</param>
        /// <param name="visible">Specifies whether the series should be displayed on the chart.</param>
        /// <param name="xValues">Specifies the category values for the series (typically, the X-Axis).</param>
        /// <param name="values">Specifies the axis values for the series (typically, the Y-Axis).</param>
        public virtual void AddDataSeries(string name, AxisType axis, ChartStyle style, bool visible, double[] xValues, double[] values)
        {
            this._dataSeries.Add(new DataSeries(name, axis, style, visible, xValues, values));
            this.UpdateLayout(false);
            this.Invalidate();
        }
        /// <summary>
        /// Updates the axis values of a data series at the specified index.
        /// </summary>
        /// <param name="seriesIndex">The index of a data series to updated.</param>
        /// <param name="values">The new axis values for a data series.</param>
        public void UpdateDataSeriesValues(int seriesIndex, double[] values)
        {
            this.DataSeries[seriesIndex].Values = values;
            this.UpdateLayout(false);
            this.Invalidate();
        }
        /// <summary>
        /// Sets the specified visible state for a data series at the specified index.
        /// </summary>
        /// <param name="seriesIndex">The index of a data series to updated.</param>
        /// <param name="visible">The new visible sate for a data series.</param>
        public void ToggleSeriesVisible(int seriesIndex, bool visible)
        {
            if (this.DataSeries[seriesIndex].Visible != visible)
            {
                this.DataSeries[seriesIndex].Visible = visible;
                this.UpdateLayout(false);
                this.Invalidate();
            }
        }
        /// <summary>
        /// Clears axis values from all data series.
        /// </summary>
        public void ClearDataSeriesValues()
        {
            for (int i = 0; i < this._dataSeries.Count; i++)
            {
                this._dataSeries[i].Values = null;
            }
            this.UpdateLayout(false);
            this.Invalidate();
        }

        /// <summary>
        /// Starts painting the tracking demo, if it is not already running.
        /// </summary>
        public void PaintTrackingDemo()
        {
            this.Animation.PaintTrackingDemo();
        }
        /// <summary>
        /// Cancels the tracking demo, if it is running.
        /// </summary>
        public void CancelTrackingDemo()
        {
            this.Animation.CancelTrackingDemo();
        }

        //----- support methods -----
        /// <summary>
        /// Returns aggregate statistics for all data-series contained within this chart.
        /// </summary>
        /// <returns>Aggregate statistics for all data-series within this chart.</returns>
        protected virtual DataSeriesStatistics GetDataSeriesStatistics()
        {
            return new DataSeriesStatistics(this);
        }

        //----- begin/end update support methods -----
        /// <summary>
        /// Suspends redrawing of the control and disables events while the control is being updated.
        /// </summary>
        protected void BeginUpdate()
        {
            Win32.BeginUpdate(this, ref this._updateCount, ref this._eventMask);
        }
        /// <summary>
        /// Resumes redrawing of the control and enables events after the control has been updated.
        /// </summary>
        protected void EndUpdate()
        {
            Win32.EndUpdate(this, ref this._updateCount, ref this._eventMask);
            base.Refresh();
        }

        #endregion

        #region /*** Properties ***/

        /// <summary>Gets the default data series to display on an empty chart, before any user series have been added.</summary>
        static List<DataSeries> DefaultDataSeries
        {
            get
            {
                return new List<DataSeries>
                {
                    new DataSeries("Series 1", AxisType.Primary  , ChartStyle.Default, true, null, new double[] { 1, 2, 3 }),
                    new DataSeries("Series 2", AxisType.Primary  , ChartStyle.Line   , true, null, new double[] { 6, 9, 3 }),
                    new DataSeries("Series 3", AxisType.Secondary, ChartStyle.Bar    , true, null, new double[] { 0.1, 0.4, -0.2 })
                };
            }
        }

        /// <summary>Gets or sets whether to display the title at the top of the chart control.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Appearance")]
        [Description("Indicates whether to display the title at the top of the chart control.")]
        public bool ShowTitle
        {
            get { return this._showTitle; }
            set
            {
                if (this._showTitle != value)
                {
                    this._showTitle = value;
                    this.Grid  .SetDirty(false);
                    this.Legend.SetDirty(false);
                    this.Invalidate();
                }
            }
        }
        /// <summary>Gets or sets the title to display at the top of the chart control.</summary>
        [Browsable(true)]
        [DefaultValue("Chart")]
        [Category("Appearance")]
        [Description("The title to display at the top of the chart control.")]
        public string Title
        {
            get { return this._title; }
            set
            {
                if (this._title != value)
                {
                    this._title = value;
                    this.UpdateTitleLayout();
                    if (this._showTitle) { this.Invalidate(); }
                }
            }
        }
        /// <summary>Gets the height of the chart's title.</summary>
        [Browsable(false)]
        public float TitleHeight { get; private set; }
        /// <summary>Gets the location of the chart's title.</summary>
        [Browsable(false)]
        PointF TitleLocation { get; set; }

        /// <summary>Gets or sets whether the chart's context menu should be disabled.</summary>
        [Browsable(true)]
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Indicates whether the chart's context menu should be disabled.")]
        public bool DisableContextMenu
        {
            get { return this._disableContextMenu; }
            set
            {
                if (this._disableContextMenu != value)
                {
                    this._disableContextMenu = value;
                }
            }
        }

        /// <summary>Gets or sets whether the chart should be selected after a left mouse click.
        ///   This setting will always be true if mouse and/or keyboard tracking is enabled.</summary>
        [Browsable(true)]
        [DefaultValue(true)]
        [Category("Behavior")]
        [Description("Indicates whether the chart should be selected after a left mouse click. This setting will always be true if mouse and/or keyboard tracking is enabled.")]
        public bool SelectOnMouseClick
        {
            get { return (this._selectOnMouseClick || this.MouseState.TrackMouse || this.TrackArrowKeys); }
            set
            {
                if (this._selectOnMouseClick != value)
                {
                    this._selectOnMouseClick = value;
                }
            }
        }

        /// <summary>Gets or sets whether the chart control should automatically expand or collapse when double-clicked by the user.</summary>
        [Browsable(true)]
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Indicates whether the chart control should automatically expand or collapse when double-clicked by the user.")]
        public bool AutoExpand { get { return this._autoExpand; } set { this._autoExpand = value; } }
        /// <summary>Gets or sets whether the chart control should expand or collapse within the top level parent control instead of its immediate parent control.</summary>
        [Browsable(true)]
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Indicates whether the chart control should expand or collapse within the top level parent control instead of its immediate parent control.")]
        public bool AutoExpandTopLevel { get { return this._autoExpandTopLevel; } set { this._autoExpandTopLevel = value; } }
        /// <summary>Gets whether the chart control is in an expanded state.</summary>
        [Browsable(false)]
        public bool IsExpanded { get { return this._isExpanded; } }

        /// <summary>Gets or sets the fade level (between 0 and 1) to blend over the chart's grid area.</summary>
        [Browsable(true)]
        [DefaultValue(1.0F)]
        [Category("Appearance")]
        public float FadeLevel
        {
            get { return this._fadeLevel; }
            set
            {
                value = Math.Max(0, Math.Min(value, 1));
                if (this._fadeLevel != value)
                {
                    this._fadeLevel = value;
                    this.Grid  .SetDirty(false);
                    this.Legend.SetDirty(false);
                    this.Invalidate();
                }
            }
        }

        /// <summary>Gets or sets the color palette to use for painting series on a chart.</summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public ColorPalette ColorPalette
        {
            get { return this._colorPalette; }
            set
            {
                if (this._colorPalette != value)
                {
                    this._colorPalette = value;
                    this.Grid.SetDirty(false);
                    this.Legend.SetDirty(false);
                    this.Series.SetDirty(false);
                    this.Invalidate();
                }
            }
        }

        /// <summary>Gets the chart style.</summary>
        [Browsable(false)]
        public ChartStyle ChartStyle { get; protected set; }

        /// <summary>Gets the data series defined within the chart.</summary>
        [Browsable(false)]
        public List<DataSeries> DataSeries { get { return (this._dataSeries.Count == 0) ? DefaultDataSeries : this._dataSeries; } }
        /// <summary>Gets whether the chart has user defined data series.</summary>
        [Browsable(false)]
        public bool HasDataSeries { get { return this._dataSeries.Count > 0; } }

        // UI scaling and fonts
        /// <summary>Gets or sets the zoom factor to apply to the chart's fonts and other visual elements.</summary>
        [Browsable(false)]
        public float ZoomFactor
        {
            get { return this._zoomFactor; }
            set
            {
                if (this._zoomFactor != value)
                {
                    this._zoomFactor = value;
                    this.UpdateLayout(true);
                    this.Invalidate();
                }
            }
        }
        /// <summary>Gets the UI scaling factor based on the chart's width and height.</summary>
        [Browsable(false)]
        protected float ScaleFactor { get { return this._scaleFactor; } }
        /// <summary>Gets the alternative scaling factor to apply to certain chart elements. This value is computed as:
        ///   Sqrt(ScaleFactor / 2) * 2</summary>
        [Browsable(false)]
        protected float AltScaleFactor { get { return this._altScaleFactor; } }
        //
        [Browsable(false)]
        protected Font TitleFont { get; private set; }
        [Browsable(false)]
        protected Font ChartFontSm { get; private set; }
        [Browsable(false)]
        protected Font ChartFontLg { get; private set; }

        #endregion

        #region /*** Variables ***/

        //----- support variables for begin/end update methods -----
        int _updateCount = 0;
        int _eventMask   = 0;

        //----- const values for layout -----
        const int MinTitleHeight = 32; // minimum title height
        const int LegendPadding  =  4; // total spacing between left+right and top+bottom edges of the legend,
                                       // between each item within a legend, and between legend's and grid's edges

        // ----- UI scaling -----
        float _zoomFactor     =  1F;
        float _scaleFactor    = -1F; // UI scale factor for fonts, grid, etc.
        float _altScaleFactor = -1F; // computed as: Sqrt(ScaleFactor/2)*2

        //----- title -----
        bool   _showTitle = true;
        string _title     = "ChartBase";

        //----- expanding/collapsing -----
        bool        _isExpanded             = false;
        bool        _autoExpand             = false;
        bool        _autoExpandTopLevel     = false;
        BorderStyle _autoExpandCachedBorder = BorderStyle.None;
        Rectangle   _autoExpandCachedBounds = Rectangle.Empty;
        Control     _autoExpandCachedParent = null;

        //----- other -----
        bool         _disableContextMenu = false;
        bool         _selectOnMouseClick = true;
        float        _fadeLevel          = 1.0F;
        ColorPalette _colorPalette       = ColorPalette.Light;

        //----- data series -----
        List<DataSeries> _dataSeries   = new List<DataSeries>();

        #endregion

    }
}
