/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.file;

import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.cburch.draw.model.AbstractCanvasObject;
import com.cburch.logisim.LogisimVersion;
import com.cburch.logisim.Main;
import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.Wire;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.comp.ComponentFactory;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeDefaultProvider;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.tools.Tool;
import com.cburch.logisim.util.InputEventUtil;
import com.cburch.logisim.util.StringUtil;

class XmlWriter {
	static void write(LogisimFile file, OutputStream out, LibraryLoader loader)
			throws ParserConfigurationException,
				TransformerConfigurationException, TransformerException {
		DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder = docFactory.newDocumentBuilder();

		Document doc = docBuilder.newDocument();
		XmlWriter context = new XmlWriter(file, doc, loader);
		context.fromLogisimFile();
		
		TransformerFactory tfFactory = TransformerFactory.newInstance();
		try {
			tfFactory.setAttribute("indent-number", Integer.valueOf(2));
		} catch (IllegalArgumentException e) { }
		Transformer tf = tfFactory.newTransformer();
		tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		tf.setOutputProperty(OutputKeys.INDENT, "yes");
		try {
			tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", 
					"2");
		} catch (IllegalArgumentException e) { }

		Source src = new DOMSource(doc);
		Result dest = new StreamResult(out);
		tf.transform(src, dest);
	}

	private LogisimFile file;
	private Document doc;
	private LibraryLoader loader;
	private HashMap<Library,String> libs = new HashMap<Library,String>();

	private XmlWriter(LogisimFile file, Document doc, LibraryLoader loader) {
		this.file = file;
		this.doc = doc;
		this.loader = loader;
	}

	Element fromLogisimFile() {
		Element ret = doc.createElement("project");
		doc.appendChild(ret);
		ret.appendChild(doc.createTextNode("\nThis file is intended to be "
				+ "loaded by Logisim (http://www.cburch.com/logisim/).\n"));
		ret.setAttribute("version", "1.0");
		ret.setAttribute("source", Main.VERSION_NAME);

		for (Library lib : file.getLibraries()) {
			Element elt = fromLibrary(lib);
			if (elt != null) ret.appendChild(elt);
		}

		if (file.getMainCircuit() != null) {
			Element mainElt = doc.createElement("main");
			mainElt.setAttribute("name", file.getMainCircuit().getName());
			ret.appendChild(mainElt);
		}

		ret.appendChild(fromOptions());
		ret.appendChild(fromMouseMappings());
		ret.appendChild(fromToolbarData());

		for (Circuit circ : file.getCircuits()) {
			ret.appendChild(fromCircuit(circ));
		}
		return ret;
	}

	Element fromLibrary(Library lib) {
		Element ret = doc.createElement("lib");
		if (libs.containsKey(lib)) return null;
		String name = "" + libs.size();
		String desc = loader.getDescriptor(lib);
		if (desc == null) {
			loader.showError("library location unknown: "
				+ lib.getName());
			return null;
		}
		libs.put(lib, name);
		ret.setAttribute("name", name);
		ret.setAttribute("desc", desc);
		for (Tool t : lib.getTools()) {
			AttributeSet attrs = t.getAttributeSet();
			if (attrs != null) {
				Element toAdd = doc.createElement("tool");
				toAdd.setAttribute("name", t.getName());
				addAttributeSetContent(toAdd, attrs, t);
				if (toAdd.getChildNodes().getLength() > 0) {
					ret.appendChild(toAdd);
				}
			}
		}
		return ret;
	}

	Element fromOptions() {
		Element elt = doc.createElement("options");
		addAttributeSetContent(elt, file.getOptions().getAttributeSet(), null);
		return elt;
	}

	Element fromMouseMappings() {
		Element elt = doc.createElement("mappings");
		MouseMappings map = file.getOptions().getMouseMappings();
		for (Map.Entry<Integer,Tool> entry : map.getMappings().entrySet()) {
			Integer mods = entry.getKey();
			Tool tool = entry.getValue();
			Element toolElt = fromTool(tool);
			String mapValue = InputEventUtil.toString(mods.intValue());
			toolElt.setAttribute("map", mapValue);
			elt.appendChild(toolElt);
		}
		return elt;
	}

	Element fromToolbarData() {
		Element elt = doc.createElement("toolbar");
		ToolbarData toolbar = file.getOptions().getToolbarData();
		for (Tool tool : toolbar.getContents()) {
			if (tool == null) {
				elt.appendChild(doc.createElement("sep"));
			} else {
				elt.appendChild(fromTool(tool));
			}
		}
		return elt;
	}

	Element fromTool(Tool tool) {
		Library lib = findLibrary(tool);
		String lib_name;
		if (lib == null) {
			loader.showError(StringUtil.format("tool `%s' not found",
				tool.getDisplayName()));
			return null;
		} else if (lib == file) {
			lib_name = null;
		} else {
			lib_name = libs.get(lib);
			if (lib_name == null) {
				loader.showError("unknown library within file");
				return null;
			}
		}

		Element elt = doc.createElement("tool");
		if (lib_name != null) elt.setAttribute("lib", lib_name);
		elt.setAttribute("name", tool.getName());
		addAttributeSetContent(elt, tool.getAttributeSet(), tool);
		return elt;
	}

	Element fromCircuit(Circuit circuit) {
		Element ret = doc.createElement("circuit");
		ret.setAttribute("name", circuit.getName());
		addAttributeSetContent(ret, circuit.getStaticAttributes(), null);
		if (!circuit.getAppearance().isDefaultAppearance()) {
			Element appear = doc.createElement("appear");
			for (Object o : circuit.getAppearance().getObjectsFromBottom()) {
				if (o instanceof AbstractCanvasObject) {
					Element elt = ((AbstractCanvasObject) o).toSvgElement(doc);
					if (elt != null) {
						appear.appendChild(elt);
					}
				}
			}
			ret.appendChild(appear);
		}
		for (Wire w : circuit.getWires()) {
			ret.appendChild(fromWire(w));
		}
		for (Component comp : circuit.getNonWires()) {
			Element elt = fromComponent(comp);
			if (elt != null) ret.appendChild(elt);
		}
		return ret;
	}

	Element fromComponent(Component comp) {
		ComponentFactory source = comp.getFactory();
		Library lib = findLibrary(source);
		String lib_name;
		if (lib == null) {
			loader.showError(source.getName() + " component not found");
			return null;
		} else if (lib == file) {
			lib_name = null;
		} else {
			lib_name = libs.get(lib);
			if (lib_name == null) {
				loader.showError("unknown library within file");
				return null;
			}
		}

		Element ret = doc.createElement("comp");
		if (lib_name != null) ret.setAttribute("lib", lib_name);
		ret.setAttribute("name", source.getName());
		ret.setAttribute("loc", comp.getLocation().toString());
		addAttributeSetContent(ret, comp.getAttributeSet(), comp.getFactory());
		return ret;
	}

	Element fromWire(Wire w) {
		Element ret = doc.createElement("wire");
		ret.setAttribute("from", w.getEnd0().toString());
		ret.setAttribute("to", w.getEnd1().toString());
		return ret;
	}

	void addAttributeSetContent(Element elt, AttributeSet attrs,
			AttributeDefaultProvider source) {
		if (attrs == null) return;
		LogisimVersion ver = Main.VERSION;
		if (source != null && source.isAllDefaultValues(attrs, ver)) return;
		for (Attribute<?> attrBase : attrs.getAttributes()) {
			@SuppressWarnings("unchecked")
			Attribute<Object> attr = (Attribute<Object>) attrBase;
			Object val = attrs.getValue(attr);
			if (attrs.isToSave(attr) && val != null) {
				Object dflt = source == null ? null : source.getDefaultAttributeValue(attr, ver);
				if (dflt == null || !dflt.equals(val)) {
					Element a = doc.createElement("a");
					a.setAttribute("name", attr.getName());
					String value = attr.toStandardString(val);
					if (value.indexOf("\n") >= 0) {
						a.appendChild(doc.createTextNode(value));
					} else {
						a.setAttribute("val", attr.toStandardString(val));
					}
					elt.appendChild(a);
				}
			}
		}
	}

	Library findLibrary(Tool tool) {
		if (libraryContains(file, tool)) {
			return file;
		}
		for (Library lib : file.getLibraries()) {
			if (libraryContains(lib, tool)) return lib;
		}
		return null;
	}

	Library findLibrary(ComponentFactory source) {
		if (file.contains(source)) {
			return file;
		}
		for (Library lib : file.getLibraries()) {
			if (lib.contains(source)) return lib;
		}
		return null;
	}

	boolean libraryContains(Library lib, Tool query) {
		for (Tool tool : lib.getTools()) {
			if (tool.sharesSource(query)) return true;
		}
		return false;
	}
}
