package at.ac.tuwien.sbc.valesriegler.xvsm;

import at.ac.tuwien.sbc.valesriegler.common.OrderId;
import at.ac.tuwien.sbc.valesriegler.common.Util;
import at.ac.tuwien.sbc.valesriegler.types.*;
import at.ac.tuwien.sbc.valesriegler.xvsm.spacehelpers.SpaceAction;
import org.mozartspaces.capi3.EntryLockedException;
import org.mozartspaces.core.MzsConstants.RequestTimeout;
import org.mozartspaces.core.MzsCoreException;
import org.mozartspaces.core.TransactionReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class WaiterXVSM extends AbstractXVSMConnector {
    private static final Logger log = LoggerFactory.getLogger(WaiterXVSM.class);
    private final int waiterId;

    public WaiterXVSM(int waiterId, int port) {
        super(port);

        this.waiterId = waiterId;

        freeTablesContainer = useContainer(Util.FREE_TABLES);
        assignTableContainer = useContainer(Util.ASSIGN_TABLE);
        takeOrderContainer = useContainer(Util.TAKE_ORDER);
        orderTakenContainer = useContainer(Util.ORDER_TAKEN);
        deliveryOrderTakenContainer = useContainer(Util.DELIVERY_ORDER_TAKEN);
        preparePizzasContainer = useContainer(Util.PREPARE_PIZZAS);
        prepareDeliveryPizzasContainer = useContainer(Util.PREPARE_DELIVERY_PIZZAS);
        orderDeliveredContainer = useContainer(Util.ORDER_COMPLETE);
        preparedPizzasContainer = useContainer(Util.DELIVER_PIZZAS);
        paymentRequestContainer = useContainer(Util.PAYMENT_REQUEST);
        paymentDoneContainer = useContainer(Util.PAYMENT_DONE);
        tableAssignedContainer = useContainer(Util.TABLE_ASSIGNED);
        pizzeriaInfoContainer = useContainer(Util.PIZZERIA_INFO);
        phoneCallsContainer = useContainer(Util.PHONE_CALLS);
        preparedDeliveryPizzasContainer = useContainer(Util.DELIVER_DELIVERY_PIZZAS);
        deliverDeliveryOrderContainer = useContainer(Util.DELIVER_DELIVERY_ORDER);
        pizzeriaGroupContainer = useContainer(Util.PIZZERIA_GROUP);
        pizzeriaTableContainer = useContainer(Util.PIZZERIA_TABLE);
        pizzeriaDeliveryContainer = useContainer(Util.PIZZERIA_DELIVERY);
    }

    public void listenForPhoneOrders() {
        getDefaultBuilder("listenForPhoneOrders").setCref(phoneCallsContainer).setLookaround(true).setSpaceAction(new SpaceAction() {
            @Override
            public void onEntriesWritten(List<? extends Serializable> entries) throws Exception {
                final List<DeliveryGroupData> phoneOrders = castEntries(entries);

                if(inNotification.get()) Collections.shuffle(phoneOrders);

                for (DeliveryGroupData phoneOrder : phoneOrders) {
                    final int id = phoneOrder.getId();
                    final DeliveryGroupData template = new DeliveryGroupData(id);
                    final String errorMsg = String.format("There was phone call with id %d. Another waiter already responded!", id);

                    final TransactionReference tx = getDefaultTransaction();
                    try {
                        // Get the  delete lock so that only one waiter can answer this particular phone call
                        takeMatchingEntity(template, phoneCallsContainer, tx, RequestTimeout.ZERO, errorMsg);

                        final DeliveryGroupData group = takeMatchingEntity(template, pizzeriaDeliveryContainer, tx, RequestTimeout.DEFAULT, errorMsg);
                        group.setWaiterIdOfOrder(WaiterXVSM.this.waiterId);
                        final Order order = group.getOrder();
                        group.setDeliveryStatus(DeliveryStatus.ORDERED);

                        updatePizzeriaOrderNumber(order, tx);

                        // send the order as a whole to the space
                        final List<DeliveryGroupData> groupsWhoHaveOrdered = Arrays.asList(group);
                        sendItemsToContainer(groupsWhoHaveOrdered,
                                deliveryOrderTakenContainer, RequestTimeout.ZERO, tx);
                        sendItemsToContainer(groupsWhoHaveOrdered,
                                pizzeriaDeliveryContainer, RequestTimeout.ZERO, tx);
                        sendItemsToContainer(order.getOrderedPizzas(),
                                prepareDeliveryPizzasContainer, RequestTimeout.ZERO, tx);
                        capi.commitTransaction(tx);

                        log.info("Waiter has taken a phone delivery call!");
                    }  catch (EntityNotFoundByTemplate e) {
                        log.debug("entitynotfound: {}", e.getMessage());
                    }  catch(EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    }catch(NullPointerException e) {
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

            }
        }).createSpaceListenerImpl();
    }

    public void listenForFreeTable() {
        getDefaultBuilder("listenForFreeTable").setCref(freeTablesContainer).setSpaceAction(new SpaceAction() {

            @Override
            public void onEntriesWritten(List<? extends Serializable> entries) throws Exception {

                List<Table> tables = castEntries(entries);

                for (Table table : tables) {

                    TransactionReference tx = getDefaultTransaction();

                    // Acquire a lock for the free table in the
                    // FreeTableContainer
                    int id = table.getId();

                    Table tableTemplate = new Table(id);
                    try {
                        Table lockedFreeTable = takeMatchingEntity(tableTemplate,
                                freeTablesContainer, tx, RequestTimeout.ZERO,
                                String.format("There was no free table found with id %d", id));
                        takeMatchingEntityIfItExists(tableTemplate, pizzeriaTableContainer, tx, RequestTimeout.DEFAULT);

                        GroupData groupTemplate = new GroupData();
                        GroupData lockedGroup = takeMatchingEntity(groupTemplate,
                                assignTableContainer, tx, RequestTimeout.DEFAULT,
                                "There is no group waiting for a table at the moment");
                        takeMatchingEntityIfItExists(lockedGroup, pizzeriaGroupContainer, tx, RequestTimeout.DEFAULT);

                        assignGroupToTable(lockedGroup, lockedFreeTable, tx);
                    }  catch(EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    } catch (Exception e) {
//						log.info(e.getMessage());
                    }
                }
            }
        }).createSpaceListenerImpl();
    }


    public void listenForNewGuests() {
        getDefaultBuilder("listenForNewGuests").setLookaround(true).setCref(assignTableContainer).setSpaceAction(new SpaceAction() {

            @Override
            public void onEntriesWritten(List<? extends Serializable> entries)
                    throws Exception {
                log.info("New guest groups have arrived");

                List<GroupData> groups = castEntries(entries);

                for (GroupData group : groups) {

                    TransactionReference tx = getDefaultTransaction();

                    // Acquire a lock for the group in the
                    // AssignTableContainer
                    String groupNotFound = String.format("Group with id %d was already assigned a table by another waiter!", group.getId());

                    try {
                        final GroupData templateGroup = new GroupData(group.getId());
                        GroupData lockedGroup = takeMatchingEntity(
                                templateGroup,
                                assignTableContainer, tx,
                                RequestTimeout.ZERO, groupNotFound);
                        takeMatchingEntityIfItExists(templateGroup, pizzeriaGroupContainer, tx, RequestTimeout.DEFAULT);
                        // Acquire a lock for one free table in the
                        // TablesContainer
                        String noFreeTable = String.format("No free table for group with id %d could be found", group.getId());
                        final Table freeTable = new Table(null);
                        freeTable.setFree(true);
                        Table lockedFreeTable = takeMatchingEntity(freeTable, freeTablesContainer, tx, RequestTimeout.DEFAULT,
                                noFreeTable);
                        takeMatchingEntityIfItExists(lockedFreeTable, pizzeriaTableContainer, tx, RequestTimeout.DEFAULT);

                        assignGroupToTable(lockedGroup, lockedFreeTable, tx);
                    }  catch(EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    } catch (Exception e) {
//						log.info(e.getMessage());
                    }
                }
            }
        }).createSpaceListenerImpl();
    }

    public void listenForPaymentRequest() {
        getDefaultBuilder("listenForPayment").setLookaround(true).setCref(paymentRequestContainer).setSpaceAction(new SpaceAction() {

            @Override
            public void onEntriesWritten(List<? extends Serializable> entries)
                    throws Exception {

                List<GroupData> groups = castEntries(entries);

                for (GroupData groupData : groups) {
                    TransactionReference tx = getDefaultTransaction();
                    GroupData entity = new GroupData(groupData.getId());
                    entity.setState(GroupState.PAY);

                    // Acquire the lock so that another waiter can't do the same
                    // thing!
                    String paymentRequestTakenByOtherWaiter = String.format(
                            "The payment request for group %d was already taken by an other waiter!",
                            groupData.getId());
                    try {
                        takeMatchingEntity(entity, paymentRequestContainer, tx, RequestTimeout.ZERO, paymentRequestTakenByOtherWaiter);
                        GroupData group = takeMatchingEntity(entity, pizzeriaGroupContainer, tx, RequestTimeout.DEFAULT, "Cannot get paying group!");
                        group.setPayingWaiter(waiterId);
                        group.setState(GroupState.GONE);

                        final Table tableTemplate = new Table(null);
                        tableTemplate.setGroupId(group.getId());
                        final Table table = takeMatchingEntity(tableTemplate, pizzeriaTableContainer, tx, RequestTimeout.DEFAULT, "Table was not found!");
                        table.setGroupId(-1);
                        final List<Table> tables = Arrays.asList(table);
                        sendItemsToContainer(tables, pizzeriaTableContainer, RequestTimeout.DEFAULT, tx);
                        sendItemsToContainer(tables, freeTablesContainer, RequestTimeout.DEFAULT, tx);

                        final List<GroupData> groupsPayed = Arrays.asList(group);
                        sendItemsToContainer(groupsPayed, paymentDoneContainer, RequestTimeout.DEFAULT, tx);
                        sendItemsToContainer(groupsPayed, pizzeriaGroupContainer, RequestTimeout.DEFAULT, tx);

                        capi.commitTransaction(tx);
                    }  catch(EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    } catch (Exception e) {
						log.info(e.getMessage());
                    }
                }
            }
        }).createSpaceListenerImpl();
    }

    public void listenForOrderRequests() {
        getDefaultBuilder("listenForOrderRequest").setLookaround(true).setCref(takeOrderContainer).setSpaceAction(new SpaceAction() {

            @Override
            public void onEntriesWritten(List<? extends Serializable> entries)
                    throws Exception {

                List<GroupData> groups = castEntries(entries);

                for (GroupData groupData : groups) {

                    TransactionReference tx = getDefaultTransaction();
                    GroupData entity = new GroupData(groupData.getId());
                    entity.setState(GroupState.SITTING);

                    try {
                        // Acquire the lock so that another waiter can't do the same thing!
                        String orderTakenByOtherWaiter = String.format(
                                "The order for group %d was already taken by an other waiter!",
                                groupData.getId());
                        takeMatchingEntity(entity, takeOrderContainer, tx, RequestTimeout.ZERO, orderTakenByOtherWaiter);
                        GroupData group = takeMatchingEntityIfItExists(entity, pizzeriaGroupContainer, tx, RequestTimeout.INFINITE);

                        group.setOrderWaiter(waiterId);
                        group.setState(GroupState.ORDERED);
                        Order order = group.getOrder();
                        order.setStatus(OrderStatus.ORDERED);

                        /*  get the id of the last order of the pizzeria and set the order accordingly and
                            update the space */
                        updatePizzeriaOrderNumber(order, tx);

                        // send the order as a whole to the space
                        final List<GroupData> groupsWhoHaveOrdered = Arrays.asList(group);
                        sendItemsToContainer(groupsWhoHaveOrdered,
                                orderTakenContainer, RequestTimeout.ZERO, tx);
                        sendItemsToContainer(groupsWhoHaveOrdered,
                                pizzeriaGroupContainer, RequestTimeout.ZERO, tx);
                        sendItemsToContainer(order.getOrderedPizzas(),
                                preparePizzasContainer, RequestTimeout.ZERO, tx);
                        capi.commitTransaction(tx);

                        log.info("Waiter has taken order from group {}",
                                group.getId());
                    }  catch(EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    } catch (Exception e) {
//						log.info(e.getMessage());
                    }
                }
            }
        }).createSpaceListenerImpl();
    }

    public void listenForPreparedPizzas() {
        /**
         * A waiter gets informed when a new pizza is complete. He takes the
         * orderId of the pizza and looks up the corresponding order from which
         * he gets the number of necessary pizzas of the order. He then tries to
         * fetch all pizzas with the corresponding orderId and compares the
         * number of those pizzas with the number of necessary pizzas. If all
         * pizzas of an order are complete he then delivers them!
         */
        getDefaultBuilder("listenForPreparedPizzas").setLookaround(true).setCref(preparedPizzasContainer).setSpaceAction(new SpaceAction() {

            @Override
            public void onEntriesWritten(List<? extends Serializable> entries)
                    throws Exception {

                List<Pizza> pizzas = castEntries(entries);

                for (Pizza pizza : pizzas) {
                    int orderId = pizza.getOrderId();
                    Order order = new Order();
                    order.setId(orderId);

                    TransactionReference tx = getDefaultTransaction();

                    try {
                        GroupData entity = new GroupData();
                        entity.setState(GroupState.ORDERED);
                        entity.setOrder(order);

                        takeMatchingEntity(entity,
                                orderTakenContainer, tx, RequestTimeout.DEFAULT,
                                "Another waiter just checks if the order is complete");
                        GroupData groupData = takeMatchingEntityIfItExists(entity,
                                pizzeriaGroupContainer, tx, RequestTimeout.INFINITE);
                        int numberOfPizzas = groupData.getOrder().getNumberOfPizzas();

                        Pizza pizzaTemplate = new Pizza();
                        pizzaTemplate.setOrderId(orderId);

                        List<Pizza> pizzasOfOrder = takeMatchingEntities(
                                pizzaTemplate, preparedPizzasContainer, tx,
                                RequestTimeout.DEFAULT,
                                "Cannot take the pizzas from the preparedPizzasContainer");

                        final List<GroupData> groupsWithCompleteOrder = Arrays.asList(groupData);
                        if (pizzasOfOrder.size() == numberOfPizzas) {
                            groupData.setServingWaiter(waiterId);
                            groupData.setState(GroupState.EATING);
                            groupData.getOrder().setStatus(OrderStatus.DELIVERED);
                            sendItemsToContainer(groupsWithCompleteOrder,
                                    orderDeliveredContainer, RequestTimeout.DEFAULT,
                                    tx);
                            sendItemsToContainer(groupsWithCompleteOrder,
                                    pizzeriaGroupContainer, RequestTimeout.DEFAULT,
                                    tx);

                            capi.commitTransaction(tx);
                        } else {
                            log.debug("Not yet all pizzas prepared! Order with id "
                                    + orderId + " has " + numberOfPizzas
                                    + " pizzas, but only " + pizzasOfOrder.size()
                                    + " pizzas are ready by now!");
                            capi.rollbackTransaction(tx);
                        }
                    } catch (NullPointerException e) {

                    } catch (Exception e) {
                        capi.rollbackTransaction(tx);
                    }
                }
            }
        }).createSpaceListenerImpl();
    }

    private void assignGroupToTable(GroupData lockedGroup,
                                    Table lockedFreeTable, TransactionReference tx)
            throws MzsCoreException {
        // The new group sits down at the table
        lockedFreeTable.setGroupId(lockedGroup.getId());

        // The new group now wants to order
        lockedGroup.setState(GroupState.SITTING);
        lockedGroup.setTable(lockedFreeTable);
        lockedGroup.setTableWaiter(waiterId);

        final List<Table> freeTables = Arrays.asList(lockedFreeTable);
        sendItemsToContainer(freeTables,
                pizzeriaTableContainer, RequestTimeout.ZERO, tx);
        sendItemsToContainer(freeTables,
                tableAssignedContainer, RequestTimeout.ZERO, tx);
        sendItemsToContainer(Arrays.asList(lockedGroup), takeOrderContainer,
                RequestTimeout.ZERO, tx);
        sendItemsToContainer(Arrays.asList(lockedGroup), pizzeriaGroupContainer,
                RequestTimeout.ZERO, tx);

        try {
            capi.commitTransaction(tx);
            log.info("Assigned table to group with groupId {}",
                    lockedGroup.getId());
        } catch (Exception e) {
            log.info("Assigning a table to group with groupId {} failed",
                    lockedGroup.getId());
            log.info(e.getMessage());
        }
    }

    private void updatePizzeriaOrderNumber(Order order, TransactionReference tx) throws MzsCoreException {
    /*  get the id of the last order of the pizzeria and set the order accordingly and
        update the space */
        final OrderId orderId = takeMatchingEntity(new OrderId(null), pizzeriaInfoContainer, tx, RequestTimeout.INFINITE, "The Order id object could not be taken from the space!");
        final int id = orderId.getId();
        final int nextId = id + 1;
        order.setId(nextId);
        final List<PizzaOrder> orderedPizzas = order.getOrderedPizzas();
        for (PizzaOrder orderedPizza : orderedPizzas) {
            orderedPizza.setOrderId(nextId);
        }
        sendItemsToContainer(Arrays.asList(new OrderId(nextId)), pizzeriaInfoContainer, RequestTimeout.DEFAULT, tx);
    }
}
