package org.springframework.prospring.ticket.web;

import java.text.*;
import java.util.*;

import javax.servlet.http.*;

import org.apache.commons.logging.*;
import org.springframework.beans.propertyeditors.*;
import org.springframework.prospring.ticket.domain.*;
import org.springframework.prospring.ticket.service.*;
import org.springframework.prospring.ticket.service.payment.*;
import org.springframework.prospring.ticket.web.confirmation.*;
import org.springframework.validation.*;
import org.springframework.web.bind.*;
import org.springframework.web.servlet.*;
import org.springframework.web.servlet.mvc.*;
import org.springframework.web.util.*;

/**
 * Kontroler formularza "Szczegóły płatoność".
 *
 * @author Uri Boness
 */
public class PaymentFormController extends SimpleFormController {

    private final static Log logger = LogFactory.getLog(PaymentFormController.class);

    // Format zapisu daty ważności karty kredytowej.
    private final static DateFormat DATE_FORMAT = new SimpleDateFormat("MM/yy");

    // Usługa BoxOffice używana przez ten kontroler.
    private BoxOffice boxOffice;

    // nadawca jaki zostanie podany w potwierdzeniach wysyłanych pocztą elektroniczną.
    private PurchaseConfirmationSender confirmationSender;

    // nazwa widoku wyświetlanego w sytuacji gdy ktoś inny wcześniej zarezerwował wybrane miejsca.
    private String seatsGoneViewName;

    /**
     * Rejestracja edytorów właściwości dla pól formularza.
     * @see BaseCommandController#initBinder(
     *      javax.servlet.http.HttpServletRequest,
     *      org.springframework.web.bind.ServletRequestDataBinder)
     */
    protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
        binder.registerCustomEditor(Date.class, new CustomDateEditor(DATE_FORMAT, false));
        binder.registerCustomEditor(Reservation.class, new ReservationEditor(boxOffice));
    }

    /**
     * Tworzy obiekt PurchaseRequest służący jako obiekt współpracujący z formularzem.
     * @see AbstractFormController#formBackingObject(javax.servlet.http.HttpServletRequest)
     */
    protected Object formBackingObject(HttpServletRequest request) throws Exception {
        Reservation reservation = loadReservation(request);
        Performance performance = loadReservationRequest(request).getPerformance();
        return createPurchaseRequest(reservation, performance);
    }

    /**
     * Sprawdzenie czy miejsca skojarzone z tą rezerwacją wciąż są dostępne. Jeśli nie są, to użytkownik 
     * zostanie przekierowany do widoku, którego nazwą jest określona przez właściwość seatsGoneViewName.
     * redirecting to "seatsGone" view.
     */
    protected ModelAndView showForm(HttpServletRequest request, HttpServletResponse response, BindException errors) throws Exception {
        Reservation reservation = loadReservation(request);
        Performance performance = loadReservationRequest(request).getPerformance();
        if (!boxOffice.areSeatsStillAvailable(reservation.getBooking())) {
            ModelAndView mav = new ModelAndView(seatsGoneViewName);
            mav.addObject("performance", performance);
            return mav;
        }
        return super.showForm(request, response, errors);
    }

    /**
     * Wywoływanan w momencie przesłania formularza (po utworzenie obiektu współpracującego z formularzem,
     * jego zainicjalizowaniu (przez obiekt wiążący), powiązaniu i zweryfikowaniu poprawności).
     *
     * @see SimpleFormController#onSubmit(
     *      javax.servlet.http.HttpServletRequest,
     *      javax.servlet.http.HttpServletResponse,
     *      Object,
     *      org.springframework.validation.BindException)
     */
    protected ModelAndView onSubmit(
        HttpServletRequest request,
        HttpServletResponse response,
        Object command,
        BindException errors) throws Exception, RequestedSeatNotAvailableException {

        PurchaseRequest purchaseRequest = (PurchaseRequest)command;

        Reservation reservation = getAndRemoveReservationFromSession(request);

        // jeśli rezerwacja jest równa null, to będzie to oznaczać, że formularz został ponownie
        // przesłany, w takim przypadku nie będziemy chcieli ponownie przetwarzać zamówienia.
        if (reservation == null) {
            return handleResubmission(purchaseRequest);
        }

        // przetworzenie zamówienia.
        Purchase purchase;
        try {

            purchase = boxOffice.purchaseTickets(purchaseRequest);

        } catch (CreditCardValidationException ccve) {
            keepReservation(request, reservation); // umieszczenie rezerwacji w sesji.
            errors.reject(ccve.getErrorCode());
            return showForm(request, errors, getFormView());
        } catch (PaymentException pe) {
            keepReservation(request, reservation); // umieszczenie rezerwacji w sesji.
            errors.reject(pe.getErrorCode());
            return showForm(request, errors, getFormView());
        }

        // wysłanie potwierdzenia pocztą elektroniczną.
        try {
            confirmationSender.sendPurchaseConfirmation(purchase, purchaseRequest);
        } catch (PurchaseConfirmationSendingException pcse) {
            logger.error("Nie udało się wysłać wiadomości potwierdzającej zamówienie nr. " + purchase.getId(), pcse);
        }

        ModelAndView mav = new ModelAndView(getSuccessView());
        mav.addObject("useBillingAddressForDelivery", shouldUseBillingAddressForDelivery(purchase));
        mav.addObject("reservation", purchaseRequest.getReservation());
        mav.addObject("performance", purchaseRequest.getPerformance());
        mav.addObject("purchase", purchase);
        return mav;
    }

    /**
     * Obsługuje powtórne przesłanie formularza do rozliczeń finansowych.
     * @param purchaseRequest Żądanie zakupu.
     * @return Model i widok używany do wyświetlenia informacji i zakupie.
     */
    protected ModelAndView handleResubmission(PurchaseRequest purchaseRequest) {
        Purchase purchase = purchaseRequest.getReservation().getBooking().getPurchase();
        ModelAndView mav = new ModelAndView(getSuccessView());
        mav.addObject("useBillingAddressForDelivery", shouldUseBillingAddressForDelivery(purchase));
        mav.addObject("reservation", purchaseRequest.getReservation());
        mav.addObject("performance", purchaseRequest.getPerformance());
        mav.addObject("purchase", purchase);

        return mav;
    }

    /**
     * Ustawia usługę BoxOffice używaną przez ten kontroler.
     * @param boxOffice Usługa BoxOffice używana przez ten kontroler.
     */
    public void setBoxOffice(BoxOffice boxOffice) {
        this.boxOffice = boxOffice;
    }

    /**
     * Ustawia usługę wysyłającą potwirdzenia zakupy używaną przez ten kontroler.
     * @param confirmationSender Usługa wysyłająca potwirdzenia zakupy używaną przez ten kontroler.
     */
    public void setConfirmationSender(PurchaseConfirmationSender confirmationSender) {
        this.confirmationSender = confirmationSender;
    }

    /**
     * Ustawia nazwę widoku wyświetlanego gdy przydzielone miejsca zostały wcześniej zarezerwowane przez kogoś innego.
     * @param seatsGoneViewName Nazwa widoku informującego o tym, że miejsca zostały zarezerwowane.
     */
    public void setSeatsGoneViewName(String seatsGoneViewName) {
        this.seatsGoneViewName = seatsGoneViewName;
    }

    //========================================= Metody pomocnicze ================================================

    /**
     * Określa czy jako adresu wysyłki należy użyć adresu do rozliczeń finansowych.
     * @param purchase Obiekt reprezentujący zakup.
     * @return True jeśli adres do rozliczeń ma zostać użyty jako adres wysyłki, false w przeciwnym razie.
     */
    protected Boolean shouldUseBillingAddressForDelivery(Purchase purchase) {
        Address address = purchase.getDeliveryAddress();
        if (address == null) {
            return Boolean.TRUE;
        }
        return (address.getCountry() == null || address.getCountry().length() == 0)? Boolean.TRUE : Boolean.FALSE;
    }

    // wczytuje rezerwację z żądania.
    protected Reservation loadReservation(HttpServletRequest request) {
        return (Reservation)WebUtils.getRequiredSessionAttribute(request, "reservation");
    }

    // wczytuje spektakl z żądania.
    protected ReservationRequest loadReservationRequest(HttpServletRequest request) {
        return (ReservationRequest)WebUtils.getRequiredSessionAttribute(request, "reservationRequest");
    }

    // umieszcza przekazaną rezerwację w sesji.
    protected void keepReservation(HttpServletRequest request, Reservation reservation) {
        WebUtils.setSessionAttribute(request, "reservation", reservation);
    }

    // umieszcza przekazany spektakl w sesji.
    protected void keepPerformance(HttpServletRequest request, Performance performance) {
        WebUtils.setSessionAttribute(request, "performance", performance);
    }

    // tworzy nowe żądanie zakupu na podstawie podanej rezerwacji.
    protected PurchaseRequest createPurchaseRequest(Reservation reservation, Performance performance) {
        PurchaseRequest request = new PurchaseRequest();
        request.setBillingAddress(new Address());
        request.setDeliveryAddress(new Address());
        request.setCreditCardDetails(new CreditCardDetails());
        request.setReservation(reservation);
        request.setPerformance(performance);
        return request;
    }

    // pobiera i usuwa rezerwację z sesji. 
    protected Reservation getAndRemoveReservationFromSession(HttpServletRequest request) {
        Reservation reservation = (Reservation)WebUtils.getSessionAttribute(request, "reservation");
        request.getSession(true).removeAttribute("reservation");
        return reservation;
    }

    // pobiera i usuwa żądanie rezerwacji z sesji.
    protected ReservationRequest getAndRemoveReservationRequestFromSession(HttpServletRequest request) {
        ReservationRequest reservationRequest = (ReservationRequest)WebUtils.getSessionAttribute(request, "reservationRequest");
        request.getSession(true).removeAttribute("reservationRequest");
        return reservationRequest;
    }

}
