/*******************************************************************************
 * Copyright (c) 2007-2010 Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is made available under the terms of the
 * Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Red Hat, Inc. - initial API and implementation
 ******************************************************************************/

package org.jboss.tools.jsf.jsf2.model;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.internal.core.JarEntryFile;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.DocumentProviderRegistry;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.wst.html.core.internal.encoding.HTMLModelLoader;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.text.JobSafeStructuredDocument;
import org.eclipse.wst.xml.core.internal.document.ElementImpl;
import org.eclipse.wst.xml.core.internal.parser.XMLSourceParser;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
import org.eclipse.wst.xml.core.internal.provisional.format.DocumentNodeFormatter;
import org.jboss.tools.jsf.JSFModelPlugin;
import org.jboss.tools.jsf.jsf2.util.JSF2ResourceUtil;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * 
 * @author yzhishko
 * 
 */

@SuppressWarnings("restriction")
public class JSF2ComponentModelManager {
	public final static String INTERFACE = "interface";
	public final static String ATTRIBUTE = "attribute";
	public final static String NAME = "name";
	public final static String XMLNS = "xmlns";
	
	public final static String COLON = ":";
	public final static String DEFAULT_COMPOSITE_PREFIX = "composite";
	public final static String XHTML_URI = "http://www.w3.org/1999/xhtml";
	public final static String HTML = "html";
	public final static String HTML_URI = "http://java.sun.com/jsf/html";
	public final static String HTML_PREFIX = "h";

	private static JSF2ComponentModelManager instance = new JSF2ComponentModelManager();

	private JSF2ComponentModelManager() {

	}

	public static JSF2ComponentModelManager getManager() {
		return instance;
	}

	public IFile updateJSF2CompositeComponentFile(
			final IFile componentFileContatiner, final String[] attrNames) {
		IFile file = updateFileContent(new EditableDOMFile() {

			@Override
			public IFile getFile() {
				return componentFileContatiner;
			}

			@Override
			protected void edit(IDOMModel model) throws CoreException,
					IOException {
				FileEditorInput input = new FileEditorInput(getFile());
				IDocumentProvider provider = DocumentProviderRegistry.getDefault().getDocumentProvider(input);
				
				provider.connect(input);
			
				IDocument document = provider.getDocument(input);
				
				updateJSF2CompositeComponent(model.getDocument(), attrNames);
				
				provider.aboutToChange(input);
				provider.saveDocument(new NullProgressMonitor(), input, document, true);
				provider.changed(input);
				provider.disconnect(input);
			}
		});
		return file;
	}

	private void updateJSF2CompositeComponent(IDOMDocument componentDoc,
			String[] attrNames) {
		Element interfaceElement = findInterfaceComponent(componentDoc);
		if(interfaceElement == null)
			interfaceElement = createCompositeInterface(componentDoc);
		createCompositeCompInterface(interfaceElement, attrNames);
	}

	private Element findInterfaceComponent(Node node) {
		if (node instanceof IDOMDocument) {
			IDOMDocument document = (IDOMDocument) node;
			Element element = findInterfaceComponent(document.getDocumentElement());
			if(element != null)
				return element;
		}
		if (node instanceof ElementImpl) {
			ElementImpl impl = (ElementImpl) node;
			String nameSpace = impl.getNamespaceURI();
			if (CompositeComponentConstants.COMPOSITE_XMLNS.equals(nameSpace)) {
				String nodeName = impl.getLocalName();
				if (INTERFACE.equals(nodeName)) { //$NON-NLS-1$
					return impl;
				}
			} else {
				NodeList nodeList = node.getChildNodes();
				if (nodeList != null) {
					for (int i = 0; i < nodeList.getLength(); i++) {
						Element element = findInterfaceComponent(nodeList.item(i));
						if(element != null)
							return element;
					}
				}
			}
		}
		return null;
	}

	private void createCompositeCompInterface(Element element,
			String[] attrNames) {
		Document document = (Document) element.getOwnerDocument();
		String prefix = element.getPrefix();
		Set<String> existInerfaceAttrs = getInterfaceAttrs(element);
		if (prefix != null && !"".equals(prefix)) { //$NON-NLS-1$
			for (int i = 0; i < attrNames.length; i++) {
				if (!existInerfaceAttrs.contains(attrNames[i])) {
					Element attrEl = document.createElementNS(
							CompositeComponentConstants.COMPOSITE_XMLNS, prefix
									+ COLON+ATTRIBUTE); //$NON-NLS-1$
					attrEl.setAttribute(NAME, attrNames[i]); //$NON-NLS-1$
					element.appendChild(attrEl);
				}
			}
		} else {
			for (int i = 0; i < attrNames.length; i++) {
				if (!existInerfaceAttrs.contains(attrNames[i])) {
					Element attrEl = document.createElementNS(
							CompositeComponentConstants.COMPOSITE_XMLNS, ATTRIBUTE); //$NON-NLS-1$
					attrEl.setAttribute(NAME, attrNames[i]); //$NON-NLS-1$
					element.appendChild(attrEl);
				}
			}
		}
		DocumentNodeFormatter formatter = new DocumentNodeFormatter();
		formatter.format(document);
	}

	public IFile revalidateCompositeComponentFile(IFile file) {
		IDOMDocument document = getReadableDOMDocument(file);
		if (document == null) {
			return null;
		}
		
		//Element interfaceElement = checkCompositeInterface(document);
		//if (interfaceElement == null)
			//return null;
		return file;
	}

	public Element checkCompositeInterface(IDOMDocument document) {
		if (document == null) {
			return null;
		}
		Element element = document.getDocumentElement();
		if (element == null) {
			return null;
		}
		
		if(!hasNamespace(element, CompositeComponentConstants.COMPOSITE_XMLNS))
			return null;
		
		return findInterfaceComponent(element);
	}
	
	private boolean hasNamespace(Node node, String URI){
		NamedNodeMap attributes = node.getAttributes();
		for(int i = 0; i < attributes.getLength(); i++){
			Node attr = attributes.item(i);
			
			if(attr.getNodeName().startsWith(XMLNS+COLON) && URI.equals(attr.getNodeValue()))
				return true;
		}
		return false;
	}

	private String getPrefix(Node node, String URI){
		NamedNodeMap attributes = node.getAttributes();
		for(int i = 0; i < attributes.getLength(); i++){
			Node attr = attributes.item(i);
			
			if(attr.getNodeName().startsWith(XMLNS+COLON) && URI.equals(attr.getNodeValue()))
				return attr.getLocalName();
		}
		return null;
	}
	
	private Element findNodeToCreateCompositeInterface(Element rootNode){
		if(hasNamespace(rootNode, CompositeComponentConstants.COMPOSITE_XMLNS))
			return rootNode;
		NodeList children = rootNode.getChildNodes();
		for(int i = 0; i < children.getLength(); i++){
			Node child = children.item(i);
			if(child instanceof Element){
				Node result = findNodeToCreateCompositeInterface((Element)child);
				if(result != null && result instanceof Element)
					return (Element)result;
			}
		}
		return null;
	}
	
	private Element createCompositeInterface(Document document){
		Element rootElement = document.getDocumentElement();
		if (rootElement == null) {
			rootElement = createDocumentElementForCompositeInterface(document);
		}
		
		Element node = findNodeToCreateCompositeInterface(rootElement);
		
		if(node == null){
			addURI(document, rootElement);
			node = rootElement;
		}
		
		String prefix = getPrefix(node, CompositeComponentConstants.COMPOSITE_XMLNS);
		Element interfaceElement = document.createElement(prefix+COLON+INTERFACE);
		
		node.appendChild(interfaceElement);
		
		DocumentNodeFormatter formatter = new DocumentNodeFormatter();
		formatter.format(document);
		
		return interfaceElement;
	}
	
	private Element createDocumentElementForCompositeInterface(Document document){
		Element rootElement = document.createElement(HTML);
		
		Attr attr = document.createAttribute(XMLNS);
		attr.setValue(XHTML_URI);
		rootElement.setAttributeNode(attr);
		
		attr = document.createAttribute(XMLNS+COLON+HTML_PREFIX);
		attr.setValue(HTML_URI);
		rootElement.setAttributeNode(attr);
		
		attr = document.createAttribute(XMLNS+COLON+DEFAULT_COMPOSITE_PREFIX);
		attr.setValue(CompositeComponentConstants.COMPOSITE_XMLNS);
		rootElement.setAttributeNode(attr);
		
		document.appendChild(rootElement);
		return rootElement;
	}
	
	private void addURI(Document document, Element rootElement){
		Attr attr = document.createAttribute(XMLNS+COLON+DEFAULT_COMPOSITE_PREFIX);
		attr.setValue(CompositeComponentConstants.COMPOSITE_XMLNS);
		rootElement.setAttributeNode(attr);
	}

	public static IDOMDocument getReadableDOMDocument(IFile file) {
		IDOMDocument document = null;
		IModelManager manager = StructuredModelManager.getModelManager();
		if (manager == null) {
			return document;
		}
		IStructuredModel model = null;
		try {
			model = manager.getModelForRead(file);
			if (model instanceof IDOMModel) {
				IDOMModel domModel = (IDOMModel) model;
				document = domModel.getDocument();
			}
		} catch (CoreException e) {
			JSFModelPlugin.getPluginLog().logError(e);
		} catch (IOException e) {
			JSFModelPlugin.getPluginLog().logError(e);
		} finally {
			if (model != null) {
				model.releaseFromRead();
			}
		}
		return document;
	}

	public static IDOMDocument getReadableDOMDocument(JarEntryFile file) {
		IDOMDocument document = null;
		IStructuredModel model = null;
		InputStream inputStream = null;
		try {
			inputStream = file.getContents();
			if (inputStream != null) {
				StringBuilder buffer = new StringBuilder(); //$NON-NLS-1$
				Scanner in = new Scanner(inputStream);
				while (in.hasNextLine()) {
					buffer.append(in.nextLine());
				}
				model = new HTMLModelLoader().newModel();
				model.setStructuredDocument(new JobSafeStructuredDocument(
						new XMLSourceParser()));
				model.getStructuredDocument().set(buffer.toString());
				if (model instanceof IDOMModel) {
					document = ((IDOMModel) model).getDocument();
				}
			}
		} catch (CoreException e) {
			JSFModelPlugin.getPluginLog().logError(e);
		} finally {
			if(inputStream!=null) {
				try {
					inputStream.close();
				} catch (IOException e) {
					// Ignore
				}
			}
			model = null;
		}
		return document;
	}

	public static IDOMDocument getReadableDOMDocument(IDocument textDocument) {
		IDOMDocument document = null;
		if (!(textDocument instanceof IStructuredDocument)) {
			return document;
		}
		IModelManager manager = StructuredModelManager.getModelManager();
		if (manager == null) {
			return document;
		}
		IStructuredModel model = null;
		try {
			model = manager.getModelForRead((IStructuredDocument) textDocument);
			if (model instanceof IDOMModel) {
				IDOMModel domModel = (IDOMModel) model;
				document = domModel.getDocument();
			}
		} finally {
			if (model != null) {
				model.releaseFromRead();
			}
		}
		return document;
	}

	public Set<String> getInterfaceAttrs(Element interfaceElement) {
		Set<String> interfaceAttrs = new HashSet<String>(0);
		if (interfaceElement != null) {
			String prefix = interfaceElement.getPrefix();
			String nodeName = ATTRIBUTE;
			if (prefix != null && !"".equals(prefix)) { //$NON-NLS-1$
				nodeName = prefix + COLON + nodeName; //$NON-NLS-1$
			}
			NodeList attrsElements = interfaceElement
					.getElementsByTagName(nodeName);
			if (attrsElements != null) {
				for (int i = 0; i < attrsElements.getLength(); i++) {
					Node el = attrsElements.item(i);
					if (el instanceof IDOMElement) {
						IDOMElement element = (IDOMElement) el;
						String attrvalue = element.getAttribute(NAME); //$NON-NLS-1$
						if (attrvalue != null && !"".equals(attrvalue)) { //$NON-NLS-1$
							interfaceAttrs.add(attrvalue);
						}
					}
				}
			}
		}
		return interfaceAttrs;
	}

	private abstract class EditableDOMFile {

		public EditableDOMFile() {
		}

		public IFile editFile() {
			IFile file = getFile();
			if (file == null) {
				return file;
			}
			IModelManager manager = StructuredModelManager.getModelManager();
			if (manager == null) {
				return file;
			}
			IStructuredModel model = null;
			try {
				model = manager.getModelForEdit(file);
				if (model instanceof IDOMModel) {
					IDOMModel domModel = (IDOMModel) model;
					edit(domModel);
				}
			} catch (CoreException e) {
				JSFModelPlugin.getPluginLog().logError(e);
			} catch (IOException e) {
				JSFModelPlugin.getPluginLog().logError(e);
			} finally {
				if (model != null) {
					model.releaseFromEdit();
				}
			}
			return file;
		};

		protected abstract void edit(IDOMModel model) throws CoreException,
				IOException;

		public abstract IFile getFile();

	}

	private IFile updateFileContent(EditableDOMFile domFile) {
		return domFile.editFile();
	}

}
