/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2021  Delft University of Technology
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package nl.tudelft.queue.service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.transaction.Transactional;
import javax.validation.constraints.NotNull;

import nl.tudelft.queue.dto.create.embeddables.SlottedLabConfigCreateDTO;
import nl.tudelft.queue.dto.create.labs.AbstractSlottedLabCreateDTO;
import nl.tudelft.queue.dto.patch.labs.AbstractSlottedLabPatchDTO;
import nl.tudelft.queue.model.ClosableTimeSlot;
import nl.tudelft.queue.model.TimeSlot;
import nl.tudelft.queue.model.embeddables.Slot;
import nl.tudelft.queue.model.labs.AbstractSlottedLab;
import nl.tudelft.queue.model.labs.ExamLab;
import nl.tudelft.queue.repository.ClosableTimeSlotRepository;
import nl.tudelft.queue.repository.TimeSlotRepository;

import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@DependsOnDatabaseInitialization
public class TimeSlotService {

	@Autowired
	private RequestService rs;

	@Autowired
	private TimeSlotRepository tsr;

	@Autowired
	private ClosableTimeSlotRepository ctsr;

	/**
	 * Closes a time slot for active use.
	 *
	 * @param timeSlot The TimeSlot to close.
	 */
	@Transactional
	public void closeTimeSlot(ClosableTimeSlot timeSlot) {
		timeSlot.setActive(false);

		timeSlot.getRequests().stream()
				.filter(r -> r.getEventInfo().getStatus().isPending())
				.forEach(rs::notPickedRequest);
	}

	/**
	 * Creates the slots needed for a slotted lab. These slots can no longer be changed after the creation of
	 * a lab in their time and duration. However, the capacity of each slot should be updateable during the
	 * lab to allow emergency changes.
	 *
	 * @param dto The DTO with information on the lab creation.
	 * @param lab The lab that was created using the DTO.
	 */
	@Transactional
	public void createSlotsForLab(@NotNull AbstractSlottedLabCreateDTO<?> dto,
			@NotNull AbstractSlottedLab<?> lab) {
		SlottedLabConfigCreateDTO config = dto.getSlottedLabConfig();
		if (config == null) {
			return;
		}

		long duration = config.getDuration();

		// Generate a stream of the start times of all timeslots and map it to a timeslot,
		// the generation of slots is accompanied by the generation of offsets.
		List<TimeSlot> slots = Stream.iterate(Pair.of(dto.getSlot().getOpensAt(), 0),
				x -> !x.getKey().plusMinutes(duration).isAfter(dto.getSlot().getClosesAt()),
				x -> Pair.of(x.getKey().plusMinutes(duration), x.getValue() + 1))
				.map(c -> createSlotForLab(lab, new Slot(c.getKey(), c.getKey().plusMinutes(duration)),
						config.getCapacity(), c.getValue()))
				.collect(Collectors.toList());

		// Simply save all slots. Maybe we will need to do something extra with these in the future.
		tsr.saveAll(slots);
	}

	/**
	 * Creates a single slot for the lab that is to have time slots.
	 *
	 * @param  lab      The lab for which the time slot is created.
	 * @param  slot     The times the slot represents.
	 * @param  capacity The capacity of the time slot.
	 * @return          The created time slot intended for the given lab.
	 */
	public TimeSlot createSlotForLab(AbstractSlottedLab<?> lab, Slot slot, int capacity, int offset) {
		if (lab instanceof ExamLab) {
			return new ClosableTimeSlot((ExamLab) lab, slot, capacity, offset);
		}
		return new TimeSlot(lab, slot, capacity, offset);
	}

	/**
	 * A scheduled job which checks all the closable time slots in Queue for slots that should now be
	 * automagically closed. This job should run every 5 minutes on the 5th second of the minute. The 5th
	 * second is added because time slots will likely be created on the full minute. 5 seconds is more than
	 * enough time to make sure that time slots only slightly more than the allowed grace period are closed.
	 */
	@Transactional
	@Scheduled(cron = "5 */5 * * * ?")
	public void checkClosableSlots() {
		ctsr.findAllClosablyActive().forEach(this::closeTimeSlot);
	}

	/**
	 * Updates time slots based on offset sequence number found in TimeSlot Object
	 *
	 * @param slottedLabPatchDTO The DTO which represents the PatchDTO for the Slotted Lab
	 * @param slottedLab         The session representing the Slotted Lab
	 */
	@Transactional
	public void updateSlots(@NotNull AbstractSlottedLabPatchDTO<?> slottedLabPatchDTO,
			@NotNull AbstractSlottedLab<?> slottedLab) {

		Long duration = slottedLab.getSlottedLabConfig().getDuration();
		List<? extends TimeSlot> timeSlots = slottedLab.getTimeSlots();
		LocalDateTime newStartTime = slottedLabPatchDTO.getSlot().getOpensAt();

		BiFunction<LocalDateTime, Integer, Slot> calculateSlotLambda = (startTime, offset) -> new Slot(
				startTime.plusMinutes(offset * duration),
				startTime.plusMinutes(offset * duration + duration));

		timeSlots.forEach(slot -> slot
				.setSlot(calculateSlotLambda.apply(newStartTime, slot.getOffsetSequenceNumber())));

	}
}
