import java.awt.Point;
import java.io.*;
import java.util.*;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


/* Encapsulating class holds all relevant data loaded from a PNML file.
 * Can also save any changes to transitions' onFire event back to the file.
 */
public class PnmlData {
	
	private final String fileName;
	private final Document pnmlDoc;
	private final Element docRoot;
	private final Element netRoot;
	final PNPlace[] Places;
	final PNTransition[] Transitions;
	final ArcData[] Arcs;
	
	
	public PnmlData(String xmlFile){
		pnmlDoc = parseXmlFile(fileName = xmlFile);
		docRoot = pnmlDoc.getDocumentElement();
		if(!docRoot.getNodeName().equals("pnml"))
			throw new RuntimeException(xmlFile + " does not have a root <pnml> element.");
		try{
			netRoot = (Element)docRoot.getElementsByTagName("net").item(0);
		}catch(Throwable t){ 
			throw new RuntimeException("PNML document " + xmlFile + " has no <net>s defined.");
		}
		NodeList placeList = netRoot.getElementsByTagName("place");
		NodeList transList = netRoot.getElementsByTagName("transition");
		NodeList arcList = netRoot.getElementsByTagName("arc");
		
		Places = new PNPlace[placeList.getLength()];
		Transitions = new PNTransition[transList.getLength()];
		Arcs = new ArcData[arcList.getLength()];
		//places
		for(int i=0; i<Places.length; i++) {
			Element p = (Element)placeList.item(i);
			Places[i] = new PNPlace(p);
		}
		//arcs, done first to properly init transitions
		for(int i=0; i<Arcs.length; i++){
			Element e = (Element)arcList.item(i);
			NodeList gfx = e.getElementsByTagName("graphics");
			if(gfx.getLength() > 0) 
				Arcs[i] = new ArcData(e.getAttribute("source"), e.getAttribute("target"), getPointsFromGraphics((Element)gfx.item(0)));
			else
				Arcs[i] = new ArcData(e.getAttribute("source"), e.getAttribute("target"), new Point[0]);
		}
		//transitions
		ArrayList<String> inp = new ArrayList<String>(); 
		ArrayList<String> outp = new ArrayList<String>();
		String[] tmp = new String[0];
		for(int i=0; i<Transitions.length; i++){
			Element e = (Element)transList.item(i);
			String eid = e.getAttribute("id");
			inp.clear();
			outp.clear();
			for(int j=0; j<Arcs.length; j++)
				if(Arcs[j].SourceId.equals(eid))
					outp.add(Arcs[j].TargetId);
				else if(Arcs[j].TargetId.equals(eid))
					inp.add(Arcs[j].SourceId);
			Transitions[i] = new PNTransition(e, inp.toArray(tmp), outp.toArray(tmp));
		}
			
	}
	
	public DocumentBuilder getDocBuilder() {
		return db;
	}

	
	public Object getNodeById(String id) {
		for(PNPlace place : this.Places)
			if(place.Id.equals(id))
				return place;
		for(PNTransition trs : this.Transitions)
			if(trs.Id.equals(id))
				return trs;
		return null;
	}
	
	public String getNetId() {
		return netRoot.getAttribute("id");
	}

	@Override
	public String toString() {
		return "PnmlData [pnmlDoc=" + pnmlDoc + ", docRoot=" + docRoot
				+ ", netRoot=" + netRoot + ", Places="
				+ Arrays.toString(Places) + ", Transitions="
				+ Arrays.toString(Transitions) + ", Arcs="
				+ Arrays.toString(Arcs) + "]";
	}


	class ArcData {
		final String SourceId;
		final String TargetId;
		final Point[] InnerPoints;
		public ArcData(String src, String trg, Point[] pts) {
			SourceId = src;
			TargetId = trg;
			InnerPoints = pts;
		}
		@Override
		public String toString() {
			return "ArcData [SourceId=" + SourceId + ", TargetId=" + TargetId
					+ ", InnerPoints=" + Arrays.toString(InnerPoints) + "]";
		}
	}

	public static Point[] getPointsFromGraphics(Element pnmlGraphicsElement) {
		Element gfx = pnmlGraphicsElement;
		if(!gfx.getNodeName().equals("graphics"))
			throw new RuntimeException("getPointFromGraphics expected <graphics> Element, got " + gfx);
		Point[] p;
		try{
			NodeList pos = gfx.getElementsByTagName("position");
			p = new Point[pos.getLength()];
			for(int i=0; i<pos.getLength(); i++) {
				p[i] = new Point();
				p[i].x = Integer.parseInt(((Element)pos.item(i)).getAttribute("x"));
				p[i].y = Integer.parseInt(((Element)pos.item(i)).getAttribute("y"));
			}
		}catch(Throwable t){
			throw new RuntimeException("Required tag <position x=\"\" y=\"\"> not found as child of tag <graphics>.");
		}
		Point offset;
		try{
			Element off = (Element)gfx.getElementsByTagName("offset").item(0);
			offset = new Point();
			offset.x = Integer.parseInt(off.getAttribute("x"));
			offset.y = Integer.parseInt(off.getAttribute("y"));
		}catch(Throwable t){offset = null;} 
		if(offset != null && p.length > 1)
			throw new RuntimeException("getPointsFromGraphics cannot handle <offset> with multiple <position>s");
		else if(offset != null) {
			//p[0].x += offset.x;
			//p[0].y += offset.y;			
		}
		return p;
	}
	
	public static void appendXmlFragment(DocumentBuilder docBuilder, Node parent, String fragment) {
		Document doc = parent.getOwnerDocument();
		Node fragmentNode;
		try {
			fragmentNode = docBuilder.parse(new InputSource(new StringReader(fragment))).getDocumentElement();
		} catch (SAXException e) {
			e.printStackTrace();
			throw new RuntimeException("Error parsing xml", e);
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException("Error parsing xml", e);
		}
		fragmentNode = doc.importNode(fragmentNode, true);
		parent.appendChild(fragmentNode);
	}

	public void saveToFile() {
		Transformer transformer = null;
		try {
			transformer = TransformerFactory.newInstance().newTransformer();
		} catch (TransformerConfigurationException e1) {
			e1.printStackTrace();
			throw new RuntimeException("config", e1); 
		} catch (TransformerFactoryConfigurationError e1) {
			e1.printStackTrace();
			throw new RuntimeException("config", e1); 
		}
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");

		//initialize StreamResult with File object to save to file
		StreamResult result = new StreamResult(new StringWriter());
		DOMSource source = new DOMSource(this.pnmlDoc);
		try {
			transformer.transform(source, result);
		} catch (TransformerException e) {
			e.printStackTrace();
			throw new RuntimeException("idk.", e); 
		}

		String xmlString = result.getWriter().toString();
		//System.out.println(xmlString);
		try {
			BufferedWriter fOut = new BufferedWriter(new FileWriter(fileName));
			fOut.write(xmlString);
			fOut.close();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException("Could not save changes: " + e.getMessage(), e);
		}
		
	}
	
	private static DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	//Using factory get an instance of document builder
	private static DocumentBuilder db = null;
	
	private static Document parseXmlFile(String filename){
		//get the factory
		Exception exFail;
		try {
			if(db == null)
				db = dbf.newDocumentBuilder();
			//parse using builder to get DOM representation of the XML file
			Document dom = db.parse(filename);
			return dom;
		}catch(ParserConfigurationException pce) {
			exFail = pce;
			pce.printStackTrace();
		}catch(SAXException se) {
			exFail = se;
			se.printStackTrace();
		}catch(IOException ioe) {
			exFail = ioe;
			ioe.printStackTrace();
		}
		throw new RuntimeException("The xml file is malformed: " + exFail.getMessage(), exFail);
	}

}
