/*
 * NPlot - A charting library for .NET
 * 
 * CandlePlot.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.Data;

namespace NPlot
{

    /// <summary>
    /// Encapsulates open, low, high and close values useful for specifying financial data
    /// over a time period, together with a [single] x-value indicating the time [period] the
    /// data corresponds to. 
    /// </summary>
    public class PointOLHC
    {

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="x">value representing the time period that the financial values refer to</param>
        /// <param name="open">The value at open of time period.</param>
        /// <param name="low">The low value over the time period</param>
        /// <param name="high">The high value over the time period.</param>
        /// <param name="close">The value at close of time period.</param>
        public PointOLHC( double x, double open, double low, double high, double close )
        {
            this.x_ = x;
            this.open_ = open;
            this.close_ = close;
            this.low_ = low;
            this.high_ = high;
        }


        /// <summary>
        /// value representing the time period that the financial values apply to.
        /// </summary>
        public double X
        {
            get
            {
                return x_;
            }
            set
            {
                this.x_ = value;
            }
        }
        private double x_;


        /// <summary>
        /// The value at open of time period.
        /// </summary>
        public double Open
        {
            get
            {
                return open_;
            }
            set
            {
                open_ = value;
            }
        }
        private double open_;


        /// <summary>
        /// The value at close of time period.
        /// </summary>
        public double Close
        {
            get
            {
                return close_;
            }
            set
            {
                close_ = value;
            }
        }
        private double close_;


        /// <summary>
        /// Low value of the time period.
        /// </summary>
        public double Low
        {
            get
            {
                return low_;
            }
            set
            {
                low_ = value;
            }
        }
        private double low_;


        /// <summary>
        /// High value of the time period.
        /// </summary>
        public double High
        {
            get
            {
                return high_;
            }
            set
            {
                high_ = value;
            }
        }
        private double high_;

    }


    /// <summary>
    /// Encapsulates functionality for drawing finacial candle charts. 
    /// </summary>
    public class CandlePlot : BasePlot, IPlot
    {

        /// <summary>
        /// 
        /// </summary>
        public abstract class CandleStyle
        {
            /// <summary>
            /// 
            /// </summary>
            /// <param name="d"></param>
            /// <returns></returns>
            public abstract CandleStyle Create(CandleDataAdapter d);
        }

        /// <summary>
        /// 
        /// </summary>
        public class Stick : CandleStyle
        {
            private Stick() { }

            /// <summary>
            /// 
            /// </summary>
            /// <param name="d"></param>
            /// <returns></returns>
            public override CandleStyle Create(CandleDataAdapter d)
            {
                return new Stick();
            }
        }
        

        /// <summary>
        /// This class is responsible for interpreting the various ways you can 
        /// specify data to CandlePlot objects
        /// </summary>
        public class CandleDataAdapter
        {
            private object openData_;
            private object lowData_;
            private object highData_;
            private object closeData_;
            private object abscissaData_;

            private object dataSource_;
            private string dataMember_;
            DataRowCollection rows_ = null;

            // speed optimizations if data is double.
            private double[] openDataArray_;
            private double[] lowDataArray_;
            private double[] highDataArray_;
            private double[] closeDataArray_;
            private double[] abscissaDataArray_;
            private bool useDoublesArrays_;
            

            /// <summary>
            /// Constructor
            /// </summary>
            /// <param name="dataSource"></param>
            /// <param name="dataMember"></param>
            /// <param name="abscissaData"></param>
            /// <param name="openData"></param>
            /// <param name="lowData"></param>
            /// <param name="highData"></param>
            /// <param name="closeData"></param>
            public CandleDataAdapter( 
                object dataSource, string dataMember, object abscissaData,
                object openData, object lowData, object highData, object closeData )
            {
                this.openData_ = openData;
                this.lowData_ = lowData;
                this.highData_ = highData;
                this.closeData_ = closeData;
                this.abscissaData_ = abscissaData;

                this.dataSource_ = dataSource;
                this.dataMember_ = dataMember;

                if (dataSource_ != null)
                {
                    if ( dataSource_ is DataSet )
                    {
                        if (dataMember_ != null)
                        {
                            rows_ = ((DataTable)((DataSet)dataSource_).Tables[dataMember_]).Rows;
                        }
                        else
                        {
                            rows_ = ((DataTable)((DataSet)dataSource_).Tables[0]).Rows;
                        }
                    }

                    else if (dataSource_ is DataTable )
                    {
                        rows_ = ((DataTable)dataSource_).Rows;
                    }

                    else
                    {
                        throw new NPlotException ( "not implemented yet" );
                    }
                }

                openDataArray_ = openData_ as System.Double[];
                lowDataArray_ = lowData_ as System.Double[];
                highDataArray_ = highData_ as System.Double[];
                closeDataArray_ = closeData_ as System.Double[];
                abscissaDataArray_ = abscissaData_ as System.Double[];
                useDoublesArrays_ = (
                    openDataArray_ != null &&
                    lowDataArray_ != null &&
                    highDataArray_ != null &&
                    lowDataArray_ != null &&
                    abscissaDataArray_ != null
                );

            }


            /// <summary>
            /// Gets the ith point in the candle adapter
            /// </summary>
            /// <param name="i">index of datapoint to get</param>
            /// <returns>the datapoint.</returns>
            public PointOLHC this[int i]
            {
                get
                {
                    
                    // try a fast track first
                    if (useDoublesArrays_)
                    {
                        return new PointOLHC(
                            abscissaDataArray_[i],
                            openDataArray_[i],
                            lowDataArray_[i],
                            highDataArray_[i],
                            closeDataArray_[i]);
                    }

                    // is the data coming from a data source?
                    else if (rows_ != null)
                    {
                        double x = Utils.ToDouble(((DataRow)(rows_[i]))[(string)abscissaData_]);
                        double open = Utils.ToDouble(((DataRow)(rows_[i]))[(string)openData_]);
                        double low = Utils.ToDouble(((DataRow)(rows_[i]))[(string)lowData_]);
                        double high = Utils.ToDouble(((DataRow)(rows_[i]))[(string)highData_]);
                        double close = Utils.ToDouble(((DataRow)(rows_[i]))[(string)closeData_]);

                        return new PointOLHC(x, open, low, high, close);
                    }

                    // the data is coming from individual arrays.
                    else if (abscissaData_ is Array && openData_ is Array && lowData_ is Array && highData_ is Array && closeData_ is Array)
                    {
                        double x = Utils.ToDouble(((Array)abscissaData_).GetValue(i));
                        double open = Utils.ToDouble(((Array)openData_).GetValue(i));
                        double low = Utils.ToDouble(((Array)lowData_).GetValue(i));
                        double high = Utils.ToDouble(((Array)highData_).GetValue(i));
                        double close = Utils.ToDouble(((Array)closeData_).GetValue(i));

                        return new PointOLHC(x, open, low, high, close);
                    }

                    else
                    {
                        throw new NPlotException("not implemented yet");
                    }

                }
            }


            /// <summary>
            /// The number of datapoints available via the candle adapter.
            /// </summary>
            /// <value>the number of datapoints available.</value>
            public int Count
            {
                get
                {
                    // this is inefficient [could set up delegates in constructor].

                    if (useDoublesArrays_)
                    {
                        return openDataArray_.Length;
                    }

                    if (openData_ == null)
                    {
                        return 0;
                    }

                    if (rows_ != null)
                    {
                        return rows_.Count;
                    }

                    if (openData_ is Array)
                    {
                        int size = ((Array)openData_).Length;
                        if (size != ((Array)closeData_).Length)
                            throw new NPlotException("open and close arrays are not of same length");
                        if (size != ((Array)lowData_).Length)
                            throw new NPlotException("open and close arrays are not of same length");
                        if (size != ((Array)highData_).Length)
                            throw new NPlotException("open and close arrays are not of same length");
                        return size;
                    }

                    throw new NPlotException( "data not in correct format" );
                }
            }


            /// <summary>
            /// Returns an x-axis that is suitable for drawing the data.
            /// </summary>
            /// <returns>A suitable x-axis.</returns>
            public Axis SuggestXAxis()
            {
                double min;
                double max;
                double minStep = 0.0;

                if (this.rows_ == null)
                {
                    Utils.ArrayMinMax((System.Collections.IList)this.abscissaData_, out min, out max);

                    if (((System.Collections.IList)abscissaData_).Count > 1)
                    {
                        double first = Utils.ToDouble(((Array)abscissaData_).GetValue(0));
                        double second = Utils.ToDouble(((Array)abscissaData_).GetValue(1));
                        minStep = Math.Abs(second - first);
                    }
                    if (((System.Collections.IList)abscissaData_).Count > 2)
                    {
                        double first = Utils.ToDouble(((Array)abscissaData_).GetValue(1));
                        double second = Utils.ToDouble(((Array)abscissaData_).GetValue(2));
                        if (Math.Abs(second - first) < minStep)
                            minStep = Math.Abs(second - first);
                    }
                    if (((System.Collections.IList)abscissaData_)[0] is DateTime)
                    {
                        return new DateTimeAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                    else
                    {
                        return new LinearAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                }
                else
                {

                    Utils.RowArrayMinMax(this.rows_, out min, out max, (string)this.abscissaData_);

                    if (rows_.Count > 1)
                    {
                        double first = Utils.ToDouble(rows_[0][(string)abscissaData_]);
                        double second = Utils.ToDouble(rows_[1][(string)abscissaData_]);
                        minStep = Math.Abs(second - first);
                    }
                    if (rows_.Count > 2)
                    {
                        double first = Utils.ToDouble(rows_[1][(string)abscissaData_]);
                        double second = Utils.ToDouble(rows_[2][(string)abscissaData_]);
                        if (Math.Abs(second - first) < minStep)
                            minStep = Math.Abs(second - first);
                    }

                    if ((rows_[0])[(string)abscissaData_] is DateTime)
                    {
                        return new DateTimeAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                    else
                    {
                        return new LinearAxis(min - minStep / 2.0, max + minStep / 2.0);
                    }
                }

            }


            /// <summary>
            /// Returns a y-axis that is suitable for drawing the data.
            /// </summary>
            /// <returns>A suitable y-axis.</returns>
            public Axis SuggestYAxis()
            {

                double min_l;
                double max_l;
                double min_h;
                double max_h;

                if (this.rows_ == null)
                {
                    Utils.ArrayMinMax((System.Collections.IList)lowData_, out min_l, out max_l);
                    Utils.ArrayMinMax((System.Collections.IList)highData_, out min_h, out max_h);
                }
                else
                {
                    Utils.RowArrayMinMax(this.rows_, out min_l, out max_l, (string)this.lowData_);
                    Utils.RowArrayMinMax(this.rows_, out min_h, out max_h, (string)this.highData_);
                }

                Axis a = new LinearAxis( min_l, max_h );
                a.IncreaseRange( 0.08 );
                return a;
            }

        }

        
        /// <summary>
        /// Default constructor.
        /// </summary>
        public CandlePlot()
        {
        }


        /// <summary>
        /// Calculates the physical (not world) separation between abscissa values.
        /// </summary>
        /// <param name="cd">Candle adapter containing data</param>
        /// <param name="xAxis">Physical x axis the data is plotted against.</param>
        /// <returns>physical separation between abscissa values.</returns>
        private static float CalculatePhysicalSeparation( CandleDataAdapter cd, PhysicalAxis xAxis )
        {
            if (cd.Count > 1)
            {
                int xPos1 = (int)(xAxis.WorldToPhysical( ((PointOLHC)cd[0]).X, false )).X;
                int xPos2 = (int)(xAxis.WorldToPhysical( ((PointOLHC)cd[1]).X, false )).X;        
                int minDist = xPos2 - xPos1;

                if (cd.Count > 2)
                {  // to be pretty sure we get the smallest gap.
                    int xPos3 = (int)(xAxis.WorldToPhysical(((PointOLHC)cd[2]).X, false)).X;
                    if (xPos3 - xPos2 < minDist) minDist = xPos3 - xPos2;

                    if (cd.Count > 3)
                    {
                        int xPos4 = (int)(xAxis.WorldToPhysical(((PointOLHC)cd[3]).X, false)).X;
                        if (xPos4 - xPos3 < minDist) minDist = xPos4 - xPos3;
                    }
                }

                return minDist;
            }

            return 0.0f;
        }


        /// <summary>
        /// Draws the candle plot on a GDI+ surface agains 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 )
        {
            CandleDataAdapter cd = new CandleDataAdapter( this.DataSource, this.DataMember, 
                this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData );

            Brush bearishBrush = new SolidBrush( BearishColor );
            Brush bullishBrush = new SolidBrush( BullishColor );

            uint offset = 0;
            if (this.centered_)
            {
                offset = (uint)(CalculatePhysicalSeparation(cd,xAxis) / 2.0f);
            }

            uint addAmount = (uint)StickWidth/2;
            uint stickWidth = (uint)StickWidth;

            if (StickWidth == AutoScaleStickWidth)
            {
                // default
                addAmount = 2;
                stickWidth = 4;
            
                float minDist = CalculatePhysicalSeparation( cd, xAxis );

                addAmount = (uint)(minDist / 3);
                stickWidth = addAmount * 2;
            }

            Pen    p =    new    Pen(this.color_);

            /*
            // brant hyatt proposed.
            if (this.Style == Styles.Stick)
            {
                p.Width = stickWidth;
                 addAmount = stickWidth + 2;
            }
            */

            for (int i=0; i<cd.Count; ++i)
            {

                PointOLHC point = (PointOLHC)cd[i];
                if ( (!double.IsNaN (point.Open)) && (!double.IsNaN(point.High)) && (!double.IsNaN (point.Low)) && (!double.IsNaN(point.Close)) )
                {

                    int xPos = (int)(xAxis.WorldToPhysical( point.X, false )).X;

                    if (xPos + offset + addAmount < xAxis.PhysicalMin.X || xAxis.PhysicalMax.X < xPos + offset - addAmount)
                        continue;

                    int yPos1 = (int)(yAxis.WorldToPhysical( point.Low, false )).Y;
                    int yPos2 = (int)(yAxis.WorldToPhysical( point.High, false )).Y;
                    int yPos3 = (int)(yAxis.WorldToPhysical( point.Open, false )).Y;
                    int yPos4 = (int)(yAxis.WorldToPhysical( point.Close, false )).Y;

                    if (this.Style == Styles.Stick)
                    {

                        /*
                        // brant hyatt proposed.
                        if (i > 0) 
                        {
                            if ( ((PointOLHC)cd[i]).Close > ((PointOLHC)cd[i-1]).Close)
                            {
                                p.Color = BullishColor;
                            }
                            else
                            {
                                p.Color = BearishColor;
                            }
                        }
                        */

                        g.DrawLine( p, xPos+offset, yPos1, xPos+offset, yPos2 );
                        g.DrawLine( p, xPos-addAmount+offset, yPos3, xPos+offset, yPos3 );
                        g.DrawLine( p, xPos+offset, yPos4, xPos+addAmount+offset, yPos4 );
                    }

                    else if (this.Style == Styles.Filled)
                    {
                        g.DrawLine( p, xPos+offset, yPos1, xPos+offset, yPos2 );
                        if (yPos3 > yPos4)
                        {
                            g.FillRectangle( bullishBrush, xPos-addAmount+offset, yPos4, stickWidth, yPos3 - yPos4 );
                            g.DrawRectangle( p, xPos-addAmount+offset, yPos4, stickWidth, yPos3 - yPos4 );
                        }
                        else if (yPos3 < yPos4)
                        {
                            g.FillRectangle( bearishBrush, xPos-addAmount+offset, yPos3, stickWidth, yPos4 - yPos3 );
                            g.DrawRectangle( p, xPos-addAmount+offset, yPos3, stickWidth, yPos4 - yPos3 );
                        }
                        else
                        {
                            g.DrawLine( p, xPos-addAmount+offset, yPos3, xPos-addAmount+stickWidth+offset, yPos3 );
                        }

                    }

                }
            }

        }


        /// <summary>
        /// Returns an x-axis that is suitable for drawing this plot.
        /// </summary>
        /// <returns>A suitable x-axis.</returns>
        public Axis SuggestXAxis()
        {
            CandleDataAdapter candleData = new CandleDataAdapter( this.DataSource, this.DataMember, 
                this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData );
        
            return candleData.SuggestXAxis();
        }


        /// <summary>
        /// Returns a y-axis that is suitable for drawing this plot.
        /// </summary>
        /// <returns>A suitable y-axis.</returns>
        public Axis SuggestYAxis()
        {
            CandleDataAdapter candleData = new CandleDataAdapter( this.DataSource, this.DataMember, 
                this.AbscissaData, this.OpenData, this.LowData, this.HighData, this.CloseData );

            return candleData.SuggestYAxis();
        }


        /// <summary>
        /// Gets or sets the data, or column name for the open values.
        /// </summary>
        public object OpenData
        {
            get
            {
                return openData_;
            }
            set
            {
                openData_ = value;
            }
        }
        private object openData_ = null;


        /// <summary>
        /// Gets or sets the data, or column name for the interval low values.
        /// </summary>
        public object LowData
        {
            get
            {
                return lowData_;
            }
            set
            {
                lowData_ = value;
            }
        }
        private object lowData_ = null;


        /// <summary>
        /// Gets or sets the data, or column name for the interval high values.
        /// </summary>
        public object HighData
        {
            get
            {
                return highData_;
            }
            set
            {
                highData_ = value;
            }
        }
        private object highData_ = null;


        /// <summary>
        /// Gets or sets the data, or column name for the close values.
        /// </summary>
        public object CloseData
        {
            get
            {
                return closeData_;
            }
            set
            {
                closeData_ = value;
            }
        }    
        private object closeData_ = null;


        /// <summary>
        /// Gets or sets the data, or column name for the abscissa [x] axis.
        /// </summary>
        public object AbscissaData
        {
            get
            {
                return abscissaData_;
            }
            set
            {
                abscissaData_ = value;
            }
        }
        private object abscissaData_ = null;


        /// <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 )
        {
            Pen    p =    new    Pen(this.color_);

            g.DrawLine( p, startEnd.Left, (startEnd.Top + startEnd.Bottom)/2, 
                startEnd.Right, (startEnd.Top + startEnd.Bottom)/2 );

        }


        /// <summary>
        /// Color of this plot [excluding interior of filled boxes if Style is fill]. To
        /// change the Bullish and Bearish colours in Filled mode, use the BullishColor
        /// and BearishColor properties.
        /// </summary>
        public System.Drawing.Color Color
        {
            get
            {
                return color_;
            }
            set
            {
                color_ = value;
            }
        }
        Color color_ = Color.Black;
        

        /// <summary>
        /// Possible CandleStick styles.
        /// </summary>
        public enum Styles
        {

            /// <summary>
            /// Draw vertical line between low and high, tick on left for open and tick on right for close.
            /// </summary>
            Stick,

            /// <summary>
            /// Draw vertical line between low and high and place on top of this a box with bottom
            /// and top determined by open and high values. The box is filled using the colors specified
            /// in BullishColor and BearishColor properties.
            /// </summary>
            Filled

        }


        /// <summary>
        /// Specifies the CandleStick style to use.
        /// </summary>
        public Styles Style = Styles.Filled;


        /// <summary>
        /// If CandlePlot.Style is Filled, then bullish open-close moves are displayed in this color.
        /// </summary>
        public Color BullishColor = Color.White;


        /// <summary>
        /// If CandlePlot.Style is Filled, then bearish moves are displayed in this color.
        /// </summary>
        public Color BearishColor = Color.Black;


        /// <summary>
        /// Width of each stick in pixels. It is best if this is an odd number.
        /// </summary>
        public int StickWidth
        {
            get
            {
                return stickWidth_;
            }
            set
            {
                if (value < 1)
                {
                    throw new NPlotException( "Stick width must be greater than 0." );
                }
                stickWidth_ = value;
            }
        }
        private int stickWidth_ = AutoScaleStickWidth;


        /// <summary>
        /// If stick width is set equal to this value, the width will be 
        /// automatically scaled dependant on the space between sticks.
        /// </summary>
        public const int AutoScaleStickWidth = 0;

        /// <summary>
        /// If true (default), bars will be centered on the abscissa times. 
        /// If false, bars will be drawn between the corresponding abscissa time
        /// and the next abscissa time. 
        /// </summary>
        /// <value></value>
        public bool Centered
        {
            get
            {
                return centered_;
            }
            set
            {
                centered_ = value;
            }
        }
        private bool centered_ = true;


        /// <summary>
        /// Write data associated with the plot as text.
        /// </summary>
        /// <param name="sb">the string builder to write to.</param>
        /// <param name="region">Only write out data in this region if onlyInRegion is true.</param>
        /// <param name="onlyInRegion">If true, only data in region is written, else all data is written.</param>
        /// <remarks>TODO: not implemented.</remarks>
        public void WriteData( System.Text.StringBuilder sb, RectangleD region, bool onlyInRegion )
        {
        }

    }
}
