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


import at.ac.tuwien.sbc.valesriegler.common.Util;
import at.ac.tuwien.sbc.valesriegler.types.DeliveryGroupData;
import at.ac.tuwien.sbc.valesriegler.types.DeliveryStatus;
import at.ac.tuwien.sbc.valesriegler.types.PizzaOrder;
import at.ac.tuwien.sbc.valesriegler.xvsm.loadbalancer.PizzeriaState;
import at.ac.tuwien.sbc.valesriegler.xvsm.loadbalancer.PizzeriaStatus;
import at.ac.tuwien.sbc.valesriegler.xvsm.spacehelpers.SpaceAction;
import org.mozartspaces.capi3.AnyCoordinator;
import org.mozartspaces.core.ContainerReference;
import org.mozartspaces.core.MzsConstants;
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.*;

public class LoadBalancerXVSM extends AbstractXVSMConnector {
    private static final Logger log = LoggerFactory.getLogger(LoadBalancerXVSM.class);
    private static final int FREQUENCY = 1500;

    private int loadBalancerId;
    private Set<Integer> pizzeriaIdentifiers;
    private Map<Integer, PizzeriaState> pizzeriaStates = Collections.synchronizedMap(new HashMap<Integer, PizzeriaState>());

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

        this.loadBalancerId = id;
        this.groupAgentInfoContainer = useContainerOfSpaceWithPort(Util.GROUP_AGENT_INFO, Util.GROUP_AGENT_PORT);
    }

    /**
     * The Load balancer listens for new pizzerias on the Group Agent Info container
     */
    public void listenForPizzerias() {
        getDefaultBuilder("listenForPizzerias").setLookaround(true).setCref(groupAgentInfoContainer).setSpaceAction(new SpaceAction() {
            @Override
            public void onEntriesWritten(List<? extends Serializable> entries) throws Exception {

                final List<PizzeriaRegistration> pizzeriaRegistrations = castEntries(entries);
                for (PizzeriaRegistration registration : pizzeriaRegistrations) {
                    final int pizzeriaId = registration.pizzeriaSpacePort;
                    if (pizzeriaStates.get(pizzeriaId) == null) {
                        final PizzeriaState pizzeriaState = new PizzeriaState(pizzeriaId);
                        pizzeriaStates.put(pizzeriaId, pizzeriaState);
                        listenToPizzeria(pizzeriaId);
                    }
                }

            }
        }).createSpaceListenerImpl();
    }

    /**
     * Whenever a new pizzeria emerges a timer is started which periodically checks the delivery load
     * of the pizzeria and contemplates moving deliveries.
     */
    private void listenToPizzeria(int pizzeriaId) {
        log.warn("Start listening to pizzeria {}", pizzeriaId);

        Timer timer = new Timer();

        timer.schedule(new LoadCheck(useContainerOfSpaceWithPort(Util.DELIVERY_ORDER_TAKEN, pizzeriaId), pizzeriaId), 100, FREQUENCY);

    }

    private class LoadCheck extends TimerTask {
        public static final int DELIVERIES_NUMBER_THRESHOLD = 20;
        public static final double DELIVERY_FACTOR_THRESHOLD = 0.2;
        public static final int INTERVAL_FACTOR = 2;

        private final ContainerReference container;
        private final int pizzeriaId;

        public LoadCheck(ContainerReference containerReference, int pizzeriaId) {
            this.container = containerReference;
            this.pizzeriaId = pizzeriaId;
        }

        @Override
        public void run() {
            log.info("Start running Check for pizzeriaId {}", pizzeriaId);
            synchronized (pizzeriaStates) {
                try {
                    List<DeliveryGroupData> deliveries = null;
                    final PizzeriaState pizzeriaState = pizzeriaStates.get(pizzeriaId);
                    try {
                        deliveries = castEntries(capi.read(container, AnyCoordinator.newSelector(MzsConstants.Selecting.COUNT_MAX), MzsConstants.RequestTimeout.DEFAULT, null));
                    } catch (Exception e) {
                        log.warn("Exception while reading deliveries from Pizzeria {}", pizzeriaId);
                        e.printStackTrace();
                        pizzeriaState.setStatus(PizzeriaStatus.OFFLINE);
                        return;
                    }
                    pizzeriaState.setStatus(PizzeriaStatus.ONLINE);

                    final long lastUpdatedTimestamp = pizzeriaState.getLastUpdateTime();
                    final long now = new Date().getTime();
                    final long interval = now - lastUpdatedTimestamp;
                    final int currentNumberDeliveries = deliveries.size();

                    // there has to be a stored value for the pizzeria and it should be current enough
                    if (lastUpdatedTimestamp != 0 && interval < FREQUENCY * INTERVAL_FACTOR && currentNumberDeliveries > DELIVERIES_NUMBER_THRESHOLD) {
                        log.info("Pizzeria is handled...");
                        final int id = getPizzeriaWithLessWaitingDeliveries(currentNumberDeliveries);
                        if (id != 0) {
                            final PizzeriaState otherPizzeria = pizzeriaStates.get(id);
                            final int numberDeliveriesOtherPizzeria = otherPizzeria.getNumberDeliveries();

                            log.info("This pizzeria has {} deliveries", currentNumberDeliveries);
                            log.info("Other pizzeria {} has {} deliveries", id, numberDeliveriesOtherPizzeria);

                            final int deliveryDifference = currentNumberDeliveries - (numberDeliveriesOtherPizzeria + currentNumberDeliveries) / INTERVAL_FACTOR;

                            moveDeliveries(deliveryDifference, deliveries, pizzeriaId, id);

                            log.info(String.format("Move %d deliveries from pizzeria %d to pizzeria %d", deliveryDifference, pizzeriaId, id));
                        } else {
                            log.info("No pizzeria with less deliveries found!");
                        }
                    }

                    pizzeriaState.setNumberDeliveries(currentNumberDeliveries);
                    pizzeriaState.setLastUpdateTime(now);


                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        private void moveDeliveries(int deliveryDifference, List<DeliveryGroupData> deliveries, int pizzeriaId, int otherPizzeriaId) {
            int movedDelivieries = 0;
            final ContainerReference sourceOrderTakenContainer = useContainerOfSpaceWithPort(Util.DELIVERY_ORDER_TAKEN, pizzeriaId);
            final ContainerReference sourceDeliveryGroupContainer = useContainerOfSpaceWithPort(Util.PIZZERIA_DELIVERY, pizzeriaId);
            final ContainerReference sourcePizzaContainer = useContainerOfSpaceWithPort(Util.PREPARE_DELIVERY_PIZZAS, pizzeriaId);
            final ContainerReference targetPhoneCallsContainer = useContainerOfSpaceWithPort(Util.PHONE_CALLS, otherPizzeriaId);
            final ContainerReference targetDeliveryGroupContainer = useContainerOfSpaceWithPort(Util.PIZZERIA_DELIVERY, otherPizzeriaId);

            /**
             * Take deliveryDifference times a delivery group from the source containers and put them into
             * the containers of the target pizzeria space
             */
            for (DeliveryGroupData delivery : deliveries) {
                if (movedDelivieries >= deliveryDifference) return;

                try {

                    final TransactionReference tx = capi.createTransaction(
                            Util.SPACE_TRANSACTION_TIMEOUT,
                            URI.create(String.format(Util.SERVER_ADDR, pizzeriaId)));
                    final String errorMsg = "Cannot move the delivery as I can't take it!";
                    DeliveryGroupData group = null;
                    try {
                        // Take any delivery from the source OrderTakenContainer
                        DeliveryGroupData template = new DeliveryGroupData();
                        template.setDeliveryStatus(null);
                        group = takeMatchingEntity(template, sourceOrderTakenContainer, tx, MzsConstants.RequestTimeout.DEFAULT, errorMsg);
                    } catch (Exception e) {
                        log.warn(e.getMessage());
                        continue;
                    }
                    try {
                        // Take the delivery with the right id from the source DeliveryGroupContainer
                        final DeliveryGroupData template = new DeliveryGroupData();
                        template.setDeliveryStatus(null);
                        template.setId(group.getId());
                        List<DeliveryGroupData> groups = takeMatchingEntities(template, sourceDeliveryGroupContainer, tx, MzsConstants.RequestTimeout.DEFAULT, "Cannot take from sourceDeliveryGroupContainer");
                        group = groups.get(0);
                    } catch (Exception e) {
                        log.warn(e.getMessage());
                        continue;
                    }

                    try {
                        // Take the pizzas from the container
                        final PizzaOrder pizza = new PizzaOrder();
                        pizza.setOrderId(group.getOrder().getId());
                        takeMatchingEntities(pizza, sourcePizzaContainer, tx, MzsConstants.RequestTimeout.DEFAULT, "Cannot take from sourcePizzaContainer");
                    } catch (MzsCoreException e) {
                        log.warn(e.getMessage());
                        continue;
                    }

                    group.setOriginalPizzeriaId(String.valueOf(pizzeriaId));
                    group.setPizzeriaId(String.valueOf(otherPizzeriaId));
                    group.setWaiterIdOfOrder(null);
                    group.setLoadBalancerId(loadBalancerId);
                    group.setDeliveryStatus(DeliveryStatus.MOVED);

                    // Inform the source pizzeria that the delivery was moved
                    sendItemsToContainer(Arrays.asList(group), sourceDeliveryGroupContainer, MzsConstants.RequestTimeout.DEFAULT, tx);
                    capi.commitTransaction(tx);

                    final TransactionReference otherTx = capi.createTransaction(
                            Util.SPACE_TRANSACTION_TIMEOUT,
                            URI.create(String.format(Util.SERVER_ADDR, otherPizzeriaId)));

                    group.setDeliveryStatus(DeliveryStatus.START);

                    // Send the deliveries to the other pizzeria
                    sendItemsToContainer(Arrays.asList(group), targetPhoneCallsContainer, MzsConstants.RequestTimeout.DEFAULT, otherTx);
                    sendItemsToContainer(Arrays.asList(group), targetDeliveryGroupContainer, MzsConstants.RequestTimeout.DEFAULT, otherTx);

                    movedDelivieries++;

                    capi.commitTransaction(otherTx);


                } catch (NullPointerException e) {
                    // strange npe from space
                } catch (Exception e) {
                    log.warn("Exception in moveDeliveries: {}", e.getMessage());
                    e.printStackTrace();
                }

            }
        }

        /**
         * Returns the port of a pizzeria for which it is true: (numberOfDeliveries+currentNumberDeliveries* DELIVERY_FACTOR_THRESHOLD)<currentNumberDeliveries.
         * If there is no such pizzeria available the method return 0.
         */
        private int getPizzeriaWithLessWaitingDeliveries(int currentNumberDeliveries) {
            final Collection<PizzeriaState> states = pizzeriaStates.values();
            final long now = new Date().getTime();
            for (PizzeriaState state : states) {
                final long lastUpdateTime = state.getLastUpdateTime();
                final long interval = now - lastUpdateTime;
                final boolean differenceDeliveries = (state.getNumberDeliveries() + currentNumberDeliveries * DELIVERY_FACTOR_THRESHOLD) < currentNumberDeliveries;
                if (differenceDeliveries && state.getStatus() != PizzeriaStatus.OFFLINE && lastUpdateTime != 0 && interval < FREQUENCY * INTERVAL_FACTOR) {
                    return state.getId();
                }
            }

            return 0;
        }
    }
}
