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

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 at.ac.tuwien.sbc.valesriegler.xvsm.spacehelpers.SpaceListenerImpl;
import org.mozartspaces.capi3.EntryLockedException;
import org.mozartspaces.core.MzsConstants;
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.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

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

    private int cookId;
    private SpaceListenerImpl listenForDeliveryPizzas;
    private SpaceListenerImpl listenForNormalPizzas;

    public CookXVSM(int id, int port) {
        super(port);

        this.cookId = id;
        orderTakenContainer = useContainer(Util.ORDER_TAKEN);
        deliveryOrderTakenContainer = useContainer(Util.DELIVERY_ORDER_TAKEN);
        preparedPizzasContainer = useContainer(Util.DELIVER_PIZZAS);
        preparePizzasContainer = useContainer(Util.PREPARE_PIZZAS);
        prepareDeliveryPizzasContainer = useContainer(Util.PREPARE_DELIVERY_PIZZAS);
        pizzaInProgressContainer = useContainer(Util.PIZZAS_IN_PROGRESS);
        preparedDeliveryPizzasContainer = useContainer(Util.DELIVER_DELIVERY_PIZZAS);
        pizzeriaGroupContainer = useContainer(Util.PIZZERIA_GROUP);
        pizzeriaDeliveryContainer = useContainer(Util.PIZZERIA_DELIVERY);
    }

    public void listenForPizzas() {
        listenForNormalPizzas = getDefaultBuilder("listenForNormalPizzas").noNotification().setCref(preparePizzasContainer).setTimeout(15000).setSpaceAction(new SpaceAction() {

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

                log.info("Normal pizzas onEntrieswritten of cook {} with size {}", cookId, entries.size());
                List<PizzaOrder> pizzas = castEntries(entries);

                Collections.sort(pizzas);

                for (PizzaOrder pizzaOrder : pizzas) {

                    TransactionReference tx = capi.createTransaction(9000, URI.create(String.format(Util.SERVER_ADDR, port)));
                    String pizzaAlreadyCooked = String.format("Normal pizza with id %d has already been cooked by another cook", pizzaOrder.getId());

                    try {
                        if (!mayPrepareCustomerPizza(pizzaOrder, tx)) {
                            capi.rollbackTransaction(tx);
                            continue;
                        }

                        // Require the lock for preparing the pizza
                        PizzaOrder order = takeMatchingEntity(new PizzaOrder(pizzaOrder.getId()), preparePizzasContainer, tx, RequestTimeout.ZERO, pizzaAlreadyCooked);

                        // tell the space that you prepare the pizza -> without a transaction!!
                        Pizza pizzaInProgress = Pizza.createPizzaFromPizzaOrder(order, cookId, true);
                        pizzaInProgress.setStatus(PizzaOrderStatus.IN_PREPARATION);

                        notifyCustomerPizzaInProgress(order, pizzaInProgress);

                        PizzaOrder pizza = createPizza(order);

                        notifyCustomerPizzaDone(pizza, tx);
                        capi.commitTransaction(tx);
                        log.info("I have completed preparing a pizza for order {}!", pizza.getOrderId());
                    } catch (NullPointerException e) {
                        // the strange nullpointer exception from the space
                    } catch (EntityNotFoundByTemplate e) {
                        log.info("entitynotfound: {}", e.getMessage());
                    } catch (EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    } catch (Exception e) {
                        log.error("outer cook");
                        log.error(e.getMessage());
                    }

                }
            }
        }).createSpaceListenerImpl();

    }

    public void listenForDeliveryPizzas() {
        listenForDeliveryPizzas = getDefaultBuilder("listenForDeliveryPizzas").noNotification().setCref(prepareDeliveryPizzasContainer).setTimeout(15000).setSpaceAction(new SpaceAction() {

            @Override
            public void onEntriesWritten(List<? extends Serializable> entries)
                    throws Exception {
                log.info("Delivery pizzas onEntrieswritten of cook {} with size {}", cookId, entries.size());

                List<PizzaOrder> pizzas = castEntries(entries);

                Collections.sort(pizzas);

                for (PizzaOrder pizzaOrder : pizzas) {

                    TransactionReference tx = capi.createTransaction(12000, URI.create(String.format(Util.SERVER_ADDR, port)));
                    String pizzaAlreadyCooked = String.format("Delivery Pizza with id %d has already been cooked by another cook", pizzaOrder.getId());

                    try {
                        if (!mayPrepareDeliveryPizza(tx, pizzaOrder)) {
                            capi.rollbackTransaction(tx);
                            continue;
                        }

                        // Require the lock for preparing the pizza
                        PizzaOrder order = takeMatchingEntity(new PizzaOrder(pizzaOrder.getId()), prepareDeliveryPizzasContainer, tx, RequestTimeout.ZERO, pizzaAlreadyCooked);

                        // tell the space that you prepare the pizza -> without a transaction!!
                        Pizza pizzaInProgress = Pizza.createPizzaFromPizzaOrder(order, cookId, true);
                        pizzaInProgress.setStatus(PizzaOrderStatus.IN_PREPARATION);

                        notifyDeliveryPizzaInProgress(order, pizzaInProgress);

                        log.debug("before creating!");

                        PizzaOrder pizza = createPizza(order);

                        log.debug("after creating!");


                        log.debug("after prepareddeliverypizzascon");

                        notifyDeliveryPizzaDone(pizza, tx);

                        log.debug("after notifydone");

                        capi.commitTransaction(tx);
                        log.debug("I have completed preparing a delivery pizza for order {}!", pizza.getOrderId());
                    } catch (NullPointerException e) {
                        // the strange nullpointer exception from the space
                    } catch (EntityNotFoundByTemplate e) {
                        log.info("entitynotfound: {}", e.getMessage());
                    } catch (EntryLockedException e) {
                        capi.rollbackTransaction(tx);
                    } catch (Exception e) {
                        log.error("outer cook");
                        log.error(e.getMessage());
                    }

                }
            }
        }).createSpaceListenerImpl();

    }

    public void start() {
        try {
            final SpaceAction spaceActionDelivery = listenForDeliveryPizzas.getSpaceAction();
            final SpaceAction spaceActionNormalGroup = listenForNormalPizzas.getSpaceAction();
            while(true) {
                spaceActionDelivery.onEntriesWritten(listenForDeliveryPizzas.getEntries());
                Thread.sleep(1000);
                spaceActionNormalGroup.onEntriesWritten(listenForNormalPizzas.getEntries());
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            log.info(e.getMessage());
        }
    }

    /**
     * A cook may prepare a pizza if there is no current delivery order or if at least a pizza of the same order is
     * in preparation or has been done!
     */
    private boolean mayPrepareCustomerPizza(PizzaOrder pizzaOrder, TransactionReference tx) throws MzsCoreException {
        boolean mayPrepare = false;

        List<PizzaOrder> deliveryPizzas = readMatchingEntities(new PizzaOrder(), prepareDeliveryPizzasContainer, tx, RequestTimeout.DEFAULT, "mayPrepareCustomerPizza: Cannot access prepareDeliveryPizzasContainer", MzsConstants.Selecting.COUNT_MAX);
        if (!deliveryPizzas.isEmpty()) {
            Pizza pizza = new Pizza();
            pizza.setIdOfOrder(pizzaOrder.getOrderId());
            final List<Pizza> pizzasAlreadyDone = readMatchingEntities(pizza, preparedPizzasContainer, tx, RequestTimeout.DEFAULT, "mayPrepareCustomerPizza: Cannot access preparedPizzasContainer", MzsConstants.Selecting.COUNT_MAX);
            if(! pizzasAlreadyDone.isEmpty()) mayPrepare = true;
            else {
                final List<Pizza> pizzasOfOrderInProgress = readMatchingEntities(pizza, pizzaInProgressContainer, tx, RequestTimeout.DEFAULT, "mayPrepareCustomerPizza: Cannot access pizzaInProgressContainer", MzsConstants.Selecting.COUNT_MAX);
                if (! pizzasOfOrderInProgress.isEmpty()) mayPrepare = true;
                else mayPrepare = false;
            }
        }
        else mayPrepare = true;
        log.info("Cook may prepare Customer pizza: {} for order {}", mayPrepare, pizzaOrder.getOrderId());
        return mayPrepare;
    }


    /**
     * A cook may prepare a delivery pizza if no customer group order is open
     */
    private boolean mayPrepareDeliveryPizza(TransactionReference tx, PizzaOrder pizzaOrder) throws MzsCoreException {
        boolean mayPrepare = false;

        Pizza pizza = new Pizza();
        pizza.setDeliveryPizza(false);
        final List<Pizza> preparedNonDeliveryPizzas  = readMatchingEntities(pizza, preparedPizzasContainer, tx, RequestTimeout.DEFAULT, "mayPrepareDeliveryPizza: cannot access preparedPizzasContainer", MzsConstants.Selecting.COUNT_MAX);
        if(! preparedNonDeliveryPizzas.isEmpty()) {
            log.info("PreparedNonDeliveryPizzas NOT empty!");
           mayPrepare = false;
        }
        else {
            final List<Pizza> nonDeliveryPizzasInProgress = readMatchingEntities(pizza, pizzaInProgressContainer, tx, RequestTimeout.DEFAULT, "mayPrepareDeliveryPizza: cannot access pizzaInProgressContainer", MzsConstants.Selecting.COUNT_MAX);
            if (! nonDeliveryPizzasInProgress.isEmpty()) mayPrepare = false;
            else mayPrepare = true;
        }
        log.info("Cook may prepare Delivery pizza: {} for order {}", mayPrepare, pizzaOrder.getOrderId());
        return mayPrepare;
    }

    private void notifyCustomerPizzaDone(PizzaOrder pizza, TransactionReference tx) throws MzsCoreException {
        sendItemsToContainer(Arrays.asList(pizza), preparedPizzasContainer, RequestTimeout.DEFAULT, tx);
        final GroupData group = new GroupData();
        final Order groupOrder = new Order();
        groupOrder.setId(pizza.getOrderId());
        group.setOrder(groupOrder);
        group.setState(GroupState.ORDERED);
        GroupData groupData = takeMatchingEntity(group, pizzeriaGroupContainer, tx, RequestTimeout.INFINITE, "Cannot take the group from pizzeriaGroupContainer");
        final List<PizzaOrder> orderedPizzas = groupData.getOrder().getOrderedPizzas();
        for (PizzaOrder orderedPizza : orderedPizzas) {
            if (orderedPizza.getId() == pizza.getId()) {
                orderedPizza.setStatus(PizzaOrderStatus.DONE);
                orderedPizza.setCookId(pizza.getCookId());
            }
        }
        sendItemsToContainer(Arrays.asList(groupData), pizzeriaGroupContainer, RequestTimeout.DEFAULT, tx);
    }

    private void sortOrShufflePizzas(List<PizzaOrder> pizzas, boolean notification) {
        if (notification) Collections.shuffle(pizzas);
        else {
            Collections.sort(pizzas);
        }
    }

    private void notifyDeliveryPizzaDone(PizzaOrder pizza, TransactionReference tx) throws MzsCoreException {
        sendItemsToContainer(Arrays.asList(pizza), preparedDeliveryPizzasContainer, RequestTimeout.DEFAULT, tx);
        final DeliveryGroupData group = new DeliveryGroupData();
        final Order groupOrder = new Order();
        groupOrder.setId(pizza.getOrderId());
        group.setOrder(groupOrder);
        group.setDeliveryStatus(null);
        DeliveryGroupData groupData = takeMatchingEntity(group, pizzeriaDeliveryContainer, tx, RequestTimeout.INFINITE, "Cannot take the delivery order from pizzeriaDeliveryContainer");
        final List<PizzaOrder> orderedPizzas = groupData.getOrder().getOrderedPizzas();
        for (PizzaOrder orderedPizza : orderedPizzas) {
            if (orderedPizza.getId() == pizza.getId()) {
                orderedPizza.setStatus(PizzaOrderStatus.DONE);
                orderedPizza.setCookId(pizza.getCookId());
            }
        }
        sendItemsToContainer(Arrays.asList(groupData), pizzeriaDeliveryContainer, RequestTimeout.DEFAULT, tx);
    }

    private void notifyDeliveryPizzaInProgress(PizzaOrder order, Pizza pizzaInProgress) throws MzsCoreException {
        try {
            log.info("I say that I now prepare a delivery pizza for order {}", pizzaInProgress.getOrderId());
            final DeliveryGroupData groupTemplate = new DeliveryGroupData();
            final Order groupOrder = new Order();
            groupOrder.setId(order.getOrderId());
            groupTemplate.setOrder(groupOrder);
            groupTemplate.setDeliveryStatus(null);
            final TransactionReference inPreparationTx = getDefaultTransaction();
            final DeliveryGroupData groupFromSpace = takeMatchingEntity(groupTemplate, pizzeriaDeliveryContainer, inPreparationTx, RequestTimeout.DEFAULT, "Cannot take the delivery order from pizzeriaDeliveryContainer");
            final List<PizzaOrder> orderedPizzas = groupFromSpace.getOrder().getOrderedPizzas();
            for (PizzaOrder orderedPizza : orderedPizzas) {
                if (orderedPizza.getId() == pizzaInProgress.getId()) {
                    orderedPizza.setStatus(PizzaOrderStatus.IN_PREPARATION);
                    orderedPizza.setCookId(pizzaInProgress.getCookId());
                }
            }
            sendItemsToContainer(Arrays.asList(groupFromSpace), pizzeriaDeliveryContainer, RequestTimeout.DEFAULT, inPreparationTx);
            sendItemsToContainer(Arrays.asList(pizzaInProgress), pizzaInProgressContainer, RequestTimeout.DEFAULT, inPreparationTx);
            log.debug("before inprogress comit!");
            capi.commitTransaction(inPreparationTx);
        }  catch(NullPointerException e) {
        } catch (Exception e) {
            log.error("notifyDeliveryPizzaInProgress: {}", e.getMessage());
            e.printStackTrace();
        }
    }

    private void notifyCustomerPizzaInProgress(PizzaOrder order, Pizza pizzaInProgress) {
        try {
            log.info("I say that I now prepare a pizza for order {}", pizzaInProgress.getOrderId());
            final GroupData groupTemplate = new GroupData();
            final Order groupOrder = new Order();
            groupOrder.setId(order.getOrderId());
            groupTemplate.setOrder(groupOrder);
            groupTemplate.setState(null);
            final TransactionReference inPreparationTx = getDefaultTransaction();
            final GroupData groupFromSpace = takeMatchingEntity(groupTemplate, pizzeriaGroupContainer, inPreparationTx, RequestTimeout.DEFAULT, "Cannot take the order from pizzeriaGroupContainer");
            final List<PizzaOrder> orderedPizzas = groupFromSpace.getOrder().getOrderedPizzas();
            for (PizzaOrder orderedPizza : orderedPizzas) {
                if (orderedPizza.getId() == pizzaInProgress.getId()) {
                    orderedPizza.setStatus(PizzaOrderStatus.IN_PREPARATION);
                    orderedPizza.setCookId(pizzaInProgress.getCookId());
                }
            }
            sendItemsToContainer(Arrays.asList(groupFromSpace), pizzeriaGroupContainer, RequestTimeout.ZERO, inPreparationTx);
            sendItemsToContainer(Arrays.asList(pizzaInProgress), pizzaInProgressContainer, RequestTimeout.ZERO, inPreparationTx);
            capi.commitTransaction(inPreparationTx);
        }  catch(NullPointerException e) {
        } catch (Exception e) {
            log.error("notifyCustomerPizzaInProgress: {}", e.getMessage());
            e.printStackTrace();
        }
    }

    private PizzaOrder createPizza(PizzaOrder order) throws InterruptedException {
        long duration = order.getPizzaType().duration;
        if (! Util.runSimulation) {
            Thread.sleep(duration * 1000);
        }

        PizzaOrder pizza = Pizza.createPizzaFromPizzaOrder(order, cookId, false);
        pizza.setStatus(PizzaOrderStatus.DONE);
        return pizza;
    }
}
