/*
 * 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;

namespace NPlot
{
    /// <summary>
    /// Encapsulates functionality for plotting data as a line chart.
    /// </summary>
    public class LinePlot : BaseSequencePlot, IPlot, ISequencePlot
    {
        private Pen pen_ = new Pen(Color.Black);
        private Color shadowColor_ = Color.FromArgb(100, 100, 100);
        private Point shadowOffset_ = new Point(1, 1);
        private bool shadow_;

        /// <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)
        {
            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)
        {
            OrdinateData = ordinateData;
            AbscissaData = abscissaData;
        }

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

        /// <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; }
        }

        /// <summary>
        /// Offset of shadow line from primary line.
        /// </summary>
        public Point ShadowOffset
        {
            get { return shadowOffset_; }
            set { shadowOffset_ = value; }
        }

        /// <summary>
        /// The pen used to draw the plot
        /// </summary>
        public Pen Pen
        {
            get { return pen_; }
            set { pen_ = value; }
        }

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

        /// <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 (shadow_)
            {
                DrawLineOrShadow(g, xAxis, yAxis, true);
            }

            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(DataSource, DataMember, OrdinateData, 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(DataSource, DataMember, OrdinateData, AbscissaData);

            return data_.SuggestYAxis();
        }

        /// <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>
        /// 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) Pen.Clone();
                shadowPen.Color = ShadowColor;
            }

            SequenceAdapter data =
                new SequenceAdapter(DataSource, DataMember, OrdinateData, 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 + ShadowOffset.X,
                               physical.Y + ShadowOffset.Y,
                               physical.X + 0.5f + ShadowOffset.X,
                               physical.Y + 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;
                }

                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]);

                    // 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);
                    }
                }
            }
        }
    }
}