package org.jgroups.protocols.pbcast;


import org.jgroups.Address;
import org.jgroups.Event;
import org.jgroups.Message;
import org.jgroups.View;
import org.jgroups.protocols.PingData;
import org.jgroups.util.Digest;
import org.jgroups.util.Promise;
import org.jgroups.util.Responses;
import org.jgroups.util.Util;

import java.util.*;


/**
 * Client part of GMS. Whenever a new member wants to join a group, it starts in the CLIENT role.
 * No multicasts to the group will be received and processed until the member has been joined and
 * turned into a SERVER (either coordinator or participant, mostly just participant). This class
 * only implements <code>Join</code> (called by clients who want to join a certain group, and
 * <code>ViewChange</code> which is called by the coordinator that was contacted by this client, to
 * tell the client what its initial membership is.
 * @author Bela Ban
 * @version $Revision: 1.78 $
 */
public class ClientGmsImpl extends GmsImpl {
    protected final Promise<JoinRsp> join_promise=new Promise<>();


    public ClientGmsImpl(GMS g) {
        super(g);
    }

    public void init() throws Exception {
        super.init();
        join_promise.reset();
    }

    public void join(Address address,boolean useFlushIfPresent) {
        joinInternal(address, false,useFlushIfPresent);
    }

    public void joinWithStateTransfer(Address local_addr, boolean useFlushIfPresent) {
        joinInternal(local_addr,true,useFlushIfPresent);
    }
    
    /**
     * Joins this process to a group. Determines the coordinator and sends a
     * unicast handleJoin() message to it. The coordinator returns a JoinRsp and
     * then broadcasts the new view, which contains a message digest and the
     * current membership (including the joiner). The joiner is then supposed to
     * install the new view and the digest and starts accepting mcast messages.
     * Previous mcast messages were discarded (this is done in PBCAST).
     * <p>
     * If successful, impl is changed to an instance of ParticipantGmsImpl.
     * Otherwise, we continue trying to send join() messages to the coordinator,
     * until we succeed (or there is no member in the group. In this case, we
     * create our own singleton group).
     *
     * @param mbr Our own address (assigned through SET_LOCAL_ADDRESS)
     */
    protected void joinInternal(Address mbr, boolean joinWithStateTransfer, boolean useFlushIfPresent) {
        long          join_attempts=0;
        leaving=false;
        join_promise.reset();
        while(!leaving) {
            if(installViewIfValidJoinRsp(join_promise, false))
                return;

            long start=System.currentTimeMillis();
            Responses responses=(Responses)gms.getDownProtocol().down(Event.FIND_INITIAL_MBRS_EVT);

            // Sept 2008 (bela): break if we got a belated JoinRsp (https://jira.jboss.org/jira/browse/JGRP-687)
            if(installViewIfValidJoinRsp(join_promise, false))
                return;

            responses.waitFor(gms.join_timeout);
            responses.done();
            long diff=System.currentTimeMillis() - start;
            if(responses.isEmpty()) {
                log.trace("%s: no members discovered after %d ms: creating cluster as first member", gms.local_addr, diff);
                becomeSingletonMember(mbr);
                return;
            }
            log.trace("%s: discovery took %d ms, members: %s", gms.local_addr, diff, responses);

            List<Address> coords=getCoords(responses);

            // We didn't get any coord responses; all responses were clients. If I'm the first of the sorted clients
            // I'll become coordinator. The others will wait and then retry the discovery and join process
            if(coords == null) { // e.g. because we have all clients only
                if(firstOfAllClients(mbr, responses))
                    return;
                continue;
            }

            if(coords.size() > 1) {
                log.debug("%s: found multiple coords: %s", gms.local_addr, coords);
                Collections.shuffle(coords); // so the code below doesn't always pick the same coord
            }

            join_attempts++;
            for(Address coord: coords) {
                log.debug("%s: sending JOIN(%s) to %s", gms.local_addr, mbr, coord);
                sendJoinMessage(coord, mbr, joinWithStateTransfer, useFlushIfPresent);
                if(installViewIfValidJoinRsp(join_promise, true))
                    return;
                log.warn("%s: JOIN(%s) sent to %s timed out (after %d ms), on try %d",
                         gms.local_addr, mbr, coord, gms.join_timeout, join_attempts);
            }

            if(gms.max_join_attempts != 0 && join_attempts >= gms.max_join_attempts) {
                log.warn("%s: too many JOIN attempts (%d): becoming singleton", gms.local_addr, join_attempts);
                becomeSingletonMember(mbr);
                return;
            }
        }
    }




    public void leave(Address mbr) {
        leaving=true;
        wrongMethod("leave");
    }


    public void handleJoinResponse(JoinRsp join_rsp) {
        join_promise.setResult(join_rsp); // will wake up join() method
    }


    /* --------------------------- Private Methods ------------------------------------ */

    // Installs a new view and sends an ack if (1) the join rsp is not null, (2) it is valid and (3) the view
    // installation is successful. If true is returned, the JOIN process can be terminated, else it needs to
    // go through discovery and JOIN-REQ again in a next iteration
    protected boolean installViewIfValidJoinRsp(final Promise<JoinRsp> join_promise, boolean block_for_rsp) {
        boolean success=false;
        JoinRsp rsp=null;
        try {
            if(join_promise.hasResult())
                rsp=join_promise.getResult(1, true);
            else if(block_for_rsp)
                rsp=join_promise.getResult(gms.join_timeout, true);

            return success=rsp != null && isJoinResponseValid(rsp) && installView(rsp.getView(), rsp.getDigest());
        }
        finally {
            if(success)
                gms.sendViewAck(rsp.getView().getCreator());
        }
    }

    /** Handles the case where no coord responses were received. Returns true if we became the coord
     * (caller needs to terminate the join() call), or false when the caller needs to continue */
    protected boolean firstOfAllClients(final Address joiner, final Responses rsps) {
        log.trace("%s: could not determine coordinator from rsps %s", gms.local_addr, rsps);

        // so the member to become singleton member (and thus coord) is the first of all clients
        SortedSet<Address> clients=new TreeSet<>();
        clients.add(joiner); // add myself again (was removed by findInitialMembers())
        for(PingData response: rsps)
            clients.add(response.getAddress());

        log.trace("%s: nodes to choose new coord from are: %s", gms.local_addr, clients);
        Address new_coord=clients.first();
        if(new_coord.equals(joiner)) {
            log.trace("%s: I (%s) am the first of the nodes, will become coordinator", gms.local_addr, joiner);
            becomeSingletonMember(joiner);
            return true;
        }
        log.trace("%s: I (%s) am not the first of the nodes, waiting for another client to become coordinator",
                  gms.local_addr, joiner);
        Util.sleep(500);
        return false;
    }

    protected boolean isJoinResponseValid(final JoinRsp rsp) {
        if(rsp.getFailReason() != null)
            throw new SecurityException(rsp.getFailReason());

        Digest tmp_digest=rsp.getDigest();
        if(tmp_digest == null || tmp_digest.capacity() == 0) {
            log.warn("%s: digest is empty: digest=%s", gms.local_addr, rsp.getDigest());
            return false;
        }

        if(!tmp_digest.contains(gms.local_addr)) {
            log.error("%s: digest in JOIN_RSP does not contain myself; join response: %s", gms.local_addr, rsp);
            return false;
        }

        if(rsp.getView() == null) {
            log.error("%s: JoinRsp has a null view, skipping it", gms.local_addr);
            return false;
        }
        return true;
    }



    /**
     * Called by join(). Installs the view returned by calling Coord.handleJoin() and becomes coordinator.
     */
    private boolean installView(View new_view, Digest digest) {
        if(!new_view.containsMember(gms.local_addr)) {
            log.error("%s: I'm not member of %s, will not install view", gms.local_addr, new_view);
            return false;
        }
        gms.installView(new_view, digest);
        if(gms.impl == null || gms.impl instanceof ClientGmsImpl) // installView() should have set the role (impl)
            gms.becomeParticipant();
        gms.getUpProtocol().up(new Event(Event.BECOME_SERVER));
        gms.getDownProtocol().down(new Event(Event.BECOME_SERVER));
        return true;
    }


    protected static String print(List<PingData> rsps) {
        StringBuilder sb=new StringBuilder();
        for(PingData rsp: rsps)
            sb.append(rsp.getAddress() + " ");
        return sb.toString();
    }

    protected static String print(Responses rsps) {
        StringBuilder sb=new StringBuilder();
        for(PingData rsp: rsps)
            sb.append(rsp.getAddress() + " ");
        return sb.toString();
    }

    void sendJoinMessage(Address coord, Address mbr,boolean joinWithTransfer, boolean useFlushIfPresent) {
        byte type=joinWithTransfer? GMS.GmsHeader.JOIN_REQ_WITH_STATE_TRANSFER : GMS.GmsHeader.JOIN_REQ;
        GMS.GmsHeader hdr=new GMS.GmsHeader(type, mbr, useFlushIfPresent);
        Message msg=new Message(coord).setFlag(Message.Flag.OOB, Message.Flag.INTERNAL).putHeader(gms.getId(), hdr);
        gms.getDownProtocol().down(msg);
    }


    /** Returns all members whose PingData is flagged as coordinator */
    private static List<Address> getCoords(Iterable<PingData> mbrs) {
        if(mbrs == null)
            return null;

        List<Address> coords=null;
        for(PingData mbr: mbrs) {
            if(mbr.isCoord()) {
                if(coords == null)
                    coords=new ArrayList<>();
                if(!coords.contains(mbr.getAddress()))
                    coords.add(mbr.getAddress());
            }
        }
        return coords;
    }



    void becomeSingletonMember(Address mbr) {
        View new_view=View.create(mbr, 0, mbr); // create singleton view with mbr as only member

        // set the initial digest (since I'm the first member)
        Digest initial_digest=new Digest(mbr, 0, 0);
        gms.installView(new_view, initial_digest);
        gms.becomeCoordinator(); // not really necessary - installView() should do it

        gms.getUpProtocol().up(new Event(Event.BECOME_SERVER));
        gms.getDownProtocol().down(new Event(Event.BECOME_SERVER));
        log.debug("%s: created cluster (first member). My view is %s, impl is %s",
                  gms.getLocalAddress(), gms.getViewId(), gms.getImpl().getClass().getName());
    }
}
