using System.Diagnostics;
using Core.Entities;
using Google.OrTools.Sat;
using IntVar = Google.OrTools.Sat.IntVar;

namespace Core.Calculation
{
	public class EventAssigner
	{
		private readonly IList<CompetitiveEvent> _events;
		private readonly IList<Student> _students;
		private readonly AssignmentParameters _parameters;
		private readonly int[] _allEvents;
		private readonly int[] _allStudents;
		// how many students have picked each event?
		private readonly int[] _eventPickCounts;
		private IList<EventAssignment> _preAssigned = new List<EventAssignment>();
		private IList<EventAssignment> _prohibited = new List<EventAssignment>();
		private IList<CompetitiveEvent> _droppedEvents;
		private IList<CompetitiveEvent> _includedEvents;

		public EventAssigner(IList<CompetitiveEvent> events, IList<Student> students, AssignmentParameters parameters)
		{
			_events = events;
			_students = students;
			_parameters = parameters;
			_allEvents = Enumerable.Range(0, _events.Count).ToArray();
			_allStudents = Enumerable.Range(0, _students.Count).ToArray();
			_eventPickCounts = new int[_allEvents.Length];
			_preAssigned = new List<EventAssignment>();
			_droppedEvents = new List<CompetitiveEvent>();
			for (var i = 0; i < _events.Count; i++)
			{
				var e = _events[i];
				_eventPickCounts[i] = _students.Count(s => s.RankedEventPicks.Contains(e));
			}
		}

		public void AssignToEvent(EventAssignment preAssigned)
		{
			_preAssigned.Add(preAssigned);
		}


		public void ExcludeFromEvent(EventAssignment prohibited)
		{
			_prohibited.Add(prohibited);
		}

		public void RemoveEvent(IList<CompetitiveEvent> events)
		{
			_droppedEvents = events;
		}
		public void IncludedEvents(IList<CompetitiveEvent> events)
		{
			_includedEvents = events;
		}

		public class SolutionPrinter : CpSolverSolutionCallback
		{
			private readonly IList<CompetitiveEvent> _events;
			private readonly IList<Student> _students;
			private readonly BoolVar[,] _eventAssignment;

			public SolutionPrinter(IList<CompetitiveEvent> events, IList<Student> students, BoolVar[,] eventAssignment)
			{
				_events = events;
				_students = students;
				_eventAssignment = eventAssignment;
			}
			public override void OnSolutionCallback()
			{
				Console.WriteLine($"Solution ");
				foreach (var evt in Enumerable.Range(0,_events.Count))
				{
					foreach (var student in Enumerable.Range(0, _students.Count))
					{
						if (Value(_eventAssignment[evt, student]) == 1L)
						{

						}
					}
				}
			}
		}

		public Team[] Solve()
		{

			// Model.
			var model = new CpModel();

			// Variables.
			var x = new BoolVar[_allEvents.Length, _allStudents.Length];
			foreach (var e in _allEvents)
			foreach (var s in _allStudents)
				x[e, s] = model.NewBoolVar($"eventAssignments[{e},{s}]");

			foreach (var preAssignment in _preAssigned)
			{
				var e = _events.IndexOf(preAssignment.Event);
				var s = _students.IndexOf(preAssignment.Student);
				model.AddAssumption(x[e, s]);
			}

			foreach (var prohibit in _prohibited)
			{
				var e = _events.IndexOf(prohibit.Event);
				var s = _students.IndexOf(prohibit.Student);
				
				var prohibitVar = new List<IntVar> { x[e, s] };

				model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
				prohibitVar.Clear();
			}

			// Limit the capacity of each event
			foreach (var e in _allEvents)
			{
				var evt = _events[e];
				var eventPickCounts = _eventPickCounts[e];

				var evtMinTeamSize = evt.MinTeamSize;
				var evtMaxTeamSize = evt.MaxTeamSize;
			
				var teamDivs = eventPickCounts / (evtMinTeamSize * 1.0);
				if (_includedEvents.Contains(evt))
					teamDivs = 1;

				//var teamsCount = (int)Math.Ceiling(teamDivs);
				var teamCount = (int)Math.Round(teamDivs);
				if (teamCount > evt.MaxTeamCountState)
					teamCount = evt.MaxTeamCountState;

				// limit to one team for group events
				if (_parameters.LimitTeamsToOne && evt.Format is EventFormat.Team && teamCount > 1) teamCount = 1;

				if (evt.Name == "Tech Bowl")
					teamCount = 1;

				var eventCapacity = new List<IntVar>();
				foreach (var s in _allStudents)
				{
					eventCapacity.Add(x[e, s]);
				}

				if (_droppedEvents != null && _droppedEvents.Contains(evt))
					teamCount = 0;

				var lb = evtMinTeamSize * teamCount;
				var ub = Math.Min(evtMaxTeamSize * teamCount, _parameters.TeamSizeLimit);
				model.AddLinearConstraint(LinearExpr.Sum(eventCapacity), lb, ub);
				Debug.WriteLine($"{evt.Name,30}\t{evt.Format,-10}\t{lb} - {ub}");

				model.Minimize(LinearExpr.Sum(eventCapacity));
				eventCapacity.Clear();
			}

			// Limit the number of events a student is assigned
			foreach (var s in _allStudents)
			{
				var student = _students[s];
			
				var studentCapacity = new List<IntVar>();
				foreach (var e in _allEvents)
				{
					studentCapacity.Add(x[e, s]);
				}

				model.AddLinearConstraint(LinearExpr.Sum(studentCapacity), _parameters.AssignmentLowerBound, _parameters.AssignmentUpperBound);
				studentCapacity.Clear();
			}

			var eventEffortCoefficients = GetEventEffortCoefficients(_events);
			foreach (var s in _allStudents)
			{
				var effortVar = new BoolVar[_allEvents.Length];

				foreach (var e in _allEvents)
				{
					effortVar[e] = x[e, s];
				}
				var student = _students[s];
				var experienceOffset = 0;
				switch (student.TsaYear)
				{
					case 1: experienceOffset = 1; break;
					default: break;
				}

				var ub = _parameters.EffortUpperBound - experienceOffset;
				var lb = _parameters.EffortLowerBound;
				if (ub <= lb)
					lb = ub - 1;

				model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) >= lb);
				model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) <= ub);
			}
		
			// each student should be assigned at least one on site activity event
			foreach (var s in _allStudents)
			{
				var onSiteActivity = new List<ILiteral>();
				foreach (var e in _allEvents)
				{
					if (_events[e].OnSiteActivity)
						onSiteActivity.Add(x[e, s]);
				}

				if (_parameters.RequireOnSite)
					model.AddAtLeastOne(onSiteActivity);
				onSiteActivity.Clear();
			}

			// students should have at maximum one individual event
			foreach (var s in _allStudents)
			{
				var individualEvent = new List<ILiteral>();
				foreach (var e in _allEvents)
				{
					if (_events[e].Format == EventFormat.Individual)
						individualEvent.Add(x[e, s]);
				}

				model.AddAtMostOne(individualEvent);
				individualEvent.Clear();
			}

			// students should have at maximum regional events
			foreach (var s in _allStudents)
			{
				var regionalEvent = new List<ILiteral>();
				foreach (var e in _allEvents)
				{
					if (_events[e].RegionalEvent)
						regionalEvent.Add(x[e, s]);
				}

				if (_parameters.RequireRegional)
					model.AddLinearConstraint(LinearExpr.Sum(regionalEvent), 1, 2);
				regionalEvent.Clear();
			}

			var maximizePicks = LinearExpr.NewBuilder();
			// optimize student selections
			foreach (var s in _allStudents)
			{
				var eventPickCoefficients = GetEventPickCoefficients(_students[s].RankedEventPicks, _events);

				foreach (var e in _allEvents)
				{
					maximizePicks.AddTerm(x[e, s], eventPickCoefficients[e]);
				}
			}
			model.Maximize(maximizePicks);

			var solver = new CpSolver();
			var cpSolverStatus = solver.Solve(model);

			//  print solver status
			Console.WriteLine($"Solver status: {cpSolverStatus}");

			var eventAssignmentsList = new List<Team>();

			if (cpSolverStatus == CpSolverStatus.Optimal || cpSolverStatus == CpSolverStatus.Feasible)
			{
				foreach (var e in _allEvents)
				{
					var students = new List<Student>();
					foreach (var s in _allStudents)
					{
						if (solver.BooleanValue(x[e, s]))
						{
							students.Add(_students[s]);
							//Console.WriteLine($"{_events[evt].Name} : {_students[s].Name}");
						}
					}
					if (students.Count > 0)
						eventAssignmentsList.Add(new Team(_events[e].Name, _events[e], students));
				}
			}
			else
			{
				//Console.WriteLine("No solution found.");
			}

			return eventAssignmentsList.ToArray();
		}

		private long[] GetEventPickCoefficients(IList<CompetitiveEvent> rankedEvents, IEnumerable<CompetitiveEvent> events)
		{
			var eventPickCount = rankedEvents.Count;

			return
				events.Select(e =>
				{
					var eventPickIndex = rankedEvents.IndexOf(e);
					return eventPickIndex switch
					{
						0 => eventPickCount + 1,
						1 => eventPickCount + 0,
						> 1 => eventPickCount ,
						_ => 0L
					};
				}).ToArray();
		}


		private long[] GetEventEffortCoefficients(IEnumerable<CompetitiveEvent> events)
		{
			return
				events.Select(e => e.Name == "Tech Bowl" ? 0 : e.LevelOfEffort.HasValue ? e.LevelOfEffort.Value : 10L).ToArray();
		}
	}
}