/*
 * NPlot - A charting library for .NET
 * 
 * HistogramPlot.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.Collections;
using System.Drawing;

namespace NPlot
{
    /// <summary>
    /// Provides ability to draw histogram plots.
    /// </summary>
    public class HistogramPlot : BaseSequencePlot, IPlot, ISequencePlot
    {
        private double baseOffset_;
        private float baseWidth_ = 1.0f;
        private bool center_ = true;
        private bool isStacked_;
        private Pen pen_ = new Pen(Color.Black);
        private IRectangleBrush rectangleBrush_ = new RectangleBrushes.Solid(Color.Black);
        private HistogramPlot stackedTo_;

        /// <summary>
        /// Set/Get the brush to use if the histogram is filled.
        /// </summary>
        public IRectangleBrush RectangleBrush
        {
            get { return rectangleBrush_; }
            set { rectangleBrush_ = value; }
        }

        /// <summary>
        /// Whether or not the histogram columns will be filled.
        /// </summary>
        public bool Filled { get; set; }

        /// <summary>
        /// The width of the histogram bar as a proportion of the data spacing
        /// (in range 0.0 - 1.0).
        /// </summary>
        public float BaseWidth
        {
            get { return baseWidth_; }
            set
            {
                if (value > 0.0 && value <= 1.0)
                {
                    baseWidth_ = value;
                }
                else
                {
                    throw new NPlotException("Base width must be between 0.0 and 1.0");
                }
            }
        }

        /// <summary>
        /// If true, each histogram column will be centered on the associated abscissa value.
        /// If false, each histogram colum will be drawn between the associated abscissa value, and the next abscissa value.
        /// Default value is true.
        /// </summary>
        public bool Center
        {
            set { center_ = value; }
            get { return center_; }
        }

        /// <summary>
        /// If this histogram plot has another stacked on top, this will be true. Else false.
        /// </summary>
        public bool IsStacked
        {
            get { return isStacked_; }
        }

        /// <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>
        /// Horizontal position of histogram columns is offset by this much (in world coordinates).
        /// </summary>
        public double BaseOffset
        {
            set { baseOffset_ = value; }
            get { return baseOffset_; }
        }

        /// <summary>
        /// Renders the histogram.
        /// </summary>
        /// <param name="g">The Graphics 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)
        {
            SequenceAdapter data =
                new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);

            float yoff;

            for (int i = 0; i < data.Count; ++i)
            {
                // (1) determine the top left hand point of the bar (assuming not centered)
                PointD p1 = data[i];
                if (double.IsNaN(p1.X) || double.IsNaN(p1.Y))
                    continue;

                // (2) determine the top right hand point of the bar (assuming not centered)
                PointD p2;
                if (i + 1 != data.Count)
                {
                    p2 = data[i + 1];
                    if (double.IsNaN(p2.X) || double.IsNaN(p2.Y))
                        continue;
                    p2.Y = p1.Y;
                }
                else if (i != 0)
                {
                    p2 = data[i - 1];
                    if (double.IsNaN(p2.X) || double.IsNaN(p2.Y))
                        continue;
                    double offset = p1.X - p2.X;
                    p2.X = p1.X + offset;
                    p2.Y = p1.Y;
                }
                else
                {
                    double offset = 1.0f;
                    p2.X = p1.X + offset;
                    p2.Y = p1.Y;
                }

                // (3) now account for plots this may be stacked on top of.
                HistogramPlot currentPlot = this;
                yoff = 0.0f;
                double yval = 0.0f;
                while (currentPlot.isStacked_)
                {
                    SequenceAdapter stackedToData = new SequenceAdapter(
                        currentPlot.stackedTo_.DataSource,
                        currentPlot.stackedTo_.DataMember,
                        currentPlot.stackedTo_.OrdinateData,
                        currentPlot.stackedTo_.AbscissaData);

                    yval += stackedToData[i].Y;
                    yoff = yAxis.WorldToPhysical(yval, false).Y;
                    p1.Y += stackedToData[i].Y;
                    p2.Y += stackedToData[i].Y;
                    currentPlot = currentPlot.stackedTo_;
                }

                // (4) now account for centering
                if (center_)
                {
                    double offset = (p2.X - p1.X)/2.0f;
                    p1.X -= offset;
                    p2.X -= offset;
                }

                // (5) now account for BaseOffset (shift of bar sideways).
                p1.X += baseOffset_;
                p2.X += baseOffset_;

                // (6) now get physical coordinates of top two points.
                PointF xPos1 = xAxis.WorldToPhysical(p1.X, false);
                PointF yPos1 = yAxis.WorldToPhysical(p1.Y, false);
                PointF xPos2 = xAxis.WorldToPhysical(p2.X, false);
                PointF yPos2 = yAxis.WorldToPhysical(p2.Y, false);

                if (isStacked_)
                {
                    currentPlot = this;
                    while (currentPlot.isStacked_)
                    {
                        currentPlot = currentPlot.stackedTo_;
                    }
                    baseWidth_ = currentPlot.baseWidth_;
                }

                float width = xPos2.X - xPos1.X;
                float height;
                if (isStacked_)
                {
                    height = -yPos1.Y + yoff;
                }
                else
                {
                    height = -yPos1.Y + yAxis.PhysicalMin.Y;
                }

                float xoff = (1.0f - baseWidth_)/2.0f*width;
                Rectangle r = new Rectangle((int) (xPos1.X + xoff), (int) yPos1.Y, (int) (width - 2*xoff), (int) height);

                if (Filled)
                {
                    if (r.Height != 0 && r.Width != 0)
                    {
                        // room for optimization maybe.
                        g.FillRectangle(rectangleBrush_.Get(r), r);
                    }
                }

                g.DrawRectangle(Pen, r.X, r.Y, r.Width, r.Height);
            }
        }

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

            Axis a = data.SuggestXAxis();
            if (data.Count == 0)
            {
                return a;
            }

            PointD p1;
            PointD p2;
            PointD p3;
            PointD p4;
            if (data.Count < 2)
            {
                p1 = data[0];
                p1.X -= 1.0;
                p2 = data[0];
                p3 = p1;
                p4 = p2;
            }
            else
            {
                p1 = data[0];
                p2 = data[1];
                p3 = data[data.Count - 2];
                p4 = data[data.Count - 1];
            }

            double offset1;
            double offset2;

            if (!center_)
            {
                offset1 = 0.0f;
                offset2 = p4.X - p3.X;
            }
            else
            {
                offset1 = (p2.X - p1.X)/2.0f;
                offset2 = (p4.X - p3.X)/2.0f;
            }

            a.WorldMin -= offset1;
            a.WorldMax += offset2;

            return a;
        }

        /// <summary>
        /// Returns a y-axis that is suitable for drawing this plot.
        /// </summary>
        /// <returns>A suitable y-axis.</returns>
        public Axis SuggestYAxis()
        {
            if (isStacked_)
            {
                double tmpMax = 0.0f;
                ArrayList adapterList = new ArrayList();

                HistogramPlot currentPlot = this;
                do
                {
                    adapterList.Add(new SequenceAdapter(
                                        currentPlot.DataSource,
                                        currentPlot.DataMember,
                                        currentPlot.OrdinateData,
                                        currentPlot.AbscissaData)
                        );
                } while ((currentPlot = currentPlot.stackedTo_) != null);

                SequenceAdapter[] adapters =
                    (SequenceAdapter[]) adapterList.ToArray(typeof (SequenceAdapter));

                for (int i = 0; i < adapters[0].Count; ++i)
                {
                    double tmpHeight = 0.0f;
                    for (int j = 0; j < adapters.Length; ++j)
                    {
                        tmpHeight += adapters[j][i].Y;
                    }
                    tmpMax = Math.Max(tmpMax, tmpHeight);
                }

                Axis a = new LinearAxis(0.0f, tmpMax);
                // TODO make 0.08 a parameter.
                a.IncreaseRange(0.08);
                return a;
            }
            else
            {
                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 void DrawInLegend(Graphics g, Rectangle startEnd)
        {
            if (Filled)
            {
                g.FillRectangle(rectangleBrush_.Get(startEnd), startEnd);
            }

            g.DrawRectangle(Pen, startEnd.X, startEnd.Y, startEnd.Width, startEnd.Height);
        }

        /// <summary>
        /// Stack the histogram to another HistogramPlot.
        /// </summary>
        public void StackedTo(HistogramPlot hp)
        {
            SequenceAdapter data =
                new SequenceAdapter(DataSource, DataMember, OrdinateData, AbscissaData);

            SequenceAdapter hpData =
                new SequenceAdapter(hp.DataSource, hp.DataMember, hp.OrdinateData, hp.AbscissaData);

            if (hp != null)
            {
                isStacked_ = true;
                if (hpData.Count != data.Count)
                {
                    throw new NPlotException("Can stack HistogramPlot data only with the same number of datapoints.");
                }
                for (int i = 0; i < data.Count; ++i)
                {
                    if (data[i].X != hpData[i].X)
                    {
                        throw new NPlotException("Can stack HistogramPlot data only with the same X coordinates.");
                    }
                    if (hpData[i].Y < 0.0f)
                    {
                        throw new NPlotException("Can stack HistogramPlot data only with positive Y coordinates.");
                    }
                }
            }
            stackedTo_ = hp;
        }
    }
}