package org.exist.xmldb.test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Random;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;

import junit.framework.TestCase;

import org.exist.storage.DBBroker;
import org.exist.xmldb.DatabaseInstanceManager;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;

/** Reproduce the EXistException "the document is too complex/irregularily structured
 * to be mapped into eXist's numbering scheme"
 * raised in {@link org.exist.dom.DocumentImpl} .
 * It creates with DOM a simple document having a branch of 16 elements depth
 * connected to the root, with width (arity) of 16 at each level.
 */
public class IndexingTest extends TestCase {
	
	private int siblingCount;
	private int depth;
	private Node deepBranch;
	private Random random;
	
	private static String driver = "org.exist.xmldb.DatabaseImpl";
	private static String baseURI = "xmldb:exist://" + DBBroker.ROOT_COLLECTION;

	private static String username = "admin";
	private static String password = ""; // <<<
	private static String name = "test.xml";
	private String EXIST_HOME = ""; // <<<
	private int effectiveSiblingCount;
	private int effectiveDepth;
	private long startTime;
	private int arity;
	private boolean randomSizes;

	/**
	 * @see junit.framework.TestCase#setUp()
	 */
	protected void setUp() {		
		siblingCount = 2;
		depth = 16;
		arity = 16;
		randomSizes = false;
		random = new Random(1234);
	}

	public IndexingTest(String arg0) {
		super(arg0);
	}
	
	public static void main(String[] args) 
	throws XMLDBException, ParserConfigurationException, 
	SAXException, IOException, ClassNotFoundException, 
	IllegalAccessException, InstantiationException {
		System.setProperty("exist.initdb", "true");
		IndexingTest tester = new IndexingTest("");
		// tester.runTestrregularilyStructured(false);
		tester.testIrregularilyStructured(true);
	}
	
	public void testIrregularilyStructured( ) 
		throws XMLDBException, ParserConfigurationException, SAXException, 
		IOException, ClassNotFoundException, InstantiationException, IllegalAccessException
 {
		
		testIrregularilyStructured( true);
	}
	
	public void testIrregularilyStructured(boolean getContentAsDOM) 
		throws XMLDBException, ParserConfigurationException, SAXException, IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
		Database database = null;
		final String testName = "IrregularilyStructured";
		startTime = System.currentTimeMillis();
		
		try {
			System.out.println("Running test " + testName + " ...");

			// Tell eXist where conf.xml is :
                        // DWES #### use Configuration.getExistHome()
			if ( EXIST_HOME != "" )
				System.setProperty("exist.home", EXIST_HOME );
			
			Class dbc = Class.forName("org.exist.xmldb.DatabaseImpl");
			database = (Database) dbc.newInstance();
            database.setProperty( "create-database", "true" );
			DatabaseManager.registerDatabase(database);

			Collection coll =
				DatabaseManager.getCollection(baseURI, username, password);
			XMLResource resource =
				(XMLResource) coll.createResource(
					name,
					XMLResource.RESOURCE_TYPE);

			Document doc =
				DocumentBuilderFactory
					.newInstance()
					.newDocumentBuilder()
					.newDocument();
			effectiveSiblingCount = populate(doc);
			printTime();
			resource.setContentAsDOM(doc);
			printTime();
			coll.storeResource(resource);
			System.out.println("TEST> stored Resource " + name );
			printTime();
			coll.close();

			coll = DatabaseManager.getCollection(baseURI, username, password);
			resource = (XMLResource) coll.getResource(name);
			System.out.println("TEST> retrieved Resource " + name );
			printTime();
			
			Node n;
			if (getContentAsDOM) {
				n = resource.getContentAsDOM();
			} else {
				String s = (String) resource.getContent();
				byte[] bytes;
				try {
					bytes = s.getBytes("UTF-8");
				} catch (UnsupportedEncodingException e) {
					bytes = s.getBytes();
				}
				ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
				DocumentBuilder db =
					DocumentBuilderFactory.newInstance().newDocumentBuilder();
				n = db.parse(bais);
			}

			System.out.println("getContentAsDOM: " + n.getNodeName());
			Element documentElement = null;
			if ( n instanceof Element ) {
				documentElement = (Element)n;
			} else if ( n instanceof Document ) {
				documentElement = ((Document)n).getDocumentElement();
			}
			
			assertions(documentElement);
			
			coll.removeResource(resource);

			System.out.println("TEST> " + testName + " : PASSED");

		} finally {
			printTime();
			if (database != null) {
					Collection coll =
						DatabaseManager.getCollection(
							baseURI,
							username,
							password);
					DatabaseManager.deregisterDatabase(database);
					DatabaseInstanceManager dim =
						(DatabaseInstanceManager) coll.getService(
							"DatabaseInstanceManager",
							"1.0");
					dim.shutdown();
					printTime();
			}
		}
	}

	private void printTime() {
		System.out.println( "Current ellapsed time : " + ( System.currentTimeMillis() - startTime )/ 1000.f );
	}
	/** Assertions and output: */
	private void assertions(Element documentElement) {
		int computedSiblingCount = documentElement.getChildNodes().getLength();
		int computedDepth = ((Element)deepBranch).getElementsByTagName("element").getLength();
		int computedElementCount = 
			documentElement.getElementsByTagName("element").getLength();

		System.out.println(" documentElement.getChildNodes().getLength(): " +
			computedSiblingCount );
		System.out.println(" documentElement.getElementsByTagName(\"element\").getLength(): " +
			computedElementCount );

		assertEquals("siblingCount", effectiveSiblingCount, computedSiblingCount );
		assertEquals("depth", depth*arity + depth, computedDepth );

		System.out.println("TEST> assertions PASSED");
		printTime();
		// dumpCatabaseContent(n);
	}

	/** This one provokes the Exception */
	private int populate(Document doc) {
		int childrenCount = addChildren (doc, siblingCount);
		
		// Add a long fat branch at root's first child :
		deepBranch = doc.getDocumentElement().getFirstChild();
		effectiveDepth = addFatBranch( doc, deepBranch, depth, null);

		System.out.println("TEST> populate() done.");
		return childrenCount;
		}
	
	/** This one doesn't provoke the Exception */
	private int populateOK(Document doc) {
		int childrenCount = addChildren (doc, siblingCount);
		
		// Add large branches at root's first and last children :
		addBranch ( doc, doc.getDocumentElement().getFirstChild(), depth, null);
		deepBranch = doc.getDocumentElement().getLastChild();
		effectiveDepth = addBranch ( doc, deepBranch, depth, null);
		
		Element documentElement = doc.getDocumentElement();
		
		// Add (small) branches everywhere at level 1 :
		int firstLevelWidth = doc.getDocumentElement().getChildNodes().getLength();
		{
			Node current = documentElement.getFirstChild();
			for (int j = 0; j < firstLevelWidth - 1; j++) {
				addBranch ( doc, current, 10, "branch");
				current = current.getNextSibling();
			}
		}
		
		// Add level 2 siblings everywhere at level 1 :
		{
			Node current = documentElement.getFirstChild();
			for (int j = 0; j < firstLevelWidth - 1; j++) {
				addChildren (current, arity, doc);
				current = current.getNextSibling();
			}
		}
		System.out.println("TEST> " + firstLevelWidth + " first Level elements populated.");
		return childrenCount;
	}

	private int addBranch( Document doc, Node branchNode, int depth, String elementName ) {
		int rdepth = 0;
		if ( branchNode != null ) {
			Node current = branchNode;
			if ( elementName == null || elementName == "" )
				elementName = "element";
			
			if ( randomSizes )
				rdepth = random.nextInt(depth);
			else
				rdepth  = depth;
			for (int j = 0; j < rdepth; j++) {
				Element el = doc.createElement(elementName);
				current.appendChild(el);
				current = el;
			}
		}
		return rdepth;
	}

	private int addFatBranch( Document doc, Node branchNode, int depth, String elementName ) {
		int rdepth = 0;
		if ( branchNode != null ) {
			Node current = branchNode;
			if ( elementName == null || elementName == "" )
				elementName = "element";
			
			if ( randomSizes )
				rdepth = random.nextInt(depth);
			else
				rdepth  = depth;
			for (int j = 0; j < rdepth; j++) {
				Element el = doc.createElement(elementName);
				addChildren( el,  arity, doc );
				current.appendChild(el);
				current = el;
			}
		}
		return rdepth;
	}
	
	/**
	 * @param doc
	 * @param i
	 */
	private int addChildren(Document doc, int length ) {
		Element rootElem = doc.createElement("root");
		doc.appendChild(rootElem);
		return addChildren(rootElem, length, doc);
	}

	private int addChildren( Node rootElem, int length, Document doc) {
		int rlength = 0;
		if ( rootElem != null ) {
			if ( randomSizes )
				rlength = random.nextInt(length);
			else
				rlength = length;
			for (int j = 0; j < rlength; j++) {
				Element el = doc.createElement("element");
				rootElem.appendChild(el);			
			}
		}
		return rlength;
	}
	
	private void dumpCatabaseContent(Node n) {
		try {
			Transformer t = TransformerFactory.newInstance().newTransformer();
			DOMSource source = new DOMSource(n);
			SAXResult result = new SAXResult(new IndexingTest.SAXHandler());
			t.transform(source, result);
        } catch (Exception e) {            
            fail(e.getMessage()); 
		}			
	}
	
	class SAXHandler implements ContentHandler {
		SAXHandler() {
		}

		public void characters(char[] ch, int start, int length) {
			System.out.println(
				"SAXHandler.characters("
					+ new String(ch)
					+ ", "
					+ start
					+ ", "
					+ length
					+ ")");
		}

		public void endDocument() {
			System.out.println("SAXHandler.endDocument()");
		}

		public void endElement(
			String namespaceURI,
			String localName,
			String qName) {
			System.out.println(
				"SAXHandler.endElement("
					+ namespaceURI
					+ ", "
					+ localName
					+ ", "
					+ qName
					+ ")");
		}

		public void endPrefixMapping(String prefix) {
			System.out.println("SAXHandler.endPrefixMapping(" + prefix + ")");
		}

		public void ignorableWhitespace(char[] ch, int start, int length) {
			System.out.println(
				"SAXHandler.ignorableWhitespace("
					+ new String(ch)
					+ ", "
					+ start
					+ ", "
					+ length
					+ ")");
		}

		public void processingInstruction(String target, String data) {
			System.out.println(
				"SAXHandler.processingInstruction("
					+ target
					+ ", "
					+ data
					+ ")");
		}

		public void setDocumentLocator(Locator locator) {
			System.out.println(
				"SAXHandler.setDocumentLocator(" + locator + ")");
		}

		public void skippedEntity(String name) {
			System.out.println("SAXHandler.skippedEntity(" + name + ")");
		}

		public void startDocument() {
			System.out.println("SAXHandler.startDocument()");
		}

		public void startElement(
			String namespaceURI,
			String localName,
			String qName,
			Attributes atts) {
			System.out.println(
				"SAXHandler.startElement("
					+ namespaceURI
					+ ", "
					+ localName
					+ ", "
					+ qName
					+ ","
					+ atts
					+ ")");
		}

		public void startPrefixMapping(String prefix, String xuri) {
			System.out.println(
				"SAXHandler.startPrefixMapping(" + prefix + ", " + xuri + ")");
		}

	}
}

