/*
 * NPlot - A charting library for .NET
 * 
 * LinePlot.cs
 * Copyright (C) 2003-2006 Matt Howlett and others.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of NPlot nor the names of its contributors may
 *    be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;
using System.Collections.Generic;


namespace NPlot
{

    /// <summary>
    /// Encapsulates functionality for plotting data as a line chart.
    /// </summary>
    public class LinePlot : BaseSequencePlot, IPlot, ISequencePlot
    {

        /// <summary>
        /// Default constructor
        /// </summary>
        public LinePlot()
        {
        }


        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="dataSource">The data source to associate with this plot</param>
        public LinePlot( object dataSource )
        {
            this.DataSource = dataSource;
        }


        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="ordinateData">the ordinate data to associate with this plot.</param>
        /// <param name="abscissaData">the abscissa data to associate with this plot.</param>
        public LinePlot( object ordinateData, object abscissaData )
        {
            this.OrdinateData = ordinateData;
            this.AbscissaData = abscissaData;
        }


        /// <summary>
        /// Draws the line plot on a GDI+ surface against the provided x and y axes.
        /// </summary>
        /// <param name="g">The GDI+ surface on which to draw.</param>
        /// <param name="xAxis">The X-Axis to draw against.</param>
        /// <param name="yAxis">The Y-Axis to draw against.</param>
        /// <param name="drawShadow">If true draw the shadow for the line. If false, draw line.</param>
        public void DrawLineOrShadow( Graphics g, PhysicalAxis xAxis, PhysicalAxis yAxis, bool drawShadow )
        {
            Pen shadowPen = null;
            if (drawShadow)
            {
                shadowPen = (Pen)this.Pen.Clone();
                shadowPen.Color = this.ShadowColor;
            }

            SequenceAdapter data = 
                new SequenceAdapter( this.DataSource, this.DataMember, this.OrdinateData, this.AbscissaData );

            ITransform2D t = Transform2D.GetTransformer( xAxis, yAxis );
            
            int numberPoints = data.Count;
            
            if (data.Count == 0)
            {
                return;
            }

            // clipping is now handled assigning a clip region in the
            // graphic object before this call
            if (numberPoints == 1)
            {
                PointF physical = t.Transform( data[0] );
                
                if (drawShadow)
                {
                    g.DrawLine( shadowPen, 
                        physical.X - 0.5f + this.ShadowOffset.X,
                        physical.Y + this.ShadowOffset.Y,
                        physical.X + 0.5f + this.ShadowOffset.X,
                        physical.Y + this.ShadowOffset.Y );
                }
                else
                {
                    g.DrawLine( Pen, physical.X-0.5f, physical.Y, physical.X+0.5f, physical.Y);
                }
            }
            else
            {

                // prepare for clipping
                double leftCutoff = xAxis.PhysicalToWorld(xAxis.PhysicalMin, false);
                double rightCutoff = xAxis.PhysicalToWorld(xAxis.PhysicalMax, false);
                if (leftCutoff > rightCutoff)
                {
                    Utils.Swap(ref leftCutoff, ref rightCutoff);
                }
                if (drawShadow)
                {
                    // correct cut-offs
                    double shadowCorrection =
                        xAxis.PhysicalToWorld(ShadowOffset, false) - xAxis.PhysicalToWorld(new Point(0,0), false);
                    leftCutoff -= shadowCorrection;
                    rightCutoff -= shadowCorrection;
                }
                
                // +++++++++++++++++++++++++++++++++++++++++++++
                // klose: patch to speed things up if zoomed out
                PointF last_p2 = PointF.Empty;
                // +++++++++++++++++++++++++++++++++++++++++++++
                
                // +++++++++++++++++++++++++++++++++++++++++++++
                // klose: draw complete line in one stroke
                List<PointF> points = new List<PointF>();
                // +++++++++++++++++++++++++++++++++++++++++++++
                
                // klose: add support for gaps
                List<Rectangle> clipping_rectangles = new List<Rectangle>();
                
                for (int i = 1; i < numberPoints; ++i)
                {
                    // check to see if any values null. If so, then continue.
                    double dx1 = data[i-1].X;
                    double dx2 = data[i].X;
                    double dy1 = data[i-1].Y;
                    double dy2 = data[i].Y;
                    if ( double.IsNaN(dx1) || double.IsNaN(dy1) ||
                         double.IsNaN(dx2) || double.IsNaN(dy2) )
                    {
                        continue;
                    }
                    
                    // do horizontal clipping here, to speed up
                    if ((dx1 < leftCutoff || rightCutoff < dx1) &&
                        (dx2 < leftCutoff || rightCutoff < dx2))
                    {
                        continue;
                    }

                    // else draw line.
                    PointF p1 = t.Transform( data[i-1] );
                    PointF p2 = t.Transform( data[i] );
                    
                    // klose: add support for gaps
                    if (this.Gaps != null && this.Gaps.Contains(i))
                    {
                        int x = (int)p1.X;
                        int y = (int)Math.Min(p1.Y, p2.Y) - 10;
                        int w = (int)(p2.X-p1.X);
                        int h = (int)Math.Abs(p2.Y-p1.Y) + 20;
                        clipping_rectangles.Add(new Rectangle(x, y, w, h));
                    }

                    // +++++++++++++++++++++++++++++++++++++++++++++
                    // klose: patch to speed things up if zoomed out
                    if (last_p2 == PointF.Empty)
                    {
                        last_p2 = p1;
                        if (drawShadow)
                            points.Add(new PointF(p1.X + ShadowOffset.X, p1.Y + ShadowOffset.Y));
                        else
                            points.Add(p1);
                    }
                    if ((Math.Pow(p2.X - last_p2.X, 2f) + Math.Pow(p2.Y - last_p2.Y, 2f) < 16f) &&
                        i < numberPoints-1)
                        continue;
                    p1 = last_p2;
                    last_p2 = p2;
                    // +++++++++++++++++++++++++++++++++++++++++++++
//                    // when very far zoomed in, points can fall ontop of each other,
//                    // and g.DrawLine throws an overflow exception
//                    if (p1.Equals(p2))
//                        continue;
                    
//                    if (drawShadow)
//                    {
//                        g.DrawLine( shadowPen, 
//                            p1.X + ShadowOffset.X,
//                            p1.Y + ShadowOffset.Y,
//                            p2.X + ShadowOffset.X,
//                            p2.Y + ShadowOffset.Y );
//                    }
//                    else
//                    {
//                        g.DrawLine( Pen, p1.X, p1.Y, p2.X, p2.Y );
//                    }
                    
                    // +++++++++++++++++++++++++++++++++++++++++++++
                    // klose: draw complete line in one stroke
                    if (drawShadow)
                        points.Add(new PointF(p2.X + ShadowOffset.X, p2.Y + ShadowOffset.Y));
                    else
                        points.Add(p2);
                    // +++++++++++++++++++++++++++++++++++++++++++++
                }
                
                Pen p = drawShadow ? shadowPen : Pen;
                
                if (points.Count > 1)
                {
                    // klose: add support for gaps
                    if (clipping_rectangles.Count > 0)
                    {
                        GraphicsState state = g.Save();
                        foreach (Rectangle r in clipping_rectangles)
                            g.ExcludeClip(r);
                        g.DrawCurve(p, points.ToArray(), 0f);
                        g.Restore(state);
                    }
                    else
                    {
                        g.DrawCurve(p, points.ToArray(), 0f);
                    }
                }
            }
        }


        /// <summary>
        /// Draws the line plot on a GDI+ surface against the provided x and y axes.
        /// </summary>
        /// <param name="g">The GDI+ surface on which to draw.</param>
        /// <param name="xAxis">The X-Axis to draw against.</param>
        /// <param name="yAxis">The Y-Axis to draw against.</param>
        public void Draw( Graphics g, PhysicalAxis xAxis, PhysicalAxis yAxis )
        {
            if (this.shadow_)
            {
                this.DrawLineOrShadow( g, xAxis, yAxis, true );
            }

            this.DrawLineOrShadow( g, xAxis, yAxis, false );
        }


        /// <summary>
        /// Returns an x-axis that is suitable for drawing this plot.
        /// </summary>
        /// <returns>A suitable x-axis.</returns>
        public Axis SuggestXAxis()
        {
            SequenceAdapter data_ = 
                new SequenceAdapter( this.DataSource, this.DataMember, this.OrdinateData, this.AbscissaData );

            return data_.SuggestXAxis();
        }


        /// <summary>
        /// Returns a y-axis that is suitable for drawing this plot.
        /// </summary>
        /// <returns>A suitable y-axis.</returns>
        public Axis SuggestYAxis()
        {
            SequenceAdapter data_ = 
                new SequenceAdapter( this.DataSource, this.DataMember, this.OrdinateData, this.AbscissaData );

            return data_.SuggestYAxis();
        }


        /// <summary>
        /// If true, draw a shadow under the line.
        /// </summary>
        public bool Shadow
        {
            get
            {
                return shadow_;
            }
            set
            {
                shadow_ = value;
            }
        }
        private bool shadow_ = false;
    

        /// <summary>
        /// Color of line shadow if drawn. Use Shadow method to turn shadow on and off.
        /// </summary>
        public Color ShadowColor
        {
            get
            {
                return shadowColor_;
            }
            set
            {
                shadowColor_ = value;
            }
        }
        private Color shadowColor_ = Color.FromArgb(100,100,100);


        /// <summary>
        /// Offset of shadow line from primary line.
        /// </summary>
        public Point ShadowOffset
        {
            get
            {
                return shadowOffset_;
            }
            set
            {
                shadowOffset_ = value;
            }
        }
        private Point shadowOffset_ = new Point( 1, 1 );


        /// <summary>
        /// Draws a representation of this plot in the legend.
        /// </summary>
        /// <param name="g">The graphics surface on which to draw.</param>
        /// <param name="startEnd">A rectangle specifying the bounds of the area in the legend set aside for drawing.</param>
        public virtual void DrawInLegend(Graphics g, Rectangle startEnd)
        {
            g.DrawLine(pen_, startEnd.Left, (startEnd.Top + startEnd.Bottom) / 2,
                startEnd.Right, (startEnd.Top + startEnd.Bottom) / 2);
        }


        /// <summary>
        /// The pen used to draw the plot
        /// </summary>
        public System.Drawing.Pen Pen
        {
            get
            {
                return pen_;
            }
            set
            {
                pen_ = value;
            }
        }
        private System.Drawing.Pen pen_ = new Pen(Color.Black);


        /// <summary>
        /// The color of the pen used to draw lines in this plot.
        /// </summary>
        public System.Drawing.Color Color
        {
            set
            {
                if (pen_ != null)
                {
                    pen_.Color = value;
                }
                else
                {
                    pen_ = new Pen(value);
                }
            }
            get
            {
                return pen_.Color;
            }
        }

        // klose: add support for gaps
        public List<int> Gaps { get; set; }
    }
}
