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

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

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.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xmi.ClassNotFoundException;
import org.eclipse.emf.ecore.xmi.PackageNotFoundException;
import org.eclipse.emf.ecore.xmi.XMIException;
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.model_manipulation.ModelWrapper;
import org.eclipse.mddi.modelbus.adapter.infrastructure.serialize.XmiUtil;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * @author P. Sriplakich
 *
 *
 */
public class ModelDeserializer {
    
    public static final String XMI_NS = "http://www.omg.org/XMI";
    
	public static DocumentBuilder docBuilder = null;
	static {
	    try {
            docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();  
        } catch (Exception e) {
            throw new RuntimeException("XML Parser cannot be initialized", e);
        }

	}
    
    
    private static Logger logger = Logger.getLogger("ModelDeserializer");
	
	/**
	 * The format used for deserializing 
	 */
	String format = "EMF";
	
    MBXmiResource resource;
	
    //IdTable idTable = null;	
    Collection createdModelElements = new Vector();
    
    // Information about links to be resolved after all elements have been created.
    List links = new Vector();
    
    public ModelDeserializer() {
        this(new MBXmiResource( URI.createURI("uri1")));
    }
    
    
	public ModelDeserializer (MBXmiResource res) {
        this.resource = res;
	    //idTable = t;    
	}
    
    public void deserialize(InputStream in) throws SAXException, IOException, XMIException  {
//      long time;        
//      time = System.currentTimeMillis();
      
      Document doc = XmiUtil.parseXml(in);
            
//      time = System.currentTimeMillis() - time;
//      System.out.println(".... [DESER]->DOM t=" +time + " (txt size=" +xmi.length() +")");
//      time = System.currentTimeMillis();
      
      createModelElementsFromRootXml(doc.getDocumentElement());
      resolveLinks();
      
//      time = System.currentTimeMillis() - time;
//      System.out.println(".... [DESER]->Model t=" +time +" (elems=" +createdModelElements.size() +")");
        
    }
    
    public void deserialize(String xmi) throws SAXException, IOException, XMIException  {
      ByteArrayInputStream in = new ByteArrayInputStream(xmi.getBytes());
      deserialize(in);     
    }
    
    
    /**
     * resolveLinks
     * 
     */   
    void resolveLinks() {
        ModelDeserializer.resolveLinks(links, resource);        
    }

    /**
     * This method enables us to extract the model that is encoded in a XML subtree 
     * 
     * @param rootElem
     * @throws XMIException
     * 
     *
     */
    public void createModelElementsFromRootXml(Element rootElem) throws XMIException {
        NodeList list = rootElem.getChildNodes();         
        for(int i=0; i<list.getLength(); i++) {
            if(list.item(i) instanceof Element) {
            Element xmlElem = (Element)list.item(i);    
            EObject o = createModelElementFrom(xmlElem);
            resource.getContents().add(o);
            }
        }        
    }
    
    /**
     * 
     * Create a new model element that is encoded in the specified XML element
     * 
     * @param xmlElem
     * @return the created model element
     * 
     * @throws XMIException
     * 
     */
    EObject createModelElementFrom(Element xmlElem) throws XMIException {
        String type = null;
        if(xmlElem.hasAttribute("xmi:type")) {
          type = xmlElem.getAttribute("xmi:type");  
        } else {
          type = xmlElem.getTagName();
        }                
        String prefix = getNsPrefix(type);
        String className = getLocalName(type);        
        String uri =lookupNamespace(xmlElem, prefix);
        EPackage p = getPackageFromURI(uri);
        if(p==null) {
            throw new PackageNotFoundException(uri +"(" +type +")", "element:" +xmlElem , 0,0);  
        }          
        EClass c = (EClass) (EClass) p.getEClassifier(className);
        if(c==null) {
            throw new ClassNotFoundException(className, null, "element:" +xmlElem , 0, 0);
        }
        // Create new model element
        ModelWrapper w = ModelWrapper.getRegistry(format);
        if(w==null) throw new XMIException("no factory found for format = " +format);        
        EObject o = w.create(c);
        // System.out.println("Created " +c.getName());
        createdModelElements.add(o);
        
        String xmiid = xmlElem.getAttribute("xmi:id"); 
        if(xmiid.equals("")) {
            printErrMessage("No id for " +xmlElem);                
        } else {
            resource.getIdTable().assignId(xmiid, o);              
        }
        setFeatures(o, xmlElem);
        return o;
    }     
    
    
    EPackage getPackageFromURI(String uri) {
        EPackage p = (EPackage) EPackage.Registry.INSTANCE.getEPackage(uri);
        if(p==null) {
           Resource r = resource.getResourceSet().getResource(URI.createURI(uri), true);
           ModelBusResourceSet.registerEPackages(r);
           p = (EPackage) EPackage.Registry.INSTANCE.getEPackage(uri);
        }
        return p;
    }
    
    
        
    /**
     * Set the features of the specified model element. 
     * The feature values are extracted from the specified XML element.
     * 
     * @param o The model element to be updated
     * @param xmlElem
     * @throws XMIException
     * 
     */
    void setFeatures(EObject o, Element xmlElem) throws XMIException {    
        EClass c = o.eClass();
        
        // for all XML attributes
        NamedNodeMap map = xmlElem.getAttributes();        
        for(int i=0; i<map.getLength(); i++) {
            Node n = map.item(i);
            String attName = n.getNodeName();
            String attNsPrefix = getNsPrefix(attName);
            String attLocalName = getLocalName(attName);
            String attValue = n.getNodeValue();

            if(attNsPrefix.length()>0) {
               if(attName.equals("xmi:id")) {
                  // do nothing
               } else if(attName.equals("xmi:type")) {
                  // do nothing   
               } else {
                  printErrMessage("Unknown attribute " +attName);    
               }            
            } else { // values of EAttribute
               EAttribute watt = (EAttribute) c.getEStructuralFeature(attName); 
               //ModelUtil.findStructuralFeature(c, attName);
               if(watt==null) {
                  printErrMessage("Unknown attribute " +attName);      
               } else {
                 Object value = EcoreUtil.createFromString( watt.getEAttributeType(), attValue);
                 o.eSet(watt, value);
               }
            }
        }        
                
        // for all XML subelements
        NodeList list = xmlElem.getChildNodes();
        for(int i=0; i<list.getLength(); i++) {
            if(!(list.item(i) instanceof Element)) continue;
            Element subXmlElem = (Element) list.item(i);      
            String elemName = subXmlElem.getTagName();
            EStructuralFeature f = c.getEStructuralFeature(elemName);
                // ModelUtil.findStructuralFeature(c, elemName);
            if(f==null) {
                printErrMessage("Unknown feature " +elemName);                   
            }           
            if(f instanceof EAttribute) {
                EAttribute watt = (EAttribute) f;
                String text = subXmlElem.getFirstChild().getNodeValue();
                Object value = EcoreUtil.createFromString( watt.getEAttributeType(), text);
                if(watt.isMany()) {
                    List l = (List) o.eGet(watt);  
                    l.add(value);
                } else {
                    o.eSet(watt, value);    
                }                
                
            } else if(f instanceof EReference) {
                EReference r = (EReference) f;                
                if( subXmlElem.hasAttribute("xmi:action") && subXmlElem.hasAttribute("href") ) {
                    updateReference(o, r, subXmlElem);
                
                } else if(subXmlElem.hasAttribute("href")) {
                   String idref = subXmlElem.getAttribute("href");
                   links.add(
                      new Link(r, o, idref)
                   );
                } else {
                   EObject value = createModelElementFrom(subXmlElem);
                   if(r.isMany()) {
                     List l = (List) o.eGet(r);  
                     l.add(value);
                   } else {
                     o.eSet(r, value);    
                   }
                }
            }
        }        
    }
    
    
    void updateReference(EObject o, EReference r, Element refXmlElem) {
        String action = refXmlElem.getAttribute("xmi:action");
        String id = refXmlElem.getAttribute("href");
        EObject referencedElem = resource.getIdTable().getModelElement(id);
        List l = (List) o.eGet(r);
        if(action.equals("add")) {
            l.add(referencedElem);            
        } else if(action.equals("remove")) {
            l.remove(referencedElem);        
        }        
    }
    
    
    
    
    static String getNsPrefix(String colontag) {
        return (colontag.indexOf(':')<0)? "" : colontag.substring(0, colontag.indexOf(':'));
    }
    
    static String getLocalName(String colontag) {
        return colontag.substring(colontag.indexOf(':')+1);
    }
    
    
    /**
     * 
     * Returns the namespace URI
     * 
     * @param xmlElem
     * @param prefix
     * 
     */
    static String lookupNamespace(Element xmlElem, String prefix) {
        String attName = (prefix.length()==0)? "xmlns": "xmlns:"+prefix;
        Element topElem = xmlElem;
        String uri = topElem.getAttribute(attName);
        while(uri.equals("") && topElem.getParentNode() instanceof Element) {
            topElem = (Element) topElem.getParentNode();
            uri = topElem.getAttribute("xmlns:"+prefix);
        }        
        return uri;
    }
    
    static void printErrMessage(String s) {
        logger.error(s);
    }
	

    /**
     * Returns the collection containing all created model elements (flatten)
     */
    public Collection getCreatedModelElements() {
        return createdModelElements;
    }
    
    
    static class Link {
        EReference eReference;
        EObject modelElement;
        String href;
        Link( EReference r, EObject o, String href) {
            this.eReference = r;
            this.modelElement = o;
            this.href = href;
        }
        
        
    }   
    
    /**
     * @return Returns the format.
     */
    public String getFormat() {
        return format;
    }
    /**
     * @param format The format to set.
     */
    public void setFormat(String format) {
        this.format = format;
    }



    /**
     * Resolves the links once all model elements have been created 
     * 
     *
     */
    static void resolveLinks(Collection links, MBXmiResource res) {
        for(Iterator it= links.iterator(); it.hasNext(); ) {
            Link link = (Link) it.next();
            String id = ModelDeserializer.getIdPart(link.href);
            String uri = ModelDeserializer.getUriPart(link.href);
            EObject value = null;
            ResourceSet rs = null;
            Resource refRes = null;
            if(uri==null) {
              value = res.getIdTable().getModelElement(id);
            } else {
              rs = res.getResourceSet();
              refRes = rs.getResource(URI.createURI(uri) , true);
              value = refRes.getEObject(id);
            }
            if(value==null) {
                printErrMessage("Element not found href=" +link.href);
                if(rs!=null) {
                  printErrMessage("RS: " +rs.toString());
                  printErrMessage("Resource: " +refRes.toString() +" " + refRes.getContents()); 
                  
                }
            } else if(link.eReference.isMany()) {
               List values = (List) link.modelElement.eGet(link.eReference);
               values.add(value);
            } else {
               link.modelElement.eSet(link.eReference, value); 
            }
        }
        links.clear();
    }
    
    public static String getUriPart(String href) {
        int i = href.indexOf((int)'#');
        if(i==-1) {
            return null;
        }
        return href.substring(0, i);
    }

    public static String getIdPart(String href) {
        int i = href.indexOf((int)'#');
        if(i==-1) {
            return href;
        }
        return href.substring(i +1, href.length());            
    }


    /**
     * @return Returns the resource.
     */
    public MBXmiResource getResource() {
        return resource;
    }


    /**
     * @param resource The resource to set.
     */
    public void setResource(MBXmiResource resource) {
        this.resource = resource;
    }   
}
