package org.eclipse.mddi.modelbus.adapter.infrastructure.serialize.modelbus_xmi;


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.mddi.modelbus.adapter.infrastructure.model_manipulation.ModelBusResourceSet;
import org.eclipse.mddi.modelbus.adapter.infrastructure.model_manipulation.ModelUtil;
import org.eclipse.mddi.modelbus.adapter.infrastructure.serialize.XmiUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;


/**
 * The model serializer that writes persistent object IDs to the XMI file.
 * It is used together with MBXmiResource, which is a resource 
 * that can associate a model witn a perststent ID table. 
 * <p><p>
 * This model serializer can also serialize the models contained in other kind of resources
 * In this case, the ID table must specified explicitely in the contructor.
 * 
 * 
 * @author P. Sriplakich
 *
 *
 */
public class ModelSerializer {
    
    static Logger logger = Logger.getLogger(ModelSerializer.class);
    
    public boolean option_omit_default_value = true;
    
    
//    /**
//     * By default,  the references to elements outside the collection will not be written.
//     * Setting this option TRUE is used for serializing with XMI diff. 
//     * 
//     */
//    public boolean option_includesReferenceToOutside = false;
    

    /**
     * The objects contained in the main resource will be serialized with short URI
     * (i.e. only the object IDs are written). The objects contained in other resources will be
     * serialized with full URI ( resource_URI#ID )
     */
    Resource mainResource;
    
    IdTable idTable = null;
    
    /**
     * The fragment of model to be serialized, 
     * null value means serializing the whole resource.
     */
    Set fragment = null;
        
    public ModelSerializer () {    
    }
    
    /**
     * prepare for serializing a model using the specified ID table.
     */
    public ModelSerializer (IdTable t) {
        idTable = t;    
    }
	
    
//	public String serializeAllInIdTable() {
//	    return serializeFragment(idTable.getModel());
//	}

    public String serializeFragment(Collection fragment) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            serializeFragment(fragment, out);
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
        return out.toString();
    }
    
    public void serializeFragment(Collection fragment, OutputStream out) throws IOException {    
        Document doc = XmiUtil.createDocument();
        Element docElem = doc.createElementNS(XmiUtil.XMI_NS,"xmi:XMI");
        docElem.setAttribute("xmlns:xmi", XmiUtil.XMI_NS);
        doc.appendChild(docElem);

//           long time;        
//           time = System.currentTimeMillis();
      
        serializeFragment(fragment, docElem);
           
//        time = System.currentTimeMillis() - time;
//        System.out.println(".... [SER]->DOM = " +time);
//        time = System.currentTimeMillis();
           
         XmiUtil.write(docElem,out);
        
//        time = System.currentTimeMillis() - time;
//        System.out.println(".... [SER]->txt = " +time +" (txt size=" +result.length() +")");
           
    }
    
    /**
     * 
     * serialize the fragment in the root XML element.
     * This operation can be used for serializing a fragment to an existing XML document
     * 
     * @param fragment
     * @param rootElem
     * 
     */
    public void serializeFragment(Collection fragment, Element rootElem) {
        if(fragment.isEmpty()) {
            logger.warn("fragment is empty");
            return;            
        }
        if(fragment instanceof Set) {
            this.fragment = (Set)fragment;
        } else {
            this.fragment = new HashSet(fragment);
        }
        EObject anObject = (EObject) this.fragment.iterator().next();
        mainResource = anObject.eResource();
        if(mainResource==null) {
            ModelBusResourceSet rs = new ModelBusResourceSet();
            mainResource = rs.createResource(
                    URI.createURI("uri1." + MBXmiResourceFactory.EXTENSION));
        }
        initIdTable();        
        Document doc = rootElem.getOwnerDocument();
        Collection writtenElems = new HashSet();
        for(Iterator it = this.fragment.iterator(); it.hasNext();) {
           EObject o = (EObject) it.next();
           EObject top = ModelUtil.getTopElemInFragment(o, this.fragment);
           if(!writtenElems.contains(top)) {
             EPackage p = top.eClass().getEPackage();
             String uri = p.getNsURI();
             String prefix = p.getNsPrefix();
             rootElem.setAttribute("xmlns:" +prefix, uri);
             Element xmlElem = doc.createElementNS(uri, prefix +":" +top.eClass().getName());  
             rootElem.appendChild(xmlElem);
             serializeThisElementAndChildren(top, xmlElem);
             writtenElems.add(top);
           }
        }        
    }    
    
    
    public void serializeResource(Resource r, OutputStream out) throws IOException {
        Document doc = XmiUtil.createDocument();
        Element docElem = doc.createElementNS(XmiUtil.XMI_NS,"xmi:XMI");
        docElem.setAttribute("xmlns:xmi", XmiUtil.XMI_NS);
        doc.appendChild(docElem);
        serializeResource(r, docElem);                      
        XmiUtil.write(docElem, out);     
    }

    public void serializeResource(Resource r, Element rootElem) {    
        mainResource = r;
        initIdTable();
        for(Iterator it = r.getContents().iterator(); it.hasNext();) {
            Document doc = rootElem.getOwnerDocument();
            EObject top = (EObject) it.next();
            EPackage p = top.eClass().getEPackage();
            String uri = p.getNsURI();
            String prefix = p.getNsPrefix();
            rootElem.setAttribute("xmlns:" +prefix, uri);
            Element xmlElem = doc.createElementNS(uri, prefix +":" +top.eClass().getName());  
            rootElem.appendChild(xmlElem);
            serializeThisElementAndChildren(top, xmlElem);
         }         
    }
    
    

    

   
    
    
    /**
     * 
     * Serializes 'o' and its subelements.
     * Only subelements that are in 'fragment' will be written.
     * 
     * 
     * @param o
     * @param fragment
     * @return
     * 
     */
    void serializeThisElementAndChildren(EObject o, Element xmlElem) {
        // assign ID
        String id = getURI(o);
        xmlElem.setAttributeNS(XmiUtil.XMI_NS, "xmi:id", id);
        
        
        // --------- for all attributes ----------------
        for(Iterator it = o.eClass().getEAllAttributes().iterator(); it.hasNext(); ) {
          EAttribute a = (EAttribute) it.next();
          if(a.isDerived() || a.isTransient() || !a.isChangeable()) {
            // do nothing
          } else {              
            serializeAttribute(o, a, xmlElem);
          }
        }
        // --------- for all references --------------------
        for(Iterator it = o.eClass().getEAllReferences().iterator(); it.hasNext(); ) {
          EReference r = (EReference) it.next();  
          if( !omitSerialize(r) ) {
            serializeReference(o, r, xmlElem);
          }
        } //--------- end for --------------
    }
    
    void serializeAttribute(EObject o, EAttribute a, Element xmlElem) {
        if(!a.isMany()) {              
            //serialize as an XML Attribute
            Object value = o.eGet(a);
            if(value!=null) {
                if(option_omit_default_value && 
                        a.getDefaultValue()!=null && 
                        value.equals(a.getDefaultValue())) {
                    // do nothing
                } else {
                  xmlElem.setAttribute(a.getName(), value.toString());
                }
            }
        } else {
            //serialize as a set of XML Elements
            List values = (List) o.eGet(a);
            Document doc = xmlElem.getOwnerDocument();
            for(int i=0; i<values.size(); i++) {
                Element subXmlElem = doc.createElement(a.getName());
                Object value = values.get(i);
                Text text = doc.createTextNode(value.toString());
                subXmlElem.appendChild(text);
                xmlElem.appendChild(subXmlElem);
            }
        }        
    }
    
    
    void serializeReference(EObject o, EReference r, Element xmlElem) {
          if(r.isContainment()) {
            // reference to the contents of this object
            if(!r.isMany()) {
                //serialize as an XML Element                         
                EObject value = (EObject) o.eGet(r);                  
                if(value!=null) {
                  if(fragment==null || fragment.contains(value)) {  
                    serializeReferenceValueAsContent(r, value, xmlElem);              
                  } 
                }
            } else {
                //serialize as a set of XML Elements          
                List values = (List) o.eGet(r);
                for(int i=0; i<values.size(); i++) {
                    EObject value = (EObject) values.get(i);  
                    if(fragment==null || fragment.contains(value)) {  
                        serializeReferenceValueAsContent(r, value, xmlElem);              
                    }             
                }
            }             
        
        } else {  // this is non-aggregation reference  
            if(!r.isMany()) {
                //serialize as an XML Element          
                EObject value = (EObject) o.eGet(r);                      
                if(value!=null) {
                    if(fragment==null || fragment.contains(value)) {
                      serializeReferenceValueAsLink(o.eResource(), r, value, xmlElem); 
                    }
                }
            } else {
                //serialize as a set of XML Elements          
                List values = (List) o.eGet(r);              
                for(int i=0; i<values.size(); i++) {
                    EObject value = (EObject) values.get(i);  
                    if(fragment==null || fragment.contains(value)) {
                      serializeReferenceValueAsLink(o.eResource(), r, value, xmlElem);
                    }
                }
            }                        
        }              
    }
    
    
    /**
     * 
     * serializeReferenceValueAsLink
     * If the reference is in the same resource as the container,
     * then the ID of this element is written.
     * If it is in a different resource, 
     * then resource#ID is written
     * 
     * 
     *
     */
    void serializeReferenceValueAsLink(Resource ownerResource, EReference r, EObject value, Element containerXmlElem) {                   
            Element subXmlElem = containerXmlElem.getOwnerDocument().createElement(r.getName());
            setXmiType(value.eClass(), subXmlElem);   
            String idref = getURI(value);
            subXmlElem.setAttribute("href", idref);
            //System.out.println("href=" +idref);
            containerXmlElem.appendChild(subXmlElem);

    }
    
    
    void serializeReferenceValueAsContent(EReference r, EObject value, Element containerXmlElem) {
            Element subXmlElem = containerXmlElem.getOwnerDocument().createElement(r.getName());
            containerXmlElem.appendChild(subXmlElem);
            setXmiType(value.eClass(), subXmlElem);
            serializeThisElementAndChildren(value, subXmlElem);   

    }
    
    
    static void setXmiType(EClass type, Element xmlElem) {
        EPackage p = type.getEPackage();
        String uri = p.getNsURI();
        String prefix = p.getNsPrefix();
        xmlElem.setAttributeNS(XmiUtil.XMI_NS, "xmi:type", prefix +":" +type.getName());    
        Document doc = xmlElem.getOwnerDocument();
        Element root = doc.getDocumentElement();
        root.setAttribute("xmlns:" +prefix, uri);
    }
    
    
    static boolean omitSerialize(EReference r) {
        if(r.isDerived() || r.isTransient() || !r.isChangeable() 
        || r.isContainer()) {
            return true; // omit
        }
        if(r.isContainment())  return false; // must serialize
        EReference op = r.getEOpposite();
        if(op==null) return false; // must serialize
        // choose to serialize the end that is "orderred".
        if(r.isOrdered()) return false; // must serialize
        if(op.isOrdered()) return true; // omit
        // choose to serialize the end that is higher alphabetically
        return r.getName().compareTo(op.getName())<0;        
    }
    

    /**
     * 
     * get the URI of the specified object.
     * If the object belongs the mainResource, 
     * the return the short form URI (just only ID).
     * Otherwise return the full URI (resourceURI # ID).
     * 
     * @param o
     * @return
     */
    String getURI(EObject o) {
        Resource r = getResource(o);
        if(r== mainResource) {
          return idTable.getOrCreateId(o);
        } 
        // serialize model elements in different resources but in the same fragment
        if(fragment!=null && fragment.contains(o)) {
            return idTable.getOrCreateId(o);
        }
        return r.getURI().toString() +"#" + r.getURIFragment(o);
    }
    
    /**
     * 
     * get the Resource containing o.
     * If no resource contains o, then put o in the main resource
     */
    Resource getResource(EObject o) {
        Resource r = o.eResource();
        if(r!=null) return r;
        EObject top = ModelUtil.getTopElement(o);
        mainResource.getContents().add(top);      
        return mainResource; 
    }
    
    
    
    /**
     * 
     * ensure that idTable!=null.
     * If idTable==null, then try to obtain the IdTable from mainResource.
     * If mainResource is not MBXmiResource, then create a new IdTable
     */
    void initIdTable() {
        if(idTable == null) {
            if(mainResource instanceof MBXmiResource) {
              idTable = ((MBXmiResource) mainResource).getIdTable();
            } else {
              idTable = new IdTable();
            }
        } else {
            if((mainResource instanceof MBXmiResource) &&
                    (((MBXmiResource) mainResource).getIdTable()!=idTable) ) {
                logger.warn("the IdTable of "+ mainResource.getURI() + " is not used");
            }
        }
    }
        

    


    
    /**
     * @return Returns the idTable.
     */
    public IdTable getIdTable() {
        return idTable;
    }
    /**
     * @param idTable The idTable to set.
     */
    public void setIdTable(IdTable idTable) {
        this.idTable = idTable;
    }
    

    
}
