#region <<< Using Directives >>>
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using Volpe.Cafe.EspressoUI.Charts;
#endregion

namespace Volpe.Cafe.EspressoUI.ModelCharts
{
    [Serializable]
    sealed class TachometerChart : Chart
    {

        #region <<< Nested Types >>>

        /// <summary>
        /// Specifies layout information for the tachometer arc.
        /// </summary>
        struct ArcInfo
        {
            //----- ctors -----
            public ArcInfo(PointF start, PointF end, float radius, RectangleF bounds) : this()
            {
                this.Start  = start;
                this.End    = end;
                this.Radius = radius;
                this.Bounds = bounds;
            }

            //----- properties -----
            public PointF     Start  { get; set; }
            public PointF     End    { get; set; }
            public float      Radius { get; set; }
            public RectangleF Bounds { get; set; }
        }
        class CustomSeriesInfo : SeriesInfo
        {
            #region <<< Ctors >>>

            internal CustomSeriesInfo(Chart parent) : base(parent) { }

            #endregion

            #region <<< Methods >>>

            //----- update layout -----
            protected override void UpdateLayoutInternal()
            {
                if (this.Parent.Grid.IsDirty)
                {
                    this.Parent.Grid.UpdateLayout();
                }
                this.CalculateBounds();
            }
            void CalculateBounds()
            {
                // calculate bounds
                this.Scale = this.Parent.AltScaleFactor;
                int  sf    = (int)this.Scale;
                int  xOff  = 2 + sf;
                int  yOff  = 3 + sf;
                int  x     = this.Parent.GridX      + xOff;
                int  y     = this.Parent.GridY      + yOff;
                int  w     = this.Parent.GridWidth  - xOff * 2;
                int  h     = this.Parent.GridHeight - yOff * 2;
                // adjust bounds
                if      (w > h) { x += (w - h) / 2; w = h; }
                else if (h > w) { y += (h - w) / 2; h = w; }

                // set chart midpoint
                this.Midpoint = new Point(x + w / 2, y + h / 2);

                // calculate outer/inner arc layout and bounds
                int  arcOff     = (int)(this.Scale / 1.5F * 6F); // offset between outer and inner arcs
                this.Angle      =  45F;
                this.StartAngle =  90F + this.Angle;
                this.SweepAngle = 360F - this.Angle * 2;
                this.OuterArc   = this.CalculateBounds(x, y, w, h, this.StartAngle, 90F - this.Angle);
                this.InnerArc   = this.CalculateBounds(x + arcOff, y + arcOff, w - 2 * arcOff, h - 2 * arcOff,
                                                       this.StartAngle, 90F - this.Angle);

                // calculate value arc and layout
                int  valOff          = (int)(this.Scale / 1.5F + arcOff); // offset between outer and value arcs
                this.ValueSweepAngle = (float)(this.SweepAngle * this.Parent.Value);
                this.ValueArc        = this.CalculateBounds(x + valOff, y + valOff, w - 2 * valOff, h - 2 * valOff,
                                                            this.StartAngle, this.StartAngle + this.ValueSweepAngle);

                // calculate hand positioning
                this.HandRadius   = (w - arcOff) / 2F;
                this.HandLocation = this.GetPointOnCircle(this.Midpoint, this.HandRadius, this.StartAngle + this.ValueSweepAngle);

                // we only want to allow painting of chart data if there is enough room to fit comfortably
                this.PaintData = (this.InnerArc.Radius > 4);
            }
            ArcInfo CalculateBounds(int x, int y, int w, int h, double startAngle, double endAngle)
            {
                var    bounds = new Rectangle(x, y, w, h);
                float  radius = w / 2F;
                PointF start  = this.GetPointOnCircle(this.Midpoint, radius, startAngle);
                PointF end    = this.GetPointOnCircle(this.Midpoint, radius, endAngle  );
                //
                return new ArcInfo(start, end, radius, bounds);
            }
            PointF GetPointOnCircle(Point pt, float r, double angle)
            {
                return this.GetPointOnCircle(pt.X, pt.Y, r, angle);
            }
            PointF GetPointOnCircle(int x, int y, float r, double angle)
            {
                double theta = Math.PI * angle / 180D;
                float  ptX   = (float)(x + Math.Cos(theta) * r);
                float  ptY   = (float)(y + Math.Sin(theta) * r);
                //
                return new PointF(ptX, ptY);
            }

            //----- paint -----
            protected override void PaintInternal(Graphics g)
            {   
                if (!this.PaintData) { return; }

                // improve graphics quality
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.SmoothingMode     = SmoothingMode.AntiAlias;

                this.PaintChartAxis(g);
                if (this.Parent.IsInitialized)
                {
                    this.PaintMinMaxLabels(g);
                    this.PaintSeriesValue (g);
                }
            }
            void PaintChartAxis(Graphics g)
            {
                var   chart       = this.Parent;
                Color borderColor = ControlPaint.Dark (chart.GridLinesColor);
                Color gradientLgt = ControlPaint.Light(chart.GridBackColor);
                Color gradientDrk = ControlPaint.Dark (chart.GridBackColor);
                //
                using (var gpOuter  = new GraphicsPath())
                using (var gpInner  = new GraphicsPath())
                using (Pen penOuter = new Pen(borderColor, this.Scale))
                using (Pen penInner = new Pen(borderColor))
                {
                    gpOuter.AddArc (this.OuterArc.Bounds, this.StartAngle, this.SweepAngle);
                    gpOuter.AddLine(this.OuterArc.End   , this.OuterArc.Start);
                    gpInner.AddArc (this.InnerArc.Bounds, this.StartAngle, this.SweepAngle);
                    gpInner.AddLine(this.InnerArc.End   , this.InnerArc.Start);
                    using (var brushOuter = new PathGradientBrush(gpOuter))
                    {
                        brushOuter.CenterPoint = this.Midpoint;
                        brushOuter.CenterColor = gradientLgt;
                        brushOuter.SurroundColors = new Color[] { gradientDrk };
                        g.FillPath(brushOuter, gpOuter);
                    }
                    g.DrawPath(penOuter, gpOuter);
                    g.DrawPath(penInner, gpInner);
                }
            }
            void PaintMinMaxLabels(Graphics g)
            {
                ArcInfo arc  = this.OuterArc;
                Font    font = this.Parent.ChartFontLg;
                string  min  = this.Parent.CategoryAxisDiscreteValues[0];
                string  max  = this.Parent.CategoryAxisDiscreteValues[1];

                SizeF sz1 = g.MeasureString(min, font);
                SizeF sz2 = g.MeasureString(max, font);
                float x1  = Math.Max(arc.Bounds.Left             , arc.Start.X - sz1.Width / 2F);
                float x2  = Math.Min(arc.Bounds.Right - sz2.Width, arc.End  .X - sz2.Width / 2F);
                float y   = arc.Start.Y + Math.Min(this.Scale, 4);

                using (var brush = new SolidBrush(this.Parent.ForeColor))
                {
                    g.DrawString(min, font, brush, x1, y);
                    if (x1 + sz1.Width < x2)
                    {
                        g.DrawString(max, font, brush, x2, y);
                    }
                }
            }
            void PaintSeriesValue(Graphics g)
            {
                Color seriesColor       = this.ColorPalette.GetFillColor(0);
                Color seriesBorderColor = this.ColorPalette.GetBorderColor(0);
                Color seriesGradientLgt = ControlPaint.LightLight(seriesColor);
                Color seriesGradientMid = seriesColor;
                Color seriesGradientDrk = ControlPaint.Dark(seriesColor);

                // paint series arc
                if (this.Parent.Value != 0)
                {
                    using (var gp = new GraphicsPath())
                    using (Pen pen = new Pen(seriesBorderColor))
                    {
                        gp.AddLine(this.Midpoint       , this.ValueArc.Start);
                        gp.AddArc (this.ValueArc.Bounds, this.StartAngle, this.ValueSweepAngle);
                        gp.AddLine(this.ValueArc.End   , this.Midpoint);
                        using (var brush = new PathGradientBrush(gp))
                        {
                            brush.CenterPoint = this.Midpoint;
                            brush.CenterColor = seriesGradientMid;
                            brush.SurroundColors = new Color[] { seriesGradientLgt };
                            g.FillPath(brush, gp);
                        }
                        g.DrawPath(pen, gp);
                    }
                }

                // paint series hand
                using (var gp = new GraphicsPath())
                using (Pen pen = new Pen(seriesBorderColor))
                {
                    int    r   = (int)(this.Scale * 3F);
                    int    d   = r * 2;
                    int    x   = this.Midpoint.X - r;
                    int    y   = this.Midpoint.Y - r;
                    PointF pt1 = this.GetPointOnCircle(this.Midpoint, r, this.StartAngle + this.ValueSweepAngle + 90F);
                    PointF pt2 = this.GetPointOnCircle(this.Midpoint, r, this.StartAngle + this.ValueSweepAngle - 90F);
                    //
                    gp.AddArc(x, y, d, d, this.StartAngle + this.ValueSweepAngle + 90F, 180F);
                    gp.AddLine(pt2, this.HandLocation);
                    gp.AddLine(this.HandLocation, pt1);
                    using (var brush = new PathGradientBrush(gp))
                    {
                        brush.CenterPoint = this.Midpoint;
                        brush.CenterColor = seriesGradientDrk;
                        brush.SurroundColors = new Color[] { seriesGradientMid };
                        g.FillPath(brush, gp);
                    }
                    g.DrawPath(pen, gp);
                }
            }

            #endregion

            #region <<< Properties >>>

            /// <summary>Gets or sets the layout information for the outer arc.</summary>
            ArcInfo OuterArc { get; set; }
            /// <summary>Gets or sets the layout information for the inner arc.</summary>
            ArcInfo InnerArc { get; set; }
            /// <summary>Gets or sets the layout information for the value arc.</summary>
            ArcInfo ValueArc { get; set; }

            // the midpoint of the tachometer chart
            Point Midpoint { get; set; }
            int Width { get; set; }

            // arc geometry
            float Angle { get; set; }
            float StartAngle { get; set; }
            float SweepAngle { get; set; }
            float ValueSweepAngle { get; set; }

            // the length and end-location of the tachometer hand
            float HandRadius { get; set; }
            PointF HandLocation { get; set; }

            // relative chart scaling
            float Scale { get; set; }

            /// <summary>Gets whether the series is enabled for painting. If the chart becomes too small to fit
            ///     comfortably in the provided grid area, this value will be set to false.</summary>
            public bool PaintData { get; private set; }

            /// <summary>Gets the parent of this object.</summary>
            new TachometerChart Parent { get { return (TachometerChart)base.Parent; } }

            #endregion

        }

        #endregion

        #region <<< Ctors >>>

        /// <summary>
        /// Initializes a new instance of the <see cref="TachometerChart"/> class.
        /// </summary>
        public TachometerChart()
        {
            this.InitializeComponent();
            this.InitializeOther();
            this.IsInitialized = false;
        }

        #endregion

        #region <<< Methods >>>

        //----- chart initialization -----
        void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // TachometerChart
            // 
            this.CategoryAxisDiscreteValues = new string[] { "MIN", "MAX" };
            this.GridBorderStyle = Border3DStyle.RaisedInner;
            this.Name = "TachometerChart";
            this.ShowLegend = false;
            this.ShowSecondaryAxis = true;
            this.ShowTitle = false;
            this.Size = new Size(480, 320);
            this.Title = "";
            this.ResumeLayout(false);

        }
        void InitializeOther()
        {
            this.Animation.PaintLegend = false;
            this.Animation.PaintAxes   = false;
            //this.Animation.PaintSeries = false;
            this.Series = new CustomSeriesInfo(this);
            this.AddDataSeries("Value");
            this.DataSeries.Values = new double[1];
        }
        /// <summary>
        /// Initializes the chart with the specified min and max value labels.
        /// </summary>
        public void InitializeChart(string minValueLabel, string maxValueLabel)
        {
            this.CategoryAxisDiscreteValues = new string[] { minValueLabel, maxValueLabel };
            this.IsInitialized = true;
        }

        //----- chart refreshing -----
        /// <summary>
        /// Refreshes the chart's data with the specified value.
        /// </summary>
        /// <param name="value">The percent value representing the current position of the tachometer dial.</param>
        public void RefreshChartData(double value)
        {
            if (this.IsInitialized)
            {
                this.Value = value;
            }
        }

        #endregion

        #region <<< Properties >>>

        /// <summary>Gets whether the chart has been initialized.</summary>
        [Browsable(false)]
        public bool IsInitialized { get; private set; }
        /// <summary>Gets or sets the percent value representing the current position of the tachometer dial.</summary>
        [Browsable(false)]
        public double Value
        {
            get { return this.DataSeries.Values[0]; }
            set
            {
                value = Math.Min(Math.Max(0, value), 1);
                if (this.DataSeries.Values[0] != value)
                {
                    this.DataSeries.Values[0] = value;
                    this.Series.SetDirty(false);
                    this.Refresh();
                }
            }
        }
        new DataSeries DataSeries { get { return base.DataSeries[0]; } }

        #endregion

    }
}
