package com.tandbergtv.metadatamanager.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.tandbergtv.metadatamanager.factoryImpl.RelationFactory;
import com.tandbergtv.metadatamanager.model.Asset;
import com.tandbergtv.metadatamanager.model.Group;
import com.tandbergtv.metadatamanager.model.Item;
import com.tandbergtv.metadatamanager.model.Relation;
import com.tandbergtv.metadatamanager.specimpl.ttv.TTVId;

/**
 * Binder - performs bidirectional converts between TTV xml into and Lists of
 * top level Asset objects.
 * 
 * @author nicholas
 * 
 */
public class Binder {

	// TODO: handle assets without ids
	private static final String TTVSPEC = "tns:TTVSpec",
			GROUP = "tns:Group",
			FIELDS = "tns:Fields",
			TTVID = "tns:TTVId",
			ID = "tns:Id",
			RELATION = "tns:Relation",
			TARGET = "tns:Target",
			ITEM = "tns:Item",
			TYPE = "type",
			XMLNS_TNS_NAME = "xmlns:tns",
			XMLNS_TNS_VALUE = "http://www.tandbergtv.com/TTVSchema",
			XMLNS_XSI_NAME = "xmlns:xsi",
			XMLNS_XSI_VALUE = "http://www.w3.org/2000/XMLSchema-instance",
			XSI_SCHEMA_LOCATION_NAME = "xsi:schemaLocation",
			XSI_SCHEMA_LOCATION_VALUE = "http://www.tandbergtv.com/TTVSchema TTVSpec.xsd";

	private DocumentBuilder builder;

	public Binder() {

		try {
			builder = XmlUtil.createDocumentBuilder();
		} catch (ParserConfigurationException e) {
			builder = null;
		}
	}

	public Document bind(Asset asset) {
		List<Asset> assets = new ArrayList<Asset>();
		assets.add(asset);
		return bind(assets);
	}

	/**
	 * Takes a list of Asset objects and creates Document of the TTV xml
	 * representation of the assets.
	 * 
	 * @param assets
	 *            List of Asset objects to be rolled.
	 * @return Document containing TTV xml.
	 */
	public Document bind(List<Asset> assets) {

		if (assets == null) {
			return null;
		}
		Set<TTVId> assetIds = new HashSet<TTVId>();
		List<Element> elements = new ArrayList<Element>();
		for (Asset asset : assets) {
			elements.add(assetRoller(assetIds, asset));
			elements.addAll(relationshipRoller(assetIds, asset.getRelations()));
		}
		Document doc = builder.newDocument();

		Element root = doc.createElementNS(XMLNS_TNS_VALUE, TTVSPEC);
		root.setAttribute(XMLConstants.XMLNS_ATTRIBUTE, XMLConstants.XMLNS_ATTRIBUTE_NS_URI);
		root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLNS_XSI_NAME, XMLNS_XSI_VALUE);
		root.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLNS_TNS_NAME, XMLNS_TNS_VALUE);
		root.setAttributeNS(XMLNS_XSI_VALUE, XSI_SCHEMA_LOCATION_NAME, XSI_SCHEMA_LOCATION_VALUE);
		doc.appendChild(root);
		for (Element element : elements) {
			if (element == null)
				continue;
			Node elementCopy = doc.importNode(element, true);
			root.appendChild(elementCopy);
		}

		return doc;
	}

	/**
	 * Takes a TTV Document and creates a list of the top level Asset objects.
	 * All assets must have TTV ids even temp ids.
	 * 
	 * @param doc
	 *            TTV xml Document.
	 * @return List<Asset> containing a list of top level Asset objects.
	 */
	public List<Asset> bind(Document doc) {

		Map<Long, Asset> assetMap = new HashMap<Long, Asset>();
		List<Asset> assetList = new ArrayList<Asset>();
		List<Relation> docRelations = new ArrayList<Relation>();
		NodeList nodeList = doc.getDocumentElement().getChildNodes();

		for (int i = 0; i < nodeList.getLength(); i++) {
			Node node = nodeList.item(i);
			if (node.getNodeType() != Node.ELEMENT_NODE)
				continue;
			Element element = (Element) node;
			Asset asset = null;

			if (element.getNodeName().equals(GROUP)) {
				asset = new Group();
				((Group) asset).setType(element.getAttribute(TYPE));
			} else { // element name is tns:Item
				asset = new Item();
				((Item) asset).setType(element.getAttribute(TYPE));
			}

			Document fieldDoc = builder.newDocument();

			Element fields = getNamedChildren(element, FIELDS).get(0);
			Element fieldsCopy = (Element) fieldDoc.importNode(fields, true);

			Element ttvIdField = getNamedChildren(fieldsCopy, TTVID).get(0);
			Element idField = getNamedChildren(ttvIdField, ID).get(0);
			long assetId = Long.parseLong(idField.getTextContent());
			asset.setTTVId(new TTVId(assetId));
			fieldsCopy.removeChild(ttvIdField);

			asset.setRelations(new ArrayList<Relation>());
			List<Relation> relations = asset.getRelations();
			for (Element relationElement : getNamedChildren(element, RELATION)) {

				String type = relationElement.getAttribute(TYPE);
				Element targetElement = getNamedChildren(relationElement,
						TARGET).get(0);
				Element idElement = getNamedChildren(targetElement, ID).get(0);
				String id = idElement.getTextContent();

				Relation relation = RelationFactory.getInstance(type);
				relation.setOwnerAsset(asset);
				relation.setTargetAsset(new Item(Long.parseLong(id)));
				relations.add(relation);
				docRelations.add(relation);
			}

			fieldDoc.appendChild(fieldsCopy);
			asset.setFields(new FieldBinder().extractXPaths(fieldDoc));
			assetMap.put(asset.getTTVId().getId(), asset);
			assetList.add(asset);
		}

		for (Relation relation : docRelations) {
			Asset asset = assetMap.get(relation.getTargetAsset().getTTVId()
					.getId());
			if (asset != null) {
				Asset ownerAsset = relation.getOwnerAsset();
				ownerAsset.addChild(asset);
				relation.setOwnerAsset(null);
				ownerAsset.getRelations().remove(relation);
				assetList.remove(asset);
			}
		}

		docRelations.clear();
		
		// TODO: remove negative IDs
		for (Asset a : assetList) {
			if (a.getTTVId().getId() < 0) {
				a.setTTVId(new TTVId(0));
			}
			for (Relation r : a.getRelations()) {
				if (r.getTargetAsset().getTTVId().getId() < 0) {
					r.getTargetAsset().setTTVId(new TTVId(0));
				}
			}
		}

		return assetList;
	}

	/**
	 * Returns a List of Elements that are direct children of element and have
	 * the name.
	 * 
	 * @param element
	 * @param name
	 * @return List<Element>
	 */
	private List<Element> getNamedChildren(Element element, String name) {

		List<Element> result = new ArrayList<Element>();
		NodeList children = element.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node elementChild = children.item(i);
			if (elementChild != null && elementChild.getNodeName().equals(name)) {
				result.add((Element) elementChild);
			}
		}
		return result;
	}

	/**
	 * Recursive method that calls assetRoller and itself. Fills the returned
	 * List with all sub Elements in the Relationship Set.
	 * 
	 * @param ids
	 * @param relationships
	 * @return
	 */
	private List<Element> relationshipRoller(Set<TTVId> ids,
			List<Relation> relationships) {

		List<Element> elements = new ArrayList<Element>();
		for (Relation relation : relationships) {
			Asset asset = relation.getTargetAsset();
			elements.add(assetRoller(ids, asset));
			elements.addAll(relationshipRoller(ids, asset.getRelations()));
		}
		return elements;
	}

	/**
	 * Creates an Element out of and Asset object while first checking for
	 * duplicates in ids.
	 * 
	 * @param ids
	 * @param asset
	 * @return
	 */
	private Element assetRoller(Set<TTVId> ids, Asset asset) {

		if (ids.contains(asset.getTTVId())) {
			return null;
		} else {
			ids.add(asset.getTTVId());
		}
		Document doc = builder.newDocument();
		Element element = null;

		if (asset instanceof Group) {
			element = doc.createElementNS(XMLNS_TNS_VALUE, GROUP);
			element.setAttributeNS(XMLNS_TNS_VALUE, TYPE, ((Group) asset).getType());
		} else if (asset instanceof Item) {
			element = doc.createElementNS(XMLNS_TNS_VALUE, ITEM);
			element.setAttributeNS(XMLNS_TNS_VALUE, TYPE, ((Item) asset).getType());
		} else {
			return null; // unknown Asset type
		}
		NodeList sourceFieldNodes = new FieldBinder().buildXml(asset.getFields()).getDocumentElement().getChildNodes();
		Element movedFields = doc.createElementNS(XMLNS_TNS_VALUE, FIELDS);
		for(int i = 0; i < sourceFieldNodes.getLength(); i++) {
			Node item = sourceFieldNodes.item(i);
			if(item instanceof Element) {
				movedFields.appendChild(doc.importNode(item, true));
			}
		}

		Element ttvIdEle = doc.createElementNS(XMLNS_TNS_VALUE, TTVID);
		Element idEle = doc.createElementNS(XMLNS_TNS_VALUE, ID);
		idEle.setTextContent("" + asset.getTTVId().getId());
		ttvIdEle.appendChild(idEle);
		movedFields.appendChild(ttvIdEle);

		element.appendChild(movedFields);
		for (Relation relation : asset.getRelations()) {
			Element relationElement = doc.createElementNS(XMLNS_TNS_VALUE, RELATION);
			relationElement.setAttributeNS(XMLNS_TNS_VALUE, TYPE, RelationFactory
					.getType(relation));
			Element targetElement = doc.createElementNS(XMLNS_TNS_VALUE, TARGET);
			Element idElement = doc.createElementNS(XMLNS_TNS_VALUE, ID);
			idElement.setTextContent(""
					+ relation.getTargetAsset().getTTVId().getId());
			targetElement.appendChild(idElement);
			relationElement.appendChild(targetElement);
			element.appendChild(relationElement);
		}

		return element;
	}
}
