﻿using System;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Datelator
{
    public class DateNotationEngine
    {
        #region Constatns

        private const string AMANHA = "AMANHA";
        private const string ONTEM = "ONTEM";
        private const string NEXT = "SEGUINTE";
        private const string PREVIOUS = "ANTERIOR";
        private const string SUM = "SOMAR";
        private const string DIA = "DIA";
        private const string MES = "MES";
        private const string SEMANA = "SEMANA";

        #endregion

        #region Regex Rules

        private static string PatternDays = "^(segunda-feira|terca-feira|terça-feira|quarta-feira|quinta-feira|sexta-feira|sabado|sábado|domingo)";
        private static string PatternSumSimple = "^somar \\-?(\\d+) (dia|semana|mes)+?([sS]\\b|\\b)$";
        private static string PatternSumSince = "^somar \\-?(\\d+) (dia|semana|mes)+?([sS]\\b|\\b) desde .*";
        private static string PatternQuantity = "\\-?(\\d+)";

        #endregion
        
        private static DateTime DefaultStartDate { get; set; }

        public static DateNotation CreateDateNotation(string input, DateTime startDate)
        {
            DefaultStartDate = startDate;
            DateNotation dateNotation = new DateNotation();
            if (input == AMANHA)
            {
                dateNotation.Value = Tomorrow();
            }
            else if (input == ONTEM)
            {
                dateNotation.Value = Yesterday();
            }
            else if (Regex.IsMatch(input, PatternDays + " (" + NEXT + ")$", RegexOptions.IgnoreCase))
            {
                dateNotation.Value = NextSelectedDay(input);
            }
            else if (Regex.IsMatch(input, PatternDays + " (" + PREVIOUS + ")$", RegexOptions.IgnoreCase))
            {
                dateNotation.Value = PreviousSelectedDay(input);
            }
            else if (input.StartsWith(SUM))
            {
                dateNotation.Value = SumNotation(input);
            }
            else
            {
                input = ReplaceInvariantMonths(input);
                DateTime result;
                if (VerifyValidFormat(input, out result))
                    dateNotation.Value = result;
            }

            return dateNotation;
        }

        public static string AvailableCommands()
        {
            return @"Available Commands:
                amanha
                ontem
                yyyy-mm-dd
                yyyy-MMM-dd
                dd-mm-yyyy
                dd-MMM-yyyy
                dd-mm
                dd-MMM
                dia-de-semana seguinte
                dia-de-semana anterior
                somar i+(dia\semana\mes) [desde data]
                clear
                exit";
        }

        #region Private Methods

        /// <summary>
        /// This method mange the sum notations
        /// Todo: try reduce some complexity
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        private static DateTime SumNotation(string input)
        {
            DateTime resultDate = DateTime.MinValue;
            if (Regex.IsMatch(input, PatternSumSimple, RegexOptions.IgnoreCase))
            {
                var match = Regex.Match(input, PatternSumSimple, RegexOptions.IgnoreCase);
                if (match.Success)
                {
                    var matchQuantity = Regex.Match(input, PatternQuantity);
                    if (match.Value.ToUpper().Contains(DIA))
                        resultDate = DefaultStartDate.SumDays(Convert.ToInt32(matchQuantity.Value));
                    else if (match.Value.ToUpper().Contains(SEMANA))
                        resultDate = DefaultStartDate.SumWeeks(Convert.ToInt32(matchQuantity.Value));
                    else if (match.Value.ToUpper().Contains(MES))
                        resultDate = DefaultStartDate.SumMonths(Convert.ToInt32(matchQuantity.Value));
                }
            }
            else if (Regex.IsMatch(input, PatternSumSince, RegexOptions.IgnoreCase))
            {
                var match = Regex.Match(input, PatternSumSince, RegexOptions.IgnoreCase);
                if (match.Success)
                {
                    string final = match.Value.Substring(match.Value.IndexOf("DESDE ") + 6);
                    DateTime aux = DefaultStartDate;

                    if (!string.IsNullOrEmpty(final))
                    {
                        if (final == AMANHA)
                        {
                            aux = Tomorrow();
                        }
                        else if (final == ONTEM)
                        {
                            aux = Yesterday();
                        }
                        else if (Regex.IsMatch(final, PatternDays + " (" + NEXT + ")$", RegexOptions.IgnoreCase))
                        {
                            aux = NextSelectedDay(final);
                        }
                        else if (Regex.IsMatch(final, PatternDays + " (" + PREVIOUS + ")$", RegexOptions.IgnoreCase))
                        {
                            aux = PreviousSelectedDay(final);
                        }
                        else
                        {
                            final = ReplaceInvariantMonths(final);
                            DateTime result;
                            if (VerifyValidFormat(final, out result))
                                aux = result;
                        }
                    }

                    var matchQuantity = Regex.Match(input, PatternQuantity);
                    if (match.Value.ToUpper().Contains(DIA))
                        resultDate = aux.SumDays(Convert.ToInt32(matchQuantity.Value));
                    else if (match.Value.ToUpper().Contains(SEMANA))
                        resultDate = aux.SumWeeks(Convert.ToInt32(matchQuantity.Value));
                    else if (match.Value.ToUpper().Contains(MES))
                        resultDate = aux.SumMonths(Convert.ToInt32(matchQuantity.Value));
                }
            }

            return resultDate;
        }

        private static DateTime PreviousSelectedDay(string input)
        {
            DateTime resultDate = DateTime.MinValue;
            var match = Regex.Match(input, PatternDays, RegexOptions.IgnoreCase);
            if (match.Success)
                resultDate = DefaultStartDate.Previous(GetEnglisgDayOfWeek(match.Value));
            return resultDate;
        }

        private static DateTime NextSelectedDay(string input)
        {
            DateTime resultDate = DateTime.MinValue;
            var match = Regex.Match(input, PatternDays, RegexOptions.IgnoreCase);
            if (match.Success)
                resultDate = DefaultStartDate.Next(GetEnglisgDayOfWeek(match.Value));
            return resultDate;
        }

        private static DateTime Yesterday()
        {
            return DefaultStartDate.PreviousDay();
        }

        private static DateTime Tomorrow()
        {
            return DefaultStartDate.NextDay();
        }

        /// <summary>
        /// This methods finds possible months in a string and replace it to english culture
        /// </summary>
        /// <param name="ruleToBerify">The string to be verified</param>
        /// <returns>The same string with the month replaced</returns>
        private static string ReplaceInvariantMonths(string ruleToBerify)
        {
            string result = string.Empty;

            if (ruleToBerify.Contains("-FEV-"))
                result = ruleToBerify.Replace("-FEV", "-FEB-");
            else if (ruleToBerify.Contains("-ABR-"))
                result = ruleToBerify.Replace("-ABR", "-APR-");
            else if (ruleToBerify.Contains("-MAI-"))
                result = ruleToBerify.Replace("-MAI", "-MAY-");
            else if (ruleToBerify.Contains("-AGO-"))
                result = ruleToBerify.Replace("-AGO", "-AUG-");
            else if (ruleToBerify.Contains("-SET-"))
                result = ruleToBerify.Replace("-SET", "-SEP-");
            else if (ruleToBerify.Contains("-OUT-"))
                result = ruleToBerify.Replace("-OUT", "-OCT-");
            else if (ruleToBerify.Contains("-DEZ-"))
                result = ruleToBerify.Replace("-DEZ", "-DEC-");
            else
                result = ruleToBerify;

            return result;
        }

        /// <summary>
        /// This method verify if the current input is a valid date format
        /// </summary>
        /// <param name="input"></param>
        /// <param name="result">The datetime that will be calculated if a valid format was found</param>
        /// <returns><see cref="true"/> if founds, otherwise <see cref="false"/></returns>
        private static bool VerifyValidFormat(string input, out DateTime result)
        {
            bool isValid = false;
            result = DateTime.MinValue;
            string[] ValidFormats = { "yyyy-mm-dd", "yyyy-MMM-dd", "dd-mm-yyyy", "dd-MMM-yyyy", "dd-mm", "dd-MMM" };
            foreach (string dateStringFormat in ValidFormats)
            {
                if (DateTime.TryParseExact(input, dateStringFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out result))
                {
                    isValid = true;
                    break;
                }
            }

            return isValid;
        }

        /// <summary>
        /// This method converts a week day in portuguese (pt-PT) to a <see cref="DayOfWeek"/>
        /// </summary>
        /// <param name="dayWeekPt"></param>
        /// <returns>An valide <see cref="DayOfWeek"/></returns>
        private static DayOfWeek GetEnglisgDayOfWeek(string dayWeekPt)
        {
            DayOfWeek currentDayOfWeek;
            dayWeekPt = dayWeekPt.ToUpper();
            switch (dayWeekPt)
            {
                case "DOMINGO":
                    currentDayOfWeek = DayOfWeek.Sunday;
                    break;

                case "SABADO":
                    currentDayOfWeek = DayOfWeek.Saturday;
                    break;

                case "SEGUNDA":
                case "SEGUNDA-FEIRA":
                    currentDayOfWeek = DayOfWeek.Monday;
                    break;

                case "TERCA":
                case "TERCA-FEIRA":
                    currentDayOfWeek = DayOfWeek.Tuesday;
                    break;

                case "QUARTA":
                case "QUARTA-FEIRA":
                    currentDayOfWeek = DayOfWeek.Wednesday;
                    break;

                case "QUINTA":
                case "QUINTA-FEIRA":
                    currentDayOfWeek = DayOfWeek.Thursday;
                    break;

                case "SEXTA":
                case "SEXTA-FEIRA":
                    currentDayOfWeek = DayOfWeek.Friday;
                    break;

                default:
                    currentDayOfWeek = DayOfWeek.Sunday;
                    break;
            }

            return currentDayOfWeek;
        }

        #endregion
    }
}