package org.springframework.prospring.ticket.service;

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

import org.apache.commons.logging.*;
import org.springframework.prospring.ticket.dao.*;
import org.springframework.prospring.ticket.domain.*;
import org.springframework.prospring.ticket.domain.support.*;
import org.springframework.prospring.ticket.service.payment.*;

/**
 * Domyślna implementacja usługi BoxOffice.
 */
public class BoxOfficeImpl implements BoxOffice {

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

    // Obiekt DAO używany przez tę usługe do pobierania informacji z bazy danych.
    private BoxOfficeDao boxOfficeDao;

    // mechanizm obsługi płatności używany do przetwarzania płatności wykonywanych przy użyciu karty kredytowej.
    private PaymentProcessor paymentProcessor;

    /**
     * Zwraca listę spektakli danego przedstawienia wraz z informacjami o dostępnych miejscach.
     * @param show Przedstawienie.
     * @return Lista spektakli podanego przedstawienia wraz z informacjami o dostępnych miejscach.
     *         Lista jest posortowana według daty spektaklu.
     */
    public PerformanceWithAvailability[] getAvailabilityForPerformances(Show show) {
        PerformanceWithAvailability[] performances = boxOfficeDao.getAvailabilityForPerformances(show);
        Arrays.sort(performances, new Comparator() {
            public int compare(Object o1, Object o2) {
                Performance p1 = (Performance)o1;
                Performance p2 = (Performance)o2;
                return p1.getDateAndTime().compareTo(p2.getDateAndTime());
            }
        });
        return performances;
    }

    /**
     * Przydziela miejsca na spektakl. Rezerwacja wraz z odpowiednimi informacjami szczegółowymi jest 
     * tworzona na podstawie podanego żądania rezerwacji. Miejsca są rezerwowane dla danego użytkownika na 
     * pewnien okres czasu, dzięki czemu będzie on miał szansę dokonania płatności.
     *
     * @param reservationRequest Żądanie rezerwacji.
     * @return Nowa rezerwacja.
     * @throws org.springframework.prospring.ticket.service.RequestedSeatNotAvailableException
     *          Zgłaszany gdy żądane miejsca nie są dostępne.
     * @throws org.springframework.prospring.ticket.service.NotEnoughSeatsException
     *          Zgłaszany gdy nie ma wystarczającej ilości wolnych miejsc.
     */
    public Reservation allocateSeats(ReservationRequest reservationRequest) throws RequestedSeatNotAvailableException, NotEnoughSeatsException {
        Performance performance = reservationRequest.getPerformance();
        SeatClass seatClass = reservationRequest.getPriceBand().getSeatClass();

        Seat[] seats = boxOfficeDao.getAvailableSeats(performance, seatClass);
        if (seats.length < reservationRequest.getNumberOfSeatsRequested()) {
            throw new NotEnoughSeatsException(seatClass.getId(), seats.length, reservationRequest.getNumberOfSeatsRequested());
        }

        Seat[] seatsRequested = selectSeatsFromAvailableSeats(seats, reservationRequest.getNumberOfSeatsRequested());

        BigDecimal price = boxOfficeDao.getCostOfSeats(performance, seatsRequested);

        // Nowe szczegóły rezerwacji
        Booking booking = createBooking(
            reservationRequest.getNumberOfSeatsRequested(),
            reservationRequest.getBookingFee().add(price),
            reservationRequest.getHoldUntil()
        );

        // Dodanie szczegółów rezerwacji i aktualizacja statusu miejsc.
        boxOfficeDao.reserveSeats(seatsRequested, performance, booking);

        // Utworzenie obiektu Reservation.
        return new Reservation(seatsRequested, booking);
    }

    /**
     * Wykonuje zamówienie biletów na podstawie podanego żądania zakupu.
     * @param purchaseRequest Żądanie zakupu.
     * @return Pomyślnie wykonane zamówienie.
     * @throws org.springframework.prospring.ticket.service.payment.PaymentException
     *          Zgłaszany jeśli podczas procesu obsługi płatności wystąpiły jakiekolwiek błędy.
     * @throws org.springframework.prospring.ticket.service.RequestedSeatNotAvailableException
     *          Zgłaszany jeśli podczas dokonywania rezerwacji, żądane miejsca nie są już dostępne.
     */
    public Purchase purchaseTickets(PurchaseRequest purchaseRequest)
        throws PaymentException, CreditCardValidationException, RequestedSeatNotAvailableException {

        // w pierwszej kolejności sprawdzamy poprawność żądania zakupu.
        paymentProcessor.validate(purchaseRequest);

        // sprawdzenie czy wszystkie miejsca są jeszcze dostępne.
        Booking booking = purchaseRequest.getReservation().getBooking();
        if (!areSeatsStillAvailable(booking)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Dokonanie rezerwacij niemożliwe, żądane miejsca nie są ju żż sostępne.; Nr rezerwacji #" +
                    booking.getId());
            }
            throw new RequestedSeatNotAvailableException("Miejsca nie są już dostępne", purchaseRequest.getPerformance());
        }

        Purchase purchase = createPurchase();
        purchase.setBillingAddress(purchaseRequest.getBillingAddress());
        Address deliveryAddress = purchaseRequest.getDeliveryAddress();
        if (deliveryAddress == null) {
            deliveryAddress = purchaseRequest.getBillingAddress();
        }

        purchase.setPaymentAuthorizationCode("");
        purchase.setDeliveryAddress(deliveryAddress);
        purchase.setEmail(purchaseRequest.getEmail());
        purchase.setPurchaseDate(new Date());
        purchase.setCustomerName(purchaseRequest.getCreditCardDetails().getNameOnCard());
        purchase.setEncryptedCardNumber(purchaseRequest.getCreditCardDetails().getEncryptedCardNumber());
        purchase.setUserCollected(purchaseRequest.isCollect());
        booking.setPurchase(purchase);
        boxOfficeDao.savePurchaseForBooking(booking);

        String code = paymentProcessor.process(purchaseRequest);
        purchase.setPaymentAuthorizationCode(code);
        boxOfficeDao.updatePurchaseAuthorizationCode(purchase);

        return purchase;
    }

    /**
     * Sprawdza czy miejsca skojarzone z daną rezerwacją są jeszcze dostępne.
     * @param booking Szczegółwe dane rezerwacji.
     * @return True miejsca są jeszcze dostępne,, false w przeciwnym przypadku.
     */
    public boolean areSeatsStillAvailable(Booking booking) {
        return boxOfficeDao.checkAvailabilityForBooking(booking);
    }

    /**
     * Zwraca wszystkie miejsca danej kategorii dostępne na podany spektak.
     * @param performance Spektakl.
     * @param seatClass   Klasa żądanych miejsc.
     * @return Wszystkie miejsca podanej kategorii, które na podany spektak są dostępne.
     */
    public Seat[] getAvailableSeats(Performance performance, SeatClass seatClass) {
        return boxOfficeDao.getAvailableSeats(performance, seatClass);
    }

    /**
     * Zwraca ilość dostępnych miejsc danej kategorii na podany spektakl.
     * @param performance Spektakl.
     * @param seatClass Kategoria miejsc.
     * @return Ilość dostępnych miejsc danej kategorii na podany spektakl.
     */
    public int getAvailableSeatsCount(Performance performance, SeatClass seatClass) {
        return boxOfficeDao.getAvailableSeatsCount(performance, seatClass);
    }

    /**
     * Zwraca grupę cenową skojarzoną o podanym identyfikatorze.
     * @param priceBandId Identyfikator grupy cenowej.
     * @return Grupa cenowa o podanym identyfikatorze.
     */
    public PriceBand getPriceBand(long priceBandId) {
        return boxOfficeDao.getPriceBand(priceBandId);
    }

    /**
     * Anuluje rezerwację.
     * @param booking Rezerwacja jaką należy anulować.
     */
    public void cancelBooking(Booking booking) {
        boxOfficeDao.removeBooking(booking);
    }

    /**
     * Zwraca szczegóły rezerwacji o podanym identyfiaktorze.
     * @param bookingId Idnetyfikator 
     * @return Szczegóły rezerwaci o podanym identyfikatorze.
     */
    public Booking getBooking(long bookingId) {
        return boxOfficeDao.getBooking(bookingId);
    }

    /**
     * Zwraca zarezerwowane miejsca.
     * @param booking Podane szczegóły rezerwacji.
     * @return Zarezerwowane miejsca.
     */
    public Seat[] getBookedSeats(Booking booking) {
        List seats = boxOfficeDao.getSeatsForBooking(booking);
        return (Seat[])seats.toArray(new Seat[seats.size()]);
    }

    /**
     * Zwraca spektakl na jaki zostały zarezerwowane miejsca.
     * @param booking Rezerwacja.
     * @return Spektakl, którego dotyczy rezerwacja.
     */
    public Performance getPerformanceOfBooking(Booking booking) {
        return boxOfficeDao.getPerformanceForBooking(booking);
    }

    //================================== Metody pobierające/ustawiające ===============================

    /**
     * Ustawia obiekt BoxOfficeDao z którego usługa będzie korzystać przy wymianie informacji z bazą danych.
     * @param boxOfficeDao Obiekt BoxOfficeDao z którego będzie korzystać ta usługa.
     */
    public void setBoxOfficeDao(BoxOfficeDao boxOfficeDao) {
        this.boxOfficeDao = boxOfficeDao;
    }

    /**
     * Ustawia mechanizm obsługi płatności przy użyciu kart z jakiego będzie korzystać usługa.
     * @param paymentProcessor Mechanizm obsługi płatności z którego będzie korzystać ta aplikac.
     */
    public void setPaymentProcessor(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    //============================== Metody pomocnicze (przydatne podczas testowania) =================================

    /**
     * Wybiera podaną ilość miejsc ze wskazanej listy wszystkich dostępnych miejsc.
     * Realnie wykorzystywana aplikacja zazwyczaj byłaby lepsza i posiadała interfejs strategii odpowiedzialny
     * za wykonywanie tej operacji oraz różne algorytmy dla różnych miejsc.
     *
     * @param availableSeats Lista dostępnych miejsc.
     * @param numberOfSeats Ilość wybieranych miejsc..
     * @return The selected seats.
     */
    protected Seat[] selectSeatsFromAvailableSeats(Seat[] availableSeats, int numberOfSeats) {
        Seat[] seatsRequested = new Seat[numberOfSeats];
        System.arraycopy(availableSeats, 0, seatsRequested, 0, numberOfSeats);
        return seatsRequested;
    }

    /**
     * Tworzy nowy obiekt szczegółów rezerwacji o podanych wartościach.
     * @param price Koszt zamówienia.
     * @param holdUntil Data wygaśnięcia ważności rezerwacji przy założeniu, że rezerwacja ta nie została rozliczona.
     * @return Nowy obiekt szczegółów rezerwacji.
     */
    protected Booking createBooking(int seatCount, BigDecimal price, Date holdUntil) {
        return new Booking(seatCount, holdUntil, price);
    }

    /**
     * Tworzy nowy obiekt zakupu.
     * @return Nowy obiekt zakupu.
     */
    protected Purchase createPurchase() {
        return new Purchase();
    }


}
