using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using Sched.CustomClasses;

namespace Sched.ScheduleAlgorithm
{
    public class SimulatedAnnealing: BaseAlgorithm
    {
        Random _rnd;

        public SimulatedAnnealing(int dayStart, int dayEnd, int periodStart, int periodEnd, int semesterId, int studentGroupId, int lessonTypePreference) : base(dayStart, dayEnd, periodStart, periodEnd, semesterId, studentGroupId, lessonTypePreference)
        {
            _rnd = new Random();
        }

        public List<Lesson> Run(List<Lesson> lessonList)
        {
            //Preprocess
            int lssnCount = lessonList.Count;
            List<LessonWithArray> lessonsToAssign = PrepareLessons(lessonList);
            List<Lesson> lessonListBest = new List<Lesson>();
            foreach(Lesson Lssn in lessonList)
            {
                lessonListBest.Add(new Lesson(Lssn));
            }

            //Insert schedule lessons
            PrepareMainArray(new ObservableCollection<object>(lessonList));

            //SA
            int maxIter = 10;
            float T_0 = 2000;
            float T_final = 20;

            float T = T_0;
            float coolingCoeff = 0.95f;
  
            int scoreNew = int.MaxValue;
            int scoreCur = ScoreFull();
            int scoreBest = scoreCur;
            while (T > T_final)
            {
                for(int i=0; i < maxIter; i++)
                {
                    //Choose random neighbours
                    int n1 = _rnd.Next(lssnCount);
                    int day1Cur = AutoscheduleData.GetWeekDayIndex(lessonsToAssign[n1].Lssn.StartDateDT.DayOfWeek.ToString());
                    int period1Cur = lessonsToAssign[n1].Lssn.PeriodStart;
                    int week1Cur = _sem.GetWeek(lessonsToAssign[n1].Lssn.StartDateDT);

                    int n2 = _rnd.Next(lssnCount);
                    if (n1 == n2)
                    {
                        n2 = _rnd.Next(lssnCount);  //try again
                    }
                    int day2Cur = AutoscheduleData.GetWeekDayIndex(lessonsToAssign[n2].Lssn.StartDateDT.DayOfWeek.ToString());
                    int period2Cur = lessonsToAssign[n2].Lssn.PeriodStart;
                    int week2Cur = _sem.GetWeek(lessonsToAssign[n2].Lssn.StartDateDT);

                    //Generate new neighbours
                    switch (i % 5)
                    {
                        case 0:
                        case 1:
                            RemoveLesson(lessonsToAssign[n1].Lssn);
                            MoveToEmpty(lessonsToAssign[n1]);
                            InsertLesson(lessonsToAssign[n1].Lssn);
                            break;
                        default:
                            RemoveLesson(lessonsToAssign[n1].Lssn);
                            RemoveLesson(lessonsToAssign[n2].Lssn);
                            SwapTwo(lessonsToAssign[n1], lessonsToAssign[n2]);
                            InsertLesson(lessonsToAssign[n1].Lssn);
                            InsertLesson(lessonsToAssign[n2].Lssn);
                            break;
                    }
                    scoreNew = ScoreFull();

                    //Revert changes if new solution is not accepted
                    if (!(IsNewSolAccepted(scoreNew, scoreCur, T)))
                    {
                        RemoveLesson(lessonsToAssign[n1].Lssn);
                        RemoveLesson(lessonsToAssign[n2].Lssn);

                        lessonsToAssign[n1].Lssn.PeriodStart = period1Cur;
                        lessonsToAssign[n1].Lssn.StartDateDT = _sem.DateStartDT.AddDays(week1Cur * 7 + day1Cur);

                        lessonsToAssign[n2].Lssn.PeriodStart = period2Cur;
                        lessonsToAssign[n2].Lssn.StartDateDT = _sem.DateStartDT.AddDays(week2Cur * 7 + day2Cur);

                        InsertLesson(lessonsToAssign[n1].Lssn);
                        InsertLesson(lessonsToAssign[n2].Lssn);
                    }
                    else
                    {  
                        //Debug.WriteLine("Cur: " + scoreCur.ToString() + " New: " + scoreNew.ToString());
                        scoreCur = scoreNew;

                        for (int k = 0; k < lssnCount; k++)
                        {
                            if (lessonList[k].Id == lessonsToAssign[n1].Lssn.Id)
                            {
                                lessonList[k].PeriodStart = lessonsToAssign[n1].Lssn.PeriodStart;
                                lessonList[k].StartDateDT = lessonsToAssign[n1].Lssn.StartDateDT;
                            }
                            else if (lessonList[k].Id == lessonsToAssign[n2].Lssn.Id)
                            {
                                lessonList[k].PeriodStart = lessonsToAssign[n2].Lssn.PeriodStart;
                                lessonList[k].StartDateDT = lessonsToAssign[n2].Lssn.StartDateDT;
                            }
                        }

                        if(scoreCur < scoreBest)
                        {
                            scoreBest = scoreCur;
                            for (int k = 0; k < lssnCount; k++)
                            {
                                for (int l = 0; l < lssnCount; l++)
                                {
                                    if (lessonListBest[k].Id == lessonsToAssign[l].Lssn.Id)
                                    {
                                        lessonListBest[k].PeriodStart = lessonsToAssign[l].Lssn.PeriodStart;
                                        lessonListBest[k].StartDateDT = lessonsToAssign[l].Lssn.StartDateDT;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }

                T = coolingCoeff * T; //geometric cooling
            }
            return lessonListBest;
        }

        protected void MoveToEmpty(LessonWithArray lwa)
        {
            List<(int day, int period, int week)> possiblePos = new List<(int, int, int)>();

            int nWeeks = lwa.OccupiedArray.GetLength(2);
            int periodicity = lwa.Lssn.Periodicity;

            int weekStep = 1;
            if (periodicity == 0)
            {
                weekStep = nWeeks;
            }
            else if (periodicity == 2)
            {
                weekStep = 2;
            }

            for (int w = 0; w < nWeeks; w += weekStep)
            {
                for (int d = _dayStart; d <= _dayEnd; d++)
                {
                    for (int p = _periodStart; p <= _periodEnd; p++)
                    {
                        if ((IsPossiblePositionInMain(lwa, d, w, p))&&(lwa.IsPossiblePosition(d, w, p)))
                        {
                            possiblePos.Add((d,p,w));
                            if (possiblePos.Count > 19)
                            {
                                w = nWeeks; //to break free of all loops
                                d = _dayEnd + 1;
                                p = _periodEnd + 1;
                            }
                           
                        }                                            
                    }
                }
            }

            if (possiblePos.Count > 0)
            {
                //Score them and choose best
                int bestFitScore = int.MaxValue;
                for (int j = 0; j < possiblePos.Count; j++)
                {
                    (int day, int period, int week) = possiblePos[j];
                    int variantScore = ScoreSingle(day, period, week, lwa.Lssn.Hours, lwa.Lssn.CourseId, lwa.Lssn.LessonTypeId);
                    if (variantScore < bestFitScore)
                    {
                        bestFitScore = variantScore;
                        lwa.Lssn.PeriodStart = period;
                        lwa.Lssn.StartDateDT = _sem.DateStartDT.AddDays(week * 7 + day);
                    }
                }
            }
        }
        protected void SwapTwo(LessonWithArray lwa1, LessonWithArray lwa2)
        {
            if ((lwa1.Lssn.Periodicity != lwa2.Lssn.Periodicity)||(lwa1.Lssn.Id == lwa2.Lssn.Id))
            {
                return;
            }

            int day1Cur = AutoscheduleData.GetWeekDayIndex(lwa1.Lssn.StartDateDT.DayOfWeek.ToString());
            int period1Cur = lwa1.Lssn.PeriodStart;
            int week1Cur = _sem.GetWeek(lwa1.Lssn.StartDateDT);

            int day2Cur = AutoscheduleData.GetWeekDayIndex(lwa2.Lssn.StartDateDT.DayOfWeek.ToString());
            int period2Cur = lwa2.Lssn.PeriodStart;
            int week2Cur = _sem.GetWeek(lwa2.Lssn.StartDateDT);

            if ((day1Cur == day2Cur)&&(week1Cur == week2Cur))
            {
                int p1Next = period1Cur + lwa1.Lssn.Hours;
                int p2Next = period2Cur + lwa2.Lssn.Hours;

                //Adjacent Lessons
                if (((p1Next == period2Cur) || (p2Next == period1Cur)))
                {
                    if (p1Next == period2Cur)
                    {
                        int period1new = period1Cur + lwa2.Lssn.Hours;
                        int period2new = period1Cur;
                        PlaceIfPossible(lwa1, day2Cur, period1new, week2Cur, lwa2, day1Cur, period2new, week1Cur);
                    }
                    else if (p2Next == period1Cur)
                    {
                        int period1new = period2Cur;
                        int period2new = period2Cur + lwa1.Lssn.Hours;
                        PlaceIfPossible(lwa1, day2Cur, period1new, week2Cur, lwa2, day1Cur, period2new, week1Cur);
                    }
                    
                }
                else
                {   //Nonadjacent Lessons
                    PlaceIfPossible(lwa1, day2Cur, period2Cur, week2Cur, lwa2, day1Cur, period1Cur, week1Cur);                   
                }
            }
            else
            {   //Different days
                PlaceIfPossible(lwa1, day2Cur, period2Cur, week2Cur, lwa2, day1Cur, period1Cur, week1Cur);
            }

            //Repair of invalid placement, can happen for lessons that are in the same week+day (but not adjacent) because of how placement is evaluated
            if (_sem.IsInCollision(lwa1.Lssn, lwa2.Lssn))
            {
                //Debug.WriteLine("COLLISION!");
                lwa1.Lssn.PeriodStart = period1Cur;
                lwa1.Lssn.StartDateDT = _sem.DateStartDT.AddDays(week1Cur * 7 + day1Cur);

                lwa2.Lssn.PeriodStart = period2Cur;
                lwa2.Lssn.StartDateDT = _sem.DateStartDT.AddDays(week2Cur * 7 + day2Cur);
            }
        }

        public bool IsPossiblePositionInMain(LessonWithArray lwa, int day, int period,int week)
        {
            if ((period + lwa.Lssn.Hours - 1) > _periodEnd) //to ensure we don't reach past the allowed timeframe
            {
                return false;
            }

            int periodicity = lwa.Lssn.Periodicity;
            int weekStep = 1;
            if (periodicity == 0)
            {
                weekStep = nWeeks;
            }
            else if (periodicity == 2)
            {
                weekStep = 2;
            }

            for (int w = week; w < MainArray.GetLength(2); w += weekStep)
            {
                for (int p = period; p < (period + lwa.Lssn.Hours); p++)
                {
                    if (MainArray[day,p,w].CourseId != _emptySpotVal)
                    {
                        return false;
                    }
                }
            }

            return true;
        }
        public void PlaceIfPossible(LessonWithArray lwa1, int day1new, int period1new, int week1new, LessonWithArray lwa2, int day2new, int period2new, int week2new)
        {
            if ((IsPossiblePositionInMain(lwa1, day1new, period1new, week1new)) && (IsPossiblePositionInMain(lwa2, day2new, period2new, week2new))) //no internal collision
            {
                if ((lwa1.IsPossiblePosition(day1new, period1new, week1new)) && (lwa2.IsPossiblePosition(day2new, period2new, week2new)))   //no external collisions
                {
                    lwa1.Lssn.PeriodStart = period1new;
                    lwa1.Lssn.StartDateDT = _sem.DateStartDT.AddDays(week1new * 7 + day1new);

                    lwa2.Lssn.PeriodStart = period2new;
                    lwa2.Lssn.StartDateDT = _sem.DateStartDT.AddDays(week2new * 7 + day2new);
                }
            }
        }
        public bool IsNewSolAccepted(int scoreNew, int scoreCur, float T)
        {
            if (scoreNew < scoreCur)
            {
                return true;
            }
            else
            {
                double r = _rnd.NextDouble();
                double exponent = -(double)Math.Abs(scoreNew - scoreCur) / (double)T;
                double threshold = Math.Exp(exponent);
                //Debug.WriteLine("r: " + r.ToString() + " exponent: " + exponent.ToString() + " threshold: " + threshold.ToString());
                if (r < threshold)
                {
                    return true;
                }
            }

            return false;
        }
    }
}
