// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

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

namespace Connected;

public class DateMask : PatternMask
{

    public DateMask(string mask, char year = 'y', char month = 'M', char day = 'd') : base(mask)
    {
        _y = year;
        _M = month;
        _d = day;
        _maskChars = _maskChars.Concat(new[] { MaskChar.Digit(year), MaskChar.Digit(month), MaskChar.Digit(day), })
            .ToArray();
    }

    private char _y;
    private char _M;
    private char _d;

    private int _year = 0;
    private int _month = 0;
    private int _day = 0;

    protected override void ModifyPartiallyAlignedMask(string mask, string text, int maskOffset, ref int textIndex, ref int maskIndex, ref string alignedText)
    {
        if (alignedText.IsEmpty())
            return;
        var y = ExtractYear(mask, alignedText, maskOffset);
        if (y >= 0)
            _year = y;
        MonthLogic(mask, text, maskOffset, ref textIndex, ref maskIndex, ref alignedText);
        DayLogic(mask, text, maskOffset, ref textIndex, ref maskIndex, ref alignedText);
    }

    private int ExtractYear(string mask, string alignedText, int maskOffset)
    {
        var yyyy = new string(_y, 4);
        var yy = new string(_y, 2);
        if (mask.Contains(yyyy))
        {
            var (yearString, _) = Extract(yyyy, mask, maskOffset, alignedText);
            if (yearString == null || yearString.Length < 4)
                return -1;
            if (int.TryParse(yearString, out var year))
                return year;
        }
        else if (mask.Contains(yy))
        {
            var (yearString, _) = Extract(yy, mask, maskOffset, alignedText);
            if (yearString == null || yearString.Length < 2)
                return -1;
            if (int.TryParse(yearString, out var y))
                return (DateTime.Today.Year / 100 * 100) + y; // this code will still work in 2100 until 2900. I guess in a thousand years we'll have to update this line ;)
        }
        return -1;
    }

    private void MonthLogic(string mask, string text, int maskOffset, ref int textIndex, ref int maskIndex, ref string alignedText)
    {
        var MM = new string(_M, 2);
        var (monthString, index) = Extract(MM, mask, maskOffset, alignedText);
        if (monthString == null)
            return;
        if (!int.TryParse(monthString, out var month))
            return;
        if (monthString.Length == 1)
        {
            // we are at the first digit of MM, only 0 and 1 are allowed
            if (month > 1)
            {
                alignedText = alignedText.Insert(index, "0");
                maskIndex++;
            }
        }
        else if (monthString.Length == 2)
        {
            var fixedMonth = FixMonth(month);
            _month = fixedMonth;
            if (fixedMonth != month)
                alignedText = alignedText.Remove(index, 2).Insert(index, $"{fixedMonth:D2}");
        }
    }

    private void DayLogic(string mask, string text, int maskOffset, ref int textIndex, ref int maskIndex, ref string alignedText)
    {
        var dd = new string(_d, 2);
        var (dayString, index) = Extract(dd, mask, maskOffset, alignedText);
        if (dayString == null)
            return;
        if (!int.TryParse(dayString, out var day))
            return;
        if (dayString.Length == 1)
        {
            // we are at the first digit of dd, only 0..3 are allowed except if month is February. 
            if (day > 3 || day == 3 && _month == 2)
            {
                // by inserting a 0 we make 09 out of 9
                alignedText = alignedText.Insert(index, "0");
                maskIndex++;
                _day = day;
            }
        }
        else if (dayString.Length == 2)
        {
            var fixedDay = FixDay(_year, _month, day);
            if (fixedDay != day)
                alignedText = alignedText.Remove(index, 2).Insert(index, $"{fixedDay:D2}");
            _day = fixedDay;
        }
    }

    /// <summary>
    /// Note: this is required for special cases where the date becomes invalid after the last character has been input
    /// For instance: MM/dd/yyyy from 02/29/000| to 02/29/0001|
    /// The year 0001 is not a leap year so the day needs to be corrected to 28
    /// Or this 0[3]/31/2000 input "4" => 04/31/2000
    /// But we do this only for a complete date!
    /// </summary>
    protected override string ModifyFinalText(string text)
    {
        try
        {
            var yyyy = new string(_y, 4);
            var yy = new string(_y, 2);
            var dd = new string(_d, 2);
            var MM = new string(_M, 2);
            var maskHasDay = Mask.Contains(dd);
            var maskHasMonth = Mask.Contains(MM);
            var maskHasYear = Mask.Contains(yy) || Mask.Contains(yyyy);
            var (dayString, dayIndex) = Extract(dd, Mask, 0, text);
            var (monthString, monthIndex) = Extract(MM, Mask, 0, text);
            var dayFound = dayIndex >= 0;
            var dayComplete = dayString?.Length == 2;
            var monthFound = monthIndex >= 0;
            var monthComplete = monthString?.Length == 2;
            var y = ExtractYear(Mask, text, 0);
            //if (maskHasYear && y < 0 || maskHasMonth && (!monthFound || !monthComplete) || maskHasDay && (!dayFound || !dayComplete))
            //    return text; // we have incomplete input, no final check necessary/possible
            int.TryParse(dayString ?? "", out var d);
            int.TryParse(monthString ?? "", out var m);
            if (!maskHasYear)
                y = 0;
            if (y < 0)
                y = _year;
            if (maskHasMonth && (monthFound || monthComplete))
            {
                var m1 = FixMonth(m);
                if (m1 != m)
                    text = text.Remove(monthIndex, 2).Insert(monthIndex, $"{m1:D2}");
            }

            if (maskHasDay && (dayFound || dayComplete))
            {
                var d1 = FixDay(y, m, d);
                text = text.Remove(dayIndex, 2).Insert(dayIndex, $"{d1:D2}");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error in ModifyFinalText: " +e.Message);
            return text;
        }
        return text;
    }

    private int FixDay(int year, int month, int day)
    {
        if (day == 0)
            return 1;
        if (day > 28)
        {
            var daysInMonth = GetDaysInMonth(year, month);
            if (day > daysInMonth)
                return daysInMonth;
        }
        return day;
    }

    private int FixMonth(int month)
    {
        if (month == 0)
            return 1;
        if (month > 12)
            return 12;
        return month;
    }

    private int GetDaysInMonth(int year, int month)
    {
        if (month <= 0 || month > 12) // we don't know yet which month the user means, so assume 31
            return 31;
        if (year == 0) // DateTime.DaysInMonth does not support year 0 but we just use 4 instead because it was a leap year too
            year = 4;
        return DateTime.DaysInMonth(year, Math.Min(12, Math.Max(1, month)));
    }

    private (string, int) Extract(string maskPart, string mask, int maskOffset, string alignedText)
    {
        var maskIndex = mask.IndexOf(maskPart);
        var index = maskIndex - maskOffset;
        if (index < 0 || index >= alignedText.Length)
            return (null, -1);
        var subString = alignedText.Substring(index, Math.Min(maskPart.Length, alignedText.Length - index));
        if (!Regex.IsMatch(subString, @"^\d+$"))
            return (null, -1);
        return (subString, index);
    }

    public override void UpdateFrom(IMask other)
    {
        base.UpdateFrom(other);
        var o = other as DateMask;
        if (o == null)
            return;
        _y = o._y;
        _M = o._M;
        _d = o._d;
    }
}

