using System.Linq.Expressions;

namespace Fast.Logic;

public static partial class Util
{
    public static class Date
    {
        public static DateOnly? FirstDayOfMonth(DateTime? date)
        {
            DateOnly? returnDate = null;
            if (date != null)
                returnDate = new DateOnly(date.Value.Year, date.Value.Month, 1);

            return returnDate;
        }

        public static DateOnly FirstDayOfMonth(DateTime date)
        {
            DateOnly returnDate = new(date.Year, date.Month, 1);
            return returnDate;
        }

        public static DateOnly? FirstDayOfMonth(DateOnly? date)
        {
            DateOnly? returnDate = null;
            if (date != null)
                returnDate = new DateOnly(date.Value.Year, date.Value.Month, 1);

            return returnDate;
        }

        public static DateOnly FirstDayOfMonth(DateOnly date)
        {
            DateOnly returnDate = new(date.Year, date.Month, 1);
            return returnDate;
        }

        public static DateOnly LastDayOfMonth(DateTime date)
        {
            DateOnly returnDate = new(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month));
            return returnDate;
        }

        public static DateOnly LastDayOfMonth(DateOnly date)
        {
            DateOnly returnDate = new(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month));
            return returnDate;
        }

        public static IEnumerable<DateOnly> EachDay(DateTime from, DateTime thru)
        {
            for (var day = DateOnly.FromDateTime(from.Date); day <= DateOnly.FromDateTime(thru.Date); day = day.AddDays(1))
                yield return day;
        }

        public static IEnumerable<DateOnly> EachDay(DateOnly from, DateOnly thru)
        {
            for (var day = from; day <= thru; day = day.AddDays(1))
                yield return day;
        }

        public static IEnumerable<DateOnly> EachMonth(DateTime from, DateTime thru)
        {
            for (var month = FirstDayOfMonth(from.Date); month <= FirstDayOfMonth(thru.Date); month = month.AddMonths(1))
                yield return month;
        }

        public static IEnumerable<DateOnly> EachMonth(DateOnly from, DateOnly thru)
        {
            for (var month = FirstDayOfMonth(from); month <= FirstDayOfMonth(thru); month = month.AddMonths(1))
                yield return month;
        }

        public static int GetBusinessDaysCount(DateTime startDate, DateTime endDate, IEnumerable<DateOnly> weekDayHolidays)
        {
            var weekDays = GetWeekdaysCount(startDate, endDate);

            int holidays = 0;
            foreach (var holidayDate in weekDayHolidays)
            {
                if (DateOnly.FromDateTime(startDate) <= holidayDate && holidayDate <= DateOnly.FromDateTime(endDate))
                    holidays += 1;
            }

            var businessDays = weekDays - holidays;
            return businessDays;
        }

        public static int GetBusinessDaysCount(DateOnly startDate, DateOnly endDate, IEnumerable<DateOnly> weekDayHolidays)
        {
            var weekDays = GetWeekdaysCount(startDate, endDate);

            int holidays = 0;
            foreach (var holidayDate in weekDayHolidays)
            {
                if (startDate <= holidayDate && holidayDate <= endDate)
                    holidays += 1;
            }

            var businessDays = weekDays - holidays;
            return businessDays;
        }

        public static int GetWeekdaysCount(DateTime startDate, DateTime endDate)
        {
            int weekDaysInFirstWeek = Math.Min(DayOfWeek.Saturday - startDate.DayOfWeek, 5);

            int weekDaysInLastWeek = Math.Min(Convert.ToInt32(endDate.DayOfWeek), 5);

            // set startdate on the saterday of the week

            startDate = startDate.AddDays(DayOfWeek.Saturday - startDate.DayOfWeek).Date;

            // set enddate on the sunday of the week

            endDate = endDate.AddDays(-1 * Convert.ToInt32(endDate.DayOfWeek)).Date;

            // number of full weeks between the dates

            int fullWeeksbetween = (endDate - startDate).Days / 7;

            return (fullWeeksbetween * 5) + weekDaysInFirstWeek + weekDaysInLastWeek;
        }

        public static int GetWeekdaysCount(DateOnly startDate, DateOnly endDate)
        {
            int weekDaysInFirstWeek = Math.Min(DayOfWeek.Saturday - startDate.DayOfWeek, 5);

            int weekDaysInLastWeek = Math.Min(Convert.ToInt32(endDate.DayOfWeek), 5);

            // set startdate on the saterday of the week

            startDate = startDate.AddDays(DayOfWeek.Saturday - startDate.DayOfWeek);

            // set enddate on the sunday of the week

            endDate = endDate.AddDays(-1 * Convert.ToInt32(endDate.DayOfWeek));

            // number of full weeks between the dates

            int fullWeeksbetween = (endDate.ToDateTime(TimeOnly.MinValue) - startDate.ToDateTime(TimeOnly.MinValue)).Days / 7;

            return (fullWeeksbetween * 5) + weekDaysInFirstWeek + weekDaysInLastWeek;
        }

        public static Expression<Func<T, bool>> GetDateRangeExpression<T>(List<DateRange> dateRanges, string dbDatePropertyName, bool allowNulls)
        {
            var expParam = Expression.Parameter(typeof(T));
            var dbDateProperty = Expression.Property(expParam, dbDatePropertyName);

            Expression myExpression;
            if (allowNulls)
                myExpression = Expression.Equal(dbDateProperty, Expression.Constant(null, typeof(DateOnly?)));
            else
                myExpression = Expression.Constant(false);

            foreach (var dateRange in dateRanges)
            {
                myExpression = Expression.OrElse(
                    myExpression,
                    Expression.AndAlso(
                        Expression.GreaterThanOrEqual(
                            dbDateProperty,
                            Expression.Constant(dateRange.FromDate, dbDateProperty.Type)
                        ),
                            Expression.LessThanOrEqual(
                            dbDateProperty,
                            Expression.Constant(dateRange.ToDate, dbDateProperty.Type)
                        )
                    )
                );
            }
            var exp = Expression.Lambda<Func<T, bool>>(myExpression, expParam);
            return exp;
        }

        public static Expression<Func<T, bool>> GetDateRangeExpression<T>(List<DateRange> dateRanges, string dbFromDatePropertyName, string dbToDatePropertyName, bool allowNulls)
        {
            var expParam = Expression.Parameter(typeof(T));
            var dbFromDateProperty = Expression.Property(expParam, dbFromDatePropertyName);
            var dbToDateProperty = Expression.Property(expParam, dbToDatePropertyName);

            Expression myExpression;
            if (allowNulls)
                myExpression = Expression.OrElse(Expression.Equal(dbFromDateProperty, Expression.Constant(null, typeof(DateOnly?))), Expression.Equal(dbToDateProperty, Expression.Constant(null, typeof(DateOnly?))));
            else
                myExpression = Expression.Constant(false);

            foreach (var dateRange in dateRanges)
            {

                myExpression = Expression.OrElse(
                    myExpression,
                    Expression.AndAlso(
                        Expression.GreaterThanOrEqual(
                            dbToDateProperty,
                            Expression.Constant(dateRange.FromDate, dbToDateProperty.Type)
                        ),
                            Expression.LessThanOrEqual(
                            dbFromDateProperty,
                            Expression.Constant(dateRange.ToDate, dbFromDateProperty.Type)
                        )
                    )
                );
            }
            var exp = Expression.Lambda<Func<T, bool>>(myExpression, expParam);
            return exp;
        }
    }

    public static DateTime ToDateTime(this DateOnly dateOnly, TimeOnly timeOnly = default)
    {
        return dateOnly.ToDateTime(timeOnly);
    }

    public static DateOnly ToDateOnly(this DateTime dateTime)
    {
        return DateOnly.FromDateTime(dateTime);
    }

}
