using System;

namespace Lucene.Net.Documents
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /// <summary>
    /// Provides support for converting dates to strings and vice-versa.
    /// The strings are structured so that lexicographic sorting orders
    /// them by date, which makes them suitable for use as field values
    /// and search terms.
    ///
    /// <para/>This class also helps you to limit the resolution of your dates. Do not
    /// save dates with a finer resolution than you really need, as then
    /// <see cref="Search.TermRangeQuery"/> and <see cref="Search.PrefixQuery"/> will require more memory and become slower.
    ///
    /// <para/>
    /// Another approach is <see cref="Util.NumericUtils"/>, which provides
    /// a sortable binary representation (prefix encoded) of numeric values, which
    /// date/time are.
    /// 
    /// For indexing a <see cref="DateTime"/>, just get the <see cref="DateTime.Ticks"/> and index
    /// this as a numeric value with <see cref="Int64Field"/> and use <see cref="Search.NumericRangeQuery{T}"/>
    /// to query it.
    /// </summary>
    public static class DateTools
    {
        private static readonly string YEAR_FORMAT = "yyyy";
        private static readonly string MONTH_FORMAT = "yyyyMM";
        private static readonly string DAY_FORMAT = "yyyyMMdd";
        private static readonly string HOUR_FORMAT = "yyyyMMddHH";
        private static readonly string MINUTE_FORMAT = "yyyyMMddHHmm";
        private static readonly string SECOND_FORMAT = "yyyyMMddHHmmss";
        private static readonly string MILLISECOND_FORMAT = "yyyyMMddHHmmssfff";

        // LUCENENET - not used
        //private static readonly System.Globalization.Calendar calInstance = new System.Globalization.GregorianCalendar();

        /// <summary>
        /// Converts a <see cref="DateTime"/> to a string suitable for indexing.
        /// </summary>
        /// <param name="date"> the date to be converted </param>
        /// <param name="resolution"> the desired resolution, see
        /// <see cref="Round(DateTime, DateTools.Resolution)"/> </param>
        /// <returns> a string in format <c>yyyyMMddHHmmssSSS</c> or shorter,
        /// depending on <paramref name="resolution"/>; using GMT as timezone  </returns>
        public static string DateToString(DateTime date, Resolution resolution)
        {
            return TimeToString(date.Ticks / TimeSpan.TicksPerMillisecond, resolution);
        }

        /// <summary>
        /// Converts a millisecond time to a string suitable for indexing.
        /// </summary>
        /// <param name="time"> the date expressed as milliseconds since January 1, 1970, 00:00:00 GMT (also known as the "epoch") </param>
        /// <param name="resolution"> the desired resolution, see
        /// <see cref="Round(long, DateTools.Resolution)"/> </param>
        /// <returns> a string in format <c>yyyyMMddHHmmssSSS</c> or shorter,
        /// depending on <paramref name="resolution"/>; using GMT as timezone </returns>
        public static string TimeToString(long time, Resolution resolution)
        {
            DateTime date = new DateTime(Round(time, resolution));

            if (resolution == Resolution.YEAR)
            {
                return date.ToString(YEAR_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }
            else if (resolution == Resolution.MONTH)
            {
                return date.ToString(MONTH_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }
            else if (resolution == Resolution.DAY)
            {
                return date.ToString(DAY_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }
            else if (resolution == Resolution.HOUR)
            {
                return date.ToString(HOUR_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }
            else if (resolution == Resolution.MINUTE)
            {
                return date.ToString(MINUTE_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }
            else if (resolution == Resolution.SECOND)
            {
                return date.ToString(SECOND_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }
            else if (resolution == Resolution.MILLISECOND)
            {
                return date.ToString(MILLISECOND_FORMAT, System.Globalization.CultureInfo.InvariantCulture);
            }

            throw new ArgumentException("unknown resolution " + resolution);
        }

        /// <summary>
        /// Converts a string produced by <see cref="TimeToString(long, Resolution)"/> or
        /// <see cref="DateToString(DateTime, Resolution)"/> back to a time, represented as the
        /// number of milliseconds since January 1, 1970, 00:00:00 GMT (also known as the "epoch").
        /// </summary>
        /// <param name="dateString"> the date string to be converted </param>
        /// <returns> the number of milliseconds since January 1, 1970, 00:00:00 GMT (also known as the "epoch")</returns>
        /// <exception cref="FormatException"> if <paramref name="dateString"/> is not in the
        /// expected format </exception>
        public static long StringToTime(string dateString)
        {
            return StringToDate(dateString).Ticks;
        }

        /// <summary>
        /// Converts a string produced by <see cref="TimeToString(long, Resolution)"/> or
        /// <see cref="DateToString(DateTime, Resolution)"/> back to a time, represented as a
        /// <see cref="DateTime"/> object.
        /// </summary>
        /// <param name="dateString"> the date string to be converted </param>
        /// <returns> the parsed time as a <see cref="DateTime"/> object </returns>
        /// <exception cref="FormatException"> if <paramref name="dateString"/> is not in the
        /// expected format </exception>
        public static DateTime StringToDate(string dateString)
        {
            DateTime date;
            if (dateString.Length == 4)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    1, 1, 0, 0, 0, 0);
            }
            else if (dateString.Length == 6)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    1, 0, 0, 0, 0);
            }
            else if (dateString.Length == 8)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    0, 0, 0, 0);
            }
            else if (dateString.Length == 10)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    0, 0, 0);
            }
            else if (dateString.Length == 12)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    Convert.ToInt16(dateString.Substring(10, 2)),
                    0, 0);
            }
            else if (dateString.Length == 14)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    Convert.ToInt16(dateString.Substring(10, 2)),
                    Convert.ToInt16(dateString.Substring(12, 2)),
                    0);
            }
            else if (dateString.Length == 17)
            {
                date = new DateTime(Convert.ToInt16(dateString.Substring(0, 4)),
                    Convert.ToInt16(dateString.Substring(4, 2)),
                    Convert.ToInt16(dateString.Substring(6, 2)),
                    Convert.ToInt16(dateString.Substring(8, 2)),
                    Convert.ToInt16(dateString.Substring(10, 2)),
                    Convert.ToInt16(dateString.Substring(12, 2)),
                    Convert.ToInt16(dateString.Substring(14, 3)));
            }
            else
            {
                throw new FormatException("Input is not valid date string: " + dateString);
            }
            return date;
        }

        /// <summary>
        /// Limit a date's resolution. For example, the date <c>2004-09-21 13:50:11</c>
        /// will be changed to <c>2004-09-01 00:00:00</c> when using
        /// <see cref="Resolution.MONTH"/>.
        /// </summary>
        /// <param name="date"> the date to be rounded </param>
        /// <param name="resolution"> The desired resolution of the date to be returned </param>
        /// <returns> the date with all values more precise than <paramref name="resolution"/>
        /// set to 0 or 1 </returns>
        public static DateTime Round(DateTime date, Resolution resolution)
        {
            return new DateTime(Round(date.Ticks / TimeSpan.TicksPerMillisecond, resolution));
        }

        /// <summary>
        /// Limit a date's resolution. For example, the date <c>1095767411000</c>
        /// (which represents 2004-09-21 13:50:11) will be changed to
        /// <c>1093989600000</c> (2004-09-01 00:00:00) when using
        /// <see cref="Resolution.MONTH"/>.
        /// </summary>
        /// <param name="time"> the time to be rounded </param>
        /// <param name="resolution"> The desired resolution of the date to be returned </param>
        /// <returns> the date with all values more precise than <paramref name="resolution"/>
        /// set to 0 or 1, expressed as milliseconds since January 1, 1970, 00:00:00 GMT 
        /// (also known as the "epoch")</returns>
        public static long Round(long time, Resolution resolution)
        {
            DateTime dt = new DateTime(time * TimeSpan.TicksPerMillisecond);

            if (resolution == Resolution.YEAR)
            {
                dt = dt.AddMonths(1 - dt.Month);
                dt = dt.AddDays(1 - dt.Day);
                dt = dt.AddHours(0 - dt.Hour);
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.MONTH)
            {
                dt = dt.AddDays(1 - dt.Day);
                dt = dt.AddHours(0 - dt.Hour);
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.DAY)
            {
                dt = dt.AddHours(0 - dt.Hour);
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.HOUR)
            {
                dt = dt.AddMinutes(0 - dt.Minute);
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.MINUTE)
            {
                dt = dt.AddSeconds(0 - dt.Second);
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.SECOND)
            {
                dt = dt.AddMilliseconds(0 - dt.Millisecond);
            }
            else if (resolution == Resolution.MILLISECOND)
            {
                // don't cut off anything
            }
            else
            {
                throw new System.ArgumentException("unknown resolution " + resolution);
            }
            return dt.Ticks;
        }

        /// <summary>
        /// Specifies the time granularity. </summary>
        public enum Resolution
        {
            /// <summary>
            /// Limit a date's resolution to year granularity. </summary>
            YEAR = 4,

            /// <summary>
            /// Limit a date's resolution to month granularity. </summary>
            MONTH = 6,

            /// <summary>
            /// Limit a date's resolution to day granularity. </summary>
            DAY = 8,

            /// <summary>
            /// Limit a date's resolution to hour granularity. </summary>
            HOUR = 10,

            /// <summary>
            /// Limit a date's resolution to minute granularity. </summary>
            MINUTE = 12,

            /// <summary>
            /// Limit a date's resolution to second granularity. </summary>
            SECOND = 14,

            /// <summary>
            /// Limit a date's resolution to millisecond granularity. </summary>
            MILLISECOND = 17
        }
    }
}