/*
 * JBoss, Home of Professional Open Source
 * Copyright 2007, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags.
 * See the copyright.txt in the distribution for a full listing
 * of individual contributors.
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License, v. 2.0.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * v. 2.0 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 *
 * (C) 2007,
 * @author JBoss Inc.
 */
package com.jboss.ba.demo.service;

import org.apache.log4j.Logger;
import org.jboss.txbridge.ba.annotation.*;
import org.jboss.txbridge.ba.compensation.CompensationManager;

import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.jws.WebMethod;
import javax.jws.WebService;
import javax.jws.HandlerChain;
import javax.jws.soap.SOAPBinding;
import javax.ejb.Stateless;
import javax.ejb.Remote;

import java.util.*;

import com.jboss.ba.demo.service.helper.HotelView;
import com.jboss.ba.demo.service.helper.CompensationView;

/**
 * This is the implementation of a stateless session bean with all the business logic used
 * by the demo application. Some methods have not been fully implemented but they provide
 * information (through logging or simple output) about the sequence of events that occur
 * within a single Business Activity.
 *
 * @author Maciej P. Machulak (mmachulak@redhat.com)
 * @version 0.1
 */
@Stateless
@Remote(Hotel.class)
@WebService(name="Hotel")
@SOAPBinding(style = SOAPBinding.Style.RPC)
@HandlerChain(file = "jaxws-handlers-server.xml")
@BACompensation(serviceClass=HotelImpl.class,ejbInterface=Hotel.class,jndiName="bademo/HotelImpl/remote")
public class HotelImpl implements Hotel
{
    // Logger
    private static Logger log = Logger.getLogger(HotelImpl.class);

    // Random Generator
    Random randomGenerator = new Random();

    // View for debug messages
    HotelView hotelView = HotelView.getSingletonInstance();

    // View for compensation actions order
    CompensationView compensationView = CompensationView.getSingletonInstance();

    @PersistenceContext(unitName="hotelPC")
    protected EntityManager em;

    @BACompensationManagement
    private CompensationManager cm;

    public HotelImpl()
    {
    }

    @WebMethod
    public void initialiseHotel(String[][] users, Integer rooms)
    {
        log.info("initialiseHotel()");

        hotelView.newMethod("initialiseHotel()");

        // Create customers
        log.info("Creating customer entities");
        for (int i = 0; i < users.length; i++)
        {
            log.info(i);
            CustomerEntityImpl customer = new CustomerEntityImpl();
            customer.setUsername(users[i][0]);
            customer.setPassword(users[i][1]);
            em.persist(customer);
        }

        // Create rooms
        log.info("Creating room entities");
        for (int i = 0; i < rooms; i++)
        {
            log.info(i);
            RoomEntityImpl room = new RoomEntityImpl();
			room.setRoomNumber(i+1);
            room.setRoomName("Room " + (i+1));
            em.persist(room);
        }

        // Display customers
        hotelView.addMessage("Customers:");
        List<CustomerEntityImpl> customers = getCustomers();
        for (CustomerEntityImpl customer: customers)
        {
            hotelView.addMessage(customer.toString());
        }

        // Display rooms
        hotelView.addMessage("Rooms:");
        List<RoomEntityImpl> roomEntities = getRooms();
        for (RoomEntityImpl room : roomEntities)
        {
            hotelView.addMessage(room.toString());
        }
        hotelView.endMethod();
    }

    @WebMethod
    public void dropHotelData()
    {
        log.info("dropHotelData()");

        hotelView.newMethod("dropHotelData()");
        hotelView.addMessage("Deleting all data:");

        // Delete all data
        // - Customers
        em.createQuery("delete from OrderEntityImpl").executeUpdate();
        hotelView.addMessage("- orders");
        // - Rooms
        em.createQuery("delete from RoomEntityImpl").executeUpdate();
        hotelView.addMessage("- rooms");
        // - Orders
        em.createQuery("delete from CustomerEntityImpl").executeUpdate();
        hotelView.addMessage("- customers");
        hotelView.endMethod();
    }

    @WebMethod
    public void showCompensation()
    {
        hotelView.showCompensation();
    }

    @WebMethod
    public void compensationView(boolean value)
    {
        compensationView.turn(value);
    }

    @WebMethod
    public void resetView()
    {
        log.info("resetView()");
        hotelView.resetHotelView();
        compensationView.resetCompensationView();
        compensationView.resetCompensationList();
    }

    @WebMethod
    public String getHotelData()
    {
        log.info("getHotelData()");

        hotelView.newMethod("getHotelData()");
        // Display customers
        StringBuffer sb = new StringBuffer();
        List<CustomerEntityImpl> customers = getCustomers();
        for (CustomerEntityImpl customer: customers)
        {
            sb.append("(");
            sb.append(customer.toString());
            sb.append(") ");
        }

        // Display rooms
        List<RoomEntityImpl> roomEntities = getRooms();
        for (RoomEntityImpl room : roomEntities)
        {
            sb.append("(");
            sb.append(room.toString());
            sb.append(") ");
        }
        hotelView.endMethod();
        return sb.toString();
    }

    @WebMethod
    @BAService(type= BAServiceType.READONLY)
    public String getHotelInfo()
    {
        log.info("getHotelInfo()");
        hotelView.newMethod("getHotelInfo()");
        String returnValue = "Some hotel information...";
        log.info("Returning: " + returnValue);
        hotelView.addMessage("Returning: " + returnValue);
        hotelView.endMethod();
        return returnValue;
    }

    @WebMethod
    @BAService(agreement=BAAgreementType.PARTICIPANT_COMPLETION)
    @BACompensatedBy(value="decreaseHotelRating",type=BACompensationType.PARAMETERS_MATCH)
    public boolean increaseHotelRating(String username,String password, Integer points)
    {
        log.info("increaseHotelRating()");
        hotelView.newMethod("increaseHotelRating()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Points: " + points);

        // TODO: Implementation...

        hotelView.endMethod();
        compensationView.compensationMethod("decreaseHotelRating()");
        return true;
    }

    @WebMethod
    @BAService(agreement=BAAgreementType.PARTICIPANT_COMPLETION)
    @BACompensatedBy(value="increaseHotelRating",type=BACompensationType.PARAMETERS_MATCH)
    public boolean decreaseHotelRating(String username,String password, Integer points)
    {
        log.info("decreaseHotelRating()");
        hotelView.newMethod("decreaseHotelRating()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Points: " + points);

        // TODO: Implementation...

        hotelView.endMethod();
        compensationView.compensationMethod("increaseHotelRating()");
        return true;
    }

    @WebMethod
    @BAService
    @BACompensatedBy(value="cancelRoom",type=BACompensationType.CUSTOM)
    @BAResult("reservationNumber")
    public Integer bookRoom(@BAParam("username")String username,@BAParam("password")String password, Integer roomNumber)
            throws HotelCustomException, RoomAlreadyOccupiedException
    {
        log.info("bookRoom()");
        hotelView.newMethod("bookRoom()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Room no: " + roomNumber);
        hotelView.addMessage("Checking password");
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Login incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        hotelView.addMessage("Getting room");
        RoomEntityImpl room = getRoom(roomNumber);
        if (room == null)
        {
            hotelView.addMessage("Room incorrect - throwing exception");
            throw new HotelCustomException("Room incorrect");
        }
        OrderEntityImpl oe = room.getOrder();
        if (oe != null)
        {
            hotelView.addMessage("Room occupied - throwing exception");
            throw new RoomAlreadyOccupiedException("Room occupied");
        }
        hotelView.addMessage("Creating order");
        OrderEntityImpl order = new OrderEntityImpl(customer,room);
        hotelView.addMessage("Persisting order");
        em.persist(order);
        Integer reservationNumber = order.getId();
        hotelView.addMessage(order.toString());
        hotelView.addMessage("Returning: " + reservationNumber);
        log.info("Returning");
        hotelView.endMethod();
        compensationView.compensationMethod("cancelRoom()");
        return reservationNumber;
    }

    @WebMethod
    public boolean cancelRoom(@BAParam("username")String username,@BAParam("password")String password,@BAParam("reservationNumber")Integer reservationNumber)
            throws HotelCustomException
    {
        log.info("cancelRoom()");
        hotelView.newMethod("cancelRoom()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        CustomerEntityImpl customer = getCustomer(username,password);
        hotelView.addMessage("Checking password");
        if (customer == null)
        {
            hotelView.addMessage("Login incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        hotelView.addMessage("Getting reservation");
        OrderEntityImpl oe = getReservation(reservationNumber);
        if (oe == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Reservation incorrect");
        }
        hotelView.addMessage("Removing reservation");
        em.remove(oe);
        hotelView.endMethod();
        return true;
    }

    @WebMethod
    @BAService
    @BACompensatedBy(value="cancelComplimentaryPackageNumber",single=true,type=BACompensationType.PARAMETERS_MATCH)
    public Integer getComplimentaryPackageNumber(String username, String password, Integer reservationNumber)
            throws HotelCustomException
    {
        log.info("getComplimentaryPackageNumber()");
        hotelView.newMethod("getComplimentaryPackageNumber()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Reservation incorrect");
        }
        Integer freePack = order.getFreePack();
        if (freePack == null)
        {
            freePack = Math.abs(randomGenerator.nextInt());
        }
        em.merge(order);
        hotelView.endMethod();
        compensationView.compensationMethod("cancelComplimentaryPackageNumber()");
        return freePack;

    }

    @WebMethod
    public boolean cancelComplimentaryPackageNumber(String username, String password,Integer reservationNumber)
            throws HotelCustomException
    {
        log.info("cancelComplimentaryPackageNumber()");
        hotelView.newMethod("cancelComplimentaryPackageNumber()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Reservation incorrect");
        }
        order.setFreePack(null);
        em.merge(order);
        hotelView.endMethod();
        return true;
    }

    @WebMethod
    @BAService
    @BACompensatedBy(value="cancelFitnessPass",type=BACompensationType.PARAMETERS_MATCH)
    public boolean bookFitnessPass(String username, String password, Integer reservationNumber, Integer amount)
            throws HotelCustomException
    {
        log.info("bookFitnessPass()");
        hotelView.newMethod("bookFitnessPack()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        hotelView.addMessage("Amount: " + amount);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Reservation incorrect");
        }
        Integer fitnessPass = order.getFitnessPass();
        hotelView.addMessage("Current number of fitness passes: " + fitnessPass);
        fitnessPass = fitnessPass + amount;
        hotelView.addMessage("New number of fitness passes: " + fitnessPass);
        order.setFitnessPass(fitnessPass);
        em.merge(order);
        hotelView.addMessage("Returning true");
        hotelView.endMethod();
        compensationView.compensationMethod("cancelFitnessPass()");
        return true;
    }

    @WebMethod
    public boolean cancelFitnessPass(String username, String password, Integer reservationNumber, Integer amount)
            throws HotelCustomException
    {
        log.info("cancelFitnessPass()");
        hotelView.newMethod("cancelFitnessPass()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        hotelView.addMessage("Amount: " + amount);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Reservation incorrect");
        }
        Integer fitnessPass = order.getFitnessPass();
        hotelView.addMessage("Current number of fitness passes: " + fitnessPass);
        fitnessPass = fitnessPass - amount;
        hotelView.addMessage("New number of fitness passes: " + fitnessPass);
        order.setFitnessPass(fitnessPass);
        em.merge(order);
        hotelView.endMethod();
        compensationView.compensationMethod("cancelFitnessPass()");
        return true;
    }

    @WebMethod
    @BAService
    @BACompensatedBy("cancelCasinoToken")
    public Integer getCasinoToken(String username, String password, Integer amount)
            throws HotelCustomException
    {
        log.info("getCasinoToken()");
        hotelView.newMethod("getCasinoToken()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Amount: " + amount);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        Integer token = Math.abs(randomGenerator.nextInt());

        // Remember token in the DB
        // TODO: Implement 
        //

        cm.put("refund",amount/2);

        hotelView.addMessage("Returning token: " + token);
        hotelView.endMethod();
        compensationView.compensationMethod("cancelCasinoToken()");
        return token;
    }

    @WebMethod
    public boolean cancelCasinoToken(Integer token)
            throws HotelCustomException
    {
        log.info("cancelCasinoToken()");
        hotelView.newMethod("cancelCasinoToken()");
        hotelView.addMessage("Token: " + token);

        // Check if the token is in the DB
        // TODO: Implement
        // ... if not - throw an exception
        // ... if yes - invalidate it

        Integer refund = (Integer) cm.get("refund");
        if (refund != null)
        {
            hotelView.addMessage("Refund given: " + refund);
        }

        hotelView.endMethod();
        return true;
    }

    @WebMethod
    @BAService
    @BACompensatedBy(value="cancelMeal",type=BACompensationType.CUSTOM)
    public MealOrder orderMeal(@BAParam("user")String username,@BAParam("password")String password,@BAParam("reservation")Integer reservationNumber)
            throws HotelCustomException
    {
        log.info("orderMeal()");
        hotelView.newMethod("orderMeal()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Login incorrect - throwing exception");
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            hotelView.addMessage("Reservation incorrect - throwing exception");
            throw new HotelCustomException("Reservation incorrect");
        }

        Integer orderNumber = Math.abs(randomGenerator.nextInt());
        hotelView.addMessage("Meal order no: " + orderNumber);
        MealOrder mealOrder = new MealOrder();
        mealOrder.setMealId(orderNumber);
        mealOrder.setName("Some name");

        // BA-related code - BEGIN
        cm.put("order",orderNumber);
        // BA-related code - END

        order.addMealOrder(mealOrder);
        em.merge(order);

        hotelView.addMessage("Returning complex object: " + mealOrder);
        hotelView.endMethod();
        compensationView.compensationMethod("cancelMeal()");
        return mealOrder;
    }

    @WebMethod
    public boolean cancelMeal(@BAParam("user")String username,@BAParam("password")String password,@BAParam("reservation")Integer reservationNumber,@BAParam("order")Integer orderNumber)
            throws HotelCustomException
    {
        log.info("cancelMeal()");
        hotelView.newMethod("cancelMeal()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Order no: " + orderNumber);

        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            hotelView.addMessage("Login incorrect");
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            hotelView.addMessage("Reservation incorrect");
            throw new HotelCustomException("Reservation incorrect");
        }
        if (order.removeMealOrder(orderNumber))
        {
            hotelView.addMessage("Order removed successfully: " + orderNumber);
            return true;
        }
        hotelView.addMessage("Order " + orderNumber + " could not be removed.");
        hotelView.endMethod();
        return false;
    }

    @WebMethod
    @BAService
    @BACompensatedBy(value="cancelExtraMiniBar",type=BACompensationType.PARAMETERS_MATCH)
    public boolean orderExtraMiniBar(String username, String password, Integer reservationNumber)
            throws HotelCustomException
    {
        log.info("orderExtraMiniBar()");
        hotelView.newMethod("orderExtraMiniBar()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            throw new HotelCustomException("Reservation incorrect");
        }
        order.setExtraMiniBar(true);
        em.merge(order);
        hotelView.endMethod();
        compensationView.compensationMethod("cancelExtraMiniBar()");
        return true;

    }

    @WebMethod
    public boolean cancelExtraMiniBar(String username, String password, Integer reservationNumber)
            throws HotelCustomException
    {
        log.info("cancelExtraMiniBar()");
        hotelView.newMethod("cancelExtraMiniBar()");
        hotelView.addMessage("Username: " + username);
        hotelView.addMessage("Password: " + password);
        hotelView.addMessage("Reservation no: " + reservationNumber);
        CustomerEntityImpl customer = getCustomer(username,password);
        if (customer == null)
        {
            throw new HotelCustomException("Login incorrect");
        }
        OrderEntityImpl order = getReservation(reservationNumber);
        if (order == null)
        {
            throw new HotelCustomException("Reservation incorrect");
        }
        order.setExtraMiniBar(false);
        em.merge(order);
        hotelView.endMethod();
        return true;
    }


    private List<CustomerEntityImpl> getCustomers()
    {
        return em.createQuery("select C from CustomerEntityImpl C").getResultList();
    }

    private List<RoomEntityImpl> getRooms()
    {
        return em.createQuery("select C from RoomEntityImpl C").getResultList();
    }

    private CustomerEntityImpl getCustomer(String username, String password)
    {
        try
        {
            return (CustomerEntityImpl) em.createQuery("select C from CustomerEntityImpl C where C.username like :username and C.password like :password")
                    .setParameter("username", username)
                    .setParameter("password",password)
                    .getSingleResult();
        }
        catch ( NoResultException ne )
        {
            return null;
        }
    }

    private OrderEntityImpl getReservation(Integer reservationNumber)
    {
        try
        {
            return (OrderEntityImpl) em.createQuery("select C from OrderEntityImpl C where C.id like :id")
                    .setParameter("id",reservationNumber)
                    .getSingleResult();
        }
        catch ( NoResultException ne )
        {
            return null;
        }
    }

    private RoomEntityImpl getRoom(Integer roomNumber)
    {
        try
        {
            return (RoomEntityImpl) em.createQuery("select C from RoomEntityImpl C where C.roomNumber like :roomNumber")
                    .setParameter("roomNumber",roomNumber)
                    .getSingleResult();
        }
        catch ( NoResultException ne )
        {
            return null;
        }
    }

}
