/*
ScPl - A plotting library for .NET

DateTimeAxis.cs
Copyright (C) 2003
Matt Howlett

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 following text in 
   the documentation and / or other materials provided with the 
   distribution: 
   
   "This product includes software developed as part of 
   the ScPl plotting library project available from: 
   http://www.netcontrols.org/scpl/" 

------------------------------------------------------------------------

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
$Id: DateTimeAxis.cs,v 1.17 2004/05/07 11:48:55 mhowlett Exp $

*/

using System;
using System.Drawing;
using System.Collections;

// TODO: More control over how labels are displayed.
// TODO: SkipWeekends property.
// TODO: Make a relative (as opposed to absolute) TimeAxis.

namespace scpl
{
	/// <summary>
	/// The DateTimeAxis class
	/// </summary>
	public class DateTimeAxis : Axis
	{

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

		/// <summary>
		/// Helper method for Clone.
		/// </summary>
		/// <param name="a">The original object to clone.</param>
		/// <param name="b">The cloned object.</param>
		protected static void DoClone( DateTimeAxis b, DateTimeAxis a )
		{
			Axis.DoClone( b, a );
		}
		#endregion

		#region Init
		private void Init()
		{
			this.largeTickTypeValid_ = false;
		}
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="a">Axis to construct from</param>
		public DateTimeAxis( Axis a )
			: base( a )
		{
			this.Init();
		}

		/// <summary>
		/// Default Constructor
		/// </summary>
		public DateTimeAxis()
			: base()
		{
			this.Init();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="worldMin">World min of axis</param>
		/// <param name="worldMax">World max of axis</param>
		public DateTimeAxis( double worldMin, double worldMax )
			: base( worldMin, worldMax )
		{
			this.Init();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="worldMin">World min of axis</param>
		/// <param name="worldMax">World max of axis</param>
		public DateTimeAxis( long worldMin, long worldMax )
			: base( (double)worldMin, (double)worldMax )
		{
			this.Init();
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="worldMin">World min of axis</param>
		/// <param name="worldMax">World max of axis</param>
		public DateTimeAxis( DateTime worldMin, DateTime worldMax )
			: base( (double)worldMin.Ticks, (double)worldMax.Ticks )
		{
			this.Init();
		}

		#endregion

		#region enum Months
		private enum Months
		{
			Jan = 1,
			Feb = 2,
			Mar = 3,
			Apr = 4,
			May = 5,
			Jun = 6,
			Jul = 7,
			Aug = 8,
			Sep = 9,
			Oct = 10,
			Nov = 11,
			Dec = 12
		}
		#endregion

		#region DrawTicks
		protected override void DrawTicks( 
			Graphics g, 
			PointF physicalMin, 
			PointF physicalMax, 
			out object labelOffset,
			out object boundingBox )
		{
			
			PointF tLabelOffset;
			RectangleF tBoundingBox;

			this.InitOffsetAndBounds( out labelOffset, out boundingBox );
		
			ArrayList largeTicks = this.LargeTickPositions;
			ArrayList smallTicks = this.SmallTickPositions;
			
			// draw small ticks.
			for (int i=0; i<smallTicks.Count; ++i)
			{
				this.DrawTick( g, (double)smallTicks[i], 
					this.SmallTickSize, "", new Point(0,0),
					physicalMin, physicalMax, 
					out tLabelOffset, out tBoundingBox );
				// assume label offset and bounding box unchanged by small tick bounds.
			}

			// draw large ticks.
			for (int i=0; i<largeTicks.Count; ++i)
			{
					
				DateTime tickDate = new DateTime( (long)((double)largeTicks[i]) );
				string label = "";

				if ( this.LargeTickLabelType_ == LargeTickLabelType.year )
				{
					label = tickDate.Year.ToString();
				}

				else if ( this.LargeTickLabelType_ == LargeTickLabelType.month )
				{
					label = ((Months)tickDate.Month).ToString();
					label += " ";
					label += tickDate.Year.ToString().Substring(2,2);
				}

				else if ( this.LargeTickLabelType_ == LargeTickLabelType.day )
				{
					label = (tickDate.Day).ToString();
					label += " ";
					label += ((Months)tickDate.Month).ToString();
				}
				
				else if ( this.LargeTickLabelType_ == LargeTickLabelType.hourMinute )
				{
					string minutes = tickDate.Minute.ToString();
					if (minutes.Length == 1)
					{
						minutes = "0" + minutes;
					}
					label = tickDate.Hour.ToString() + ":" + minutes;
				}

				this.DrawTick( g, (double)largeTicks[i], 
					this.LargeTickSize, label, new Point(0,0),
					physicalMin, physicalMax, out tLabelOffset, out tBoundingBox );

				this.UpdateOffsetAndBounds( ref labelOffset, ref boundingBox, tLabelOffset, tBoundingBox );
			}

		}
		#endregion

		#region LargeTickLabelType
		private enum LargeTickLabelType 
		{
			none = 0,
			year = 1,
			month = 2,
			day = 3,
			hourMinute = 4,
			hourMinuteSeconds = 5
		}
		#endregion

		// this gets set after a get LargeTickPositions.
		private LargeTickLabelType LargeTickLabelType_;
		// this keeps track of whether we can believe the above.
		// get SmallTickPositions needs to check that this is true.
		private bool largeTickTypeValid_ = false;

		#region LargeTickPositions
		/// <summary>
		/// An ArrayList containing the positions of the large ticks.
		/// </summary>
		/// <remarks>
		/// 
		/// </remarks>
		public override ArrayList LargeTickPositions
		{
			get
			{
				largeTickTypeValid_ = true;

				ArrayList toRet = new ArrayList();

				const int daysInMonth = 30;

				TimeSpan timeLength = new TimeSpan( (long)(WorldMax-WorldMin));
				DateTime worldMinDate = new DateTime( (long)this.WorldMin );
				DateTime worldMaxDate = new DateTime( (long)this.WorldMax );


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

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

					double minuteSkip;

					if ( timeLength < new TimeSpan(0,0,10,0,0) )
						minuteSkip = 1.0;
					else if ( timeLength < new TimeSpan(0,0,20,0,0) )
						minuteSkip = 2.0;
					else if ( timeLength < new TimeSpan(0,0,50,0,0) )
						minuteSkip = 5.0;
					else if ( timeLength < new TimeSpan(0,2,30,0,0) )
						minuteSkip = 15.0;
					else //( timeLength < new TimeSpan( 0,5,0,0,0) )
						minuteSkip = 30.0;

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

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

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

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

						currentTickDate = currentTickDate.AddMinutes( minuteSkip );
					}
				}

				// Less than 2 days, then large ticks on hour spacings.

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

					double hourSkip;
					if ( timeLength < new TimeSpan(0,10,0,0,0) )
						hourSkip = 1.0;
					else if ( timeLength < new TimeSpan(0,20,0,0,0) )
						hourSkip = 2.0;
					else
						hourSkip = 6.0;


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

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

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

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

						currentTickDate = currentTickDate.AddHours( hourSkip );
					}

				}


				// less than 5 months, then large ticks on day spacings.

				else if ( timeLength < new TimeSpan(daysInMonth*4,0,0,0,0))
				{
					this.LargeTickLabelType_ = LargeTickLabelType.day;

					double daySkip;
					if ( timeLength < new TimeSpan(10,0,0,0,0) )
						daySkip = 1.0;
					else if (timeLength < new TimeSpan(20,0,0,0,0) )
						daySkip = 2.0;
					else if (timeLength < new TimeSpan(7*10,0,0,0,0) )
						daySkip = 7.0;
					else 
						daySkip = 14.0;

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

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

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

						currentTickDate = currentTickDate.AddDays(daySkip);
					}
				}


				// else ticks on month or year spacings.

				else if ( timeLength >= new TimeSpan(daysInMonth*4,0,0,0,0) )
				{

					int monthSpacing = 0;
				
					if ( timeLength.Days < daysInMonth*(12*3+6) )
					{
						LargeTickLabelType_ = LargeTickLabelType.month;

						if ( timeLength.Days < daysInMonth*10 )
							monthSpacing = 1;
						else if ( timeLength.Days < daysInMonth*(12*2) )
							monthSpacing = 3;
						else // if ( timeLength.Days < daysInMonth*(12*3+6) )
							monthSpacing = 6;
					}
					else
					{
						LargeTickLabelType_ = LargeTickLabelType.year;

						if ( timeLength.Days < daysInMonth*(12*6) )
							monthSpacing = 12;
						else if ( timeLength.Days < daysInMonth*(12*12) )
							monthSpacing = 24;
						else if ( timeLength.Days < daysInMonth*(12*30) )
							monthSpacing = 60;
						else 
							LargeTickLabelType_ = LargeTickLabelType.none;
					}

					// truncate start
					DateTime currentTickDate = new DateTime( 
						worldMinDate.Year,
						worldMinDate.Month, 
						1 );
				
					if (monthSpacing > 1)
					{
						currentTickDate = currentTickDate.AddMonths(
							-(currentTickDate.Month-1)%monthSpacing );
					}

					// Align on 2 or 5 year boundaries if necessary.
					if (monthSpacing >= 24)
					{
						currentTickDate = currentTickDate.AddYears(
							-(currentTickDate.Year)%(monthSpacing/12) );						
					}
	
					//this.firstLargeTick_ = (double)currentTickDate.Ticks;

					if ( LargeTickLabelType_ != LargeTickLabelType.none )
					{
						while ( currentTickDate < worldMaxDate )
						{
							double world = (double)currentTickDate.Ticks;

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

							currentTickDate = currentTickDate.AddMonths( monthSpacing );
						}
					}
				}

				return toRet;
			}
		}
		#endregion

		#region SmallTickPositions
		public override ArrayList SmallTickPositions
		{
			get
			{
				ArrayList toRet = new ArrayList();

				/*
				// if large tick type not valid, then determine it.
				if (this.largeTickTypeValid_ == false)
				{
					ArrayList throwAway = this.LargeTickPositions;
				}

				if ( this.LargeTickLabelType_ == LargeTickLabelType.twoYear )
				{
					DateTime first = new DateTime( (long)this.firstLargeTick_ );
					DateTime realFirst = first.AddYears( -2 );
					while (realFirst.Ticks < this.WorldMax)
					{
						realFirst = realFirst.AddYears( 1 );
						if ((double)realFirst.Ticks < this.WorldMax)
						{
							toRet.Add( (double)realFirst.Ticks );
						}
						realFirst = realFirst.AddYears( 1 );
					}

					return toRet;
				}

				if ( this.LargeTickType_ == LargeTickType.fiveYear )
				{
					DateTime first = new DateTime( (long)this.firstLargeTick_ );
					DateTime realFirst = first.AddYears( -5 );
					while (realFirst.Ticks < this.WorldMax)
					{
						realFirst = realFirst.AddYears( 1 );
						for (int j=0; j<4; ++j)
						{
							if ((double)realFirst.Ticks < this.WorldMax)
							{
								toRet.Add( (double)realFirst.Ticks );
							}
							realFirst = realFirst.AddYears( 1 );
						}
					}

					return toRet;
				}

				if ( this.LargeTickType_ == LargeTickType.threeMonth )
				{
					DateTime first = new DateTime( (long)this.firstLargeTick_ );
					DateTime realFirst = first.AddMonths( -3 );
					while (realFirst.Ticks < this.WorldMax)
					{
						realFirst = realFirst.AddMonths( 1 );
						for (int j=0; j<2; ++j)
						{
							if ((double)realFirst.Ticks < this.WorldMax)
							{
								toRet.Add( (double)realFirst.Ticks );
							}
							realFirst = realFirst.AddMonths( 1 );
						}
					}

					return toRet;
				}

				if ( this.LargeTickType_ == LargeTickType.sixMonth )
				{
					DateTime first = new DateTime( (long)this.firstLargeTick_ );
					DateTime realFirst = first.AddMonths( -6 );
					while (realFirst.Ticks < this.WorldMax)
					{
						realFirst = realFirst.AddMonths( 3 );
						if ((double)realFirst.Ticks < this.WorldMax)
						{
							toRet.Add( (double)realFirst.Ticks );
						}
						realFirst = realFirst.AddMonths( 3 );
					}

					return toRet;
				}
				*/

				// default.
				return toRet;
			}
		}
		#endregion

		#region get/set WorldMin (override)
		public override double WorldMin
		{
			get
			{
				return base.WorldMin;
			}
			set
			{
				this.largeTickTypeValid_ = false;
				base.WorldMin = value;
			}
		}
		#endregion
		#region get/set WorldMax (override)
		public override double WorldMax
		{
			get
			{
				return base.WorldMax;
			}
			set
			{
				this.largeTickTypeValid_ = false;
				base.WorldMax = value;
			}
		}
		#endregion

	}
}
