/*
Copyright (C) 2000-2010  Ministere de la culture et de la communication (France), AJLSM
See LICENCE file
*/
package fr.gouv.culture.sdx.utils;

import fr.gouv.culture.sdx.exception.SDXException;
import fr.gouv.culture.sdx.exception.SDXExceptionCode;
import fr.gouv.culture.sdx.utils.configuration.ConfigurationUtils;
import fr.gouv.culture.sdx.utils.constants.Node;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.DefaultContext;
import org.apache.avalon.framework.logger.Logger;
import org.apache.cocoon.ProcessingException;
import org.apache.excalibur.source.impl.FileSource;
import org.apache.excalibur.source.SourceException;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.xml.sax.XMLizable;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.apache.cocoon.components.source.SourceUtil;

import java.net.MalformedURLException;
import java.io.File;
import java.util.*;
import java.io.IOException;


public abstract class AbstractSdxObject implements SdxObject {
    /** Avalon logger to write information. */
    protected transient Logger _logger = null;
    protected transient DefaultContext _context = null;
    protected transient ServiceManager _manager = null;
    protected transient Configuration _configuration = null;
    protected String _id = "";
    /**The default encoding to be used when working with strings*/
    protected String _encoding = Encodable.DEFAULT_ENCODING;
    protected Locale _locale = null;
    protected String _xmlLang = "";
    //the following can be set externally or by an implementation of the configure() method
    protected transient Source _description = null;
    protected transient HashMap _xmlizable_objects = new HashMap();
    protected boolean isToSaxInitialized = false;
    
    /**
     * Sets the super.getLog() for this object.
     *
     * <p>
     * Setting the super.getLog() ensures proper logging of potential
     * errors. If not set, this class won't be able to log any
     * information. But it will still work.
     */
    public void enableLogging(org.apache.avalon.framework.logger.Logger logger) {
        if (Utilities.checkString(_id) && _id.indexOf("..") == -1)//avoiding empty category for logger name ".."
            this._logger = logger.getChildLogger(_id);
        else
            this._logger = logger;
    }

    public void contextualize(Context context) throws ContextException {
        this._context = new DefaultContext(context);
    }

    public void service(ServiceManager serviceManager) throws ServiceException {
        ConfigurationUtils.checkServiceManager(serviceManager);
        this._manager = serviceManager;
    }

    public void configure(Configuration configuration) throws ConfigurationException {
        ConfigurationUtils.checkConfiguration(configuration);
        verifyConfigurationResources();
        this._configuration = configuration;

        //setting the id with the default being the current value if already set externally by setId();
        try {
            setId(configuration.getAttribute(Identifiable.ConfigurationNode.ID, getId()));
        } catch (SDXException e) {
            throw new ConfigurationException(e.getMessage(), e);
        }
        //building the locale
        setLocale(Utilities.buildLocale(configuration, null));
        //xml:lang attribute is now required for an application
        setXmlLang(configuration.getAttribute(Localizable.ConfigurationNode.XML_LANG, getXmlLang()));
        //setting the string encoding for this object
        setEncoding(configuration.getAttribute(Encodable.ConfigurationNode.ENCODING, _encoding));
        //configuring the description xml file
        configureDescription(configuration);
    }

    protected void configureDescription(Configuration configuration) throws ConfigurationException {
    	String l_descriptionSrc = configuration.getAttribute(Describable.ConfigurationNode.DESCRIPTION_SRC, null);
    	if (!Utilities.checkString(l_descriptionSrc)) return; //no description-src attribute provided
    	try {
    		File l_descriptionFile = Utilities.resolveFile(null, configuration.getLocation(), getContext(), l_descriptionSrc, false);
    		String l_descriptionUrl = l_descriptionFile.toURI().toURL().toExternalForm();
    		// TODO_FileSource
    		setDescription(new FileSource(l_descriptionUrl));
    	} catch (SourceException e) {
    		throw new ConfigurationException(e.getMessage(), e);
    	} catch (SDXException e) {
    		throw new ConfigurationException(e.getMessage(), e);
    	} catch (MalformedURLException e) {
    		throw new ConfigurationException(e.getMessage(), e);
    	}

    }


    public void setId(String id) throws SDXException {
        if (!Utilities.checkString(id)) {
            String[] args = new String[1];
            args[0] = this.toString();
            throw new SDXException(getLog(), SDXExceptionCode.ERROR_INVALID_ID, args, null);
        }
        this._id = id;
    }

    public void setDescription(Source description) {
        this._description = description;
    }

    public void setEncoding(String encoding) {
        this._encoding = encoding;
    }

    public void setLocale(Locale locale) {
        this._locale = locale;
    }

    public void setXmlLang(String lang) {
        this._xmlLang = lang;
    }

    public Logger getLog() {
    	return this._logger;
    }

    protected DefaultContext getContext() {
        return this._context;
    }

    protected ServiceManager getServiceManager() {
        return this._manager;
    }

    protected Configuration getConfiguration() {
        return _configuration;
    }

    public Locale getLocale() {
        return this._locale;
    }

    public String getId() {
        return this._id;
    }

    public String getEncoding() {
        return this._encoding;
    }

    public Source getDescription() {
        return this._description;
    }

    /**Returns the xml:lang attibute value from the configuration*/
    public String getXmlLang() {
        if (Utilities.checkString(this._xmlLang))
            return _xmlLang;
        else
            return this.getLocale().getLanguage();
    }

    /**Returns an SAX Attributes object containing the xml:lang, id, locale, encoding
     * @return A SAX Attributes object
     */
    protected Attributes getBaseAttributes() {
        AttributesImpl l_baseAtts = new AttributesImpl();
        //adding xml:lang
        l_baseAtts.addAttribute(Node.Uri.XML, Node.Name.XML_LANG, Node.Name.XML_LANG, Node.Type.CDATA, String.valueOf(getXmlLang()));
        //adding the id
        l_baseAtts.addAttribute("", Node.Name.ID, Node.Name.ID, Node.Type.CDATA, String.valueOf(getId()));
        //adding locale
        l_baseAtts.addAttribute("", Node.Name.LOCALE, Node.Name.LOCALE, Node.Type.CDATA, String.valueOf(getLocale()));
        //adding encoding
        l_baseAtts.addAttribute("", Node.Name.ENCODING, Node.Name.ENCODING, Node.Type.CDATA, String.valueOf(getEncoding()));
        //adding the object toString id
        l_baseAtts.addAttribute("", Node.Name.OBJECT, Node.Name.OBJECT, Node.Type.CDATA, String.valueOf(this));
        return l_baseAtts;
    }

    /**Sub classes should element this method to return their class name suffix for SAX output*/
    protected abstract String getClassNameSuffix();

    /**Init the LinkedHashMap _xmlizable_objects with the objects in order to describ them in XML*/
	protected abstract boolean initToSax();

	/**Init the LinkedHashMap _xmlizable_volatile_objects with the objects in order to describ them in XML
	 * Some objects need to be refresh each time a toSAX is called*/
	protected abstract void initVolatileObjectsToSax();

	public void toSAX(ContentHandler contentHandler) throws SAXException {

        if (contentHandler == null) return;//we don't have a handler to feed
        if (!isToSaxInitialized)
        	isToSaxInitialized = initToSax();
        initVolatileObjectsToSax();


        try {
            String l_classNameSuffix = Utilities.getElementName(getClassNameSuffix());

            contentHandler.startElement(Node.Uri.SDX, l_classNameSuffix, Utilities.prefixNodeNameSDX(l_classNameSuffix), getBaseAttributes());

            //call sax on all sub-objects referenced in class member _xmlizable_objects
            Iterator keys = _xmlizable_objects.keySet().iterator();
            while(keys.hasNext()){
                String key = (String)keys.next();
                Object l_xmlizable_object = _xmlizable_objects.get(key);
                //the object is XMLizable we call it's toSAX method
	            if (l_xmlizable_object instanceof XMLizable)
                    ((XMLizable) l_xmlizable_object).toSAX(contentHandler);
                //if the object is a String we send it contained in an element named for its key
                else if (l_xmlizable_object instanceof String){
                    contentHandler.startElement(Node.Uri.SDX, key, Utilities.prefixNodeNameSDX(key), new AttributesImpl());
                        SAXUtils.sendElementContent(contentHandler, (String) l_xmlizable_object);
                    contentHandler.endElement(Node.Uri.SDX, key, Utilities.prefixNodeNameSDX(key));
                }
                //if the object in position is actually a java.util.Collection we iterate on its values
                //and use it's "key" String as the name for the element containing the sub-elements
                else if (l_xmlizable_object instanceof Collection) {
                    Collection l_collection = (Collection) l_xmlizable_object;
                    Iterator l_xmlizable_values = l_collection.iterator();
                    contentHandler.startElement(Node.Uri.SDX, key, Utilities.prefixNodeNameSDX(key), new AttributesImpl());
                    while (l_xmlizable_values.hasNext()) {
                        Object l_xmlizable_value = l_xmlizable_values.next();
                        if (l_xmlizable_value instanceof XMLizable)
                            ((XMLizable) l_xmlizable_value).toSAX(contentHandler);
                        else if (l_xmlizable_value instanceof String) {//we have String
                            contentHandler.startElement(Node.Uri.SDX, Node.Name.VALUE, Utilities.prefixNodeNameSDX(Node.Name.VALUE), new AttributesImpl());
                            SAXUtils.sendElementContent(contentHandler, (String) l_xmlizable_value);
                            contentHandler.endElement(Node.Uri.SDX, Node.Name.VALUE, Utilities.prefixNodeNameSDX(Node.Name.VALUE));
                        }

                    }
                    contentHandler.endElement(Node.Uri.SDX, key, Utilities.prefixNodeNameSDX(key));
                }
                else if (l_xmlizable_object instanceof Map) {
                	if(l_xmlizable_object!=null){
                		Map l_map = (Map) l_xmlizable_object;
	                    Iterator l_xmlizable_keys = l_map.keySet().iterator();
	                    contentHandler.startElement(Node.Uri.SDX, key, Utilities.prefixNodeNameSDX(key), new AttributesImpl());
	                    while (l_xmlizable_keys.hasNext()) {
	                        String l_mapKey = (String) l_xmlizable_keys.next();
	                        Object l_xmlizable_value = l_map.get(l_mapKey);
	                        if (l_xmlizable_value instanceof XMLizable){
	                            ((XMLizable) l_xmlizable_value).toSAX(contentHandler);
	                        }
	                        else if (l_xmlizable_value instanceof String){//we have String
	                            contentHandler.startElement(Node.Uri.SDX, l_mapKey, Utilities.prefixNodeNameSDX(l_mapKey), new AttributesImpl());
	                            SAXUtils.sendElementContent(contentHandler, (String)l_xmlizable_value);
	                            contentHandler.endElement(Node.Uri.SDX, l_mapKey, Utilities.prefixNodeNameSDX(l_mapKey));
	                        }
	                    }

                    	contentHandler.endElement(Node.Uri.SDX, key, Utilities.prefixNodeNameSDX(key));
                	}
                }
			}

	        //we send the object's description file XML
	        //if (_description != null) _description.toSAX(contentHandler);
	        if(_description != null) SourceUtil.toSAX(_description, contentHandler);

	        contentHandler.endElement(Node.Uri.SDX, l_classNameSuffix, Utilities.prefixNodeNameSDX(l_classNameSuffix));

        } catch (ProcessingException e) {
            throw new SAXException(e);
        }
		catch (IOException e) {
			throw new SAXException(e);
		}
    }


    protected synchronized SdxObject setUpSdxObject(SdxObject sdxObj) throws ConfigurationException {
        SdxObject l_sdxObj = sdxObj;
        l_sdxObj = Utilities.setUpSdxObject(sdxObj, getLog(), Utilities.createNewReadOnlyContext(getContext()), getServiceManager());
        //inheriting current objects fields
        l_sdxObj.setEncoding(getEncoding());
        l_sdxObj.setLocale(getLocale());
        l_sdxObj.setXmlLang(getXmlLang());
        return l_sdxObj;
    }

    protected synchronized SdxObject setUpSdxObject(SdxObject sdxObj, Configuration configuration) throws ConfigurationException {
        SdxObject l_sdxObj = sdxObj;
        l_sdxObj = setUpSdxObject(sdxObj);
        l_sdxObj.configure(configuration);
        return l_sdxObj;
    }

    protected void verifyConfigurationResources() throws ConfigurationException {
        try {
            ConfigurationUtils.checkLogger(getLog());
            ConfigurationUtils.checkContext(getContext());
            ConfigurationUtils.checkServiceManager(getServiceManager());
        } catch (ContextException e) {
            throw new ConfigurationException(e.getMessage(), e);
        } catch (ServiceException e) {
            throw new ConfigurationException(e.getMessage(), e);
        }

    }
}

