namespace Jovian.Calendar {
    /// <summary>
    /// Core calendar clock. Feed it a 0-1 normalized day float each tick.
    /// Internally accumulates world-minutes and rolls them into the calendar.
    /// </summary>
    public class WorldClock {
        /// <summary>The calendar configuration driving this clock.</summary>
        public CalendarSettings Settings { get; }

        /// <summary>The current world date and time.</summary>
        public WorldDateTime Now { get; private set; }

        /// <summary>
        /// Total in-world minutes elapsed since the start date.
        /// This is the single source of truth. WorldDateTime is derived from it.
        /// </summary>
        public long TotalElapsedMinutes { get; private set; }

        /// <summary>The normalized time (0-1) from the last tick.</summary>
        public float LastNormalizedTime { get; private set; }

        private float prevNormalized;
        private bool initialized;
        private float fractionalMinutes;
        private readonly long startOffsetMinutes;

        /// <summary>
        /// Creates a new world clock from the given calendar settings.
        /// Validates settings and initializes to the configured start date.
        /// </summary>
        public WorldClock(CalendarSettings settings) {
            settings.Validate();
            Settings = settings;

            startOffsetMinutes = ComputeMinuteOffset(
                settings, settings.startYear, settings.startMonth,
                settings.startDay, settings.startHour, settings.startMinute
            );

            TotalElapsedMinutes = 0;
            prevNormalized = -1f;
            initialized = false;

            Now = MakeDateTime(startOffsetMinutes);
        }

        /// <summary>
        /// Call every frame with the current normalized day progress (0..1).
        /// The clock detects the delta since last tick and advances time accordingly.
        /// Handles wrap-around (1 to 0 = new day crossing).
        /// </summary>
        public void Tick(float normalizedTime) {
            normalizedTime = Clamp01(normalizedTime);
            LastNormalizedTime = normalizedTime;

            if(!initialized) {
                prevNormalized = normalizedTime;
                initialized = true;
                return;
            }

            var delta = normalizedTime - prevNormalized;

            if(delta < 0f) {
                delta += 1f;
            }

            if(delta < 1e-7f) {
                prevNormalized = normalizedTime;
                return;
            }

            var minutesAdvanced = delta * Settings.MinutesPerDay;
            fractionalMinutes += minutesAdvanced;

            var wholeMinutes = (int)fractionalMinutes;
            if(wholeMinutes > 0) {
                fractionalMinutes -= wholeMinutes;
                TotalElapsedMinutes += wholeMinutes;
                Now = MakeDateTime(startOffsetMinutes + TotalElapsedMinutes);
            }

            prevNormalized = normalizedTime;
        }

        /// <summary>
        /// Manually advance the clock by a number of in-world minutes.
        /// Useful for sleep/rest skips, cutscenes, etc.
        /// </summary>
        public void AdvanceMinutes(int minutes) {
            if(minutes <= 0) {
                return;
            }
            TotalElapsedMinutes += minutes;
            Now = MakeDateTime(startOffsetMinutes + TotalElapsedMinutes);

            var dayFraction = (float)((Now.hour * Settings.minutesPerHour) + Now.minute) / Settings.MinutesPerDay;
            prevNormalized = dayFraction;
        }

        /// <summary>
        /// Converts a total-minutes-from-epoch value into a WorldDateTime.
        /// </summary>
        private WorldDateTime MakeDateTime(long totalMinutes) {
            if(totalMinutes < 0) {
                totalMinutes = 0;
            }

            var minutesPerDay = Settings.MinutesPerDay;
            var minutesPerHour = Settings.minutesPerHour;

            var totalDays = totalMinutes / minutesPerDay;
            var remainderMinutes = (int)(totalMinutes % minutesPerDay);

            var hour = remainderMinutes / minutesPerHour;
            var minute = remainderMinutes % minutesPerHour;

            var daysInYear = Settings.TotalDaysInYear;

            var year = (int)(totalDays / daysInYear);
            var remainingDays = (int)(totalDays % daysInYear);

            var month = 0;
            for(var m = 0; m < Settings.daysPerMonth.Length; m++) {
                if(remainingDays < Settings.daysPerMonth[m]) {
                    month = m;
                    break;
                }
                remainingDays -= Settings.daysPerMonth[m];

                if(m == Settings.daysPerMonth.Length - 1) {
                    month = m;
                    break;
                }
            }

            return new WorldDateTime {
                year = year,
                month = month,
                day = remainingDays,
                hour = hour,
                minute = minute
            };
        }

        /// <summary>
        /// Inverse of MakeDateTime: given a calendar date, produce total minutes from epoch.
        /// </summary>
        private static long ComputeMinuteOffset(
            CalendarSettings s, int year, int month, int day, int hour, int minute) {
            var totalDays = (long)year * s.TotalDaysInYear;
            for(var m = 0; m < month; m++) {
                totalDays += s.daysPerMonth[m];
            }
            totalDays += day;

            var totalMinutes = totalDays * s.MinutesPerDay;
            totalMinutes += (long)hour * s.minutesPerHour;
            totalMinutes += minute;
            return totalMinutes;
        }

        /// <summary>
        /// Get the current day-of-week (0-indexed), or -1 if weeks are disabled.
        /// </summary>
        public int DayOfWeek {
            get {
                if(Settings.daysPerWeek <= 0) {
                    return -1;
                }
                var totalDays = (startOffsetMinutes + TotalElapsedMinutes) / Settings.MinutesPerDay;
                return (int)(totalDays % Settings.daysPerWeek);
            }
        }

        /// <summary>
        /// Normalized time of day as 0-1 float, derived from the current WorldDateTime.
        /// Useful for lighting, skybox lerp, etc.
        /// </summary>
        public float NormalizedTimeOfDay {
            get {
                float currentMinute = (Now.hour * Settings.minutesPerHour) + Now.minute;
                return currentMinute / Settings.MinutesPerDay;
            }
        }

        /// <summary>Returns the name of the current month from settings.</summary>
        public string GetMonthName() {
            var month = Now.month;
            if(Settings.monthNames == null || month < 0 || month >= Settings.monthNames.Length) {
                return "???";
            }
            return Settings.monthNames[month];
        }

        /// <summary>Returns a full display string with named month and ordinal day.</summary>
        public string FullStringNamed() {
            var displayDay = Now.day;
            var postFix = displayDay switch {
                1 => "st",
                2 => "nd",
                3 => "rd",
                _ => "th"
            };
            var year = Now.year;
            var timeString = Now.TimeString;
            return $"{timeString}, {displayDay}{postFix} day of {GetMonthName()}, \nyear {year}";
        }

        private static float Clamp01(float v) {
            return v < 0f ? 0f : v > 1f ? 1f : v;
        }
    }
}
