package org.rmatil.sync.network.core;

import net.tomp2p.futures.FutureDirect;
import org.rmatil.sync.network.api.*;
import org.rmatil.sync.network.core.exception.ConnectionFailedException;
import org.rmatil.sync.network.core.exception.ObjectSendFailedException;
import org.rmatil.sync.network.core.messaging.FutureDirectListener;
import org.rmatil.sync.network.core.model.ClientDevice;
import org.rmatil.sync.network.core.model.ClientLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Handles communication between multiple clients.
 * Requires, that each client to which a request has been sent must respond with
 * the corresponding response to the configured request.
 * <p>
 * Once all clients responded, handleResult() will be invoked.
 * Note, that the concrete result computation is implemented by the extending class.
 *
 * @param <T> The result type which should be returned, once the information exchange between clients has been completed
 */
public abstract class ANetworkHandler<T> implements INetworkHandler<T>, IResponseCallback {

    private final static Logger logger = LoggerFactory.getLogger(ANetworkHandler.class);

    public static final long MAX_WAITING_TIME = 30000L;

    /**
     * The countdown latch which will be completed once all
     * connected clients have been notified
     */
    private final CountDownLatch waitForSentCountDownLatch;

    /**
     * The countdown latch which will be completed once all
     * notified clients have responded
     */
    private CountDownLatch countDownLatch;

    /**
     * The client of this device
     */
    protected IClient client;

    /**
     * A map having the client device which responded along with its response
     * representing all clients which have responded to the initial request
     */
    protected final Map<ClientDevice, FutureDirect> notifiedClients;

    /**
     * Indicates whether a request has been sent yet to any client
     */
    protected boolean hasStartedToNotify = false;

    /**
     * @param client The client of this device
     */
    public ANetworkHandler(IClient client) {
        this.client = client;
        this.notifiedClients = new HashMap<>();
        this.waitForSentCountDownLatch = new CountDownLatch(1);
    }

    @Override
    public abstract void run();

    @Override
    public void sendRequest(IRequest request)
            throws ConnectionFailedException {
        this.hasStartedToNotify = true;

        List<ClientLocation> clientLocations = request.getReceiverAddresses();

        // offer file
        for (ClientLocation entry : clientLocations) {
            if (entry.getPeerAddress().equals(this.client.getPeerAddress())) {
                logger.debug("Ignoring receiver address " + entry.getIpAddress() + ":" + entry.getPort() + " since it is the own client's address");
                continue;
            }

            logger.debug("Sending request " + request.getExchangeId() + " to client " + entry.getIpAddress() + ":" + entry.getPort() + ". Timestamp: " + System.currentTimeMillis());
            try {
                FutureDirect futureDirect = this.client.sendDirect(entry.getPeerAddress(), request);
                FutureDirectListener futureDirectListener = new FutureDirectListener();
                futureDirect.addListener(futureDirectListener);

                // await using a listener instead of the future direct directly
                futureDirectListener.await();

                if (futureDirect.isFailed()) {
                    throw new ObjectSendFailedException("Failed to sent request " + request.getExchangeId() + ". Message: " + futureDirect.failedReason());
                }

                ClientDevice clientDevice = new ClientDevice(
                        this.client.getUser().getUserName(),
                        entry.getClientDeviceId(),
                        entry.getPeerAddress()
                );

                this.notifiedClients.put(clientDevice, futureDirect);

            } catch (ObjectSendFailedException e) {
                logger.error("Failed to send request to client " + entry.getClientDeviceId() + " (" + entry.getPeerAddress().inetAddress().getHostAddress() + ":" + entry.getPeerAddress().tcpPort() + "). Message: " + e.getMessage());
            } catch (InterruptedException e) {
                logger.error("Failed to send request to client " + entry.getClientDeviceId() + " (" + entry.getPeerAddress().inetAddress().getHostAddress() + ":" + entry.getPeerAddress().tcpPort() + "). Got interrupted while waiting for completion. Message: " + e.getMessage());
            }
        }

        // init count down latch with size of all clients
        this.countDownLatch = new CountDownLatch(this.notifiedClients.size());
        this.waitForSentCountDownLatch.countDown();
    }

    @Override
    public void await()
            throws InterruptedException {
        // first wait that count down latch for sending is initialized
        this.waitForSentCountDownLatch.await(MAX_WAITING_TIME, TimeUnit.MILLISECONDS);
        this.countDownLatch.await(MAX_WAITING_TIME, TimeUnit.MILLISECONDS);

    }

    @Override
    public void await(long timeout, TimeUnit timeUnit)
            throws InterruptedException {
        // first wait that count down latch for sending is initialized
        this.waitForSentCountDownLatch.await(timeout, timeUnit);
        this.countDownLatch.await(timeout, timeUnit);

    }

    @Override
    public boolean isCompleted() {
        return null != this.countDownLatch && 0 == this.countDownLatch.getCount();
    }

    @Override
    public int getProgress() {
        if (this.hasStartedToNotify && this.notifiedClients.size() > 0) {
            return Math.round(((this.notifiedClients.size() - this.countDownLatch.getCount()) / this.notifiedClients.size()) * 100);
        }

        if (this.hasStartedToNotify) {
            return 100;
        }

        return 0;
    }

    @Override
    public void onResponse(IResponse response) {
		logger.info("Received response for exchange " + response.getExchangeId() + " of client " + response.getClientDevice().getClientDeviceId() + " (" + response.getClientDevice().getPeerAddress().inetAddress().getHostName() + ":" + response.getClientDevice().getPeerAddress().tcpPort() + ")");

		try {
            this.waitForSentCountDownLatch.await(MAX_WAITING_TIME, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            logger.error("Got interrupted while waiting that request has been sent to al clients. Message: " + e.getMessage());
        }

        this.countDownLatch.countDown();
    }


    @Override
    public abstract T getResult();
}

@@ -434,24 +434,92 @@
 ntLocation;%0A
+import org.rmatil.sync.persistence.exceptions.InputOutputException;%0A
 import org.s
@@ -4480,37 +4480,365 @@
 %22). 
-Message: %22 + e.getMessage());
+Removing this client from connected client locations. Message: %22 + e.getMessage());%0A                try %7B%0A                    this.client.getClientManager().removeClientLocation(this.client.getUser(), entry);%0A                %7D catch (InputOutputException e1) %7B%0A                    logger.error(%22Failed to remove client location %22 + entry);%0A                %7D
 %0A   
@@ -6576,18 +6576,24 @@
 onse) %7B%0A
-%09%09
+        
 logger.i
@@ -6878,10 +6878,16 @@
 );%0A%0A
-%09%09
+        
 try 

Remove client location from connected locations if request timed out