/*
 * 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.model;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import lombok.*;
import nl.tudelft.queue.model.embeddables.Slot;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.model.labs.AbstractSlottedLab;
import nl.tudelft.queue.model.labs.Lab;

/**
 * Entity class representing a time-slot created for a certain lab. Labs create time-slots when the lab is set
 * to be a slotted lab.
 * <p>
 * This entity has a unique constraint on the lab and opensAt columns so as to prevent timeslots that start at
 * the same exact time in the same lab from occurring. If one such timeslot is attempted to be made, that must
 * be a mistake.
 */
@Data
@Entity
@NoArgsConstructor
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(uniqueConstraints = {
		@UniqueConstraint(columnNames = { "lab_id", "offset_sequence_number" })
})
public class TimeSlot {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	/**
	 * The lab that this TimeSlot is a part of.
	 */
	@NotNull
	@ManyToOne
	@JoinColumn(name = "lab_id", updatable = false)
	private Lab lab;

	/**
	 * The actual start and end time of the slot stored in an embedded object.
	 * <p>
	 * This column cannot be updated after initial creation. This is to prevent people losing their timeslot
	 * because of changes made by a teacher or the server.
	 */
	@NotNull
	@Embedded
	@Column
	private Slot slot;

	/**
	 * The capacity of the time slot. Sometimes many people can enrol for one time-slot because many TAs are
	 * available, sometimes very little are available.
	 */
	@Min(0)
	@NotNull
	private int capacity;

	/**
	 * The 'block' number in which this slot is processed in, represented as a zero-based offset.
	 */
	@Min(0)
	@NotNull
	@Column(name = "offset_sequence_number")
	private Integer offsetSequenceNumber;

	/**
	 * The requests that currently claim this slot.
	 */
	@NotNull
	@ToString.Exclude
	@EqualsAndHashCode.Exclude
	@OneToMany(mappedBy = "timeSlot")
	private Set<LabRequest> requests = new HashSet<>();

	/**
	 * Constructs a TimeSlot using the relevant configurable fields in the TimeSlot object.
	 *
	 * @param lab      The lab the TimeSlot is a part of.
	 * @param slot     The slot that the TimeSlot occupies.
	 * @param capacity The capacity of the slot.
	 */
	public TimeSlot(@NotNull AbstractSlottedLab<? extends TimeSlot> lab,
			@NotNull Slot slot,
			@Min(1) @NotNull int capacity,
			@Min(0) @NotNull int offsetSequenceNumber) {
		this.lab = lab;
		this.slot = slot;
		this.capacity = capacity;
		this.offsetSequenceNumber = offsetSequenceNumber;
	}

	/**
	 * Checks whether any users may still take this time slot. IF the time slot has passed, no users may still
	 * take the slot.
	 *
	 * @return Whether any user may still claim a slot in this timeslot.
	 */
	public boolean canTakeSlot() {
		if (lab instanceof AbstractSlottedLab<?>) {
			boolean baseCondition = !isFull()
					&& (((AbstractSlottedLab<?>) lab).getCanSelectDueSlots() || !slot.closed());
			if (((AbstractSlottedLab<?>) lab).isConsecutive()) {
				return baseCondition && !numEmptySlotsBeforeExceedsThreshold();
			} else {
				return baseCondition;
			}
		}
		return false;
	}

	/**
	 * Checks whether the number of slots taken is larger or equal to the capacity of this time slot. This
	 * indicates whether more requests may still be placed within this time slot.
	 *
	 * @return Whether the time slot is filled or not.
	 */
	public boolean isFull() {
		return countSlotsOccupied() >= capacity;
	}

	/**
	 * Checks whether this time slot has any pending requests.
	 *
	 * @return Whether this time slot has pending requests.
	 */
	public boolean hasPendingRequests() {
		return requests.stream().anyMatch(r -> r.getEventInfo().getStatus().isPending());
	}

	/**
	 * Counts the number of slots that are taken within this time slot. This is the true count of how many
	 * requests are enqueued in this time slot and therefore may determine how many requests can still be done
	 * within this time slot.
	 *
	 * @return The count of how many requests are active within this time slot.
	 */
	public int countSlotsOccupied() {
		return (int) requests.stream()
				.filter(r -> r.getEventInfo().getStatus() != RequestStatus.REVOKED)
				.count();
	}

	/**
	 * Counts the number of slots that are taken and finished processing. This is the count of how many
	 * requests in a certain slot have been handled by a TA. This excludes revoked requests.
	 *
	 * @return The number of requests that have been handled by a TA.
	 */
	public int countHandledRequests() {
		return (int) requests.stream()
				.filter(r -> r.getEventInfo().getStatus().isHandled())
				.count();
	}

	/**
	 * Checks if the slot is filled sufficiently according to the threshold set by the teacher. This is mainly
	 * used by consecutive labs.
	 *
	 * @return true iff it is full according to this definition or actually full, false otherwise.
	 */
	public boolean isFullAccordingToPercentage() {
		var labConfig = ((AbstractSlottedLab<?>) lab).getSlottedLabConfig();
		return isFull() || ((int) ((countSlotsOccupied() * 100.0) / this.capacity) >= labConfig
				.getConsideredFullPercentage());
	}

	/**
	 * Checks that the number of preceding slots that are not above the 'full-threshold' exceeds the threshold
	 * defined by the user.
	 *
	 * @return True iff, it exceeds the threshold, false otherwise.
	 */
	public boolean numEmptySlotsBeforeExceedsThreshold() {
		var slottedLab = ((AbstractSlottedLab<?>) lab);
		return slottedLab.getTimeSlots()
				.stream()
				.filter(ts -> ts.getSlot().getOpensAt().isBefore(this.getSlot().getOpensAt())
						&& ts.getSlot().getOpensAt().isAfter(LocalDateTime.now())
						&& !ts.isFullAccordingToPercentage() && !ts.getSlot().closed())
				.count() > slottedLab.getSlottedLabConfig().getPreviousEmptyAllowedThreshold();
	}

	/**
	 * Counts the number of slots that are taken but did not finish processing. This is the count of how many
	 * requests in a certain slot are pending.
	 *
	 * @return The number of requests that are pending.
	 */
	public int countPendingRequests() {
		return (int) requests.stream()
				.filter(r -> r.getEventInfo().getStatus().isPending())
				.count();
	}
}
