include "globals.mzn";

%%---------Input------------------------------------------------------------------------------------------------------------------------------------------

int: l; %scheduling horizon
set of int: TIMES = 0..l;
int: a; %number of attributes
set of int: ATTRIBUTES = 1..a;
array[1..a+1,1..a] of int: setup_costs; %setup_costs[i,j] costs for switching from attribute i to attribute j
array[1..a+1,1..a] of int: setup_times; %setup_times[i,j] time for switching from attribute i to attribute j
int: m; %number of machines
set of int: MACHINES = 1..m;
array[MACHINES] of int: min_cap; %minimum capacity per machine
array[MACHINES] of int: max_cap; %maximum capacity per machine
array[MACHINES] of int: initState; %initial state (=attribute) of every machine
int: s; %maximum number of shifts per machine
array[MACHINES,1..s] of TIMES: m_a_s; %machine availability start times
array[MACHINES,1..s] of TIMES: m_a_e; %machine availability end times
int: n; %number of jobs
set of int: JOBS = 1..n;
array[JOBS] of set of MACHINES: eligible_machine; %eligible_machine[i] contains j <-> job i can be executed on machine j
array[JOBS] of int: earliest_start; %earliest start date for every job
array[JOBS] of int: latest_end; %latest end date for every job
array[JOBS] of int: min_time; %minimum time in machine for every job
array[JOBS] of int: max_time; %maximum time in machine for every job
array[JOBS] of int: size; %occupied space in machine for every job
array[JOBS] of int: attribute; %attribute of every job

%new
%constants needed for the calculation of the objective function
int: upper_bound_integer_objective;
int: mult_factor_total_runtime;
int: mult_factor_finished_toolate;
int: mult_factor_total_setuptimes;
int: mult_factor_total_setupcosts;
int: running_time_bound;
int: min_duration;
int: max_duration;
int: max_setup_time;
int: max_setup_cost;
%end new

%%---------Decision variables-----------------------------------------------------------------------------------------------------------------------------
%Jobs
array[JOBS] of var opt TIMES: start; %start of job (optional because only one job per batch is scheduled)
array[JOBS] of var 0..max_duration: dur;
array[JOBS, MACHINES] of var opt TIMES: startOnMach; 
array[JOBS, MACHINES] of var 0..max_duration: durOnMach;

%if job is not scheduled, pointer to job that is scheduled
array[JOBS] of var opt JOBS: inBatchWithJob;	

% %helper variables
%needed for setup times and costs
array[JOBS, MACHINES] of var 0..1: firstOnMach; %firstOnMach[j,ma]=1 if job is first job on machine ma
array[JOBS, MACHINES] of var TIMES: endPrevJob; %end of previous job on machine (=0 if no prev job)
array[JOBS, MACHINES] of var 1..a+1: attrPrevJob; %attribute of previous job on machine (=0 if no prev job)

%setup times before jobs
array[JOBS, MACHINES] of var opt 0..max_setup_time: setupTime;
var 0..n: b = sum(j in JOBS)(occurs(start[j])); %total number of batches that are used
array[JOBS, MACHINES] of var opt 1..s: shiftForJob; %shift on machine for which job is scheduled

% %%---------Constraints------------------------------------------------------------------------------------------------------------------------------------


%every job that is present is scheduled to exactly one machine
constraint forall (j in JOBS)(
alternative(
  start[j],dur[j],
  [startOnMach[j,ma]|ma in MACHINES], [durOnMach[j,ma]|ma in MACHINES])
);
%every job that is present is scheduled to exactly one eligible machine
constraint forall (j in JOBS where occurs(start[j]))(
  sum (ma in eligible_machine[j])(occurs(startOnMach[j,ma])) == 1
);

%job is either present or pointer is set to some other job 
%that is present and has lower index
constraint forall (j in JOBS)(
  (occurs(start[j]) /\ absent(inBatchWithJob[j]))
   \/
  (absent(start[j]) /\ occurs(inBatchWithJob[j]))
);
constraint forall (j in JOBS, i in JOBS)(
  inBatchWithJob[j]==i
   ->
  (occurs(start[i]) /\ i<j)
);

%duration of absent jobs is set to 0
%duration of present job is between min and max time
constraint forall (j in JOBS, ma in MACHINES)(
  min_time[j]*occurs(start[j]) <= dur[j]
  /\
  dur[j]<=occurs(start[j])*max_time[j]
  /\
  min_time[j]*occurs(startOnMach[j,ma]) <= dur[j]
  /\
  durOnMach[j,ma]<=occurs(startOnMach[j,ma])*max_time[j]
  /\
  startOnMach[j,ma] + durOnMach[j,ma] <= l
);

%present jobs cannot start before earliest start date
constraint forall (j in JOBS, ma in MACHINES)(
  earliest_start[j] <= start[j]
  /\
  earliest_start[j] <= startOnMach[j,ma]
);

%jobs can only be processed in the same batch if they have the same attribute,
%if processing times are compatible
%and start is after earliest start
constraint forall (j in JOBS, i in JOBS)(
  inBatchWithJob[j]==i
  ->
  (attribute[i]==attribute[j]
  /\
  dur[i] <= max_time[j]
  /\
  dur[i] >= min_time[j]
  /\
  earliest_start[j] <= start[i])
);

%jobs can only be processed together with other job if assigned machine is eligible
constraint forall (j in JOBS, i in JOBS)(
  inBatchWithJob[j] == i
  ->
  (sum (ma in eligible_machine[j])(occurs(startOnMach[i,ma])) == 1)
);

%batches on the same machine may not overlap 
constraint forall(ma in MACHINES)
   (disjunctive([startOnMach[j,ma]|j in JOBS],[durOnMach[j,ma]|j in JOBS])
);
   
 
%set firstOnMach
constraint forall(j in JOBS, ma in MACHINES)(
   (occurs(startOnMach[j,ma])
   /\
   sum(i in JOBS)(occurs(startOnMach[i,ma])*(startOnMach[i,ma] < startOnMach[j,ma])) = 0
   )
   ->
   (firstOnMach[j,ma]=1)
); 
constraint forall(j in JOBS, ma in MACHINES)(
   (occurs(startOnMach[j,ma])
   /\
   sum(i in JOBS)(occurs(startOnMach[i,ma])*(startOnMach[i,ma] < startOnMach[j,ma])) > 0
   )
   ->
   (firstOnMach[j,ma]=0)
);
%at most m many jobs can be first on machine
constraint 
   sum(j in JOBS, ma in MACHINES)
     (firstOnMach[j,ma]=1)
   <= m
;
%define previous job on same machine:
 %1a. if job is not present on machine, end of previous job == 0, attr prev job == a+1
 constraint forall(j in JOBS, ma in MACHINES)(
   (absent(startOnMach[j,ma])
   )
   ->
   (endPrevJob[j,ma] == 0
   /\
   attrPrevJob[j,ma] == a+1
   )
);%1. if job is first job on machine ma, end of previous job == 0, attr prev job == initState[ma]
 constraint forall(j in JOBS, ma in MACHINES)(
   (
   firstOnMach[j,ma]=1
   )
   ->
   (endPrevJob[j,ma] == 0
   /\
   attrPrevJob[j,ma] == initState[ma]
   )
);
%2. if job is not first on machine, previous job needs to be defined
constraint forall(j in JOBS, ma in MACHINES)(
   (occurs(startOnMach[j,ma])
   /\
   firstOnMach[j,ma]=0
   )
   ->
   (
   endPrevJob[j,ma] > 0
   /\
   attrPrevJob[j,ma] < a+1)
); 
%3. endPrevJob must be equal to 0 or to the end time of some job that has been scheduled
constraint forall (j in JOBS, ma in MACHINES)(
  	endPrevJob[j,ma] == 0 
  	\/
	sum(i in JOBS)  ((occurs(startOnMach[i,ma]))*(endPrevJob[j,ma] == startOnMach[i,ma] + durOnMach[i,ma])) > 0
);
%4. if previous job of job j exists it must be scheduled before j and no other jobs are scheduled in between
 constraint forall(j in JOBS, i in JOBS, ma in MACHINES)(
   (endPrevJob[j,ma] == startOnMach[i,ma] + durOnMach[i,ma]
   /\
   occurs(startOnMach[i,ma])
   )
   ->
   (
   (startOnMach[i,ma] < startOnMach[j,ma])
   /\ %no other jobs scheduled in between
   sum(k in JOBS)
       (occurs(startOnMach[k,ma])*
         (startOnMach[i,ma] < startOnMach[k,ma] /\ startOnMach[k,ma] < startOnMach[j,ma])) 
   == 0
   /\ %set attribute accordingly
   attrPrevJob[j,ma] == attribute[i]
   )
 ); 
 %5. endprevjob = 0 if attr prev job = a+1  
constraint forall(j in JOBS, ma in MACHINES)( 
  attrPrevJob[j,ma] == a+1
  ->
  endPrevJob[j,ma] == 0  
);

%schedule setup times between jobs
constraint forall (j in JOBS, ma in MACHINES)(
  %setup is present iff job is present on machine
  occurs(startOnMach[j,ma]) == occurs(setupTime[j,ma])  
  /\
  %length of setup time
  (occurs(startOnMach[j,ma])
  ->
  (setupTime[j,ma] == setup_times[attrPrevJob[j,ma],attribute[j]]
  /\
  endPrevJob[j,ma] + setupTime[j,ma] 
  <=
  startOnMach[j,ma])
  )
);
 
% %shift for batch
constraint forall (j in JOBS, ma in MACHINES)(
  %shift is present iff job is present on machine
  occurs(startOnMach[j,ma]) == occurs(shiftForJob[j,ma]) 
);
constraint forall (j in JOBS, ma in MACHINES where occurs(startOnMach[j,ma]))( 
  shiftForJob[j,ma] = max(i in 1..s where  startOnMach[j,ma] >= m_a_s[ma,i])(i)
); 

%job processing time on machine ma must lie within one shift on machine ma
%1. job is not first job on machine
constraint forall (j in JOBS, ma in MACHINES where occurs(startOnMach[j,ma]) /\ attrPrevJob[j,ma] <= a)( 
   startOnMach[j,ma] + durOnMach[j,ma] <= m_a_e[ma,shiftForJob[j,ma]]
     /\
   startOnMach[j,ma] - setupTime[j,ma] >=  m_a_s[ma,shiftForJob[j,ma]]
);
%2. job is first job on machine
constraint forall (j in JOBS, ma in MACHINES where occurs(startOnMach[j,ma]) /\ attrPrevJob[j,ma] = a + 1)( 
   startOnMach[j,ma] + durOnMach[j,ma] <= m_a_e[ma,shiftForJob[j,ma]]
     /\
   startOnMach[j,ma] >=  m_a_s[ma,shiftForJob[j,ma]]
);

%total batch size must be less than max machine capacity
constraint forall (j in JOBS, ma in MACHINES)(
  (occurs(startOnMach[j,ma]) * 
    (size[j]
    + sum(i in JOBS)(size[i]*(deopt(inBatchWithJob[i])==j)))
   )
  <=
  max_cap[ma]
);


%%---------Objective--------------------------------------------------------------------------------------------------------------------------------------

var min_duration*a..running_time_bound: running_time_oven :: output_var;
running_time_oven = sum(j in JOBS)(dur[j]); 

var int: finished_too_late :: output_var;
finished_too_late = 
  count(j in JOBS)(
    occurs(start[j])
    /\ start[j] + dur[j] > latest_end[j]
    )
  + count(j in JOBS, i in JOBS)(
    inBatchWithJob[j]=i
    /\ start[i] + dur[i] > latest_end[j]
    )
  ;

  
var 0..n*max_setup_cost: total_setup_costs :: output_var;
total_setup_costs = sum(ma in MACHINES, j in JOBS)
  (occurs(startOnMach[j,ma])*
  setup_costs[attrPrevJob[j,ma],attribute[j]]);

   
var 0..upper_bound_integer_objective: objective :: output_var;
objective = 
  mult_factor_total_runtime * running_time_oven
 + mult_factor_finished_toolate * finished_too_late
 + mult_factor_total_setupcosts * total_setup_costs
 ;


%%%% Output %%%%%
output [
"objective = \(objective);\n",
"running_time_oven = \(running_time_oven);\n",
"total_setup_costs = \(total_setup_costs);\n",
"finished_too_late = \(finished_too_late);\n",
"number of jobs = \(n);\n",
"Total number of batches: \(b);\n",
 "Start times of representative jobs are: \(start);\n",
 "Durations of representative jobs are: \(dur);\n",
 "Start times of representative jobs on machines are: \(startOnMach);\n",
 "Durations of representative jobs on machines are: \(durOnMach);\n",
 "Job is together in same batch with: \(inBatchWithJob);\n",
"Setup times between batches: \(setupTime);\n",
% %"Setup costs between batches: \(setup_costs_between_batches);\n",
% "Attribute of batch: \(attribute_for_batch);\n",
"The shift chosen for job is: \(shiftForJob);\n"
,"upper_bound_integer_objective: \(upper_bound_integer_objective);\n"
% ,"firstOnMach = \(firstOnMach);\n"
% , "end of previous job on machine: \(endPrevJob);\n",
%   "attribute of previous job on machine: \(attrPrevJob);\n"
];
%end new
