ReservationService.java

package sk.iway.iwcm.components.reservation.rest;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.servlet.http.HttpServletRequest;

import sk.iway.iwcm.Constants;
import sk.iway.iwcm.DateTools;
import sk.iway.iwcm.Identity;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.SendMail;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.common.CloudToolsForCore;
import sk.iway.iwcm.components.reservation.jpa.ReservationEntity;
import sk.iway.iwcm.components.reservation.jpa.ReservationObjectEntity;
import sk.iway.iwcm.components.reservation.jpa.ReservationObjectPriceEntity;
import sk.iway.iwcm.components.reservation.jpa.ReservationObjectPriceRepository;
import sk.iway.iwcm.components.reservation.jpa.ReservationObjectRepository;
import sk.iway.iwcm.components.reservation.jpa.ReservationObjectTimesEntity;
import sk.iway.iwcm.components.reservation.jpa.ReservationRepository;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.system.datatable.json.LabelValueInteger;
import sk.iway.iwcm.system.jpa.DefaultTimeValueConverter;
import sk.iway.iwcm.users.UserDetails;
import sk.iway.iwcm.users.UserGroupsDB;
import sk.iway.iwcm.users.UsersDB;

public class ReservationService {

    public static final String FE_DATEPICKER_FORMAT = "yyyy-MM-dd";
    public static final String FE_MONTHPICKER_FORMAT = "yyyy-MM";
    public static final String REGEX_YYYY_MM_DD = "\\d{4}-\\d{2}-\\d{2}";
    public static final String REGEX_YYYY_MM = "\\d{4}-\\d{2}";

    private final String emailAllDay;
    private final String emailDateFrom;
    private final String emailDateTo;
    private final String emailNext2;
    private final String emailNext3;

    public ReservationService() {
        this(Prop.getInstance());
    }

    public ReservationService(Prop prop) {
        emailAllDay = prop.getText("reservation.reservations.email_for_all_day");
        emailDateFrom = prop.getText("reservation.reservations.email_date_from");
        emailDateTo = prop.getText("reservation.reservations.email_date_to");
        emailNext2 = prop.getText("components.reservation.mail.next2");
        emailNext3 = prop.getText("components.reservation.mail.next3");
    }

    /**
     * !! Beware, reservation time must be already set into reservation date (date and time must be combined).
     * Check this validation requirements :
     * 1. Reservation time from is < reservation time to (interval must have 1 minute at least)
     * 2. Reservation time interval inutes) set in reservation object
     * 3. If reservation range reservationTimeFrom-reservationTimeTo fits inside of reservationObject range reservationTimeFrom-reservationTimeTo
     *    (check it separately for every day in date range dateFrom-DateTo because reservation object range can be different for every day in week)
     * @param reservation reservation to check
     * @param reservationObject reservation object that we trying reserve
     * @return If any of this validations are violated, text key with belonging error message is returned.
     *         Otherwise return null;
     */
    public String checkReservationTimeRangeValidity(ReservationEntity reservation, ReservationObjectEntity reservationObject) {

        //Compute day range (how many days we want reservate)
        long diffD = reservation.getDateTo().getTime() - reservation.getDateFrom().getTime();
        long reservationDaysRange = TimeUnit.DAYS.convert(diffD, TimeUnit.MILLISECONDS) + 1;

        //In case that date range is >= than 7, we must check every dayOfWeek
        boolean checkEveryDayOfWeek = false;
        if(reservationDaysRange >= 7) checkEveryDayOfWeek = true;

        //Get days of week during our reservation
        List<Integer> daysOfWeek = new ArrayList<>();
        if(checkEveryDayOfWeek) {
            daysOfWeek.add(1);
            daysOfWeek.add(2);
            daysOfWeek.add(3);
            daysOfWeek.add(4);
            daysOfWeek.add(5);
            daysOfWeek.add(6);
            daysOfWeek.add(7);
        } else { //Compute day o week
            for(int i = 0; i < reservationDaysRange; i++) {
                //We need day of week (but stupid Calendar start from Sunday = 1, Monday = 2 ... Saturday = 7 )
                Calendar cld = Calendar.getInstance();
                cld.setTime(reservation.getDateFrom());
                //We adding days because we need loop date range and get dayOfWeek for every of them
                cld.add(Calendar.DAY_OF_MONTH, i);
                int dayOfweek = cld.get(Calendar.DAY_OF_WEEK);
                //Format day of week to our standart where Monday = 1, Tuesday = 2 ... Sunday = 7
                dayOfweek = (dayOfweek - 1) == 0 ? 7 : (dayOfweek - 1);
                daysOfWeek.add(dayOfweek);
            }
        }

        //Validate reservation time values (if we want compare times, date values must have set yyyy-mm-dd at same value "2000-01-01")
        Date reservationTimeFrom = DefaultTimeValueConverter.getValidTimeValue(reservation.getDateFrom());
        Date reservationTimeTo = DefaultTimeValueConverter.getValidTimeValue(reservation.getDateTo());

        //Validate time range if have correct values from-to
        if(reservationTimeFrom.getTime() >= reservationTimeTo.getTime()) return "reservation.reservations.time_range_in_bad_order.js";

        //Validate if time range is big enough (because reservation object has set a minimum time range set)
        //Only if reservation object cant be reserve only for whole day
        if(Tools.isFalse(reservationObject.getReservationForAllDay())) {
            long diffT = reservationTimeTo.getTime() - reservationTimeFrom.getTime();
            long reservationTimesRange = TimeUnit.MILLISECONDS.toMinutes(diffT);
            if(reservationTimesRange < reservationObject.getTimeUnit()) return "reservation.reservations.time_range_to_short.js";
        }

        for(Integer dayOfWeek : daysOfWeek) {
            //First set default reservation object times range
            Date objectTimeFrom = reservationObject.getReservationTimeFrom();
            Date objectTimeTo = reservationObject.getReservationTimeTo();

            //Loop reservationObjectTimes and check if our dayOfWeek have special reservation time
            for(ReservationObjectTimesEntity objectTime : reservationObject.getReservationObjectTimeEntities()) {
                if(objectTime.getDay().equals(dayOfWeek)) {
                    objectTimeFrom = objectTime.getTimeFrom();
                    objectTimeTo = objectTime.getTimeTo();
                    break;
                }
            }

            //Now we have reservation object time range for specific day of week
            //We must check if "reservation" time range is inside "reservation object" time range
            if((objectTimeFrom.getTime() <= reservationTimeFrom.getTime())
            && (objectTimeTo.getTime() >= reservationTimeTo.getTime())) {
                //"reservation" time range IS inside "reservation object" time range
            } else {
                //"reservation" time range IS NOT inside "reservation object" time range
                return "reservation.reservations.time_range_validity_error.js";
            }
        }

        //No problem found so return null
        return null;
    }

    /**
     * !! Beware, reservation time must be already set into reservation date (date and time must be combined).
     * Check this validation requirements :
     * 1. Reservation date from is <= than reservation date to (if from = to reservation is for 1 day)
     * 2. Reservation date from/to are'nt in past
     * 3. Reservation overlapping validity, reservation can overlap with other reservations BUT max number of overlaps is set
     *    by reservation object (we count ONLY already approved reservations)
     * @param reservation reservation to check
     * @param reservationObject reservation object that we trying reserve
     * @param rr reservation repository (needed in proces of validation)
     * @return If any of this validations are violated, text key with belonging error message is returned.
     *         Otherwise return null;
     */
    public String checkReservationOverlappingValidity(ReservationEntity reservation, ReservationObjectEntity reservationObject, ReservationRepository rr, boolean isImporting) {
        //validate date range
        int result = DateTools.validateRange(reservation.getDateFrom(), reservation.getDateTo(), false, Tools.isFalse(reservationObject.getReservationForAllDay()));
        if(result == 1 && (isImporting || Tools.isTrue(reservation.getEditorFields().getAllowHistorySave()))) {
            //Range is in past BUT we are importing - so it's allowed
        } else {
            switch (result){
                case -1: return "datatable.error.fieldErrorMessage";
                case 1 :return "reservation.reservations.range_in_past.js";
                case 2: return "reservation.reservations.date_range_in_bad_order.js";
                default:
                    break;
            }
        }

        //Potentially overlapping reservations
        // !! We are finding only ACCEPTED reservations
        //We dont count reservations if they are rejected or still waiting for acceptance
        List<ReservationEntity> potentiallyOverlappingReservations =
            rr.findAllByReservationObjectIdAndDomainIdAndDateFromLessThanEqualAndDateToGreaterThanEqualAndAcceptedTrue(reservationObject.getId(), CloudToolsForCore.getDomainId(), reservation.getDateTo(), reservation.getDateFrom());

        //When we do EDIT on reservation, we must remove this reservation from list
        Long reservationId = reservation.getId();
        if(reservationId != null && reservationId > 0 && potentiallyOverlappingReservations != null)
            potentiallyOverlappingReservations.removeIf(item -> item.getId().equals(reservationId));

        //Now check if even time interval overlaps
        List<ReservationEntity> overlappingReservations = new ArrayList<>();

        if(potentiallyOverlappingReservations != null && potentiallyOverlappingReservations.isEmpty() == false) {
            for(ReservationEntity overlapReservation : potentiallyOverlappingReservations) {
                if(checkOverlap(reservation.getDateFrom(), reservation.getDateTo(), overlapReservation.getDateFrom(), overlapReservation.getDateTo(), Tools.isFalse(reservationObject.getReservationForAllDay())))
                    overlappingReservations.add(overlapReservation);
            }
        }

        /*
         * Now we know that all reservations in List "overlapReservation" are overlapping with our new reservation, but
         * we still must figure out if they are overlapping each other. Reason is we need compute max number of overlaps
         * in same time. Reason is because every "reservationObject" has it own set max number of reservations in same time.
        */
        int maxOverlapCount = 0;
        for(int i = 0; i < overlappingReservations.size(); i++) {
            int overlapCount = 0;
            ReservationEntity re1 = overlappingReservations.get(i);

            for(int j = 0; j < overlappingReservations.size(); j++) {
                //Do not compare with same entity
                if(i == j) continue;

                ReservationEntity re2 = overlappingReservations.get(j);

                if(checkOverlap(re1.getDateFrom(), re1.getDateTo(), re2.getDateFrom(), re2.getDateTo(), Tools.isFalse(reservationObject.getReservationForAllDay())))
                    overlapCount++;
            }

            //Save max overlap count
            maxOverlapCount = maxOverlapCount > overlapCount ? maxOverlapCount : overlapCount;
        }

        //+1 because 1 overlap means that there are 2 reservation in same time (2 overlaps means 3 reservation in same time etc)
        if(overlappingReservations.isEmpty() == false) maxOverlapCount++;

        //+1 because we want add new reservation (+1 our new reservation)
        //Validate if still can add our reservation (due to number limitation)
        if((maxOverlapCount + 1) > reservationObject.getMaxReservations()) {
            //We have too many reservations in same time
            if(isImporting || Tools.isTrue(reservation.getEditorFields().getAllowOverbooking())) {
                //IF we are importing OR user is ok with this, we can add reservation
            } else {
                //Not allowed
                return "reservation.reservations.max_reservations_error.js";
            }
        }

        //No problem found so return null
        return null;
    }

    /**
     * Prepare reservation to validation. First check if needed values are not null. Then set reservation date based on input value "isReservationForAllDay".
     * If isReservationForAllDay is true, date is same but hh:mm:ss:ms are set to 0 (because reservation upon reservation object for whole day cant set other time then default).
     * If isReservationForAllDay is false, date is set as combination of date from reservation and time from reservationEditorFields.
     * @param reservation
     * @param isReservationForAllDay
     */
    public void prepareReservationToValidation(ReservationEntity reservation, boolean isReservationForAllDay) throws IllegalArgumentException {
        //Check of values
        if(reservation.getDateFrom() == null || reservation.getDateTo() == null || reservation.getEditorFields() == null)
            throwError("html_area.insert_image.error_occured");

        if(isReservationForAllDay == false && (reservation.getEditorFields().getReservationTimeFrom() == null || reservation.getEditorFields().getReservationTimeTo() == null))
            throwError("html_area.insert_image.error_occured");

        if(isReservationForAllDay == true && (reservation.getEditorFields().getArrivingTime() == null || reservation.getEditorFields().getDepartureTime() == null))
            throwError("html_area.insert_image.error_occured");

        //Prepare dates
        ReservationService.prepareDates(reservation, isReservationForAllDay);
    }

    /**
     * Get reservation object "email accepter" and send mail to notify accepter about new waiting reservation for this reservation object.
     * Email includes link to this reservation waiting for approve.
     * @param reservation
     * @param reservationObject
     */
    public void sendAcceptationEmail(ReservationEntity reservation, HttpServletRequest request) {
        ReservationObjectEntity reservationObject = reservation.getReservationObjectForReservation();
        if(reservation == null || reservationObject == null) return;
        //Validate recipient email
        String recipientEmail = reservationObject.getEmailAccepter();
        try {
            InternetAddress emailAddr = new InternetAddress(recipientEmail);
            emailAddr.validate();
        } catch (AddressException ex) {
            return;
        }

        Prop prop = Prop.getInstance(request);
		String senderName = notNull(reservation.getName()) + " " + notNull(reservation.getSurname());
		String senderEmail = notNull(reservation.getEmail());
		String reservationObjectName = notNull(reservationObject.getName());
		String dateFrom = Tools.formatDate(reservation.getDateFrom());
		String dateTo = Tools.formatDate(reservation.getDateTo());
        String timeFrom = Tools.formatTime(reservation.getDateFrom());
		String timeTo = Tools.formatTime(reservation.getDateTo());
		String subject = prop.getText("components.reservation.mail.title") + senderName + " - " + reservationObjectName;
		String phoneNumber = notNull(reservation.getPhoneNumber());

        StringBuilder message = new StringBuilder();
        message.append(prop.getText("components.reservation.mail.greeting")).append("<br /><br />").append(senderName);
        message.append(" (").append(prop.getText("user.phone")).append(senderName).append(" )").append(phoneNumber).append(" ");
        message.append(prop.getText("components.reservation.mail.next")).append(" <b>").append(reservationObjectName).append("</b> ");

        if(Tools.isTrue(reservationObject.getReservationForAllDay())) {
            message.append(emailAllDay).append(" ");
        }

        message.append(emailDateFrom).append(" ").append(dateFrom).append(" ");
        message.append(emailDateTo).append(" ").append(dateTo).append(" ");

        if(Tools.isFalse(reservationObject.getReservationForAllDay())) {
            message.append(emailNext2).append(" ").append(timeFrom).append(" ");
            message.append(emailNext3).append(" ").append(timeTo).append(" ");
        }

        message.append(prop.getText("components.reservation.mail.next4")).append("<br/> ").append(reservation.getPurpose()).append(" <br /><br />");
        message.append(prop.getText("components.reservation.mail.next5")).append(" <a href=\"").append(Tools.getBaseHref(request)).append("/apps/reservation/admin/#dt-filter-id=").append(reservation.getId()).append("\">");
        message.append(prop.getText("components.reservation.mail.accept")).append("</a>");

		SendMail.send(senderName, senderEmail, recipientEmail, null, null, subject, message.toString(), null);
    }

    /**
     * Send confirmation email to email address set in reservation. Email subject and text is based on reservation "accepted" value where :
     * (true, reservation was accepted),
     * (false, reservation was rejected),
     * (null, reservation status was reset and reservation is waiting for approve)
     * @param reservation approved reservation
     * @param reservationObject reservation object that reservation is trying reserve
     * @param request HttpServletRequest instance
     * @param loggedUserName full name of actually logged user who change reservation accepted status
     */
    public void sendConfirmationEmail(ReservationEntity reservation, HttpServletRequest request, Identity loggedUser) {
        if(reservation == null) return;
        ReservationObjectEntity reservationObject = reservation.getReservationObjectForReservation();
        if(reservationObject == null) return;
        //Validate recipient email
        String recipientEmail = reservation.getEmail();
        try {
            InternetAddress emailAddr = new InternetAddress(recipientEmail);
            emailAddr.validate();
        } catch (AddressException ex) {
            return;
        }

        Prop prop = Prop.getInstance(request);
        String recipientName = notNull(reservation.getName() + " " + reservation.getSurname());
		String senderName = SendMail.getDefaultSenderName("reservation", loggedUser.getFullName());
		String senderEmail = SendMail.getDefaultSenderEmail("reservation", loggedUser.getEmail());
		String subject = "";
        String status = "";
        String endOfEmail = ".";
        Boolean approved = reservation.getAccepted();

        if(approved == null) {
            subject = prop.getText("reservation.reservations.email_subject_reset");
            status = prop.getText("reservation.reservations.email_was_reset");
            endOfEmail = prop.getText("reservation.reservations.reset_end_of_email");
        } else if(Tools.isTrue(approved)) {
            subject = prop.getText("reservation.reservations.email_subject_accepted");
            status = prop.getText("reservation.reservations.email_was_accepted");
        } else if(Tools.isFalse(approved)) {
            subject =  prop.getText("reservation.reservations.email_subject_rejected");
            status = prop.getText("reservation.reservations.email_was_rejected");
        }

        StringBuilder message = new StringBuilder();
        message.append(prop.getText("reservation.reservations.email_greeting")).append(" ").append(recipientName).append(",<br><br>");
        message.append(getReservationSpecForEmail(reservation, reservationObject, prop.getText("reservation.reservations.email_your_reservation")));
        message.append("<b>").append(status).append("<b/> : ").append(loggedUser.getFullName()).append(" ").append(endOfEmail);

		SendMail.send(senderName, senderEmail, recipientEmail, null, null, subject, message.toString(), null);

        if(Tools.isTrue(approved)) {
            //Send notification emails
            sendNotifEmails(reservationObject.getNotifEmails(), senderName, senderEmail, getReservationSpecForEmail(reservation, reservationObject, prop.getText("components.reservation.reservation_object.notify_body")), prop);
        }
	}

    /**
     * Send email to recipient (who created reservation) about created reservation. Email includes reservation details + info if reservation is approved or awaiting for acceptation.
     *
     * @param reservation - created reservation
     * @param request - HttpServletRequest instance
     */
    public void sendCreatedReservationEmail(ReservationEntity reservation, HttpServletRequest request) {
        if(reservation == null) return;
        ReservationObjectEntity reservationObject = reservation.getReservationObjectForReservation();
        if(reservationObject == null) return;

        //Validate recipient email
        String recipientEmail = reservation.getEmail();
        try {
            InternetAddress emailAddr = new InternetAddress(recipientEmail);
            emailAddr.validate();
        } catch (AddressException ex) {
            return;
        }

        Prop prop = Prop.getInstance(request);
        String recipientName = notNull(reservation.getName() + " " + reservation.getSurname());
		String senderName = SendMail.getDefaultSenderName("reservation", notNull(reservation.getName() + reservation.getSurname()));
		String senderEmail = SendMail.getDefaultSenderEmail("reservation", null);

        StringBuilder message = new StringBuilder();
        message.append(prop.getText("reservation.reservations.email_greeting")).append(" ").append(recipientName).append(",<br><br>");
        message.append(getReservationSpecForEmail(reservation, reservationObject, prop.getText("reservation.reservations.email_your_reservation")));

        if(Tools.isTrue(reservationObject.getMustAccepted()) && reservation.getAccepted() == null) {
            message.append(prop.getText("components.reservation.was_created_and_waiting"));
        } else {
            message.append(prop.getText("components.reservation.was_created"));

            //Send notification emails
            sendNotifEmails(reservationObject.getNotifEmails(), senderName, senderEmail, getReservationSpecForEmail(reservation, reservationObject, prop.getText("components.reservation.reservation_object.notify_body")), prop);
        }

        SendMail.send(senderName, senderEmail, recipientEmail, null, null, prop.getText("components.reservation.saved_reservation_subject"), message.toString(), null);
    }

    private void sendNotifEmails(String notifEmails, String senderName, String senderEmail, String specs, Prop prop) {
        if(Tools.isEmpty(notifEmails)) return;

        String[] emails = Tools.getTokens(notifEmails.trim(), ",");
        for(String recipientEmail : emails) {
            if(Tools.isEmail(recipientEmail))  {
                StringBuilder message = new StringBuilder();
                message.append(prop.getText("reservation.reservations.email_greeting")).append(",<br><br>");
                message.append(specs).append(".");

                SendMail.send(senderName, senderEmail, recipientEmail, null, null, prop.getText("components.reservation.saved_reservation_subject"), message.toString(), null);
            }
        }
    }

    private String getReservationSpecForEmail(ReservationEntity reservation, ReservationObjectEntity reservationObject, String startMsg) {
        String reservationObjectName = notNull(reservationObject.getName());
        String dateFrom = Tools.formatDate(reservation.getDateFrom());
        String dateTo = Tools.formatDate(reservation.getDateTo());
        String timeFrom = Tools.formatTime(reservation.getDateFrom());
        String timeTo = Tools.formatTime(reservation.getDateTo());

        StringBuilder specs = new StringBuilder();
        specs.append(startMsg).append(" <b>").append(reservationObjectName).append("</b> ");

        if(Tools.isTrue(reservationObject.getReservationForAllDay()))
            specs.append(emailAllDay).append(" ");

        specs.append(emailDateFrom).append(" ").append(dateFrom).append(" ");
        specs.append(emailDateTo).append(" ").append(dateTo).append(" ");

        if(Tools.isFalse(reservationObject.getReservationForAllDay())) {
            specs.append(emailNext2).append(" ").append(timeFrom).append(" ");
            specs.append(emailNext3).append(" ").append(timeTo).append(" ");
        }

        return specs.toString();
    }

    //Now validate if date range s1-e1 and range s2-e2 overlaps
    //Formula ((s1 <= e2) && (s2 <= e1)) , if true, then two dates are overlapping

    /**
     * Validate if interval s1-e1 and interval s2-e2 are overlapping using logical formula.
     * Used formula ((s1 <= e2) && (s2 <= e1)), return true if they are overlapping.
     * Intervals are overlapping even if one start when second ends (08:00-09:00 and 09:00-10:00).
     * IF function is used to compare date values (intervals) representing TIME we want all the values to share same yyyy-mm-dd part and for this reason
     * with input param "prepareDates" set to true, every date part of value will be set to 2000-01-01, so we can compare times.
     * @param s1 date value representing START of FIRST interval
     * @param e1 date value representing END of FIRST interval
     * @param s2 date value representing START of SECOND interval
     * @param e2 date value representing END of SECOND interval
     * @param prepareDates if true set date part of intervals to 2000-01-01, false/null - do nothing
     * @return true - if interval are overlapping, otherwise false
     */
    public boolean checkOverlap(Date s1, Date e1, Date s2, Date e2, Boolean prepareDates) {
        if(Tools.isTrue(prepareDates)) {
            s1 = DefaultTimeValueConverter.getValidTimeValue(s1);
            e1 = DefaultTimeValueConverter.getValidTimeValue(e1);
            s2 = DefaultTimeValueConverter.getValidTimeValue(s2);
            e2 = DefaultTimeValueConverter.getValidTimeValue(e2);
        }

        return ((s1.getTime() <= e2.getTime()) && (s2.getTime() <= e1.getTime()));
    }

    /**
     * Check if input string is null and if yes return empty string. Otherwise return original string.
     * @param s input string to check
     * @return not null string
     */
    private String notNull(String s) {
		if (s == null) return "";
		return s;
	}

    /**
     * Custom function to throw new RuntimeException with message.
     * @param errorTextKey translate key of error message
     */
    public void throwError(String errorTextKey) {
        Prop prop = Prop.getInstance();
        String message = prop.getText(errorTextKey);
        throw new IllegalArgumentException(message);
    }

    /**
     * Calculate price of reservation based on reservation object, reservation dateTime range and reservation object special prices.
     * @param entity - reservation entity
     * @param ror - ReservationObjectRepository instance
     * @param ropr - ReservationObjectPriceRepository instance
     * @return
     */
    public static BigDecimal calculateReservationPrice(ReservationEntity entity, int userIdToPay, ReservationObjectRepository ror, ReservationObjectPriceRepository ropr) {
        if(entity == null) return new BigDecimal(-1);
        if(entity.getEditorFields() == null) return new BigDecimal(-1);
        return calculateReservationPrice(entity.getDateFrom(), entity.getDateTo(), entity.getEditorFields().getReservationTimeFrom(), entity.getEditorFields().getReservationTimeTo(), entity.getReservationObjectId(), userIdToPay, ror, ropr);
    }

    /**
     * Calculate price of reservation based on reservation object, reservation dateTime range and reservation object special prices.
     * @param dateFrom - start date of reservation
     * @param dateTo - end date of reservation
     * @param timeFrom - start time of reservation
     * @param timeTo    - end time of reservation
     * @param objectId - id of reservation object
     * @param ror - ReservationObjectRepository instance
     * @param ropr - ReservationObjectPriceRepository instance
     * @return
     */
    public static BigDecimal calculateReservationPrice (Date dateFrom, Date dateTo, Date timeFrom, Date timeTo, Long objectId, int userIdToPay, ReservationObjectRepository ror, ReservationObjectPriceRepository ropr) {
        Long dateFromL = dateFrom == null ? null : dateFrom.getTime();
        Long dateToL = dateTo == null ? null : dateTo.getTime();
        Long timeFromL = timeFrom == null ? null : timeFrom.getTime();
        Long timeToL = timeTo == null ? null : timeTo.getTime();
        return calculateReservationPrice(dateFromL, dateToL, timeFromL, timeToL, objectId, userIdToPay, ror, ropr);
    }

    /**
     * Calculate price of reservation based on reservation object, reservation dateTime range and reservation object special prices.
     * @param dateFrom - start date of reservation
     * @param dateTo - end date of reservation
     * @param timeFrom - start time of reservation
     * @param timeTo    - end time of reservation
     * @param objectId - id of reservation object
     * @param ror - ReservationObjectRepository instance
     * @param ropr - ReservationObjectPriceRepository instance
     * @return
     */
    public static BigDecimal calculateReservationPrice (Long dateFrom, Long dateTo, Long timeFrom, Long timeTo, Long objectId, int userIdToPay, ReservationObjectRepository ror, ReservationObjectPriceRepository ropr) {
        Map<String, BigDecimal> prices = getMapOfPrices(dateFrom, dateTo, timeFrom, timeTo, objectId, userIdToPay, ror, ropr);
        if(prices == null) return new BigDecimal(-1);
        //
        return prices.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public static Map<String, BigDecimal> getMapOfPrices(Long dateFrom, Long dateTo, Long timeFrom, Long timeTo, Long objectId, int userIdToPay, ReservationObjectRepository ror, ReservationObjectPriceRepository ropr) {
        if(dateFrom == null || dateTo == null || objectId == null) return null;
        ReservationObjectEntity reservationObject = ror.findFirstByIdAndDomainId(objectId, CloudToolsForCore.getDomainId()).orElse(null);
        if(Tools.isFalse(reservationObject.getReservationForAllDay()) && (timeFrom == null || timeTo == null)) return null;

        List<ReservationObjectPriceEntity> prices = ropr.findAllByObjectIdAndDomainIdAndDateFromLessThanEqualAndDateToGreaterThanEqual(objectId, CloudToolsForCore.getDomainId(), new Date(dateTo), new Date(dateFrom));

        dateTo = DateTools.setTimePart(new Date(dateTo), 23, 59, 0, 0).getTime();

        Map<String, BigDecimal> mapOfPrices = new HashMap<>();

        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(dateFrom);
        cal.set(Calendar.HOUR_OF_DAY, 12); //Set it to middle of day
        while(cal.getTimeInMillis() < dateTo) {
            BigDecimal specialPrice = null;
            for(ReservationObjectPriceEntity priceEntity : prices) {
                if(priceEntity.getDateFrom().before(cal.getTime()) && priceEntity.getDateTo().after(cal.getTime())) {
                    specialPrice = priceEntity.getPrice();
                    break;
                }
            }

            String dayId = getDateId(cal, Tools.isFalse(reservationObject.getReservationForAllDay()));

            //add day - move in range
            cal.add(Calendar.DAY_OF_MONTH, 1);

            if(Tools.isTrue(reservationObject.getReservationForAllDay())) {
                //This condition will skip 1 last day, this day user do not pay because he is leaving
                if(cal.getTimeInMillis() < dateTo) {
                    if(specialPrice != null) mapOfPrices.put(dayId, specialPrice);
                    else mapOfPrices.put(dayId, reservationObject.getPriceForDay());
                }
            } else {
                //Unit of time which is charged - IN MINUTES
                BigDecimal timeUnit = new BigDecimal( reservationObject.getTimeUnit() );
                BigDecimal defaultPriceForTimeUnit = reservationObject.getPriceForHour();
                BigDecimal reservedTimeEveryDay = new BigDecimal( (timeTo - timeFrom + 1000) / (60 * 1000) );
                if(specialPrice != null) {
                    mapOfPrices.put(dayId, (reservedTimeEveryDay.divide(timeUnit, 2, RoundingMode.HALF_EVEN)).multiply(specialPrice) );
                } else {
                    mapOfPrices.put(dayId, (reservedTimeEveryDay.divide(timeUnit, 2, RoundingMode.HALF_EVEN)).multiply(defaultPriceForTimeUnit) );
                }
            }
        }

        //There can be DISCOUNT for certain user groups
        UserGroupsDB ugdb = UserGroupsDB.getInstance();
        return ugdb.calculatePrices(mapOfPrices, UsersDB.getUser(userIdToPay));
    }

    /**
     * If reservation object needs ACCEPTATION and logged user is not accepter (or none is logged), set reservation accepted to null (waiting for acceptation) AND return false.
     *
     * ELSE set reservation accepted to true and return true.
     *
     * @param reservation
     * @param request
     * @return
     */
    public static boolean acceptation(ReservationEntity reservation, HttpServletRequest request) {
        reservation.setAccepted(Boolean.TRUE);
        ReservationObjectEntity reservationObject = reservation.getReservationObjectForReservation();
        if(Tools.isTrue(reservationObject.getMustAccepted()) && reservationObject.getEmailAccepter() != null) {
            Identity loggedUser = UsersDB.getCurrentUser(request);
            if(loggedUser == null || loggedUser.getEmail().equals(reservationObject.getEmailAccepter()) == false) {
                //Set to null, it means waiting for acceptation
                reservation.setAccepted(null);
                return false;
            }
        }
        return true;
    }

    /************* MVC METHODS ***************************/

    /**
     * Get list of reservation objects for select (as options)/
     * This reservation objects are filtered by domainId and reservationForAllDay = false.
     * @return
     */
    public static List<LabelValueInteger> getReservationObjectHoursSelectList() {
        ReservationObjectRepository reservationObjectRepository = Tools.getSpringBean("reservationObjectRepository", ReservationObjectRepository.class);
        List<ReservationObjectEntity> reservationObjects = reservationObjectRepository.findAllByDomainIdAndReservationForAllDayFalse(CloudToolsForCore.getDomainId());
        return prepareSelectList(reservationObjects);
    }

    public static List<LabelValueInteger> getReservationObjectDaysSelectList() {
        ReservationObjectRepository reservationObjectRepository = Tools.getSpringBean("reservationObjectRepository", ReservationObjectRepository.class);
        List<ReservationObjectEntity> reservationObjects = reservationObjectRepository.findAllByDomainIdAndReservationForAllDayTrue(CloudToolsForCore.getDomainId());
        return prepareSelectList(reservationObjects);
    }

    private static List<LabelValueInteger> prepareSelectList(List<ReservationObjectEntity> reservationObjects) {
        return prepareSelectList(reservationObjects, false, null);
    }

    private static List<LabelValueInteger> prepareSelectList(List<ReservationObjectEntity> reservationObjects, boolean addDefaultOption, String defaultLabelKey) {
        List<LabelValueInteger> reservationOptions = new ArrayList<>();

        if(addDefaultOption) {
            Prop prop = Prop.getInstance();
            String defaultLabel = "DEFAULT";
            if(Tools.isNotEmpty(defaultLabelKey)) defaultLabel = prop.getText(defaultLabelKey);
            reservationOptions.add(new LabelValueInteger(defaultLabel, -1));
        }

        for(ReservationObjectEntity ro : reservationObjects) {
            reservationOptions.add(new LabelValueInteger(ro.getName(), ro.getId().intValue()));
        }

        return reservationOptions;
    }

    /**
     * If this day have special time range (ReservationObjectTimesEntity) return it, otherwise return default time range (reservationObject).
     * On top of that, remove Minutes/Seconds/MiliSeconds from date values.
      *
      * @param dateToCheck - date to check (if there is special time range for this day of week)
      * @param reservationObject
      * @return
      */
    public static Long[] getReservationTimeRange(Date dateToCheck, ReservationObjectEntity reservationObject) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(dateToCheck);
        int dayOfweek = calendar.get(Calendar.DAY_OF_WEEK);
        //Format day of week to our standart where Monday = 1, Tuesday = 2 ... Sunday = 7
        dayOfweek = (dayOfweek - 1) == 0 ? 7 : (dayOfweek - 1);

        ReservationObjectTimesEntity rote = null;
        for(ReservationObjectTimesEntity entity : reservationObject.getReservationObjectTimeEntities()) {
            if(entity.getDay() == dayOfweek) rote = entity;
        }

        if(rote != null)
            return new Long[] {
                prepareDateHourReservation(dateToCheck, rote.getTimeFrom()),
                prepareDateHourReservation(dateToCheck, rote.getTimeTo())
            };
        else
            return new Long[] {
                prepareDateHourReservation(dateToCheck, reservationObject.getReservationTimeFrom()),
                prepareDateHourReservation(dateToCheck, reservationObject.getReservationTimeTo())
            };
    }

    /**
     * Prepare reservation fo date-hour reservation style.
     * @param date
     * @param time
     * @return
     */
    private static Long prepareDateHourReservation(Date date, Date time) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);

        Calendar calTime = Calendar.getInstance();
        calTime.setTime(time);

        cal.set(Calendar.HOUR_OF_DAY, calTime.get(Calendar.HOUR_OF_DAY));
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTimeInMillis();
    }

    /**
     * Get list of hours for table (reservation table) based on from-to date values.
     * @param from
     * @param to
     * @return
     */
    public static List<String> getHoursForTable(Long from, Long to) {
        List<String> hours = new ArrayList<>();
        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(from);
        for(int i = 0; i < ((to - from) / (60 * 60 * 1000)); i++) {
            hours.add( Tools.formatTime(cal.getTimeInMillis()) );
            cal.add(Calendar.HOUR_OF_DAY, 1);
        }
        return hours;
    }

    /**
     * Used as object that is sent to FE for reservation table.
     */
    public static class ReservationTableCell {
        private String id;
        private String value;
        private String cssClass;

        public ReservationTableCell(String id, String value, String cssClass) {
            this.id = id;
            this.value = value;
            this.cssClass = cssClass;
        }

        public String getId() { return id; }
        public String getValue() { return value; }
        public String getCssClass() { return cssClass; }
    }

    /**
     * Prepare list of ReservationTableCell objects for reservation table.
     * @param roe - reservation object entity
     * @param rr - ReservationRepository
     * @param from
     * @param to
     * @param supportedRange - range in which reservation can be made
     * @return
     */
    public static List<ReservationTableCell> computeReservationUsageByHours(ReservationObjectEntity roe, ReservationRepository rr, Long from, Long to, Long[] supportedRange) {
        List<ReservationTableCell> tableCellsList = new ArrayList<>();
        int hoursDiff = (int)((to - from) / (60 * 60 * 1000));

        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(from);
        cal.set(Calendar.MINUTE, 30); //middle of hour

        int isThere = 0;
        List<ReservationEntity> reservations = rr.findAllByReservationObjectIdAndDomainIdAndDateFromLessThanEqualAndDateToGreaterThanEqualAndAcceptedTrue(roe.getId(), CloudToolsForCore.getDomainId(), new Date(to), new Date(from));
        for(int i = 0; i < hoursDiff; i++) {
            //Check if can make reservation in this time
            if(cal.getTimeInMillis() < supportedRange[0] || cal.getTimeInMillis() > supportedRange[1])  {
                tableCellsList.add(
                    new ReservationTableCell(roe.getId() + "_" + cal.get(Calendar.HOUR_OF_DAY), "", "unsupported")
                );
                cal.add(Calendar.HOUR_OF_DAY, 1);
                continue;
            }

            for(ReservationEntity re : reservations) {
                if(re.getDateFrom().getTime() <= cal.getTimeInMillis() && re.getDateTo().getTime() >= cal.getTimeInMillis()) {
                    isThere++;
                }
            }

            boolean isActive = true;
            if( (cal.getTimeInMillis() - 30*60*1000) < System.currentTimeMillis() ) isActive = false; //Minus 30 minutes, so it's start of hour
            if(isThere >= roe.getMaxReservations()) isActive = false;
            tableCellsList.add(
                new ReservationTableCell(roe.getId() + "_" + cal.get(Calendar.HOUR_OF_DAY), isThere + "/" + roe.getMaxReservations(), (isActive == false ? "full" : "free"))
            );

            isThere = 0;
            cal.add(Calendar.HOUR_OF_DAY, 1);
        }

        return tableCellsList;
    }

    /**
     * Get user id of user who should pay for reservation.
     * @param email
     * @param reservationId
     * @param rr
     * @param request
     * @return
     */
    public static int getUserToPay(String email, Long reservationId, ReservationRepository rr, HttpServletRequest request) {
        if (reservationId != null && reservationId.longValue() > 0L) {
            //Check if reservation exist -> if yes return userId of reservation
            ReservationEntity reservation = rr.findFirstByIdAndDomainId(reservationId, CloudToolsForCore.getDomainId()).orElse(null);
            if(reservation != null) return reservation.getUserId();
        }

        //Get user id -> of logged user
        int userId = Tools.getUserId(request);
        if (userId > 0) return userId;

        if(Tools.isEmail(email)) {
            //If user is NOT logged, try to find user by email
            UserDetails userToPay = UsersDB.getUserByEmail(email);
            if (userToPay != null) return userToPay.getUserId();
        }

        return -1;
    }

    /**
     * Get reservation date from string. If string is empty or do not match regex, return current date.
     * @param reservationDateString
     * @param formatString
     * @return
     */
    public static Date getReservationDate(String reservationDateString, String formatString) {
        String regex = "";
        if(FE_MONTHPICKER_FORMAT.equals(formatString)) {
            regex = REGEX_YYYY_MM;
        } else if(FE_DATEPICKER_FORMAT.equals(formatString)) {
            regex = REGEX_YYYY_MM_DD;
        }

        Date reservationDate;
        if(Tools.isEmpty(reservationDateString)) {
            reservationDate = new Date();
        } else {
            if(reservationDateString.matches(regex)) {
                reservationDate = Tools.getDateFromString(reservationDateString, formatString);
            } else {
                reservationDate = new Date();
            }
        }
        return reservationDate;
    }

    /**
     * Get dateId from date object. If addHourToId is true, add hour to dateId.
     * @param date
     * @param addHourToId
     * @return yyyy-MM-dd or yyyy-MM-ddTHH
     */
    public static String getDateId(Date date, boolean addHourToId) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return getDateId(cal, addHourToId);
    }

    /**
     * Get dateId from date object. If addHourToId is true, add hour to dateId.
     * @param cal
     * @param addHourToId
     * @return yyyy-MM-dd or yyyy-MM-ddTHH
     */
    public static String getDateId(Calendar cal, boolean addHourToId) {
        String dateId = cal.get(Calendar.YEAR) + "-";

        dateId += ( cal.get(Calendar.MONTH) + 1) < 10 ? "0" + (cal.get(Calendar.MONTH) + 1) : (cal.get(Calendar.MONTH) + 1);
        dateId += "-";

        dateId += cal.get(Calendar.DAY_OF_MONTH) < 10 ? "0" + cal.get(Calendar.DAY_OF_MONTH) : cal.get(Calendar.DAY_OF_MONTH);

        if(addHourToId) {
            dateId += "T";
            dateId += cal.get(Calendar.HOUR_OF_DAY) < 10 ? "0" + cal.get(Calendar.HOUR_OF_DAY) : cal.get(Calendar.HOUR_OF_DAY);
        }

        return dateId;
    }

    /**
     * Compute reservation usage by days aka how many reservations are exist for selected reservation object in selected date range (each day in range).
     * @param selectedReservationId
     * @param startDate
     * @param endDate
     * @param rr
     * @return Map where key is dateId and value is number of reservations in this day.
     */
    public static Map<String, Integer> computeReservationUsageByDays(Long selectedReservationId, Date startDate, Date endDate, ReservationRepository rr) {
        List<ReservationEntity> reservations = rr.findAllByReservationObjectIdAndDomainIdAndDateFromLessThanEqualAndDateToGreaterThanEqualAndAcceptedTrue(selectedReservationId, CloudToolsForCore.getDomainId(), endDate, startDate);

        Map<String, Integer> reservationCountMap = new HashMap<>();

        for(ReservationEntity re : reservations) {
            Date reservationFrom = re.getDateFrom();
            Date reservationTo = re.getDateTo();

            Calendar cal = Calendar.getInstance();
            cal.setTime(reservationFrom);

            while(cal.getTime().before(reservationTo)) {
                String dateId = ReservationService.getDateId(cal.getTime(), false);
                if(reservationCountMap.containsKey(dateId)) {
                    reservationCountMap.put(dateId, reservationCountMap.get(dateId) + 1);
                } else {
                    reservationCountMap.put(dateId, 1);
                }

                cal.add(Calendar.DAY_OF_YEAR, 1);
            }
        }

        return reservationCountMap;
    }

    public static void prepareDates(ReservationEntity entity, boolean isReservationForAllDay) {
        if(isReservationForAllDay) {
            //Reservation for whole day
            entity.setDateFrom(DefaultTimeValueConverter.combineDateWithTime(entity.getDateFrom(), entity.getEditorFields().getArrivingTime()));
            entity.setDateTo(DefaultTimeValueConverter.combineDateWithTime(entity.getDateTo(), entity.getEditorFields().getDepartureTime()));
        } else {
            entity.setDateFrom(DefaultTimeValueConverter.combineDateWithTime(entity.getDateFrom(), entity.getEditorFields().getReservationTimeFrom()));
            entity.setDateTo(DefaultTimeValueConverter.combineDateWithTime(entity.getDateTo(), entity.getEditorFields().getReservationTimeTo()));

            //Need to add 1 second, help with time compare
            entity.setDateFrom(new Date(entity.getDateFrom().getTime() + 1000));
        }
    }

    /**
     * Get arrival time for reservation. Arrival time is obtained from constant reservationAllDayStartTime.
     * @param entity
     * @return Date with time set to reservationAllDayStartTime OR null if constant has wrong format.
     */
    public static Date getArrivalTime(ReservationEntity entity) {
        String arrivalTimeStr = Constants.getString("reservationAllDayStartTime");
        if(arrivalTimeStr.matches("\\d{2}:\\d{2}")) {
            int hour = Integer.parseInt(arrivalTimeStr.split(":")[0]);
            int minute = Integer.parseInt(arrivalTimeStr.split(":")[1]);
            return DateTools.setTimePart(entity.getDateFrom(), hour, minute, 0, 0);
        } else {
            Logger.error(ReservationService.class, "Value of constant reservationAllDayStartTime has wrong format. Must be HH:MM");
            return null;
        }
    }

    /**
     * Get departure time for reservation. Departure time is obtained from constant reservationAllDayEndTime.
     * @param entity
     * @return Date with time set to reservationAllDayEndTime OR null if constant has wrong format.
     */
    public static Date getDepartureTime(ReservationEntity entity) {
        String departureTimeStr = Constants.getString("reservationAllDayEndTime");
        if(departureTimeStr.matches("\\d{2}:\\d{2}")) {
            int hour = Integer.parseInt(departureTimeStr.split(":")[0]);
            int minute = Integer.parseInt(departureTimeStr.split(":")[1]);
            return DateTools.setTimePart(entity.getDateTo(), hour, minute, 00, 0);
        } else {
            Logger.error(ReservationService.class, "Value of constant reservationAllDayEndTime has wrong format. Must be HH:MM");
            return null;
        }
    }

    /**
     * Check if reservation was changed - compare it with DB version.
     * @param entityToCheck
     * @param reservationObject
     * @param rr
     * @return
     */
    public static boolean wasReservationChanged(ReservationEntity entityToCheck, ReservationRepository rr) {
        Long id = entityToCheck.getId();
        if(id == null || id < 1) return true;

        ReservationEntity dbVersion = rr.findById(id).orElse(null);
        if(dbVersion == null) return true;

        //now compare values
        if(entityToCheck.getReservationObjectId().equals( dbVersion.getReservationObjectId()) == false) return true;
        if(entityToCheck.getDateFrom().equals( dbVersion.getDateFrom()) == false) return true;
        if(entityToCheck.getDateTo().equals( dbVersion.getDateTo()) == false) return true;

        return false;
    }
}