package org.springframework.prospring.ticket.web;

import java.util.*;
import java.math.*;

import javax.servlet.http.*;

import org.springframework.prospring.ticket.domain.*;
import org.springframework.prospring.ticket.service.*;
import org.springframework.validation.*;
import org.springframework.web.bind.*;
import org.springframework.web.servlet.*;
import org.springframework.web.servlet.view.*;
import org.springframework.web.servlet.mvc.*;
import org.springframework.web.util.*;

/**
 * Form controller issuing a ReservationRequest on behalf of the user.
 * The user has selected a show, a price band and a date in the previous
 * screen(s) and this form controller will create the reservation
 * request based on this. Together with the amount of seats the user will
 * be selected, this form controller completes the request.<p>
 * 
 * Kontroler formularza zgłaszającego w imieniu użytkownika żądanie 
 * rezerwacji (ReservationRequest). Użytkownik, na wcześniejszych stronach, 
 * wybrał już spektakl, grupę cenową biletów oraz datę. Po określeniu 
 * ilości miejsc, ten kontroler będzie mógł zakończyć obsługę żądania rezerwacji.
 *  
 * Dane wejściowe:
 * <ul>
 *   <li><code>performanceId</code>: identyfikator przedstawienia</li>
 *   <li><code>priceBandId</code>: identyfikator grupy cenowej</li>
 * </ul>
 * Warunki błędów:
 * <ul>
 *  <li>Brak żądanej ilości dostępnych miejsc</li>
 * </ul>
 */
public class BookSeatsController extends SimpleFormController {

    private final static int DEFAULT_MINUTES_TO_HOLD = 5;
    private final static int DEFAULT_DEFAULT_NUMBER_OF_SEATS_SELECTED = 1;
    private final static int DEFAULT_MAXIMUM_NUMBER_OF_SEATS = 12;
    private final static BigDecimal DEFAULT_BOOKING_FEE = new BigDecimal(0.0);

    // obiekt usługi BoxService używany przez ten kontroler.
    private BoxOffice boxOffice;

    // kalendarz zdarzeń używany przez ten kontroler.
    private EventsCalendar eventsCalendar;

    // ilość domyślnie rezerwowanych miejsc.
    private int defaultNumberOfSeatsSelected = DEFAULT_DEFAULT_NUMBER_OF_SEATS_SELECTED;

    // maksymalna ilość miejsc jakie użytkownik może zarezerwować.
    private int maximumNumberOfSeats = DEFAULT_MAXIMUM_NUMBER_OF_SEATS;

    // ilość minut określająca jak długo miejsca pozostaną zarezerwowane.
    private int minutesReservationWillBeValid = DEFAULT_MINUTES_TO_HOLD;

    // opłata za obsługę rezerwacji.
    // UWAGA: Tak na prawdę, ta informacja nie powinna być umieszczona w warstwie prezentacji.
    // Należy ona do zagadnień ściśle związanych z logiką biznesową i tam powinna być 
    // podana (w odpowiedniej usłudze - BoxOffice). Umieściliśmy ją tutaj, aby zapewnić 
    // zgodność z fragmentami kodów zamieszczonymi w tekście książki.
    private BigDecimal bookingFee = DEFAULT_BOOKING_FEE;

    /*
	 * @see org.springframework.web.servlet.mvc.AbstractFormController#formBackingObject(javax.servlet.http.HttpServletRequest)
	 */
    protected Object formBackingObject(HttpServletRequest req) throws Exception {

        long performanceId = RequestUtils.getRequiredLongParameter(req, "performanceId");
        Performance performance = eventsCalendar.getPerformance(performanceId);

        long priceBandId = RequestUtils.getRequiredLongParameter(req, "priceBandId");
        PriceBand priceBand = boxOffice.getPriceBand(priceBandId);

        return createReservationRequest(performance, priceBand, minutesReservationWillBeValid);
    }

    /**
     * Binds the maximum number of seats the user will be able to select
     * Kojarzy maksymalną ilość miejsc jakie użytkownik może zarezerwować
     * (klucz = <code>maximumSeats</code>) oraz domyślną ilość rezerwowanych miejsc
     * (<code>defaultSelected</code>). 
     */
    protected Map referenceData(HttpServletRequest request, Object o, Errors errors) throws Exception {
        ReservationRequest reservationRequest = (ReservationRequest)o;
        int availableSeatsCount = boxOffice.getAvailableSeatsCount(
            reservationRequest.getPerformance(), reservationRequest.getPriceBand().getSeatClass());

        int maximumSeats = Math.min(maximumNumberOfSeats, availableSeatsCount);

        int defaultSelected = Math.min(defaultNumberOfSeatsSelected, availableSeatsCount);

        Map m = new HashMap();
        m.put("maximumSeats", new Integer(maximumSeats));
        m.put("defaultSelected", new Integer(defaultSelected));
        return m;
    }

    /**
     * @see SimpleFormController#onSubmit(
     *      javax.servlet.http.HttpServletRequest,
            javax.servlet.http.HttpServletResponse,
            Object,
            org.springframework.validation.BindException)
     */
    protected ModelAndView onSubmit(
        HttpServletRequest httpRequest,
        HttpServletResponse httpResponse,
        Object o,
        BindException errors) throws Exception {

        // obiekt to żądanie rezerwacji zawierające już stosowne dane
        ReservationRequest request = (ReservationRequest)o;
        Reservation reservation = null;
        try {
            reservation = boxOffice.allocateSeats(request);
        } catch (RequestedSeatNotAvailableException e) {
            errors.reject(e.getErrorCode(), "Żądane miejsca nie są dostępne");
        } catch (NotEnoughSeatsException e) {
            errors.reject(e.getErrorCode(), "Nie można było zarezerwować wystarczającej ilości miejsc");
        }

        // jeśli pojawiły się błędy, to wyświetlamy widok formularza ze stosownym komunikatem
        if (reservation == null) {
            return showForm(httpRequest, errors, getFormView());
        }

        // umieszczamy rezerwację oraz spektakl w sesji (do wykorzystania w przyszłości).
        WebUtils.setSessionAttribute(httpRequest, "reservation", reservation);
        WebUtils.setSessionAttribute(httpRequest, "reservationRequest", request);

        // kierujemy użytkownika do innego kontrolera, który wyświetli rezerwację.
        // Ma to na celu uniemożliwienie ponownego przesłania formularza.
        return new ModelAndView(new RedirectView(getSuccessView()));
    }

    /**
     * Ustaiwa usługę BoxOffice jakiej powinien używać ten kontroler.
     * @param boxOffice Usługa BoxOffice jakiej powinien używać ten kontroler.
     */
    public void setBoxOffice(BoxOffice boxOffice) {
        this.boxOffice = boxOffice;
    }

    /**
     * Ustawia usługe kalendarza zdarzeń jakiej powinien używać ten kontroler.
     * @param eventsCalendar Usługa kalendarza zdarzeń jakiej powinien używać ten kontroler.
     */
    public void setEventsCalendar(EventsCalendar eventsCalendar) {
        this.eventsCalendar = eventsCalendar;
    }

    /**
     * Ustawia ilość minut przez które miejsca będą rezerwowane zanim zostaną zamówione.
     * @param minutesReservationWillBeValid Ilość minut przez które miejsca będą rezerwowane zanim zostaną zamówione.
     */
    public void setMinutesReservationWillBeValid(int minutesReservationWillBeValid) {
        this.minutesReservationWillBeValid = minutesReservationWillBeValid;
    }

    /**
     * Ustawia domyślną ilość rezerwowanych miejsc.
     * @param defaultNumberOfSeatsSelected Domyślna ilość rezerwowanych miejsc.
     */
    public void setDefaultNumberOfSeatsSelected(int defaultNumberOfSeatsSelected) {
        this.defaultNumberOfSeatsSelected = defaultNumberOfSeatsSelected;
    }

    /**
     * Ustawia maksymalną ilość miejsc jakie można zarezerwować w ramach jednej rezerwacji.
     * @param maximumNumberOfSeats Maksymalna ilość miejsc jakie można zarezerwować w ramach jednej rezerwacji.
     */
    public void setMaximumNumberOfSeats(int maximumNumberOfSeats) {
        this.maximumNumberOfSeats = maximumNumberOfSeats;
    }

    /**
     * Ustawia opłatę za obsługę rezerwacji.
     * @param bookingFee Opłata za obsługę rezerwacji.
     */
    public void setBookingFee(BigDecimal bookingFee) {
        this.bookingFee = bookingFee;
    }

    //====================================== Metody pomocnicze (używane do testowania) ====================================

    /**
     * Tworzy nowy obiekt ReservationRequest o podanych wartościach. Ta metoda może być
     * używana w testach do tworzenia żądań rezerwacji.
     *
     * @param performance Spektak.
     * @param priceBand Grupa cenowa.
     * @param minutesToHold Ilość minut przez które miejsca mają być rezerwowane.
     * @return Nowe żądanie rezerwacji.
     */
    protected ReservationRequest createReservationRequest(
        Performance performance,
        PriceBand priceBand,
        int minutesToHold) {

        ReservationRequest request = new ReservationRequest();
        request.setPerformance(performance);
        request.setPriceBand(priceBand);
        request.holdFor(minutesToHold);
        request.setBookingFee(bookingFee);

        return request;
    }

}
