package diffpatch;

import java.io.*;
import java.util.*;
import org.xml.sax.*;
import org.w3c.dom.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;

import org.w3c.dom.events.*;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.events.MutationEvent.*;

import editor.ShapeMgr;

/* The basic idea is as follows:
   1) The synchronization message sent by the peer is parsed using SAX;
   2) The listener used in XMLDiff is de-registered and a separate listener is 
      (PathListener) used to track the events, that are fired when the app-DOM 
      is processed, during 1).
   3) Application specific code should be added in the PatchListener.
   4) The PatchListener is removed after processing the XML input stream.
*/     

/** Class XMLPatch is used to complement the functions of XMLDiff;
 *  It uses SAX to parse the synchronization message sent by the peer
 *  and uses a separate listener to track the events generated, to
 *  do some appilcation specific operations.
 */  
public class XMLPatch extends DefaultHandler {

  Document appDOMDoc; //The Document object of the app-DOM.

  /* refName, refPath - the name and path of the reference node
   *                    (related node of the target node).
   * affectName, affectPath - the name and path of the affected node
   *                          (target node).
   */                          
  String refName = null, affectName = null, parentPath = null, 
         refPath = null, affectPath = null; // gives the name of the refnode(parent, sibling) 
  boolean isParent = false;


  ShapeMgr shMgr;
  XMLDiff domDiff;
  PatchListener patchListener;


  public XMLPatch(Document appDom, ShapeMgr shapemgr, 
                  XMLDiff xmldiff) {
  
   appDOMDoc = appDom;
   shMgr = shapemgr;
   domDiff = xmldiff;
   patchListener = new  PatchListener(shMgr);

  } 

  public void addPatchListener() {
    ((EventTarget) appDOMDoc).addEventListener("DOMNodeInserted", patchListener,
					        false);
    ((EventTarget) appDOMDoc).addEventListener("DOMAttrModified", patchListener, 
					       false);
    ((EventTarget) appDOMDoc).addEventListener("DOMNodeRemoved", patchListener, 
					       false);
  }
    
   public void removePatchListener() { 
    ((EventTarget) appDOMDoc).removeEventListener("DOMNodeInserted", patchListener, 
						  false);
    ((EventTarget) appDOMDoc).removeEventListener("DOMNodeRemoved", patchListener, 
						  false);
    ((EventTarget) appDOMDoc).removeEventListener("DOMAttrModified", patchListener, 
						  false);
   }
 
  public void beginParsing(InputStream networkIn ) {

   DiffListener diffListener = domDiff.getDiffListener(); 
   domDiff.removeDiffListener();
   addPatchListener(); 
   SAXParserFactory fac = SAXParserFactory.newInstance();
   try { 
      SAXParser saxparser = fac.newSAXParser();
      //Parse the syncnronization message.
      saxparser.parse(networkIn, this);
   }
   catch (Throwable t) {
     t.printStackTrace();
   }
 
   removePatchListener(); 
   domDiff.addDiffListener();  
  }

  public void startElement(String namespaceURI,
                           String sName, // simple name
                           String qName, // qualified name
                           Attributes attrs)
  throws SAXException  { 
     NamedNodeMap attrnodeMap; 
     /* Elements of consideration while parsing are, children of the <action-list>
        like <node-insert>, <attr-XX>.
     */
  
     if ( qName.startsWith("node") || qName.startsWith("attr") ) { 
       processActionNode(qName, attrs);                    
     }
     else if ( !qName.startsWith("diff") && !qName.equals("action-list") ) { 
      // The element that is "affected, the target node.      
      affectPath = attrs.getValue("new"); refPath = attrs.getValue("ref"); 
      isParent = isParentChild(affectPath, refPath);
     }
   }


  public void endElement(String namespaceURI,
                         String sName, // simple name
                         String qName // qualified name 
                        ) 
         throws SAXException {
   		 
  } 

  public void startDocument()
         throws SAXException {
    		 
  }

  public void endDocument()
         throws SAXException {
   
      //Debugging: print the diff doc here..
      DiffDOMDoc diff = new DiffDOMDoc();
      diff.printDiffDom(appDOMDoc);
  } 
 
  public void characters(char buf[], int offset, int len)
         throws SAXException {
     ; 
  }
  
  /* Method takes the Xpath of the affected-node, reference-node
     and returns true if they are parent-child, else false.
  */   
  public boolean isParentChild(String affpath, String refpath ) {
    int affLastSlash = parseXPath(affpath, true),
        affPrevSlash = parseXPath(affpath, false),
        refLastSlash = parseXPath(refPath, true),
        refPrevSlash = parseXPath(refpath, false); 
    boolean ret = false, isParentChild = false;
    String parentInAff = null, parentInRef = null;
    /* affpath is always of the form: "/XX/YY.." i-e:- "/Root/A" etc.
       refpath is of the form "/" OR "/XX/YY.."
    */

   if ( affLastSlash == 0 ) {
      parentInAff = "doc";
      parentInRef = "doc";
    }
    else {
      parentInAff = affpath.substring(affPrevSlash+1, affLastSlash);
    }
    affectName = affpath.substring(affLastSlash+1, affpath.indexOf('['));

    /* The PatchListener takes the target-node's name and conveys it 
       to the application
    */   
    patchListener.setAffectedNodeName(affpath.substring(affLastSlash+1,
			                                affpath.length()));
    if (refpath.equals("/") ){
      isParentChild = true;
    }
    else {
      if ( refLastSlash != 0 )  {
       parentInRef = refpath.substring(refPrevSlash+1, refLastSlash);
      }
      else {
        parentInRef = "doc";
      }
    }
    if (parentInAff.equals(parentInRef) && !isParentChild ) {
       parentPath = affpath.substring(0, affLastSlash);
       ret = false;  //its sibling.
    }
    else {
       parentPath = refpath; 
       ret =true; //its parent-child
    }
    return(ret);    
  } //isParentChild


  //Debugging info
  public void printIsParent(String affName, String parentpath) {
   System.out.println("affectName: " + affectName +   
                     "|parentPath: " + parentPath );
  } 

  /* Returns the index of the "/" depending on the 
     input argument.
  */   
  public int parseXPath(String xpath, boolean isLast) {
    int slashIndex = xpath.lastIndexOf('/');
     if ( slashIndex == 0) 
        return(0);
     else {
        if ( isLast ) {
         return (slashIndex);
        } 
        else {
           //Could be of the form "/Root", "/".
           return (xpath.substring(0, slashIndex)).lastIndexOf('/');
        } //xpath.lastIndexOf('/') 
     } 
  } 

 /* Processes the elements in the <action-list> node;
    The action node could be <atteXX> or <node-insert>, etc.
 */   
 public void processActionNode(String elementName, Attributes attrlist) {  
    
    Node affectNode = null, refNode = null, parentNode = null;
    if (elementName.startsWith("attr"))  { 
       String qAttrName,attrVal; 
       patchListener.setAttributesCount(attrlist.getLength());
       for(int i= 0; i< attrlist.getLength(); i++) { 
          qAttrName = attrlist.getQName(i);
          affectNode = getNodeReference(affectPath);
         if ( elementName.equals(DiffNodeInfo.ACTION_ATTRADD) || 
	      elementName.equals(DiffNodeInfo.ACTION_ATTRMODIFY) )  {
            ((Element)affectNode).setAttribute(qAttrName, 
                                  attrlist.getValue(qAttrName)) ; 
         }
         else  {
            ((Element)affectNode).removeAttribute(qAttrName);
         }
      } //for  
   } //if 
   else  {
    /* Handle the case of <node-XX>. */ 
       parentNode = getNodeReference(parentPath);
       /*  If its ACTION_INSERT then a new node is created only 
           the first time, as no reference exists for it.
       */	   
       if (elementName.equals(DiffNodeInfo.ACTION_INSERT) ) {
         parentNode.appendChild(appDOMDoc.createElement(affectName));     
       }
       else if (elementName.equals(DiffNodeInfo.ACTION_REMOVE) ) {
         affectNode = getNodeReference(affectPath);
         parentNode.removeChild(affectNode);
       }
       else if (elementName.equals(DiffNodeInfo.ACTION_INSERTBEFORE) ) {
         parentNode.insertBefore(getNodeReference(affectPath), 
			         getNodeReference(refPath));
       } 
       else if (elementName.equals(DiffNodeInfo.ACTION_REPLACE) ) {  
          parentNode.replaceChild(getNodeReference(affectPath),  
			          getNodeReference(refPath));
       }
   } //else 
 } 

// Obtains the node reference of the given Xpath expression.
public Node getNodeReference(String xpath) {
  StringTokenizer tokenizer = new StringTokenizer(xpath, "/");
  Node retNode  = (Node)appDOMDoc;
  String nodeName = null, name;
  int    nodePos = 0;
  boolean isFormatted = false;
  if (xpath.equals("/")) {  //The case if parentPath is "/"
     return(retNode);
  }
  else {
    while ( tokenizer.hasMoreTokens() ) {
      name = tokenizer.nextToken();
      if (name.endsWith("]")) {
	 //Strip the []s to get the node name and the count within it.    
         nodeName = name.substring(0, name.indexOf('[')); 
	 nodePos  = Integer.parseInt(name.substring(
				             name.indexOf('[') + 1, name.lastIndexOf(']')));
	 isFormatted = true;
      }
      NodeList list = retNode.getChildNodes();
      if (!isFormatted) { 
        for(int loop=0; loop < list.getLength(); loop++) { 
          if (list.item(loop).getNodeName().equals(name) ) {
            retNode = list.item(loop);
            break;
	  }  
        } //for
      } //if	
      else {
        retNode = list.item(nodePos);       
      } // else
    } //while  
   return(retNode);	
 } //else
}

/* public int extractNodePos(String nodename) {
   int nodePos  = Integer.parseInt(nodename.substring(nodename.indexOf('[') + 1, 
			                      nodename.lastIndexOf(']')));
   return (nodePos);
} */
  
} //XMLPatch 
