/*
 * System.DateTimeUtils
 *
 *  Copyright (C) 2007 Novell, Inc (http://www.novell.com) 
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

using System.Globalization;
using System.Text;

namespace System {
	internal 
#if NET_2_0
	static
#endif
	class DateTimeUtils {
		public static int CountRepeat (string fmt, int p, char c)
		{
			int l = fmt.Length;
			int i = p + 1;
			while ((i < l) && (fmt [i] == c)) 
				i++;
			
			return i - p;
		}

		public static unsafe void ZeroPad (StringBuilder output, int digits, int len)
		{
			// more than enough for an int
			char* buffer = stackalloc char [16];
			int pos = 16;
			
			do {
				buffer [-- pos] = (char) ('0' + digits % 10);
				digits /= 10;
				len --;
			} while (digits > 0);
			
			while (len -- > 0)
				buffer [-- pos] = '0';
			
			output.Append (new string (buffer, pos, 16 - pos));
		}

		public static int ParseQuotedString (string fmt, int pos, StringBuilder output)
		{
			// pos == position of " or '
			
			int len = fmt.Length;
			int start = pos;
			char quoteChar = fmt [pos++];
			
			while (pos < len) {
				char ch = fmt [pos++];
				
				if (ch == quoteChar)
					return pos - start;
				
				if (ch == '\\') {
					// C-Style escape
					if (pos >= len)
						throw new FormatException("Un-ended quote");
	
					output.Append (fmt [pos++]);
				} else {
					output.Append (ch);
				}
			}

			throw new FormatException("Un-ended quote");
		}

		public static string GetStandardPattern (char format, DateTimeFormatInfo dfi, out bool useutc, out bool use_invariant)
		{
			return GetStandardPattern (format, dfi, out useutc, out use_invariant, false);
		}

		public static string GetStandardPattern (char format, DateTimeFormatInfo dfi, out bool useutc, out bool use_invariant, bool date_time_offset)
		{
			String pattern;

			useutc = false;
			use_invariant = false;

			switch (format)
			{
			case 'd':
				pattern = dfi.ShortDatePattern;
				break;
			case 'D':
				pattern = dfi.LongDatePattern;
				break;
			case 'f':
				pattern = dfi.LongDatePattern + " " + dfi.ShortTimePattern;
				break;
			case 'F':
				pattern = dfi.FullDateTimePattern;
				break;
			case 'g':
				pattern = dfi.ShortDatePattern + " " + dfi.ShortTimePattern;
				break;
			case 'G':
				pattern = dfi.ShortDatePattern + " " + dfi.LongTimePattern;
				break;
			case 'm':
			case 'M':
				pattern = dfi.MonthDayPattern;
				break;
#if NET_2_0
			case 'o':
			case 'O':
				pattern = dfi.RoundtripPattern;
				use_invariant = true;
				break;
#endif
			case 'r':
			case 'R':
				pattern = dfi.RFC1123Pattern;
				if (date_time_offset)
					useutc = true;
				use_invariant = true;
				break;
			case 's':
				pattern = dfi.SortableDateTimePattern;
				use_invariant = true;
				break;
			case 't':
				pattern = dfi.ShortTimePattern;
				break;
			case 'T':
				pattern = dfi.LongTimePattern;
				break;
			case 'u':
				pattern = dfi.UniversalSortableDateTimePattern;
				if (date_time_offset)
					useutc = true;
				use_invariant = true;
				break;
			case 'U':
				if (date_time_offset)
					pattern = null;
				else {
//					pattern = dfi.LongDatePattern + " " + dfi.LongTimePattern;
					pattern = dfi.FullDateTimePattern;
					useutc = true;
				}
				break;
			case 'y':
			case 'Y':
				pattern = dfi.YearMonthPattern;
				break;
			default:
				pattern = null;
				break;
//				throw new FormatException (String.Format ("Invalid format pattern: '{0}'", format));
			}

			return pattern;
		}

#if NET_2_0
		public static string ToString (DateTime dt, string format, DateTimeFormatInfo dfi)
		{
			return ToString (dt, null, format, dfi);
		}

		public static string ToString (DateTime dt, TimeSpan? utc_offset, string format, DateTimeFormatInfo dfi)
#else
		public static string ToString (DateTime dt, string format, DateTimeFormatInfo dfi)
#endif
		{
			// the length of the format is usually a good guess of the number
			// of chars in the result. Might save us a few bytes sometimes
			// Add + 10 for cases like mmmm dddd
			StringBuilder result = new StringBuilder (format.Length + 10);

			// For some cases, the output should not use culture dependent calendar
			DateTimeFormatInfo inv = DateTimeFormatInfo.InvariantInfo;
			if (format == inv.RFC1123Pattern)
				dfi = inv;
			else if (format == inv.UniversalSortableDateTimePattern)
				dfi = inv;

			int i = 0;

			while (i < format.Length) {
				int tokLen;
				bool omitZeros = false;
				char ch = format [i];

				switch (ch) {

				//
				// Time Formats
				//
				case 'h':
					// hour, [1, 12]
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);

					int hr = dt.Hour % 12;
					if (hr == 0)
						hr = 12;

					DateTimeUtils.ZeroPad (result, hr, tokLen == 1 ? 1 : 2);
					break;
				case 'H':
					// hour, [0, 23]
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					DateTimeUtils.ZeroPad (result, dt.Hour, tokLen == 1 ? 1 : 2);
					break;
				case 'm':
					// minute, [0, 59]
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					DateTimeUtils.ZeroPad (result, dt.Minute, tokLen == 1 ? 1 : 2);
					break;
				case 's':
					// second [0, 29]
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					DateTimeUtils.ZeroPad (result, dt.Second, tokLen == 1 ? 1 : 2);
					break;
#if NET_2_0
				case 'F':
					omitZeros = true;
					goto case 'f';
#endif
				case 'f':
					// fraction of second, to same number of
					// digits as there are f's

					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					if (tokLen > 7)
						throw new FormatException ("Invalid Format String");

					int dec = (int)((long)(dt.Ticks % TimeSpan.TicksPerSecond) / (long) Math.Pow (10, 7 - tokLen));
					int startLen = result.Length;
					DateTimeUtils.ZeroPad (result, dec, tokLen);

					if (omitZeros) {
						while (result.Length > startLen && result [result.Length - 1] == '0')
							result.Length--;
						// when the value was 0, then trim even preceding '.' (!) It is fixed character.
						if (dec == 0 && startLen > 0 && result [startLen - 1] == '.')
							result.Length--;
					}

					break;
				case 't':
					// AM/PM. t == first char, tt+ == full
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					string desig = dt.Hour < 12 ? dfi.AMDesignator : dfi.PMDesignator;

					if (tokLen == 1) {
						if (desig.Length >= 1)
							result.Append (desig [0]);
					}
					else
						result.Append (desig);

					break;
				case 'z':
					// timezone. t = +/-h; tt = +/-hh; ttt+=+/-hh:mm
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					TimeSpan offset = 
#if NET_2_0
						utc_offset ?? 
#endif
						TimeZone.CurrentTimeZone.GetUtcOffset (dt);

					if (offset.Ticks >= 0)
						result.Append ('+');
					else
						result.Append ('-');

					switch (tokLen) {
					case 1:
						result.Append (Math.Abs (offset.Hours));
						break;
					case 2:
						result.Append (Math.Abs (offset.Hours).ToString ("00"));
						break;
					default:
						result.Append (Math.Abs (offset.Hours).ToString ("00"));
						result.Append (':');
						result.Append (Math.Abs (offset.Minutes).ToString ("00"));
						break;
					}
					break;
#if NET_2_0
				case 'K': // 'Z' (UTC) or zzz (Local)
					tokLen = 1;

					if (utc_offset != null || dt.Kind == DateTimeKind.Local) {
						offset = utc_offset ?? TimeZone.CurrentTimeZone.GetUtcOffset (dt);
						if (offset.Ticks >= 0)
							result.Append ('+');
						else
							result.Append ('-');
						result.Append (Math.Abs (offset.Hours).ToString ("00"));
						result.Append (':');
						result.Append (Math.Abs (offset.Minutes).ToString ("00"));
					} else if (dt.Kind == DateTimeKind.Utc)
						result.Append ('Z');
					break;
#endif
				//
				// Date tokens
				//
				case 'd':
					// day. d(d?) = day of month (leading 0 if two d's)
					// ddd = three leter day of week
					// dddd+ full day-of-week
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);

					if (tokLen <= 2)
						DateTimeUtils.ZeroPad (result, dfi.Calendar.GetDayOfMonth (dt), tokLen == 1 ? 1 : 2);
					else if (tokLen == 3)
						result.Append (dfi.GetAbbreviatedDayName (dfi.Calendar.GetDayOfWeek (dt)));
					else
						result.Append (dfi.GetDayName (dfi.Calendar.GetDayOfWeek (dt)));

					break;
				case 'M':
					// Month.m(m?) = month # (with leading 0 if two mm)
					// mmm = 3 letter name
					// mmmm+ = full name
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					int month = dfi.Calendar.GetMonth(dt);
					if (tokLen <= 2)
						DateTimeUtils.ZeroPad (result, month, tokLen);
					else if (tokLen == 3)
						result.Append (dfi.GetAbbreviatedMonthName (month));
					else
						result.Append (dfi.GetMonthName (month));

					break;
				case 'y':
					// Year. y(y?) = two digit year, with leading 0 if yy
					// yyy+ full year with leading zeros if needed.
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);

					if (tokLen <= 2)
						DateTimeUtils.ZeroPad (result, dfi.Calendar.GetYear (dt) % 100, tokLen);
					else
						DateTimeUtils.ZeroPad (result, dfi.Calendar.GetYear (dt), tokLen);
					break;

				case 'g':
					// Era name
					tokLen = DateTimeUtils.CountRepeat (format, i, ch);
					result.Append (dfi.GetEraName (dfi.Calendar.GetEra (dt)));
					break;

				//
				// Other
				//
				case ':':
					result.Append (dfi.TimeSeparator);
					tokLen = 1;
					break;
				case '/':
					result.Append (dfi.DateSeparator);
					tokLen = 1;
					break;
				case '\'': case '"':
					tokLen = DateTimeUtils.ParseQuotedString (format, i, result);
					break;
				case '%':
					if (i >= format.Length - 1)
						throw new FormatException ("% at end of date time string");
					if (format [i + 1] == '%')
						throw new FormatException ("%% in date string");

					// Look for the next char
					tokLen = 1;
					break;
				case '\\':
					// C-Style escape
					if (i >= format.Length - 1)
						throw new FormatException ("\\ at end of date time string");

					result.Append (format [i + 1]);
					tokLen = 2;

					break;
				default:
					// catch all
					result.Append (ch);
					tokLen = 1;
					break;
				}
				i += tokLen;
			}
			return result.ToString ();
		}
	
	}
}	
