/*
 * NPlot - A charting library for .NET
 * 
 * TradingDateTimeAxis.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 a DateTime axis that removes non-trading days.
    /// </summary>
    public class TradingDateTimeAxis : DateTimeAxis
    {
        // we keep shadow "virtual" copies of WorldMin/Max for speed
        // which are already remapped, so it is essential that changes
        // to WorldMin/Max are captured here

        private long endTradingTime_;
        private long startTradingTime_;
        private long tradingTimeSpan_;
        private double virtualWorldMax_ = double.NaN;
        private double virtualWorldMin_ = double.NaN;

        /// <summary>
        /// Constructor
        /// </summary>
        public TradingDateTimeAxis()
        {
            Init();
        }

        /// <summary>
        /// Copy Constructor
        /// </summary>
        /// <param name="a">construct a TradingDateTimeAxis based on this provided axis.</param>
        public TradingDateTimeAxis(Axis a) : base(a)
        {
            Init();
            if (a is TradingDateTimeAxis)
                DoClone((TradingDateTimeAxis) a, this);
            else if (a is DateTimeAxis)
                DoClone((DateTimeAxis) a, this);
            else
            {
                DoClone(a, this);
                NumberFormat = null;
            }
        }

        /// <summary>
        /// The axis world min value.
        /// </summary>
        public override double WorldMin
        {
            get { return base.WorldMin; }
            set
            {
                base.WorldMin = value;
                virtualWorldMin_ = SparseWorldRemap(value);
            }
        }

        /// <summary>
        /// The axis world max value.
        /// </summary>
        public override double WorldMax
        {
            get { return base.WorldMax; }
            set
            {
                base.WorldMax = value;
                virtualWorldMax_ = SparseWorldRemap(value);
            }
        }

        /// <summary>
        /// Optional time at which trading begins.
        /// All data points earlied than that (same day) will be collapsed.
        /// </summary>
        public virtual TimeSpan StartTradingTime
        {
            get { return new TimeSpan(startTradingTime_); }
            set
            {
                startTradingTime_ = value.Ticks;
                tradingTimeSpan_ = endTradingTime_ - startTradingTime_;
            }
        }

        /// <summary>
        /// Optional time at which trading ends.
        /// All data points later than that (same day) will be collapsed.
        /// </summary>
        public virtual TimeSpan EndTradingTime
        {
            get { return new TimeSpan(endTradingTime_); }
            set
            {
                endTradingTime_ = value.Ticks;
                tradingTimeSpan_ = endTradingTime_ - startTradingTime_;
            }
        }

        /// <summary>
        /// Get whether or not this axis is linear.
        /// </summary>
        public override bool IsLinear
        {
            get { return false; }
        }

        /// <summary>
        /// World extent in virtual (sparse) units.
        /// </summary>
        public double SparseWorldLength
        {
            get { return SparseWorldRemap(WorldMax) - SparseWorldRemap(WorldMin); }
        }

        /// <summary>
        /// Helper function for constructors.
        /// </summary>
        private void Init()
        {
            startTradingTime_ = 0;
            endTradingTime_ = TimeSpan.TicksPerDay;
            tradingTimeSpan_ = endTradingTime_ - startTradingTime_;
            virtualWorldMin_ = SparseWorldRemap(WorldMin);
            virtualWorldMax_ = SparseWorldRemap(WorldMax);
        }

        /// <summary>
        /// Deep copy of DateTimeAxis.
        /// </summary>
        /// <returns>A copy of the DateTimeAxis Class.</returns>
        public override object Clone()
        {
            TradingDateTimeAxis a = new TradingDateTimeAxis();
            // ensure that this isn't being called on a derived type. If it is, then oh no!
            if (GetType() != a.GetType())
            {
                throw new NPlotException("Clone not defined in derived type. Help!");
            }
            DoClone(this, a);
            return a;
        }

        /// <summary>
        /// Helper method for Clone.
        /// </summary>
        /// <param name="a">The cloned target object.</param>
        /// <param name="b">The cloned source object.</param>
        protected static void DoClone(TradingDateTimeAxis b, TradingDateTimeAxis a)
        {
            DateTimeAxis.DoClone(b, a);
            a.startTradingTime_ = b.startTradingTime_;
            a.endTradingTime_ = b.endTradingTime_;
            a.tradingTimeSpan_ = b.tradingTimeSpan_;
            a.WorldMin = b.WorldMin;
            a.WorldMax = b.WorldMax;
        }

        /// <summary>
        /// World to physical coordinate transform.
        /// </summary>
        /// <param name="coord">The coordinate value to transform.</param>
        /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
        /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
        /// <param name="clip">if false, then physical value may extend outside worldMin / worldMax. If true, the physical value returned will be clipped to physicalMin or physicalMax if it lies outside this range.</param>
        /// <returns>The transformed coordinates.</returns>
        /// <remarks>
        /// Not sure how much time is spent in this often called function. If it's lots, then
        /// worth optimizing (there is scope to do so).
        /// </remarks>
        public override PointF WorldToPhysical(
            double coord,
            PointF physicalMin,
            PointF physicalMax,
            bool clip)
        {
            // (1) account for reversed axis. Could be tricky and move
            // this out, but would be a little messy.

            PointF _physicalMin;
            PointF _physicalMax;

            if (Reversed)
            {
                _physicalMin = physicalMax;
                _physicalMax = physicalMin;
            }
            else
            {
                _physicalMin = physicalMin;
                _physicalMax = physicalMax;
            }

            // (2) if want clipped value, return extrema if outside range.

            if (clip)
            {
                if (WorldMin < WorldMax)
                {
                    if (coord > WorldMax)
                    {
                        return _physicalMax;
                    }
                    if (coord < WorldMin)
                    {
                        return _physicalMin;
                    }
                }
                else
                {
                    if (coord < WorldMax)
                    {
                        return _physicalMax;
                    }
                    if (coord > WorldMin)
                    {
                        return _physicalMin;
                    }
                }
            }

            // (3) we are inside range or don't want to clip.

            coord = SparseWorldRemap(coord);
            double range = virtualWorldMax_ - virtualWorldMin_;
            double prop = ((coord - virtualWorldMin_)/range);
            //double range = WorldMax - WorldMin;
            //double prop = (double)((coord - WorldMin) / range);
            //if (range1 != range)
            //    range1 = range;

            // Force clipping at bounding box largeClip times that of real bounding box
            // anyway. This is effectively at infinity.
            const double largeClip = 100.0;
            if (prop > largeClip && clip)
                prop = largeClip;

            if (prop < -largeClip && clip)
                prop = -largeClip;

            if (range == 0)
            {
                if (coord >= virtualWorldMin_)
                    prop = largeClip;

                if (coord < virtualWorldMin_)
                    prop = -largeClip;
            }

            // calculate the physical coordinate.
            PointF offset = new PointF(
                (float) (prop*(_physicalMax.X - _physicalMin.X)),
                (float) (prop*(_physicalMax.Y - _physicalMin.Y)));

            return new PointF(_physicalMin.X + offset.X, _physicalMin.Y + offset.Y);
        }

        /// <summary>
        /// Transforms a physical coordinate to an axis world
        /// coordinate given the physical extremites of the axis.
        /// </summary>
        /// <param name="p">the point to convert</param>
        /// <param name="physicalMin">the physical minimum extremity of the axis</param>
        /// <param name="physicalMax">the physical maximum extremity of the axis</param>
        /// <param name="clip">whether or not to clip the world value to lie in the range of the axis if it is outside.</param>
        /// <returns></returns>
        public override double PhysicalToWorld(
            PointF p,
            PointF physicalMin,
            PointF physicalMax,
            bool clip)
        {
            // (1) account for reversed axis. Could be tricky and move
            // this out, but would be a little messy.

            PointF _physicalMin;
            PointF _physicalMax;

            if (Reversed)
            {
                _physicalMin = physicalMax;
                _physicalMax = physicalMin;
            }
            else
            {
                _physicalMin = physicalMin;
                _physicalMax = physicalMax;
            }

            // normalised axis dir vector
            float axis_X = _physicalMax.X - _physicalMin.X;
            float axis_Y = _physicalMax.Y - _physicalMin.Y;
            float len = (float) Math.Sqrt(axis_X*axis_X + axis_Y*axis_Y);
            axis_X /= len;
            axis_Y /= len;

            // point relative to axis physical minimum.
            PointF posRel = new PointF(p.X - _physicalMin.X, p.Y - _physicalMin.Y);

            // dist of point projection on axis, normalised.
            float prop = (axis_X*posRel.X + axis_Y*posRel.Y)/len;

            //double world = prop * (WorldMax - WorldMin) + WorldMin;
            double world = prop*(virtualWorldMax_ - virtualWorldMin_) + virtualWorldMin_;
            world = ReverseSparseWorldRemap(world);

            // if want clipped value, return extrema if outside range.
            if (clip)
            {
                world = Math.Max(world, WorldMin);
                world = Math.Min(world, WorldMax);
            }

            return world;
        }

        /// <summary>
        /// Remap a world coordinate into a "virtual" world, where non-trading dates and times are collapsed.
        /// </summary>
        /// <remarks>
        /// This code works under asumption that there are exactly 24*60*60 seconds in a day
        /// This is strictly speaking not correct but apparently .NET 2.0 does not count leap seconds.
        /// Luckilly, Ticks == 0  =~= 0001-01-01T00:00 =~= Monday
        /// First tried a version fully on floating point arithmetic,
        /// but failed hopelessly due to rounding errors.
        /// </remarks>
        /// <param name="coord">world coordinate to transform.</param>
        /// <returns>equivalent virtual world coordinate.</returns>
        protected double SparseWorldRemap(double coord)
        {
            long ticks = (long) coord;
            long whole_days = ticks/TimeSpan.TicksPerDay;
            long ticks_in_last_day = ticks%TimeSpan.TicksPerDay;
            long full_weeks = whole_days/7;
            long days_in_last_week = whole_days%7;
            if (days_in_last_week >= 5)
            {
                days_in_last_week = 5;
                ticks_in_last_day = 0;
            }
            if (ticks_in_last_day < startTradingTime_) ticks_in_last_day = startTradingTime_;
            else if (ticks_in_last_day > endTradingTime_) ticks_in_last_day = endTradingTime_;
            ticks_in_last_day -= startTradingTime_;

            long whole_working_days = (full_weeks*5 + days_in_last_week);
            long working_ticks = whole_working_days*tradingTimeSpan_;
            long new_ticks = working_ticks + ticks_in_last_day;
            return new_ticks;
        }

        /// <summary>
        /// Remaps a "virtual" world coordinates back to true world coordinates.
        /// </summary>
        /// <param name="coord">virtual world coordinate to transform.</param>
        /// <returns>equivalent world coordinate.</returns>
        protected double ReverseSparseWorldRemap(double coord)
        {
            long ticks = (long) coord;
            //ticks += startTradingTime_;
            long ticks_in_last_day = ticks%tradingTimeSpan_;
            ticks /= tradingTimeSpan_;
            long full_weeks = ticks/5;
            long week_part = ticks%5;

            long day_ticks = (full_weeks*7 + week_part)*TimeSpan.TicksPerDay;
            return (day_ticks + ticks_in_last_day + startTradingTime_);
        }

        /// <summary>
        /// Adds a delta amount to the given world coordinate in such a way that
        /// all "sparse gaps" are skipped.  In other words, the returned value is
        /// in delta distance from the given in the "virtual" world.
        /// </summary>
        /// <param name="coord">world coordinate to shift.</param>
        /// <param name="delta">shif amount in "virtual" units.</param>
        /// <returns></returns>
        public double SparseWorldAdd(double coord, double delta)
        {
            return ReverseSparseWorldRemap(SparseWorldRemap(coord) + delta);
        }

        /// <summary>
        /// Check whether the given coordinate falls within defined trading hours.
        /// </summary>
        /// <param name="coord">world coordinate in ticks to check.</param>
        /// <returns>true if in trading hours, false if in non-trading gap.</returns>
        public bool WithinTradingHours(double coord)
        {
            long ticks = (long) coord;
            long whole_days = ticks/TimeSpan.TicksPerDay;
            long ticks_in_last_day = ticks%TimeSpan.TicksPerDay;
            long days_in_last_week = whole_days%7;
            if (days_in_last_week >= 5)
                return false;

            if (ticks_in_last_day < startTradingTime_) return false;
            if (ticks_in_last_day >= endTradingTime_) return false;

            return true;
        }

        /// <summary>
        /// Check whether the given coordinate falls on trading days.
        /// </summary>
        /// <param name="coord">world coordinate in ticks to check.</param>
        /// <returns>true if on Mon - Fri.</returns>
        public bool OnTradingDays(double coord)
        {
            long ticks = (long) coord;
            long whole_days = ticks/TimeSpan.TicksPerDay;
            long days_in_last_week = whole_days%7;
            return (days_in_last_week < 5);
        }

        /// <summary>
        /// Determines the positions of all Large and Small ticks.
        /// </summary>
        /// <remarks>
        /// The method WorldTickPositions_FirstPass() from the base works just fine, except that it
        /// does not account for non-trading gaps in time, therefore, when less than two days are visible
        /// an own algorithm is used (to show intraday time).  Otherwise the base class implementation is used
        /// but the output is corrected to remove ticks on non-trading days (Sat, Sun).
        /// </remarks>
        /// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
        /// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
        /// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
        /// <param name="smallTickPositions">null</param>
        internal override void WorldTickPositions_FirstPass(
            Point physicalMin,
            Point physicalMax,
            out ArrayList largeTickPositions,
            out ArrayList smallTickPositions
            )
        {
            if (LargeTickStep != TimeSpan.Zero || SparseWorldLength > 2.0*tradingTimeSpan_) // utilise base class
            {
                ArrayList largeTickPositions_FirstPass;
                base.WorldTickPositions_FirstPass(physicalMin, physicalMax, out largeTickPositions_FirstPass, out smallTickPositions);

                if (largeTickPositions_FirstPass.Count < 2)
                {
                    // leave it alone, whatever that single tick may be (better something than nothing...)
                    largeTickPositions = largeTickPositions_FirstPass;
                }
                else if ((double) largeTickPositions_FirstPass[1] - (double) largeTickPositions_FirstPass[0] > 27.0*TimeSpan.TicksPerDay)
                {
                    // For distances between ticks in months or longer, just accept all ticks
                    largeTickPositions = largeTickPositions_FirstPass;
                }
                else
                {
                    // for daily ticks, ignore non-trading hours but obey (skip) non-trading days
                    largeTickPositions = new ArrayList();
                    foreach (object tick in largeTickPositions_FirstPass)
                    {
                        if (OnTradingDays((double) tick))
                            largeTickPositions.Add(tick);
                    }
                }
            }
            else // intraday ticks, own algorithm
            {
                smallTickPositions = null;
                largeTickPositions = new ArrayList();

                TimeSpan timeLength = new TimeSpan((long) SparseWorldLength);
                DateTime worldMinDate = new DateTime((long) WorldMin);
                DateTime worldMaxDate = new DateTime((long) WorldMax);

                DateTime currentTickDate;
                long skip; // in time ticks

                // The following if-else flow establishes currentTickDate to the beginning of series
                // and skip to the optimal distance between ticks

                // if less than 10 minutes, then large ticks on second spacings.

                if (timeLength < new TimeSpan(0, 0, 10, 0, 0))
                {
                    LargeTickLabelType_ = LargeTickLabelType.hourMinuteSeconds;

                    int secondsSkip;

                    if (timeLength < new TimeSpan(0, 0, 0, 10, 0))
                        secondsSkip = 1;
                    else if (timeLength < new TimeSpan(0, 0, 0, 20, 0))
                        secondsSkip = 2;
                    else if (timeLength < new TimeSpan(0, 0, 0, 50, 0))
                        secondsSkip = 5;
                    else if (timeLength < new TimeSpan(0, 0, 2, 30, 0))
                        secondsSkip = 15;
                    else
                        secondsSkip = 30;

                    int second = worldMinDate.Second;
                    second -= second%secondsSkip;

                    currentTickDate = new DateTime(
                        worldMinDate.Year,
                        worldMinDate.Month,
                        worldMinDate.Day,
                        worldMinDate.Hour,
                        worldMinDate.Minute,
                        second, 0);

                    skip = secondsSkip*TimeSpan.TicksPerSecond;
                }

                    // Less than 2 hours, then large ticks on minute spacings.

                else if (timeLength < new TimeSpan(0, 2, 0, 0, 0))
                {
                    LargeTickLabelType_ = LargeTickLabelType.hourMinute;

                    int minuteSkip;

                    if (timeLength < new TimeSpan(0, 0, 10, 0, 0))
                        minuteSkip = 1;
                    else if (timeLength < new TimeSpan(0, 0, 20, 0, 0))
                        minuteSkip = 2;
                    else if (timeLength < new TimeSpan(0, 0, 50, 0, 0))
                        minuteSkip = 5;
                    else if (timeLength < new TimeSpan(0, 2, 30, 0, 0))
                        minuteSkip = 15;
                    else //( timeLength < new TimeSpan( 0,5,0,0,0) )
                        minuteSkip = 30;

                    int minute = worldMinDate.Minute;
                    minute -= minute%minuteSkip;

                    currentTickDate = new DateTime(
                        worldMinDate.Year,
                        worldMinDate.Month,
                        worldMinDate.Day,
                        worldMinDate.Hour,
                        minute, 0, 0);

                    skip = minuteSkip*TimeSpan.TicksPerMinute;
                }

                    // Else large ticks on hour spacings.

                else
                {
                    LargeTickLabelType_ = LargeTickLabelType.hourMinute;

                    int hourSkip;
                    if (timeLength < new TimeSpan(0, 10, 0, 0, 0))
                        hourSkip = 1;
                    else if (timeLength < new TimeSpan(0, 20, 0, 0, 0))
                        hourSkip = 2;
                    else
                        hourSkip = 6;

                    int hour = worldMinDate.Hour;
                    hour -= hour%hourSkip;

                    currentTickDate = new DateTime(
                        worldMinDate.Year,
                        worldMinDate.Month,
                        worldMinDate.Day,
                        hour, 0, 0, 0);

                    skip = hourSkip*TimeSpan.TicksPerHour;
                }

                // place ticks

                while (currentTickDate < worldMaxDate)
                {
                    double world = currentTickDate.Ticks;

                    if (!WithinTradingHours(world))
                    {
                        // add gap boundary instead
                        world = ReverseSparseWorldRemap(SparseWorldRemap(world)); // moves forward
                        long gap = (long) world;
                        gap -= gap%skip;
                        currentTickDate = new DateTime(gap);
                    }

                    if (world >= WorldMin && world <= WorldMax)
                    {
                        largeTickPositions.Add(world);
                    }

                    currentTickDate = currentTickDate.AddTicks(skip);
                }
            }
        }

        /// <summary>
        /// Get an appropriate label name, given the DateTime of a label
        /// </summary>
        /// <param name="tickDate">the DateTime to get the label name for</param>
        /// <returns>A label name appropriate to the supplied DateTime.</returns>
        protected override string LargeTickLabel(DateTime tickDate)
        {
            string label;

            if (NumberFormat == null
                && (LargeTickLabelType_ == LargeTickLabelType.hourMinute ||
                    LargeTickLabelType_ == LargeTickLabelType.hourMinuteSeconds)
                && tickDate.TimeOfDay == StartTradingTime)
            {
                // in such case always show the day date
                label = (tickDate.Day).ToString();
                label += " ";
                label += tickDate.ToString("MMM");
            }
            else
            {
                label = base.LargeTickLabel(tickDate);
            }
            return label;
        }
    }
}