package nl.nikhef.tools.xml;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

public class FXML {

	private FXMLElement _root;
	
	public FXML(Reader r) throws XMLStreamException {
		readInternal(r);
	}
	
	public FXML(URL resource) throws IOException, XMLStreamException {
		Reader r = new InputStreamReader(resource.openStream());
		readInternal(r);
		r.close();
	}

	public FXMLElement getRoot() 
	{
		return _root;
	}
	
	public void loadOverlay(URL resource) throws XMLStreamException, IOException
	{
		FXML overlay = new FXML(resource);
	
		FXMLElement xml = overlay.getRoot();
		merge(xml);
	}
	
	private void merge(FXMLElement xml) 
	{
		if (!xml.tag.equals("overlay")) throw new RuntimeException("Overlay should have root element 'overlay'");
		
		for (FXMLElement child : xml.children())
		{
			if (!child.hasAttribute("id")) throw new RuntimeException("Children of 'overlay' must have ID to link");
			String id = child.getAttibute("id");
			
			FXMLElement toMerge = findById(id);
			mergeElements(toMerge, child);
		}
	}
	
	

	private void mergeElements(FXMLElement mergeInto, FXMLElement mergeFrom) 
	{
		// First merge attributes
		for (Map.Entry<String, String> att : mergeFrom.attributes().entrySet())
		{
			mergeInto.setAttribute(att.getKey(), att.getValue());
		}
		
		if (mergeFrom.hasChars()) {
			if (mergeInto.hasChildren()) throw new RuntimeException("Can't merge, target has children, source has char data!");
			mergeInto.setChars(mergeFrom.getChars());
		} else if (mergeFrom.hasChildren()) 
		{
			if (mergeInto.hasChars()) throw new RuntimeException("Can't merge, target has char data, source has children!");
			// Them merge children:
			for (FXMLElement fromChild : mergeFrom.children())
			{
				if (fromChild.hasAttribute("name") && mergeInto.getChildByName(fromChild.getAttibute("name")) != null) 
				{
					mergeElements(mergeInto.getChildByName(fromChild.getAttibute("name")), fromChild);					
				} else {
					mergeInto.addChild(fromChild);
				}
			}
		}
		
		
		
	}

	public FXMLElement findById(String id) 
	{
		if (_root.hasAttribute("id") && _root.getAttibute("id").equals(id)) return _root;
		return findById(id, _root);
	}

	private FXMLElement findById(String id, FXMLElement root)
	{
		for (FXMLElement child : root.children())
		{
			if (child.hasAttribute("id") && child.getAttibute("id").equals(id)) return child;
			if (child.children().size() > 0) {
				FXMLElement found = findById(id, child);
				if (found != null) return found;
			}
		}
			
		return null;
	}
		

	private void readInternal(Reader r) throws XMLStreamException
	{
		XMLInputFactory f = XMLInputFactory.newInstance();
		XMLEventReader er = f.createXMLEventReader(r);
		LinkedList<FXMLElement> stack = new LinkedList<FXMLElement>();

		FXMLElement cur = null;
		
		while(er.hasNext()) {
			XMLEvent event = er.nextEvent();
			
			switch (event.getEventType())
			{
			case XMLEvent.START_ELEMENT:
			{
				StartElement se = event.asStartElement();
				String name = se.getName().toString();
				
				
				FXMLElement newCur = new FXMLElement(name);
				
				if (cur != null) {
					stack.push(cur);
					cur.addChild(newCur);
				}
				
				cur = newCur;
				
				if (_root == null) _root = cur;
				
				Iterator<?> it = se.getAttributes();
				while (it.hasNext()) 
				{
					Attribute attr = (Attribute)it.next();
					String attName = attr.getName().toString();
					String attValue = attr.getValue();
					cur.setAttribute(attName, attValue);
				}
				
			}
			break;
			case XMLEvent.CDATA:
			case XMLEvent.CHARACTERS:
				Characters chars = event.asCharacters();
				
				String result = chars.getData().trim();
				if (result.length() == 0) break;
				
				cur.setChars(result);
				
			break;
			case XMLEvent.END_ELEMENT:
			{
				if (stack.size() > 0) {
					cur = stack.pop();
				} else {
					cur = null;
				}
			}
			break;
			case XMLEvent.END_DOCUMENT:
			case XMLEvent.START_DOCUMENT:
			case XMLEvent.COMMENT: 
				break;
			default:
				System.out.printf("Unprocessed event! %s (type=%^s)\n", event, event.getClass().getSimpleName());
			}
			
		}
	}
	
	public void parseOverlay(Reader r)
	{
		
	}
	
	
}
