/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2006-2008 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id:BaseCluster.java 8660 2006-06-20 08:15:28Z danesa $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.management.domain.cluster;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.lib.management.domain.DeployAction;
import org.ow2.jonas.lib.management.domain.DomainMonitor;
import org.ow2.jonas.lib.management.domain.proxy.clusterd.ClusterDaemonProxy;
import org.ow2.jonas.lib.management.domain.proxy.server.ServerProxy;
import org.ow2.jonas.lib.management.javaee.J2EEServerState;
import org.ow2.jonas.lib.util.JonasObjectName;
import org.ow2.jonas.lib.util.Log;

/**
 * Implements basics for cluster management.
 * To be extended by the different cluster types.
 * It implements cluster state transition diagram based on the members's state.
 * A member is represented by a ClusterMember class instance.
 * @see ClusterMember
 * @author Adriana Danes
 * @author S. Ali Tokmen
 */
public abstract class BaseCluster implements BaseClusterMBean {

    /**
     * The name of this Cluster.
     * This String is included in the MBean OBJECT_NAME.
     */
    protected String name = null;

    /**
     * MBean OBJECT_NAME
     * <domain>:type=<type>,name=<name>
     * type may be one among: JkCluster, TomcatCluster, etc...
     */
    protected ObjectName objectName = null;

    /**
     * The list of ClusterMember objects that compose this Cluster
     * Key = name of the Member.
     */
    protected Map members = new HashMap();

    /**
     * ClusterFactory that created this Cluster
     */
    protected ClusterFactory cf;

    /**
     * The cluster state
     */
    protected int state = STATE_INIT;

    /**
     * This is the initial state, all members are in INITIAL state
     */
    public static final int STATE_INIT = 0;

    /**
     * All the members are in RUNNING state
     */
    public static final int STATE_UP = 1;

    /**
     * All the members are in STOPPED state
     */
    public static final int STATE_DOWN = 2;

    /**
     * All the members are in FAILED state
     */
    public static final int STATE_FAILED = 3;

    /**
     * At least one members is in FAILED state
     */
    public static final int STATE_PARTIALLY_FAILED = 4;

    /**
     * At least one members is in RUNNING state, there is no failed member
     */
    public static final int STATE_PARTIALLY_UP = 5;

    /**
     * At least one members is in STOPPED state, there is no failed member,
     * there is no running memeber
     */
    public static final int STATE_PARTIALLY_DOWN = 6;

    /**
     * No member in FAILED state, no member in RUNNING state, no memeber in STOPPED state
     * The members' state may be UNREACHABLE or UNKNOWN
     */
    public static final int STATE_UNKNOWN = 7;

    /**
     * domain management logger
     */
    protected static Logger logger =  Log.getLogger("org.ow2.jonas.management.cluster");

    /**
     * ref to the Jmx Service
     */
    protected JmxService jmx = null;

    /**
     * ref to the domainMonitor
     */
    protected DomainMonitor dm;
    protected String domainName = null;

    /**
     * Constructor
     * @param cf Cluster Factory
     */
    public BaseCluster(final ClusterFactory cf) {
        logger.log(BasicLevel.DEBUG, "");
        this.cf = cf;
        dm = cf.getDomainMonitor();
        domainName = dm.getDomainName();
        jmx = dm.getJmxService();
    }

    /**
     * Create a new ClusterMember. Depends on the underlaying class.
     * @param svname
     * @param proxy
     */
    public abstract ClusterMember createClusterMember(String svname, ServerProxy proxy);

    /**
     * Set the MBean name, that may be unknown when constructor is called.
     * @param name its name.
     * @return the MBean ObjectName
     * @throws JMException could not create MBean instance
     */
    public ObjectName setName(final String name) throws JMException {
        this.name = name;
        objectName = JonasObjectName.cluster(domainName, name, getType());
        return objectName;
    }

    /**
     * @return The MBean OBJECT_NAME
     */
    public String getObjectName() {
        return objectName.toString();
    }

    /**
     * @return the type of this Cluster (string form)
     */
    public abstract String getType();

    /**
     * Add a Member to the Cluster
     * @param m Member to add
     * @return true if added, false if already there.
     */
    public synchronized boolean addMember(final ClusterMember m) {
        String mbr = m.getName();
        logger.log(BasicLevel.DEBUG, mbr);
        /*
        if (members.containsKey(mbr)) {
            return false;
        }*/
        members.put(mbr, m);
        return true;
    }

    /**
     * Get a server by its name.
     * @param name fo the server
     * @return the ServerProxy or null if not found.
     */
    public synchronized ServerProxy getServerProxy(final String name) {
        for (Iterator it = members.values().iterator(); it.hasNext();) {
            ClusterMember member = (ClusterMember) it.next();
            try {
                if (member.getServerName().equals(name)) {
                    return member.getProxy();
                }
            } catch (JMException e) {
                logger.log(BasicLevel.WARN, "Cannot get server name: " + e);
                return null;
            }
        }
        return null;
    }

    /**
     * Get a server by its name.
     * @param name fo the server
     * @return the ServerProxy or null if not found.
     */
    public synchronized ServerProxy getRunningServerProxy() {
        Collection<ServerProxy> proxys = getServerProxyList();
        for (Iterator<ServerProxy> it = proxys.iterator(); it.hasNext();) {
            ServerProxy proxy = it.next();
            if (proxy.getJ2EEServerState().equals(J2EEServerState.RUNNING)) {
                return proxy;
            }
        }
        return null;
    }

    /**
     * @return The list of ServerProxy
     */
    public synchronized Collection<ServerProxy> getServerProxyList() {
        Collection<ServerProxy> ret = new ArrayList();
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            ret.add(m.getProxy());
        }
        return ret;
    }

    // --------------------------------------------------------------------------
    // BaseClusterMBean Implementation
    // --------------------------------------------------------------------------

    /**
     * Get the Cluster State
     * @return A String representing the cluster current state
     */
    public String getState() {
        updateState();
        switch (state) {
            case STATE_INIT:
                return "INIT";
            case STATE_UP:
                return "UP";
            case STATE_DOWN:
                return "DOWN";
            case STATE_FAILED:
                return "FAILED";
            case STATE_PARTIALLY_UP:
                return "PARTIALLY_UP";
            case STATE_PARTIALLY_DOWN:
                return "PARTIALLY_DOWN";
            case STATE_PARTIALLY_FAILED:
                return "PARTIALLY_FAILED";
        }
        return "UNKNOWN";
    }

    /**
     * @return the cluster name
     */
    public String getName() {
        return name;
    }

    /**
     * @return the member number
     */
    public int getNbMembers() {
        return members.size();
    }

    /**
     * @return the Member MBean OBJECT_NAMES
     */
    public synchronized String[] getMembers() {
        logger.log(BasicLevel.DEBUG, "");
        Collection col = members.values();
        String[] result = new String[col.size()];
        int i = 0;
        for (Iterator it = col.iterator(); it.hasNext();) {
            ClusterMember m = (ClusterMember) it.next();
            result[i++] = m.getObjectName().toString();
        }
        return result;
    }

    /**
     * Calls {@link BaseCluster#addServer(String, String[], String, String, String)}(name, urls, clusterd, null, null)
     * @param name logical name of the server
     * @param urls array of urls for connection
     * @param clusterd possible clusterdaemon managing the server
     * @deprecated {@link BaseCluster#addServer(String, String[], String, String, String)}
     */
    @Deprecated
    public void addServer(final String name, final String [] urls, final String clusterd) throws JMException {
        addServer(name, urls, clusterd, null, null);
    }

    /**
     * MBean operation
     * Add a server in the cluster (jonasAdmin) - this is only called
     * for LogicalCluster
     * @param svname logical name of the server
     * @param urls array of urls for connection
     * @param cdn clusterDaemon used to manage te server
     * @param username user name to use when connecting if any. Null otherwise.
     * @param password password to use when connecting if any. Null otherwise.
     */
    public void addServer(final String svname, final String [] urls, final String cdn, final String username, final String password) throws JMException {

        // Possible ClusterDaemon attached to the server
        ClusterDaemonProxy cdp = null;
        if (cdn != null && cdn.length() > 0) {
            cdp = dm.findClusterDaemonProxy(cdn);
            if (cdp == null) {
                logger.log(BasicLevel.WARN, "Unknown ClusterDaemon :"  + cdn);
            }
        }

        // Create the proxy and add it to the list
        ServerProxy proxy = dm.findServerProxy(svname);
        if (proxy == null) {
            ArrayList urlcol = new ArrayList();
            for (int i = 0; i < urls.length; i++) {
                urlcol.add(urls[i]);
            }
            if (username != null && password != null) {
                try {
                    this.dm.addAuthenticationInformation(svname, username, password);
                } catch (UnsupportedEncodingException e) {
                    throw new JMException("Failed saving authentication information for server "
                            + svname + ": " + e.getLocalizedMessage());
                }
            }
            proxy = new ServerProxy(dm, svname, urlcol, cdp);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Adding server : "  + svname);
            }
            // give a name and register MBean
            ObjectName on = JonasObjectName.serverProxy(domainName, svname);
            proxy.setObjectName(on.toString());
        }
        // Create the ClusterMember object
        ClusterMember member = createClusterMember(svname, proxy);

        // Add this member if not already there
        boolean added = addMember(member);
        if (!added) {
            throw new RuntimeException("Server already in cluster: " + svname);
        }
        // Build the ObjectName and register MBean for the ClusterMember
        ObjectName on = JonasObjectName.clusterMember(domainName, svname, getType(), name);
        member.setObjectName(on);
        MBeanServer mbeanServer = jmx.getJmxServer();
        if (mbeanServer.isRegistered(on)) {
            mbeanServer.unregisterMBean(on);
        }
        mbeanServer.registerMBean(member, on);
        //discover cluster
        dm.notifyServerProxyRunning(proxy);
    }

    /**
     * MBean operation
     * Remove a server from the cluster (jonasAdmin)
     * @param svname logical name of the server
     */
    public synchronized void removeServer(final String svname) {
        this.dm.removeAuthenticationInformation(svname);
        this.members.remove(svname);
    }

    /**
     * Check if a server is member of  a cluster. Suppose that
     * the ClusterMember is inserted in the 'members' table using
     * its name as key.
     * @param serverName name of the server the caller would like to
     * know if it is a cluster member
     * @return true if the server is member of the current cluster
     */
    public synchronized boolean isMember(final String serverName) {
        if (members.containsKey(serverName)){
            return true;
        }
        return false;
    }
    /**
     * MBean operation
     * Start all cluster nodes.
     */
    public synchronized void startit() throws JMException {
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            DeployThread th = new DeployThread(m.getProxy(), null, DeployAction.START, false);
            th.start();
        }
    }

    /**
     * MBean operation
     * Stop all cluster nodes.
     */
    public synchronized void stopit() throws JMException {
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            DeployThread th = new DeployThread(m.getProxy(), null, DeployAction.STOP, false);
            th.start();
        }
    }

    /**
     * MBean operation
     * Deploy a module on all nodes.
     * @param file file to upload. One among .war,.jar,.ear,.rar
     */
    public synchronized void deployModule(final String file) {
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            DeployThread th = new DeployThread(m.getProxy(), file, DeployAction.DEPLOY, false);
            th.start();
        }
    }

    /**
     * MBean operation
     * Undeploy a module on all nodes.
     * @param file file to upload. One among .war,.jar,.ear,.rar
     */
    public synchronized void undeployModule(final String file) {
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            DeployThread th = new DeployThread(m.getProxy(), file, DeployAction.UNDEPLOY, false);
            th.start();
        }
    }

    /**
     * MBean operation
     * Upload a file on all nodes.
     * @param file file to upload. One among .war,.jar,.ear,.rar
     * @param repl true if the uploaded file can replace a file with the same name in the jars directory
     */
    public void uploadFile(final String file, final boolean repl) {
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            DeployThread th = new DeployThread(m.getProxy(), file, DeployAction.UPLOAD, repl);
            th.start();
        }
    }

    /**
     * MBean operation
     * Upload adn deploy a module on all nodes.
     * @param file file to upload. One among .war,.jar,.ear,.rar
     * @param repl true if the uploaded file can replace a file with the same name in the jars directory
     */
    public synchronized void uploadDeployModule(final String file, final boolean repl) {
        for (Iterator i = members.values().iterator(); i.hasNext();) {
            ClusterMember m = (ClusterMember) i.next();
            DeployThread th = new DeployThread(m.getProxy(), file, DeployAction.UPLOADDEPLOY, repl);
            th.start();
        }
    }

    public class DeployThread extends Thread {

        private ServerProxy proxy;
        private String filename;
        private int action;
        private boolean replace;

        public DeployThread(final ServerProxy proxy, final String filename, final int action, final boolean replace) {
            this.proxy = proxy;
            this.filename = filename;
            this.action = action;
            this.replace = replace;
        }

        @Override
        public void run() {
            switch(action) {
                //TODO: pass standby mode as parameter
                case DeployAction.START:
                    proxy.start(false);
                    break;
                //TODO: pass standby mode as parameter
                case DeployAction.STOP:
                    proxy.stop(false);
                    break;
                case DeployAction.DEPLOY:
                    proxy.deployModule(filename);
                    break;
                case DeployAction.UNDEPLOY:
                    proxy.undeployModule(filename);
                    break;
                case DeployAction.UPLOADDEPLOY:
                    proxy.uploadDeployModule(filename, replace);
                    break;
                case DeployAction.UPLOAD:
                    proxy.uploadFile(filename, replace);
                    break;
            }
        }
    }

    // --------------------------------------------------------------------------
    // private methods
    // --------------------------------------------------------------------------

    /**
     * State diagram implementation.
     */
    private synchronized void updateState() {
        int nbFailed = 0;
        int nbRunning = 0;
        int nbStopped = 0;
        int nbInitial = 0;
        int nbMembers = 0;
        for (Iterator it = members.values().iterator(); it.hasNext();) {
            ClusterMember member = (ClusterMember) it.next();
            String memberState = member.getState();
            if (J2EEServerState.UNKNOWN.toString().equals(memberState)
                || J2EEServerState.INITIAL.toString().equals(memberState)) {
                nbInitial++;
            } else if (J2EEServerState.RUNNING.toString().equals(memberState)
                    || J2EEServerState.STANDBY.toString().equals(memberState)) {
                nbRunning++;
            } else if (J2EEServerState.FAILED.toString().equals(memberState)) {
                nbFailed++;
            } else if (J2EEServerState.STOPPED.toString().equals(memberState)) {
                nbStopped++;
            }
            nbMembers++;
        }
        if (nbInitial == nbMembers) {
            state = STATE_INIT;
        } else if (nbFailed == nbMembers) {
            state = STATE_FAILED;
        } else if (nbRunning == nbMembers) {
            state = STATE_UP;
        } else if (nbStopped == nbMembers) {
            state = STATE_DOWN;
        } else if (nbFailed > 0) {
            state = STATE_PARTIALLY_FAILED;
        } else if (nbRunning > 0) {
            state = STATE_PARTIALLY_UP;
        } else if (nbStopped > 0) {
            state = STATE_PARTIALLY_DOWN;
        } else {
            state = STATE_UNKNOWN;
        }
    }

}
