using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Vrh.OneReport.Lib.Areas.OneReport.Helpers
{
    /// <summary>
    /// Parse a date/time string.
    /// 
    /// Can handle time expressions like:
    ///  - "yesterday": Yesterday
    ///  - "today+12D": Today twelve days later
    ///  - "today": This day at midnight.
    ///  - "now": Right now (date and time).
    ///  - "2010-12-31"
    /// 
    /// In the expression the folowing words can be used: now, today, tommorrow, yesterday.
    /// The units are:
    ///  - Y = year 
    ///  - M = month
    ///  - D = day
    ///  - h = hour
    ///  - m = minute
    ///  - s = second
    /// </summary>
    public class RelativeDateParser
    {
        private const string ValidUnits = "Y|M|D|h|m|s";
        private const string ValidItems = "now|today|tomorrow|yesterday|DAYSBACK";

        private static readonly Regex _completeRelativeRegex = new Regex(@"^(" + ValidItems + @")?(\s*(\+|-)?(\d)*(" + ValidUnits + "))*$");

        /// <summary>
        /// Parses the given text and converts it to a DateTime if possible.
        /// </summary>
        /// <param name="input">The text to be parsed.</param>
        /// <returns>Datetime</returns>
        public static DateTime Parse(string input, string dateFormat, string timeFormat)
        {
            DateTime? result = TryParseCompleteRelativeDateTime(input.Trim());
            if (result.HasValue)
            {
                return result.Value;
            }



            // Try parse fixed dates like "01/01/2000".
            DateTime dt;
            List<string> formats = new List<string>();

            if (dateFormat != null)
            {
                dateFormat = dateFormat.Replace("\\/", "/").Replace("/", "\\/");
                formats.Add(dateFormat);
                if (timeFormat != null)
                {
                    formats.Add(dateFormat + " " + timeFormat);
                    formats.Add(dateFormat + "T" + timeFormat);
                }
            }
            if (timeFormat != null)
            {
                formats.Add("T" + timeFormat);
                formats.Add(timeFormat);
            }
            if (formats.Count == 0 || !DateTime.TryParseExact(input, formats.ToArray(), null, System.Globalization.DateTimeStyles.NoCurrentDateDefault | System.Globalization.DateTimeStyles.AssumeLocal, out dt))
            {
                dt = DateTime.Parse(input);
            }
            return dt;
        }

        /// <summary>
        /// Parses the given text, chacks if it is an expression and converts it to a DateTime if possible.
        /// </summary>
        /// <param name="input">The text to be parsed.</param>
        /// <returns>Datetime or null if the text is not an expression.</returns>
        private static DateTime? TryParseCompleteRelativeDateTime(string input)
        {
            var match = _completeRelativeRegex.Match(input);
            if (!match.Success)
                return null;

            DateTime dt = DateTime.Now;

            if (match.Groups[1].Success)
            {
                switch (match.Groups[1].Captures[0].Value)
                {
                    case "now":
                        dt = DateTime.Now;
                        break;
                    case "today":
                        dt = DateTime.Today;
                        break;
                    case "yesterday":
                        dt = DateTime.Today.AddDays(-1);
                        break;
                    case "tomorrow":
                        dt = DateTime.Today.AddDays(1);
                        break;
                }
            }

            var items = match.Groups[2].Captures;
            var signs = match.Groups[3].Captures;
            var values = match.Groups[4].Captures;
            var units = match.Groups[5].Captures;

            //Assumes that each item has a unit member.
            Debug.Assert(items.Count == units.Count);
            int unitidx, signidx, valueidx;
            int value;

            for (unitidx = signidx = valueidx = 0; unitidx < units.Count; ++unitidx)
            {
                var unit = units[unitidx].Value;
                var ui = units[unitidx].Index;

                if (values[valueidx].Index < ui)    //OK, we have a value;
                {
                    value = Convert.ToInt32(values[valueidx++].Value);
                }
                else
                {
                    value = 1;
                }
                if (signs[signidx].Index < ui)    //OK, we have a sign;
                {
                    value *= (signs[signidx++].Value == "-" ? -1 : 1);
                }

                dt = AddOffset(unit, value, dt);
            }

            return dt;
        }

        /// <summary>
        /// Add/Remove years/days/hours... to a datetime.
        /// </summary>
        /// <param name="unit">Must be one of ValidUnits</param>
        /// <param name="value">Value in given unit to add to the datetime</param>
        /// <param name="dateTime">Relative datetime</param>
        /// <returns>Relative datetime</returns>
        private static DateTime AddOffset(string unit, int value, DateTime dateTime)
        {
            switch (unit)
            {
                case "Y":
                    return dateTime.AddYears(value);
                case "M":
                    return dateTime.AddMonths(value);
                case "D":
                    return dateTime.AddDays(value);
                case "h":
                    return dateTime.AddHours(value);
                case "m":
                    return dateTime.AddMinutes(value);
                case "s":
                    return dateTime.AddSeconds(value);
                default:
                    throw new Exception("Internal error: Unhandled relative date/time case.");
            }
        }
    }
}
