/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2006 Bull S.A.
 * Contact: jonas-team@objectweb.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$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.management.monitoring;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Vector;

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

import org.objectweb.jonas_domain.api.DomainMap;
import org.objectweb.jonas_domain.api.DomainMapException;
import org.objectweb.jonas_domain.lib.wrapper.DomainManagerWrapper;
import org.objectweb.jonas_domain.xml.Cluster;
import org.objectweb.jonas_domain.xml.ClusterDaemon;
import org.objectweb.jonas_domain.xml.Location;
import org.objectweb.jonas_domain.xml.Server;

import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.discovery.DiscEvent;
import org.objectweb.jonas.discovery.DiscoveryState;
import org.objectweb.jonas.jmx.JmxService;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.jonas.management.cluster.BaseCluster;
import org.objectweb.jonas.management.cluster.ClusterFactory;
import org.objectweb.jonas.management.cluster.CmiClusterFactory;
import org.objectweb.jonas.management.cluster.EjbHaClusterFactory;
import org.objectweb.jonas.management.cluster.JkClusterFactory;
import org.objectweb.jonas.management.cluster.LogicalCluster;
import org.objectweb.jonas.management.cluster.LogicalClusterFactory;
import org.objectweb.jonas.management.cluster.TomcatClusterFactory;
import org.objectweb.jonas.server.J2EEServer;
import org.objectweb.jonas.service.ServiceManager;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * This class represents the Master Server of the Domain.
 * Is created by the DiscoveryService if it detects it is the master.
 * It is not instanciated for slave servers.
 * @author danesa
 * @author durieuxp refactoring
 */
public class DomainMonitor implements DomainMonitorMBean {

    /**
     * logger for traces
     */
    private static Logger logger = Log.getLogger("org.objectweb.jonas.domain.management");

    /**
     * List of known ClusterFactory
     */
    private ArrayList clusterFactoryList = new ArrayList(5);

    /**
     * List of known ClusterDaemon
     */
    private HashMap myclusterdaemons = new HashMap();

    /**
     * ref to the Jmx Service
     */
    private JmxService jmx = (JmxService) ServiceManager.getInstance().getJmxService();

    /**
     * ref to the MBeanServer.
     */
    private MBeanServer mbeanServer;

    /**
     * Thread object allowing to check the proxys state in order to detect if
     * they can be reached using the established connection.
     */
    private StateMonitor stateMonitor = null;

    /**
     * My Domain Name
     */
    private String domainName;

    /**
     * The local server name (name of the master)
     */
    private String masterName;

    /**
     * Domain description string (from domain.xml)
     */
    private String description;

    /**
     * list of delpoyement operations
     */
    private ArrayList deployList = new ArrayList();

    /**
     * The LogicalClusterFactory
     */
    private LogicalClusterFactory lcl;

    /**
     * Constructor
     */
    public DomainMonitor(String domain) {
        domainName = domain;
        mbeanServer = jmx.getJmxServer();

        // Create the ClusterFactory objects
        lcl = new LogicalClusterFactory(this);
        clusterFactoryList.add(lcl);
        clusterFactoryList.add(new JkClusterFactory(this));
        clusterFactoryList.add(new TomcatClusterFactory(this));
        clusterFactoryList.add(new CmiClusterFactory(this));
        clusterFactoryList.add(new EjbHaClusterFactory(this));
    }

    public void readDomainConfig() {

        // Create domain map based on the info contained in domain.xml file.
        try {
            createDomainMap();
        } catch (Exception e) {
            logger.log(BasicLevel.INFO, "Cannot read domain.xml");
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Exception raised:" + e);
            }
        }

        // Create state monitor thread
        stateMonitor = new StateMonitor(this);
        stateMonitor.start();
        logger.log(BasicLevel.DEBUG, "State monitor created");
    }

    public String getDescription() {
        return description;
    }

    public String getDomainName() {
        return domainName;
    }

    public void setMonitoringPeriod(int sec) {
        stateMonitor.setSamplingPeriod(sec);
    }

    public int getMonitoringPeriod() {
         return stateMonitor.getSamplingPeriod();
    }

    /**
     * Create the domain representation found in domain.xml
     * @throws DomainMapException Error reading domain.xml
     * @throws JMException
     */
    private void createDomainMap() throws DomainMapException, JMException {

        // Parse the domain.xml file.
        ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
        DomainMap domainMap = DomainManagerWrapper.getDomainMap(null, currentLoader);
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Domain Map :"  + domainMap);
        }

        // Check domain name.
        String definedName =  domainMap.getName();
        if (! domainName.equals(definedName)) {
            logger.log(BasicLevel.INFO, "Domain name in domain.xml is not the actual domain name: " + domainName);
            return;
        }

        description = domainMap.getDescription();

        // Get list of clusterdaemons
        Vector daemons = domainMap.getClusterDaemons();
        for (int i = 0; i < daemons.size(); i++) {
            ClusterDaemon cd = (ClusterDaemon) daemons.elementAt(i);
            String cdname = cd.getName();
            // Create the proxy and add it to the list
            Location loc = cd.getLocation();
            ClusterDaemonProxy cdp = new ClusterDaemonProxy(this, cdname, loc.getUrlList());
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Adding clusterdaemon : "  + cdname);
            }
            // give a name and register mbean
            ObjectName on = JonasObjectName.clusterDaemonProxy(cdname);
            cdp.setObjectName(on.toString());
            // add it to the list
            myclusterdaemons.put(cdname, cdp);
        }

        // Get list of servers
        Vector servers = domainMap.getServers();
        for (int i = 0; i < servers.size(); i++) {
            Server sv = (Server) servers.elementAt(i);
            String svname = sv.getName();
            // Check ClusterDaemon attached to this server
            String cdn = sv.getClusterDaemon();
            ClusterDaemonProxy cdp = null;
            if (cdn != null) {
                cdp = (ClusterDaemonProxy) myclusterdaemons.get(cdn);
                if (cdp == null) {
                    logger.log(BasicLevel.WARN, "Undeclared ClusterDaemon :"  + cdn);
                }
            }
            // Create the proxy and add it to the list
            Location loc = sv.getLocation();
            ServerProxy sp = findServerProxy(svname);
            if (sp == null) {
                sp = new ServerProxy(this, svname, loc.getUrlList(), cdp);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Adding server : "  + svname);
                }
                // give a name and register mbean
                ObjectName on = JonasObjectName.serverProxy(svname);
                sp.setObjectName(on.toString());
                // add it to the list
                lcl.notifyServer(sp);
            } else {
                logger.log(BasicLevel.WARN, "Server already declared: " + svname);
            }
        }

        // Get list of logical clusters
        Vector clusters = domainMap.getClusters();
        for (int i = 0; i < clusters.size(); i++) {
            Cluster cl = (Cluster) clusters.elementAt(i);
            String clname = cl.getName();
            LogicalCluster locl = (LogicalCluster) lcl.getCluster(clname);
            if (locl == null) {
                // Create a Logical Cluster
                locl = createLogicalCluster(clname);
            }
            if (locl != null) {
                // Add servers to the new cluster
                LinkedList serverList = cl.getServerList();
                for (int j = 0; j < serverList.size(); j++) {
                    Server sv = (Server) serverList.get(j);
                    String svname = sv.getName();
                    ServerProxy sp = findServerProxy(svname);
                    if (sp == null) {
                        // Server is defined directly in the Cluster
                        String cdn = sv.getClusterDaemon();
                        ClusterDaemonProxy cdp = null;
                        if (cdn != null) {
                            cdp = (ClusterDaemonProxy) myclusterdaemons.get(cdn);
                            if (cdp == null) {
                                logger.log(BasicLevel.WARN, "Undeclared ClusterDaemon :"  + cdn);
                            }
                        }
                        // Create the proxy and add it to the list
                        Location loc = sv.getLocation();
                        sp = new ServerProxy(this, svname, loc.getUrlList(), cdp);
                        // give a name and register mbean
                        ObjectName on = JonasObjectName.serverProxy(svname);
                        sp.setObjectName(on.toString());
                    }
                    locl.addServer(svname, sp);
                    // Notify the cluster factories
                    notifyServerProxyRunning(sp);
                }
            }
        }

    }

    /**
     * Add the local J2EEServer to the list of servers
     */
    public ServerProxy addLocalServer(String svname, Collection urls) {
        logger.log(BasicLevel.DEBUG, svname);

        // Add a ServerProxy even for the local server
        ServerProxy sp = findServerProxy(svname);
        if (sp == null) {
            sp = new ServerProxy(this, svname, urls, null);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Adding server : "  + svname);
            }
            // give a name and register mbean
            ObjectName on;
            try {
                on = JonasObjectName.serverProxy(svname);
                sp.setObjectName(on.toString());
            } catch (MalformedObjectNameException e) {
                logger.log(BasicLevel.ERROR, "MalformedObjectName: "  + svname);
            }
            // add it to the list
            lcl.notifyServer(sp);
        }
        // remember the master server name
        masterName = svname;
        return sp;
    }

    /**
     * Get a Cluster by its name. Look in all cluster factories.
     * @param name The name of the cluster
     * @return the Cluster or null if unknown by the domain.
     */
    public BaseCluster findCluster(String name) {
        logger.log(BasicLevel.DEBUG, name);
        BaseCluster cluster = null;
        // Look in each cluster factory to find if this cluster exists somewhere
        for (Iterator it = clusterFactoryList.iterator(); it.hasNext();) {
            ClusterFactory cf = (ClusterFactory) it.next();
            cluster = cf.getCluster(name);
            if (cluster != null) {
                break;
            }
        }
        return cluster;
    }

    /**
     * Get a Server by its name
     * @param name The name of the server
     * @return the Server or null if unknown by the domain.
     */
    public J2EEServer findServer(String name) {
        logger.log(BasicLevel.DEBUG, name);
        // Look in the domain cluster, holding all servers in the domain.
        BaseCluster cluster = (BaseCluster) lcl.getCluster(domainName);
        return cluster.getServer(name);
    }

    /**
     * Get a Server by its name
     * @param name The name of the server
     * @return the ServerProxy or null if unknown by the domain.
     */
    public ServerProxy findServerProxy(String name) {
        logger.log(BasicLevel.DEBUG, name);
        // Look in the domain cluster, holding all servers in the domain.
        BaseCluster cluster = (BaseCluster) lcl.getCluster(domainName);
        if (cluster == null) {
            // Not initialized yet: no server known at this time.
            return null;
        }
        return cluster.getServerProxy(name);
    }

    /**
     * Get a ClusterDaemon by its name
     * @param name The name of the clusterdaemon
     * @return the ClusterDaemonProxy or null if unknown by the domain.
     */
    public ClusterDaemonProxy findClusterDaemonProxy(String name) {
        return (ClusterDaemonProxy) myclusterdaemons.get(name);
    }

    /**
     * Get the list of ServerProxy
     * @return Collection of all ServerProxy in the Domain
     */
    public Collection getServerList() {

        // All servers are set in the Domain Cluster
        LogicalCluster domaincluster = (LogicalCluster) lcl.getCluster(domainName);
        if (domaincluster == null) {
            return new ArrayList();
        }
        return domaincluster.getServerProxyList();
    }

    /**
     * Get the list of all ClusterDaemonProxy
     */
    public Collection getClusterDaemonList() {
        return myclusterdaemons.values();
    }

    /**
     * Get the list of all clusters of any type
     * @return Collection of all Clusters in the domain
     */
    public Collection getTotalClusterList() {
        ArrayList ret = new ArrayList();
        for (Iterator it = clusterFactoryList.iterator(); it.hasNext();) {
            ClusterFactory cf = (ClusterFactory) it.next();
            ret.addAll(cf.getClusterList());
        }
        return ret;
    }

    /**
     * Create a logical cluster
     */
    public LogicalCluster createLogicalCluster(String name) {
        return lcl.createLogicalCluster(name);
    }

    /**
     * Get the list of logical clusters
     */
    public Collection getLogicalClusterList() {
        return lcl.getClusterList();
    }


    /**
     * MBean method
     * To be replaced by J2EEDomain.getServers()
     * @return Array of OBJECT_NAMEs of all ServerProxy
     */
    public String[] getProxys() {
        // Look in the domain cluster, holding all servers in the domain.
        BaseCluster cluster = (BaseCluster) lcl.getCluster(domainName);
        Collection col = cluster.getServerProxyList();
        String [] ret = new String[col.size()];
        int i = 0;
        for (Iterator it = col.iterator(); it.hasNext(); ) {
            ServerProxy proxy = (ServerProxy) it.next();
            ret[i++] = proxy.getObjectName();
        }
        return ret;
    }

    /**
     * MBean method
     * @return Array of OBJECT_NAMEs of all cluster MBeans
     */
    public String[] getClusters() {
        ArrayList al = new ArrayList();
        for (int i = 0; i < clusterFactoryList.size(); i++) {
            ClusterFactory clFactory = (ClusterFactory) clusterFactoryList.get(i);
            Iterator clIt = clFactory.getClusterList().iterator();
            for (; clIt.hasNext();) {
                BaseCluster cl = (BaseCluster) clIt.next();
                al.add(cl.getObjectName());
            }
        }
        String [] ret = new String[al.size()];
        for (int i = 0; i < al.size(); i++) {
            ret[i] = (String) al.get(i);
        }
        return ret;
    }

    /**
     * Handle Notifications from Discovery Service
     * @param event
     */
    public void discoveryNotification(DiscEvent event) {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Get notification:" + event);
        }
        if (! event.getDomainName().equals(domainName)) {
            logger.log(BasicLevel.WARN, "Discovery notification from another domain. Forget it.");
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Notification Domain = " + event.getDomainName());
            }
            return;
        }
        String state = event.getState();
        String svname = event.getServerName();

        // Look if the server is known
        ServerProxy proxy = findServerProxy(svname);
        if (proxy == null) {
            // Create an entry for this server
            proxy = new ServerProxy(this, svname);
            // give a name and register mbean
            ObjectName on;
            try {
                on = JonasObjectName.serverProxy(svname);
                proxy.setObjectName(on.toString());
            } catch (MalformedObjectNameException e) {
                logger.log(BasicLevel.ERROR, "MalformedObjectName - Cannot register MBEAN:" + svname);
            }
        }

        // If server is running, get its urls to establish a connection.
        if (state.equals(DiscoveryState.RUNNING)) {
            // If this is myself, just forget it
            if (svname.equals(masterName)) {
                logger.log(BasicLevel.DEBUG, "Yes, I'm already running, I knew that.");
                return;
            }
            // Establish the connection
            ArrayList urls = new ArrayList();
            String [] urlstr = event.getConnectorURL();
            for (int i = 0; i < urlstr.length; i++) {
                urls.add(urlstr[i]);
            }
            proxy.notifyStarting(urls);
            // Notify each ClusterFactory that a server started in the domain
            notifyServerProxyRunning(proxy);
        } else if (state.equals(DiscoveryState.STOPPING)) {
            proxy.notifyStopping();
            // TODO Notify ClusterFactory's
        }
    }

    /**
     * Notify to all the cluster factories that a server started in the domain
     * @param sp the server's proxy
     */
    void notifyServerProxyRunning(ServerProxy sp) {
        for (Iterator it = clusterFactoryList.iterator(); it.hasNext();) {
            ClusterFactory cf = (ClusterFactory) it.next();
            cf.notifyServer(sp);
        }
    }

    /**
     * Register a DeployAction
     * @return true if correctly aded in the list
     */
    public synchronized boolean registerDeployAction(DeployAction action) {
        // Don't add if already another similar action
        for (Iterator it = deployList.iterator(); it.hasNext(); ) {
            DeployAction act = (DeployAction) it.next();
            if (act.getFileName().equals(action.getFileName()) &&
                    act.getServerName().equals(action.getServerName())) {
                logger.log(BasicLevel.DEBUG, "Already a similar action");
                return false;
            }
        }
        return deployList.add(action);
    }

    /**
     * Get the list of server where a file is being deployed
     * @param filename file to deploy
     * @return server name
     */
    public synchronized String [] getDeployServers(String filename) {
        ArrayList slist = new ArrayList();
        for (Iterator it = deployList.iterator(); it.hasNext(); ) {
            DeployAction act = (DeployAction) it.next();
            if (act.getFileName().equals(filename)) {
                slist.add(act.getServerName());
            }
        }
        return (String []) slist.toArray(new String [0]);
    }

    /**
     * Get the current state (string form) of deployment operation
     * @param filename file to deploy
     * @param servername server where deployment is done
     * @return one of "progress","ok","fail"
     */
    public synchronized String getDeployState(String filename, String servername) {
        String ret = "error";
        for (Iterator it = deployList.iterator(); it.hasNext(); ) {
            DeployAction act = (DeployAction) it.next();
            if (act.getFileName().equals(filename) && act.getServerName().equals(servername)) {
                ret = act.getStateAsString();
                break;
            }
        }
        return ret;
    }

    /**
     * Get the error message associated to the error state
     * @param filename file to deploy
     * @param servername server where deployment is done
     * @return error message
     */
    public synchronized String getErrorMessage(String filename, String servername) {
        String ret = "";
        for (Iterator it = deployList.iterator(); it.hasNext(); ) {
            DeployAction act = (DeployAction) it.next();
            if (act.getFileName().equals(filename) && act.getServerName().equals(servername)) {
                ret = act.getErrorMessage();
            }
        }
        return ret;
    }

    /**
     * Forget all deploy information
     */
    public synchronized void forgetAllDeploy() {
        deployList.clear();
    }

    /**
     * Try to refresh the server states
     * TODO notify thread only.
     */
    public void refreshStates() {
        try {
            checkServerStates(false);
        } catch (JMException e) {
            logger.log(BasicLevel.DEBUG, "Cannot refresh states:", e);
        }
    }

    // ------------------------------------------------------------------------------
    // Thread associated to the DomainMonitor
    // ------------------------------------------------------------------------------

    /**
     * Check state of all remote servers in the domain
     * @throws JMException
     */
    private synchronized void checkServerStates(boolean readall) throws JMException {
        for (Iterator i = getServerList().iterator(); i.hasNext();) {
            ServerProxy proxy = (ServerProxy) i.next();
            proxy.checkit(readall);
        }
        for (Iterator i = getClusterDaemonList().iterator(); i.hasNext();) {
            ClusterDaemonProxy proxy = (ClusterDaemonProxy) i.next();
            proxy.checkit();
        }
    }

    /**
     * Monitoring thread that checks managed servers periodically
     */
    class StateMonitor extends Thread {

        /**
         * My DomainMonitor
         */
        private DomainMonitor domainMonitor;

        /**
         * Logger object to log events
         */
        private Logger logger = Log.getLogger("org.objectweb.jonas.domain.management");

        /**
         * Default sampling period of 60 seconds
         */
        private long  samplingPeriod = 60000L;

        /**
         * To stop the thread
         */
        private boolean stopped = false;

        /**
         * Constructor
         * @param domainMonitor reference on the DomainMonitor instance
         * @param log the logger object
         */
        public StateMonitor(DomainMonitor domainMonitor) {
            super("DomainStateMonitor");
            setDaemon(true);
            this.domainMonitor = domainMonitor;
        }

        /**
         * Set the sampling period. Allows reconfiguration.
         * @param sec sampling period in sec.
         */
        public void setSamplingPeriod(int sec) {
            samplingPeriod = sec * 1000L;
        }

        /**
         * @return Sampling period in seconds
         * @return
         */
        public int getSamplingPeriod() {
            return new Long(samplingPeriod / 1000L).intValue();
        }

        /**
         * Stop this thread.
         */
        public void stopit() {
            stopped  = true;
        }

        /**
         * Start the state monitor thread
         */
        public void run() {
            while (!stopped) {
                try {
                    sleep(samplingPeriod);
                    domainMonitor.checkServerStates(true);
                } catch (JMException me) {
                    logger.log(BasicLevel.WARN, "Exception while checking server states", me);
                } catch (InterruptedException e) {
                    logger.log(BasicLevel.ERROR, "Interrupted", e);
                    break;
                }
            }
        }
    }

}