// $Id: ClientGmsImpl.java,v 1.12.6.1 2007/04/27 08:03:51 belaban Exp $

package org.jgroups.protocols;


import org.jgroups.Address;
import org.jgroups.Event;
import org.jgroups.View;
import org.jgroups.ViewId;
import org.jgroups.blocks.GroupRequest;
import org.jgroups.blocks.MethodCall;
import org.jgroups.util.Util;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;


/**
 * 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.12.6.1 $
 */
public class ClientGmsImpl extends GmsImpl {
    final Vector initial_mbrs=new Vector(7);
    final Object view_installation_mutex=new Object();
    boolean joined=false;


    public ClientGmsImpl(GMS g) {
        gms=g;
    }


    public void init() {
        initial_mbrs.removeAllElements();
        joined=false;
    }


    /**
     * Will generate a CONNECT_OK event. Determines the coordinator and sends a unicast
     * join() message to it. If successful, we wait for a ViewChange (can time out).
     * If view change is received, 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).
     * <p>When GMS.disable_initial_coord is set to true, then we won't become coordinator on receiving an initial
     * membership of 0, but instead will retry (forever) until we get an initial membership of > 0.
     * 
     * @param mbr Our own address (assigned through SET_LOCAL_ADDRESS)
     */
    public void join(Address mbr) {
        Address coord;
        Event view_evt;

        while(!joined) {
            findInitialMembers();
            if(joined) {
                 if(log.isInfoEnabled()) log.info("joined successfully");
                return;
            }
            if(initial_mbrs.size() == 0) {
                if(gms.disable_initial_coord) {
                    if(log.isInfoEnabled()) log.info("received an initial membership of 0, but " +
                            "cannot become coordinator (disable_initial_coord=" + gms.disable_initial_coord +
                            "), will retry fetching the initial membership");
                    continue;
                }
                joined=true;
                gms.view_id=new ViewId(mbr);       // create singleton view with mbr as only member
                gms.mbrs.add(mbr);
                view_evt=new Event(Event.VIEW_CHANGE,
                                   GMS.makeView(gms.mbrs.getMembers(), gms.view_id));
                gms.passDown(view_evt);
                gms.passUp(view_evt);
                gms.becomeCoordinator();

                gms.passUp(new Event(Event.BECOME_SERVER));
                gms.passDown(new Event(Event.BECOME_SERVER));
                if(log.isInfoEnabled()) log.info("created group (first member)");
                break;
            }

            coord=determineCoord(initial_mbrs);
            if(coord == null) {
                if(log.isWarnEnabled()) log.warn("could not determine coordinator from responses " + initial_mbrs);
                continue;
            }

            synchronized(view_installation_mutex) {
                try {
                    if(log.isInfoEnabled()) log.info("sending handleJoin() to " + coord);
                    MethodCall call=new MethodCall("handleJoin", new Object[]{mbr}, new Class[]{Address.class});
                    gms.callRemoteMethod(coord, call, GroupRequest.GET_NONE, 0);
                    view_installation_mutex.wait(gms.join_timeout);  // wait for view -> handleView()
                }
                catch(Exception e) {
                    if(log.isErrorEnabled()) log.error("exception is " + e);
                    continue;
                }
            } // end synchronized

            if(joined) {
                if(log.isInfoEnabled()) log.info("joined successfully");
                return;  // --> SUCCESS
            }
            else {
                if(log.isInfoEnabled()) log.info("failed, retrying");
                Util.sleep(gms.join_retry_timeout);
            }

        }  // end while
    }


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


    public void suspect(Address mbr) {
        // wrongMethod("suspect");
    }


    public void merge(Vector other_coords) {
        wrongMethod("merge");
    }


    public boolean handleJoin(Address mbr) {
        wrongMethod("handleJoin");
        return false;
    }


    /**
     * Returns false. Clients don't handle leave() requests
     */
    public void handleLeave(Address mbr, boolean suspected) {
        wrongMethod("handleLeave");
    }


    /**
     * Install the first view in which we are a member. This is essentially a confirmation
     * of our JOIN request (see join() above).
     */
    public void handleViewChange(ViewId new_view, Vector mems) {
        if(gms.local_addr != null && mems != null && mems.contains(gms.local_addr)) {
            synchronized(view_installation_mutex) {  // wait until JOIN is sent (above)
                joined=true;
                view_installation_mutex.notifyAll();
                gms.installView(new_view, mems);
                gms.becomeParticipant();
                gms.passUp(new Event(Event.BECOME_SERVER));
                gms.passDown(new Event(Event.BECOME_SERVER));
            }
            synchronized(initial_mbrs) {    // in case findInitialMembers() is still running:
                initial_mbrs.notifyAll();   // this will unblock it
            }
        }
        else
            if(log.isWarnEnabled()) log.warn("am not member of " + mems + ", will not install view");
    }


    /**
     * Returns immediately. Clients don't handle merge() requests
     */
    public View handleMerge(ViewId other_view, Vector other_members) {
        wrongMethod("handleMerge");
        return null;
    }


    /**
     * Returns immediately. Clients don't handle suspect() requests
     */
    public void handleSuspect(Address mbr) {
        wrongMethod("handleSuspect");
    }


    public boolean handleUpEvent(Event evt) {
        Vector tmp;

        switch(evt.getType()) {

            case Event.FIND_INITIAL_MBRS_OK:
                tmp=(Vector)evt.getArg();
                synchronized(initial_mbrs) {
                    if(tmp != null && tmp.size() > 0)
                        for(int i=0; i < tmp.size(); i++)
                            initial_mbrs.addElement(tmp.elementAt(i));
                    initial_mbrs.notifyAll();
                }
                return false;  // don't pass up the stack
        }

        return true;
    }





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







    /**
     * Pings initial members. Removes self before returning vector of initial members.
     * Uses IP multicast or gossiping, depending on parameters.
     */
    void findInitialMembers() {
        PingRsp ping_rsp;

        synchronized(initial_mbrs) {
            initial_mbrs.removeAllElements();
            gms.passDown(Event.FIND_INITIAL_MBRS_EVT);
            if(initial_mbrs.size() == 0) {
                try {
                    initial_mbrs.wait();
                }
                catch(Exception e) {
                }
            }

            for(int i=0; i < initial_mbrs.size(); i++) {
                ping_rsp=(PingRsp)initial_mbrs.elementAt(i);
                if(ping_rsp.own_addr != null && gms.local_addr != null &&
                        ping_rsp.own_addr.equals(gms.local_addr)) {
                    initial_mbrs.removeElementAt(i);
                    break;
                }
            }
        }
    }


    /**
     * The coordinator is determined by a majority vote. If there are an equal number of votes for
     * more than 1 candidate, we determine the winner randomly.
     */
    Address determineCoord(Vector mbrs) {
        PingRsp mbr;
        Hashtable votes;
        int count, most_votes;
        Address winner=null, tmp;

        if(mbrs == null || mbrs.size() < 1)
            return null;

        votes=new Hashtable(5);

        // count *all* the votes (unlike the 2000 election)
        for(int i=0; i < mbrs.size(); i++) {
            mbr=(PingRsp)mbrs.elementAt(i);
            if(mbr.coord_addr != null) {
                if(!votes.containsKey(mbr.coord_addr))
                    votes.put(mbr.coord_addr, new Integer(1));
                else {
                    count=((Integer)votes.get(mbr.coord_addr)).intValue();
                    votes.put(mbr.coord_addr, new Integer(count + 1));
                }
            }
        }

        if(votes.size() > 1)
            if(log.isWarnEnabled()) log.warn("there was more than 1 candidate for coordinator: " + votes);
        else
            if(log.isInfoEnabled()) log.info("election results: " + votes);


        // determine who got the most votes
        most_votes=0;
        for(Enumeration e=votes.keys(); e.hasMoreElements();) {
            tmp=(Address)e.nextElement();
            count=((Integer)votes.get(tmp)).intValue();
            if(count > most_votes) {
                winner=tmp;
// fixed July 15 2003 (patch submitted by Darren Hobbs, patch-id=771418)
                most_votes=count;
            }
        }
        votes.clear();
        return winner;
    }


}
