using System.Diagnostics;
using Core.Entities;
using Google.OrTools.Sat;
namespace Core.Calculation;
///
/// Solves the team meeting scheduling problem using constraint programming.
/// Assigns teams to time slots while minimizing student schedule conflicts.
///
public class TeamScheduler
{
private readonly IList _studentObjects;
private readonly IList _teamObjects;
private readonly double _maxSolveTimeSeconds;
private readonly int[] _students;
private readonly int[] _teams;
private readonly int[] _timeSlots;
private readonly List<(int team1, int team2)> _scheduleSeparateTeams = [];
///
/// Creates a new team scheduler instance.
///
/// The teams to schedule (must not be null or empty)
/// The number of available time slots (must be positive)
/// All students participating in teams (must not be null or empty)
/// Maximum solver time in seconds (default: 10.0)
/// Thrown when teams or allStudents is null
/// Thrown when collections are empty or numTimeSlots is invalid
public TeamScheduler(IEnumerable teams, int numTimeSlots, IEnumerable allStudents, double maxSolveTimeSeconds = 10.0)
{
if (teams == null)
throw new ArgumentNullException(nameof(teams));
if (allStudents == null)
throw new ArgumentNullException(nameof(allStudents));
if (numTimeSlots <= 0)
throw new ArgumentException("Number of time slots must be positive", nameof(numTimeSlots));
_teamObjects = teams.ToArray();
_studentObjects = allStudents.ToList();
_maxSolveTimeSeconds = maxSolveTimeSeconds;
if (_teamObjects.Count == 0)
throw new ArgumentException("Teams collection cannot be empty", nameof(teams));
if (_studentObjects.Count == 0)
throw new ArgumentException("Students collection cannot be empty", nameof(allStudents));
_students = Enumerable.Range(0, _studentObjects.Count).ToArray();
_teams = Enumerable.Range(0, _teamObjects.Count).ToArray();
_timeSlots = Enumerable.Range(0, numTimeSlots).ToArray();
}
///
/// Adds a constraint requiring two teams to be scheduled in different time slots.
///
/// First team
/// Second team
/// Thrown when either team is not found in the scheduler
public void ScheduleSeparate(Team team1, Team team2)
{
var one = _teamObjects.IndexOf(team1);
var two = _teamObjects.IndexOf(team2);
if (one == -1)
throw new ArgumentException($"Team '{team1}' not found in scheduler", nameof(team1));
if (two == -1)
throw new ArgumentException($"Team '{team2}' not found in scheduler", nameof(team2));
_scheduleSeparateTeams.Add((one, two));
}
///
/// Solves the team scheduling problem using constraint programming.
/// Minimizes the number of time slots where students have conflicting team meetings.
///
/// A solution containing team assignments to time slots and conflict information
public TeamSchedulerSolution Solve()
{
// Create constraint programming model
var model = new CpModel();
// Build membership matrix: m[i,t] = 1 if student i is on team t, else 0
// Use team.Students to support PartialTeam objects with omitted students
var m = new int[_students.Length,_teams.Length];
foreach (var i in _students)
foreach (var t in _teams)
m[i, t] = _teamObjects[t].Students.Any(s => s.Id == _studentObjects[i].Id) ? 1 : 0;
// Decision variables:
// x[t,s] = 1 if meeting of team t takes place at time slot s, else 0
var x = new IntVar[_teams.Length, _timeSlots.Length];
foreach (var t in _teams)
foreach (var s in _timeSlots)
x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]");
// y[i,s] = 1 if student i has at least one meeting at time slot s, else 0
var y = new IntVar[_students.Length, _timeSlots.Length];
foreach (var i in _students)
foreach (var s in _timeSlots)
y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]");
// Constraint: each team meets exactly once
foreach (var t in _teams)
model.AddLinearConstraint(LinearExpr.Sum(_timeSlots.Select(s => x[t, s])), 1L, 1L);
// Constraint: Link y[i,s] to whether student i has meetings at slot s
// y[i,s] <= sum over all teams t of (m[i,t] * x[t,s])
// This forces y[i,s] to be 1 if student i has at least one team meeting at slot s
foreach (var i in _students)
foreach (var s in _timeSlots)
model.Add(
new BoundedLinearExpression(
y[i, s],
LinearExpr.Sum(_teams.Select(t => m[i, t] * x[t, s])),
false));
// Objective: minimize the sum of y[i,s] values (minimize student conflicts)
var indTimeSlotVars = LinearExpr.NewBuilder();
foreach (var i in _students)
foreach (var s in _timeSlots)
indTimeSlotVars.Add(y[i, s]);
model.Minimize(indTimeSlotVars);
// Constraint: teams marked as "separate" must be in different time slots
foreach (var (team1, team2) in _scheduleSeparateTeams)
foreach (var s in _timeSlots)
model.Add(x[team1, s] != x[team2, s]);
// Configure and run solver
var solver = new CpSolver();
solver.StringParameters = $"max_time_in_seconds:{_maxSolveTimeSeconds}";
var cpSolverStatus = solver.Solve(model);
Debug.WriteLine($"Solver status: {cpSolverStatus}");
var timeSlotTeams = new Team[_timeSlots.Length][];
if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
// Extract solution: which teams are assigned to each time slot
foreach (var s in _timeSlots)
{
var teams = (from t in _teams where solver.Value(x[t, s]) > 0 select _teamObjects[t]).ToArray();
timeSlotTeams[s] = teams;
}
return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
}
}