package org.jgroups.util;

import org.jgroups.Address;
import org.jgroups.annotations.GuardedBy;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/** Similar to AckCollector, but collects responses from cluster members, not just acks. Null is not a valid key.
 * @author Bela Ban
 */
public class ResponseCollector<T> {
    @GuardedBy("lock")
    private final Map<Address,T> responses;
    private final Lock           lock=new ReentrantLock(false);
    private final CondVar        cond=new CondVar(lock);


    /**
     *
     * @param members List of members from which we expect responses
     */
    public ResponseCollector(Collection<Address> members) {
        responses=members != null? new HashMap<>(members.size()) : new HashMap<>();
        reset(members);
    }

    public ResponseCollector(Address ... members) {
        responses=members != null? new HashMap<>(members.length) : new HashMap<>();
        reset(members);
    }

    public ResponseCollector() {
        responses=new HashMap<>();
    }

    public void add(Address member, T data) {
        if(member == null)
            return;
        lock.lock();
        try {
            if(responses.containsKey(member)) {
                responses.put(member, data);
                cond.signal(true);
            }
        }
        finally {
            lock.unlock();
        }
    }

    public void remove(Address member) {
        if(member == null)
            return;
        lock.lock();
        try {
            if(responses.remove(member) != null)
                cond.signal(true);
        }
        finally {
            lock.unlock();
        }
    }

    public void remove(List<Address> members) {
        if(members == null || members.isEmpty())
            return;
        lock.lock();
        try {
            members.forEach(responses::remove);
            cond.signal(true);
        }
        finally {
            lock.unlock();
        }
    }

    public void retainAll(List<Address> members) {
        if(members == null || members.isEmpty())
            return;
        lock.lock();
        try {
            if(responses.keySet().retainAll(members))
                cond.signal(true);
        }
        finally {
            lock.unlock();
        }
    }

    public void suspect(Address member) {
        remove(member);
    }


    public boolean hasAllResponses() {
        lock.lock();
        try {
            return responses.isEmpty() || responses.entrySet().stream().allMatch(entry -> entry.getValue() != null);
        }
        finally {
            lock.unlock();
        }
    }

    public int numberOfValidResponses() {
        int retval=0;
        lock.lock();
        try {
            for(Map.Entry<Address,T> entry: responses.entrySet()) {
                if(entry.getValue() != null)
                    retval++;
            }
            return retval;
        }
        finally {
            lock.unlock();
        }
    }

    /** Returns a list of members which didn't send a valid response */
    public List<Address> getMissing() {
        return responses.entrySet().stream().filter(entry -> entry.getValue() == null).map(Map.Entry::getKey).collect(Collectors.toList());
    }

    public List<Address> getValidResults() {
        return responses.entrySet().stream().filter(entry -> entry.getValue() != null).map(Map.Entry::getKey).collect(Collectors.toList());
    }


    public Map<Address,T> getResults() {
        return responses;
    }

    public int size() {
        lock.lock();
        try {
            return responses.size();
        }
        finally {
            lock.unlock();
        }
    }


    /**
     * Waits until all responses have been received, or until a timeout has elapsed.
     * @param timeout Number of milliseconds to wait max. This value needs to be greater than 0, or else
     * it will be adjusted to 2000
     * @return boolean True if all responses have been received within timeout ms, else false (e.g. if interrupted)
     */
    public boolean waitForAllResponses(long timeout) {
        if(timeout <= 0)
            timeout=2000L;
        return cond.waitFor(this::hasAllResponses, timeout, TimeUnit.MILLISECONDS);
    }

    public void reset() {
        reset((Collection<Address>)null);
    }

    public void reset(Collection<Address> members) {
        lock.lock();
        try {
            responses.clear();
            if(members != null) {
                for(Address mbr: members)
                    responses.put(mbr, null);
            }
            cond.signal(true);
        }
        finally {
            lock.unlock();
        }
    }

    public void reset(Address ... members) {
        lock.lock();
        try {
            responses.clear();
            if(members != null) {
                for(Address mbr: members)
                    responses.put(mbr, null);
            }
            cond.signal(true);
        }
        finally {
            lock.unlock();
        }
    }



    public String toString() {
        StringBuilder sb=new StringBuilder();
        sb.append(responses).append(", complete=").append(hasAllResponses());
        return sb.toString();
    }


}
