/*
 *  soapUI, copyright (C) 2004-2012 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 *  See the GNU Lesser General Public License for more details at gnu.org.
 */

package com.eviware.soapui.support.xml;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreePath;
import javax.xml.namespace.QName;

import org.apache.log4j.Logger;
import org.apache.xmlbeans.SchemaGlobalElement;
import org.apache.xmlbeans.SchemaParticle;
import org.apache.xmlbeans.SchemaProperty;
import org.apache.xmlbeans.SchemaType;
import org.apache.xmlbeans.SchemaTypeSystem;
import org.apache.xmlbeans.XmlBeans;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlCursor.TokenType;
import org.apache.xmlbeans.XmlLineNumber;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl;
import org.jdesktop.swingx.treetable.TreeTableModel;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.eviware.soapui.impl.wsdl.support.xsd.SchemaUtils;
import com.eviware.soapui.support.types.StringToStringMap;

public class XmlObjectTreeModel implements TreeTableModel
{
	private XmlObject xmlObject;
	private Set<TreeModelListener> listeners = new HashSet<TreeModelListener>();
	private XmlCursor cursor;
	private Map<XmlObject, XmlTreeNode> treeNodeMap = new HashMap<XmlObject, XmlTreeNode>();

	public final static Class<?> hierarchicalColumnClass = TreeTableModel.class;
	private SchemaTypeSystem typeSystem;
	private RootXmlTreeNode root;
	@SuppressWarnings( "unused" )
	private final static Logger log = Logger.getLogger( XmlObjectTreeModel.class );

	public XmlObjectTreeModel( XmlObject xmlObject )
	{
		this( XmlBeans.getBuiltinTypeSystem(), xmlObject );
	}

	public XmlObjectTreeModel()
	{
		this( XmlObject.Factory.newInstance() );
	}

	public XmlObjectTreeModel( SchemaTypeSystem typeSystem, XmlObject xmlObject )
	{
		if( typeSystem == null )
			typeSystem = XmlBeans.getBuiltinTypeSystem();

		this.typeSystem = typeSystem;
		this.xmlObject = xmlObject;
		init();
	}

	public XmlObjectTreeModel( SchemaTypeSystem typeSystem )
	{
		this( typeSystem, XmlObject.Factory.newInstance() );
	}

	private void init()
	{
		cursor = null;

		if( xmlObject != null )
		{
			cursor = xmlObject.newCursor();
			cursor.toStartDoc();
		}

		root = new RootXmlTreeNode( cursor );
	}

	public SchemaTypeSystem getTypeSystem()
	{
		return typeSystem;
	}

	public void setTypeSystem( SchemaTypeSystem typeSystem )
	{
		if( typeSystem == null )
			typeSystem = XmlBeans.getBuiltinTypeSystem();

		this.typeSystem = typeSystem;
	}

	public XmlObject getXmlObject()
	{
		return xmlObject;
	}

	public void setXmlObject( XmlObject xmlObject )
	{
		if( cursor != null )
			cursor.dispose();

		this.xmlObject = xmlObject;
		init();

		XmlTreeNode xmlTreeNode = ( ( XmlTreeNode )getRoot() );
		fireTreeStructureChanged( xmlTreeNode );
	}

	protected void fireTreeStructureChanged( XmlTreeNode rootNode )
	{
		for( TreeModelListener listener : listeners )
		{
			listener.treeStructureChanged( new XmlTreeTableModelEvent( this, rootNode.getTreePath(), -1 ) );
		}
	}

	public Class<?> getColumnClass( int arg0 )
	{
		return arg0 == 0 ? hierarchicalColumnClass : XmlTreeNode.class;
	}

	public int getColumnCount()
	{
		return 3;
	}

	public String getColumnName( int arg0 )
	{
		return null;
	}

	public Object getValueAt( Object arg0, int arg1 )
	{
		return arg0; // ((XmlTreeNode)arg0).getValue( arg1 );
	}

	public boolean isCellEditable( Object arg0, int arg1 )
	{
		return ( ( XmlTreeNode )arg0 ).isEditable( arg1 );
	}

	public void setValueAt( Object arg0, Object arg1, int arg2 )
	{
		XmlTreeNode treeNode = ( XmlTreeNode )arg1;
		if( treeNode.setValue( arg2, arg0 ) )
		{
			fireTreeNodeChanged( treeNode, arg2 );
		}
	}

	protected void fireTreeNodeChanged( XmlTreeNode treeNode, int column )
	{
		for( TreeModelListener listener : listeners )
		{
			listener.treeNodesChanged( new XmlTreeTableModelEvent( this, treeNode.getTreePath(), column ) );
		}
	}

	public void addTreeModelListener( TreeModelListener l )
	{
		listeners.add( l );
	}

	public Object getChild( Object parent, int index )
	{
		return ( ( XmlTreeNode )parent ).getChild( index );
	}

	public int getChildCount( Object parent )
	{
		return ( ( XmlTreeNode )parent ).getChildCount();
	}

	public int getIndexOfChild( Object parent, Object child )
	{
		return ( ( XmlTreeNode )parent ).getIndexOfChild( ( XmlTreeNode )child );
	}

	public Object getRoot()
	{
		return getRootNode();
	}

	public RootXmlTreeNode getRootNode()
	{
		return root;
	}

	public boolean isLeaf( Object node )
	{
		return ( ( XmlTreeNode )node ).isLeaf();
	}

	public void removeTreeModelListener( TreeModelListener l )
	{
		listeners.remove( l );
	}

	public void valueForPathChanged( TreePath path, Object newValue )
	{
	}

	private class TreeBookmark extends XmlCursor.XmlBookmark
	{
	}

	public interface XmlTreeNode
	{
		public int getChildCount();

		public XmlTreeNode getChild( int ix );

		public int getIndexOfChild( XmlTreeNode childNode );

		public String getNodeName();

		public String getNodeText();

		public boolean isEditable( int column );

		public boolean isLeaf();

		public boolean setValue( int column, Object value );

		public XmlLineNumber getNodeLineNumber();

		public XmlLineNumber getValueLineNumber();

		public XmlObject getXmlObject();

		public Node getDomNode();

		public TreePath getTreePath();

		public XmlTreeNode getParent();

		public SchemaType getSchemaType();

		public String getDocumentation();
	}

	private abstract class AbstractXmlTreeNode implements XmlTreeNode
	{
		protected Node node;
		protected TreeBookmark bm;
		private final XmlTreeNode parent;
		private XmlLineNumber lineNumber;
		protected SchemaType schemaType;
		protected String documentation;

		@SuppressWarnings( "unchecked" )
		protected AbstractXmlTreeNode( XmlCursor cursor, XmlTreeNode parent )
		{
			this.parent = parent;

			if( cursor != null )
			{
				node = cursor.getDomNode();

				ArrayList list = new ArrayList();
				cursor.getAllBookmarkRefs( list );

				for( Object o : list )
					if( o instanceof XmlLineNumber )
						lineNumber = ( XmlLineNumber )o;

				bm = new TreeBookmark();
				cursor.setBookmark( bm );

				treeNodeMap.put( cursor.getObject(), this );
			}
		}

		protected SchemaType findSchemaType()
		{
			if( cursor == null )
				return null;

			positionCursor( cursor );

			SchemaType resultType = null;
			XmlObject xo = cursor.getObject();
			if( xo != null )
			{
				Node domNode = xo.getDomNode();

				// check for xsi:type
				if( domNode.getNodeType() == Node.ELEMENT_NODE )
				{
					Element elm = ( Element )domNode;
					String xsiType = elm.getAttributeNS( "http://www.w3.org/2001/XMLSchema-instance", "type" );
					if( xsiType != null && xsiType.length() > 0 )
					{
						resultType = findXsiType( xsiType );
					}
				}

				if( resultType == null )
					resultType = typeSystem.findType( xo.schemaType().getName() );

				if( resultType == null )
					resultType = xo.schemaType();

				if( resultType.isNoType() )
				{
					QName nm = cursor.getName();

					if( parent != null && parent.getSchemaType() != null )
					{
						SchemaType parentSchemaType = parent.getSchemaType();
						SchemaParticle contentModel = parentSchemaType.getContentModel();

						if( contentModel != null )
						{
							SchemaParticle[] children = contentModel.getParticleChildren();

							for( int c = 0; children != null && c < children.length; c++ )
							{
								if( nm.equals( children[c].getName() ) )
								{
									resultType = children[c].getType();
									documentation = SchemaUtils.getDocumentation( resultType );
									break;
								}
							}

							if( resultType.isNoType() && nm.equals( contentModel.getName() ) )
								resultType = contentModel.getType();

							if( resultType.isNoType() )
							{
								SchemaType[] anonymousTypes = parentSchemaType.getAnonymousTypes();
								for( int c = 0; anonymousTypes != null && c < anonymousTypes.length; c++ )
								{
									QName name = anonymousTypes[c].getName();
									if( name != null && name.equals( nm ) )
									{
										resultType = anonymousTypes[c];
										break;
									}
									else if( anonymousTypes[c].getContainerField().getName().equals( nm ) )
									{
										resultType = anonymousTypes[c].getContainerField().getType();
										break;
									}
								}
							}
						}
					}

					if( resultType.isNoType() )
					{
						SchemaGlobalElement elm = typeSystem.findElement( nm );
						if( elm != null )
						{
							resultType = elm.getType();
						}
						else if( typeSystem.findDocumentType( nm ) != null )
						{
							resultType = typeSystem.findDocumentType( nm );
						}
					}
				}
			}

			if( resultType == null )
				resultType = XmlAnyTypeImpl.type;

			if( documentation == null )
				documentation = SchemaUtils.getDocumentation( resultType );

			return resultType;
		}

		@SuppressWarnings( "unused" )
		protected String getUserInfo( SchemaType schemaType )
		{
			if( schemaType.getAnnotation() != null )
			{
				XmlObject[] userInformation = schemaType.getAnnotation().getUserInformation();
				if( userInformation != null && userInformation.length > 0 )
				{
					return userInformation[0].toString(); // XmlUtils.getElementText(
																		// ( Element )
																		// userInformation[0].getDomNode());
				}
			}

			return null;
		}

		public String getDocumentation()
		{
			return documentation;
		}

		private SchemaType findXsiType( String xsiType )
		{
			SchemaType resultType;
			int ix = xsiType.indexOf( ':' );
			QName name = null;

			if( ix == -1 )
			{
				name = new QName( xsiType );
				resultType = typeSystem.findType( name );
			}
			else
			{
				StringToStringMap map = new StringToStringMap();
				cursor.getAllNamespaces( map );

				name = new QName( map.get( xsiType.substring( 0, ix ) ), xsiType.substring( ix + 1 ) );
				resultType = typeSystem.findType( name );
			}

			return resultType;
		}

		public XmlTreeNode getParent()
		{
			return parent;
		}

		protected void positionCursor( XmlCursor cursor )
		{
			cursor.toBookmark( bm );
		}

		public XmlTreeNode getChild( int ix )
		{
			return null;
		}

		public int getChildCount()
		{
			return 0;
		}

		public int getIndexOfChild( XmlTreeNode childNode )
		{
			return -1;
		}

		@SuppressWarnings( "unused" )
		public Object getValue( int column )
		{
			if( column == 0 )
				return getNodeName();
			else if( column == 1 )
				return getNodeText();

			return null;
		}

		public Node getDomNode()
		{
			return node;
		}

		public String getNodeName()
		{
			return node == null ? null : node.getNodeName();
		}

		public String getNodeText()
		{
			if( node == null )
				return null;

			String nodeValue = node.getNodeValue();
			return nodeValue == null ? null : nodeValue.trim();
		}

		public boolean isEditable( int column )
		{
			return false;
		}

		public boolean isLeaf()
		{
			return getChildCount() == 0;
		}

		public boolean setValue( int column, Object value )
		{
			return false;
		}

		public String toString()
		{
			return getNodeName();
		}

		public boolean equals( Object obj )
		{
			if( obj == this )
				return true;
			if( obj instanceof AbstractXmlTreeNode )
				return ( ( AbstractXmlTreeNode )obj ).node == this.node;
			else
				return super.equals( obj );
		}

		public XmlLineNumber getNodeLineNumber()
		{
			return lineNumber;
		}

		public XmlLineNumber getValueLineNumber()
		{
			return lineNumber;
		}

		public XmlObject getXmlObject()
		{
			if( cursor != null && cursor.toBookmark( bm ) )
			{
				XmlObject object = cursor.getObject();

				if( object != null )
					return object;
				else if( parent != null )
					return parent.getXmlObject();
			}

			return null;
		}

		public TreePath getTreePath()
		{
			List<XmlTreeNode> nodes = new ArrayList<XmlTreeNode>();
			nodes.add( this );

			XmlTreeNode node = this;

			while( node.getParent() != null )
			{
				nodes.add( 0, node.getParent() );
				node = node.getParent();
			}

			return new TreePath( nodes.toArray() );
		}

		public SchemaType getSchemaType()
		{
			if( schemaType == null )
				schemaType = findSchemaType();

			return schemaType;
		}
	}

	public class RootXmlTreeNode extends AbstractXmlTreeNode
	{
		private ElementXmlTreeNode rootNode;

		protected RootXmlTreeNode( XmlCursor cursor )
		{
			super( cursor, null );

			if( cursor != null )
			{
				cursor.toFirstContentToken();
				rootNode = new ElementXmlTreeNode( cursor, this );
			}
		}

		public XmlTreeNode getChild( int ix )
		{
			return ix == 0 ? rootNode : null;
		}

		public int getChildCount()
		{
			return rootNode == null ? 0 : 1;
		}

		public int getIndexOfChild( XmlTreeNode childNode )
		{
			return childNode == rootNode ? 0 : -1;
		}
	}

	public class ElementXmlTreeNode extends AbstractXmlTreeNode
	{
		private LinkedList<XmlTreeNode> elements = new LinkedList<XmlTreeNode>();
		private TextXmlTreeNode textTreeNode;
		private int attrCount;

		protected ElementXmlTreeNode( XmlCursor cursor, XmlTreeNode parent )
		{
			super( cursor, parent );

			TokenType token = cursor.toNextToken();
			while( token == TokenType.ATTR || token == TokenType.NAMESPACE )
			{
				if( token == TokenType.ATTR )
				{
					elements.add( new AttributeXmlTreeNode( cursor, this ) );
				}

				token = cursor.toNextToken();
			}

			attrCount = elements.size();

			positionCursor( cursor );
			cursor.toFirstContentToken();

			while( true )
			{
				while( cursor.isComment() || cursor.isProcinst() )
					cursor.toNextToken();

				if( cursor.isContainer() )
				{
					elements.add( new ElementXmlTreeNode( cursor, this ) );
					cursor.toEndToken();
					cursor.toNextToken();
				}

				if( cursor.isText() )
				{
					elements.add( new TextXmlTreeNode( cursor, this ) );
					cursor.toNextToken();
				}

				if( cursor.isEnd() || cursor.isEnddoc() )
					break;
			}

			if( elements.size() == attrCount + 1 && ( elements.get( attrCount ) instanceof TextXmlTreeNode ) )
			{
				textTreeNode = ( TextXmlTreeNode )elements.remove( attrCount );
			}
			else
			{
				for( int c = attrCount; c < elements.size(); c++ )
				{
					if( elements.get( c ) instanceof TextXmlTreeNode )
					{
						TextXmlTreeNode treeNode = ( TextXmlTreeNode )elements.get( c );
						String text = treeNode.getNodeText().trim();
						if( text.length() == 0 )
						{
							elements.remove( c );
							c-- ;
						}
					}
				}
			}

			positionCursor( cursor );
		}

		public XmlTreeNode getChild( int ix )
		{
			return elements.get( ix );
		}

		public boolean isEditable( int column )
		{
			return column == 1 && elements.size() == attrCount;
		}

		public boolean setValue( int column, Object value )
		{
			if( column == 1 )
			{
				if( textTreeNode != null )
				{
					textTreeNode.setValue( 1, value );
				}
				else
				{
					positionCursor( cursor );
					cursor.toEndToken();
					cursor.insertChars( value.toString() );
					positionCursor( cursor );
					cursor.toFirstContentToken();

					textTreeNode = new TextXmlTreeNode( cursor, this );
				}
			}
			return column == 1;
		}

		public int getChildCount()
		{
			return elements.size();
		}

		public int getIndexOfChild( XmlTreeNode childNode )
		{
			return elements.indexOf( childNode );
		}

		public String getNodeText()
		{
			return textTreeNode == null ? "" : textTreeNode.getNodeText();
		}

		public XmlLineNumber getValueLineNumber()
		{
			return textTreeNode == null ? super.getValueLineNumber() : textTreeNode.getValueLineNumber();
		}
	}

	public class AttributeXmlTreeNode extends AbstractXmlTreeNode
	{
		private boolean checkedType;

		protected AttributeXmlTreeNode( XmlCursor cursor, ElementXmlTreeNode parent )
		{
			super( cursor, parent );
		}

		public String getNodeName()
		{
			return "@" + super.getNodeName();
		}

		public XmlLineNumber getNodeLineNumber()
		{
			return getParent().getNodeLineNumber();
		}

		public boolean isEditable( int column )
		{
			return column == 1;
		}

		public boolean setValue( int column, Object value )
		{
			if( column == 1 )
				node.setNodeValue( value.toString() );

			return column == 1;
		}

		public SchemaType getSchemaType()
		{
			if( schemaType == null && !checkedType )
			{
				SchemaType parentSchemaType = getParent().getSchemaType();
				if( parentSchemaType != null )
				{
					positionCursor( cursor );
					SchemaProperty attributeProperty = parentSchemaType.getAttributeProperty( cursor.getName() );
					if( attributeProperty != null )
					{
						schemaType = attributeProperty.getType();
						documentation = SchemaUtils.getDocumentation( schemaType );

						// SchemaAnnotation annotation = schemaType.getAnnotation();
						// if( annotation != null )
						// {
						// XmlObject[] userInformation =
						// annotation.getUserInformation();
						// if( userInformation != null && userInformation.length > 0 )
						// {
						// //userInformation[0].toString(); //XmlUtils.getElementText(
						// ( Element ) userInformation[0].getDomNode());
						// }
						// }
					}
				}

				checkedType = true;
			}

			return schemaType;
		}
	}

	public class TextXmlTreeNode extends AbstractXmlTreeNode
	{
		protected TextXmlTreeNode( XmlCursor cursor, ElementXmlTreeNode parent )
		{
			super( cursor, parent );
		}

		public boolean isEditable( int column )
		{
			return column == 1;
		}

		public boolean setValue( int column, Object value )
		{
			if( column == 1 && node != null )
				node.setNodeValue( value.toString() );

			return column == 1;
		}

		public TreePath getTreePath()
		{
			return super.getTreePath().getParentPath();
		}
	}

	public TreePath findXmlTreeNode( int line, int column )
	{
		line++ ;

		XmlTreeNode treeNode = findXmlTreeNode( root, line, column );
		if( treeNode instanceof AttributeXmlTreeNode )
			return treeNode.getParent().getTreePath();
		else if( treeNode != null )
			return treeNode.getTreePath();

		return null;
	}

	private XmlTreeNode findXmlTreeNode( XmlTreeNode treeNode, int line, int column )
	{
		for( int c = 0; c < treeNode.getChildCount(); c++ )
		{
			XmlTreeNode child = treeNode.getChild( c );
			XmlLineNumber ln = child.getNodeLineNumber();
			if( ln != null && ( line < ln.getLine() || ( line == ln.getLine() && column <= ln.getColumn() ) ) )
			{
				if( c == 0 )
					return treeNode;
				else
					return findXmlTreeNode( treeNode.getChild( c - 1 ), line, column );
			}
		}

		if( treeNode.getChildCount() > 0 )
		{
			return findXmlTreeNode( treeNode.getChild( treeNode.getChildCount() - 1 ), line, column );
		}

		return treeNode;
	}

	public class XmlTreeTableModelEvent extends TreeModelEvent
	{
		private final int column;

		public XmlTreeTableModelEvent( Object source, Object[] path, int[] childIndices, Object[] children, int column )
		{
			super( source, path, childIndices, children );
			this.column = column;
		}

		public XmlTreeTableModelEvent( Object source, Object[] path, int column )
		{
			super( source, path );
			this.column = column;
		}

		public XmlTreeTableModelEvent( Object source, TreePath path, int[] childIndices, Object[] children, int column )
		{
			super( source, path, childIndices, children );
			this.column = column;
		}

		public XmlTreeTableModelEvent( Object source, TreePath path, int column )
		{
			super( source, path );
			this.column = column;
		}

		public int getColumn()
		{
			return column;
		}
	}

	public XmlTreeNode getXmlTreeNode( XmlObject object )
	{
		return treeNodeMap.get( object );
	}

	public XmlTreeNode[] selectTreeNodes( String xpath )
	{
		XmlObject[] nodes = xmlObject.selectPath( xpath );
		List<XmlTreeNode> result = new ArrayList<XmlTreeNode>();

		for( XmlObject xmlObject : nodes )
		{
			XmlTreeNode tn = getXmlTreeNode( xmlObject );
			if( tn != null )
				result.add( tn );
		}

		return result.toArray( new XmlTreeNode[result.size()] );
	}

	public void release()
	{
		typeSystem = null;
		treeNodeMap.clear();

		listeners.clear();
	}

	public int getHierarchicalColumn()
	{
		return 0;
	}
}
