/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.modeler.modules;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.management.Attribute;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.loading.MLet;
import javax.xml.transform.TransformerException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.modeler.AttributeInfo;
import org.apache.commons.modeler.BaseModelMBean;
import org.apache.commons.modeler.ManagedBean;
import org.apache.commons.modeler.Registry;
import org.apache.commons.modeler.util.DomUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Node;


/** This will create mbeans based on a config file.
 *  The format is an extended version of MLET.
 *
 * Classloading. We don't support any explicit classloader tag. 
 * A ClassLoader is just an mbean ( it can be the standard MLetMBean or
 * a custom one ). 
 * 
 * XXX add a special attribute to reference the loader mbean,
 * XXX figure out how to deal with private loaders
 */
public class MbeansSource extends ModelerSource implements MbeansSourceMBean
{
    private static Log log = LogFactory.getLog(MbeansSource.class);
    Registry registry;
    String type;

    // true if we are during the original loading
    boolean loading=true;
    List mbeans=new ArrayList();
    static boolean loaderLoaded=false;
    private Document document;
    private HashMap object2Node = new HashMap();

    long lastUpdate;
    long updateInterval=10000; // 10s

    public void setRegistry(Registry reg) {
        this.registry=reg;
    }          

    public void setLocation( String loc ) {
        this.location=loc;
    }

    /** Used if a single component is loaded
     *
     * @param type
     */
    public void setType( String type ) {
       this.type=type;
    }

    public void setSource( Object source ) {
        this.source=source;
    }

    public Object getSource() {
        return source;
    }

    public String getLocation() {
        return location;
    }
    
    /** Return the list of mbeans created by this source.
     *  It can be used to implement runtime services.
     */
    public List getMBeans() {
        return mbeans;
    }

    public List loadDescriptors( Registry registry, String location,
                                 String type, Object source)
            throws Exception
    {
        setRegistry(registry);
        setLocation(location);
        setType(type);
        setSource(source);
        execute();
        return mbeans;
    }
    
    public void start() throws Exception {
        registry.invoke(mbeans, "start", false);        
    }

    public void stop() throws Exception {
        registry.invoke(mbeans, "stop", false);        
    }
    
    public void init() throws Exception {
        if( mbeans==null) execute();
        if( registry==null ) registry=Registry.getRegistry();
        
        registry.invoke(mbeans, "init", false);
    }
    
    public void destroy() throws Exception {
        registry.invoke(mbeans, "destroy", false);                
    }
    
    public void load() throws Exception {
        execute(); // backward compat
    }

    public void execute() throws Exception {
        if( registry==null ) registry=Registry.getRegistry();
        try {
            InputStream stream=getInputStream();
            long t1=System.currentTimeMillis();
            document = DomUtil.readXml(stream);

            // We don't care what the root node is.
            Node descriptorsN=document.getDocumentElement();

            if( descriptorsN == null ) {
                log.error("No descriptors found");
                return;
            }

            Node firstMbeanN=DomUtil.getChild(descriptorsN, null);

            if( firstMbeanN==null ) {
                // maybe we have a single mlet
                if( log.isDebugEnabled() )
                    log.debug("No child " + descriptorsN);
                firstMbeanN=descriptorsN;
            }

            MBeanServer server=(MBeanServer)Registry.getServer();

            // XXX Not very clean...  Just a workaround
            if( ! loaderLoaded ) {
                // Register a loader that will be find ant classes.
                ObjectName defaultLoader= new ObjectName("modeler",
                        "loader", "modeler");
                MLet mlet=new MLet( new URL[0], this.getClass().getClassLoader());
                server.registerMBean(mlet, defaultLoader);
                loaderLoaded=true;
            }
        
            // Process nodes
            for (Node mbeanN = firstMbeanN; mbeanN != null;
                 mbeanN= DomUtil.getNext(mbeanN, null, Node.ELEMENT_NODE))
            {
                String nodeName=mbeanN.getNodeName();

                // mbean is the "official" name
                if( "mbean".equals(nodeName) || "MLET".equals(nodeName) )
                {
                    String code=DomUtil.getAttribute( mbeanN, "code" );
                    String objectName=DomUtil.getAttribute( mbeanN, "objectName" );
                    if( objectName==null ) {
                        objectName=DomUtil.getAttribute( mbeanN, "name" );
                    }
                    
                    if( log.isDebugEnabled())
                        log.debug( "Processing mbean objectName=" + objectName +
                                " code=" + code);

                    // args can be grouped in constructor or direct childs
                    Node constructorN=DomUtil.getChild(mbeanN, "constructor");
                    if( constructorN == null ) constructorN=mbeanN;

                    ArgsInfo info = processArg(constructorN);

                    try {
                        ObjectName oname=new ObjectName(objectName);
                        if( ! server.isRegistered( oname )) {
                            // We wrap everything in a model mbean.
                            // XXX need to support "StandardMBeanDescriptorsSource"
                            String modelMBean=BaseModelMBean.class.getName();                            
                            if(info == null) {
                                server.createMBean(modelMBean, oname,
                                                   new Object[] { code, this},
                                                   new String[] { String.class.getName(),
                                                                  ModelerSource.class.getName() } 
                                                   );
                            } else {
                                server.createMBean(modelMBean, oname,
                                                   new Object[] { code, this,
                                                                  info.getValues(),
                                                                  info.getSigs()
                                                   },
                                                   new String[] { String.class.getName(),
                                                                  ModelerSource.class.getName(),
                                                                  Object[].class.getName(),
                                                                  String[].class.getName()
                                                   }
                                                   );
                            }
                                                   
                            mbeans.add(oname);
                        }
                        object2Node.put( oname, mbeanN );
                        // XXX Arguments, loader !!!
                    } catch( Exception ex ) {
                        log.error( "Error creating mbean " + objectName, ex);
                    }

                    Node firstAttN=DomUtil.getChild(mbeanN, "attribute");
                    for (Node descN = firstAttN; descN != null;
                         descN = DomUtil.getNext( descN ))
                    {
                        processAttribute(server, descN, objectName);
                    }
                } else if("jmx-operation".equals(nodeName) ) {
                    String name=DomUtil.getAttribute(mbeanN, "objectName");
                    if( name==null )
                        name=DomUtil.getAttribute(mbeanN, "name");

                    String operation=DomUtil.getAttribute(mbeanN, "operation");

                    if( log.isDebugEnabled())
                        log.debug( "Processing invoke objectName=" + name +
                                " code=" + operation);
                    try {
                        ObjectName oname=new ObjectName(name);

                        ArgsInfo info = processArg( mbeanN );
                        if(info == null) {
                            server.invoke( oname, operation, null, null);
                        } else {
                            server.invoke( oname, operation, info.getValues(), info.getSigs());
                        }
                    } catch (Exception e) {
                        log.error( "Error in invoke " + name + " " + operation);
                    }
                }

                ManagedBean managed=new ManagedBean();
                DomUtil.setAttributes(managed, mbeanN);
                Node firstN;

                // process attribute info
                firstN=DomUtil.getChild( mbeanN, "attribute");
                for (Node descN = firstN; descN != null;
                     descN = DomUtil.getNext( descN ))
                {
                    AttributeInfo ci=new AttributeInfo();
                    DomUtil.setAttributes(ci, descN);
                    managed.addAttribute( ci );
                }

            }

            long t2=System.currentTimeMillis();
            log.info( "Reading mbeans  " + (t2-t1));
            loading=false;
        } catch( Exception ex ) {
            log.error( "Error reading mbeans ", ex);
        }
    }
    
    public void updateField( ObjectName oname, String name, 
                             Object value )
    {
        if( loading ) return;
        // nothing by default
        //log.info( "XXX UpdateField " + oname + " " + name + " " + value);
        Node n=(Node)object2Node.get( oname );
        if( n == null ) {
            log.info( "Node not found " + oname );
            return;
        }
        Node attNode=DomUtil.findChildWithAtt(n, "attribute", "name", name);
        if( attNode == null ) {
            // found no existing attribute with this name
            attNode=n.getOwnerDocument().createElement("attribute");
            DomUtil.setAttribute(attNode, "name", name);
            n.appendChild(attNode);
        } 
        String oldValue=DomUtil.getAttribute(attNode, "value");
        if( oldValue != null ) {
            // we'll convert all values to text content
            DomUtil.removeAttribute( attNode, "value");
        }
        DomUtil.setText(attNode, value.toString());

        //store();
    }
    
    /** Store the mbeans. 
     * XXX add a background thread to store it periodically 
     */ 
    public void save() {
        // XXX customize no often than ( based on standard descriptor ), etc.
        // It doesn't work very well if we call this on each set att - 
        // the triger will work for the first att, but all others will be delayed
        long time=System.currentTimeMillis();
        if( location!=null &&
                time - lastUpdate > updateInterval ) {
            lastUpdate=time;
            try {
                FileOutputStream fos=new FileOutputStream(location);
                DomUtil.writeXml(document, fos);
            } catch (TransformerException e) {
                log.error( "Error writing");
            } catch (FileNotFoundException e) {
                log.error( "Error writing" ,e );
            }
        }
    }

    private void processAttribute(MBeanServer server,
                                  Node descN, String objectName ) {
        String attName=DomUtil.getAttribute(descN, "name");
        String value=DomUtil.getAttribute(descN, "value");
        String type=null; // DomUtil.getAttribute(descN, "type");
        if( value==null ) {
            // The value may be specified as CDATA
            value=DomUtil.getContent(descN);
        }
        try {
            if( log.isDebugEnabled())
                log.debug("Set attribute " + objectName + " " + attName +
                        " " + value);
            ObjectName oname=new ObjectName(objectName);
            // find the type
            if( type==null )
                type=registry.getType(  oname, attName );

            if( type==null ) {
                log.info("Can't find attribute " + objectName + " " + attName );

            } else {
                Object valueO=registry.convertValue( type, value);
                server.setAttribute(oname, new Attribute(attName, valueO));
            }
        } catch( Exception ex) {
            log.error("Error processing attribute " + objectName + " " +
                    attName + " " + value, ex);
        }

    }

    private ArgsInfo processArg(Node mbeanN) {
        Node firstArgN=DomUtil.getChild(mbeanN, "arg" );
        if(firstArgN == null) {
            return null;
        }
        ArgsInfo info = new ArgsInfo();
        // process all args
        for (Node argN = firstArgN; argN != null;
             argN = DomUtil.getNext( argN ))
        {
            String type=DomUtil.getAttribute(argN, "type");
            String value=DomUtil.getAttribute(argN, "value");
            if( value==null ) {
                // The value may be specified as CDATA
                value=DomUtil.getContent(argN);
            }
            info.addArgPair(type, registry.convertValue(type,value));
        }
        return info;
    }

    private static class ArgsInfo {
        private List sigs = new ArrayList();
        private List values = new ArrayList();

        ArgsInfo() {
        }

        public String [] getSigs() {
            return (String [])sigs.toArray(new String[sigs.size()]);
        }

        public Object[] getValues () {
            return values.toArray(new Object[values.size()]);
        }

        public void addArgPair(String name, Object val) {
            sigs.add(name);
            values.add(val);
        }
    }
}
