/*
 * Created on Feb 28, 2005 <p>
 * By Eddie <p>
 */
package org.biomoby.shared;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.biomoby.client.CentralImpl;
import org.biomoby.shared.data.MobyDataFloat;
import org.biomoby.shared.data.MobyDataInt;
import org.biomoby.shared.data.MobyDataObject;
import org.biomoby.shared.data.MobyDataString;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * @author Eddie Kawas
 * <p><B>Note:</B> if you are using this class with java 1.4.x, 
 * use the Endorsed Standards Override Mechanism to over-ride org.w3c.dom. Place the 
 * Xerces-J jars in the <java-home>\lib\endorsed 
 * directory, where <java-home> is where the runtime software is installed. This class
 * behaves well with java 1.5.0.
 * <p>For questions, comments, or bugs
 * <p>email me at edward.kawas@gmail.com
 */
public class MobyObjectDecompositionImpl implements MobyObjectDecomposition {

    private static final String URL_OBJECT = "http://mobycentral.icapture.ubc.ca:8090/authority/metadata?lsid=";

    /* (non-Javadoc)
     * @see org.biomoby.client.gui.util.MobyObjectDecomposition#getFlattenedPrims(java.lang.String)
     */
    public final MobyDataObject[] getFlattenedPrims(String object) {
        ArrayList list = new ArrayList();
        if (isPrimitive(object)) {
            return new MobyDataObject[] {};
        }
        String lsid = "urn:lsid:biomoby.org:objectclass:" + object;
        String rdf = getObjectRDF(lsid);
        if (rdf == null) {
            // could throw an exception instead return null
            return null;
        }
        // create a DOM document
        Document doc = getDOMDocument(rdf);
        // start parsing
        Element root = doc.getDocumentElement();
        NodeList rdfsClassList = root.getElementsByTagName("rdfs:Class");
        for (int x = 0; x < rdfsClassList.getLength(); x++) {
            // process each class:
            Element _class = ((Element) rdfsClassList.item(x));
            NodeList has = _class.getElementsByTagName("moby:has");
            NodeList hasa = _class.getElementsByTagName("moby:hasa");
            // subClassOf can contain attributes or just an element
            NodeList subClassOf = _class
                    .getElementsByTagName("rdfs:subClassOf");
            for (int i = 0; i < has.getLength(); i++) {
                NodeList children = ((Element) has.item(i)).getChildNodes();
                for (int j = 0; j < children.getLength(); j++) {
                    if (!(children.item(j) instanceof Element))
                        continue;
                    Element child = (Element) children.item(j);
                    Node articleNode = child.getElementsByTagName(
                            "moby:articleName").item(0);
                    String localName = child.getLocalName();
                    String articleName = (articleNode == null) ? ""
                            : getTextContent(articleNode);
                    //                  TODO - recurse if object is not a primitive
                    if (isPrimitive(localName)) {
                        MobyDataObject mdsi = createPrimitiveType(
                                localName, articleName);
                        list.add(mdsi);
                    } else {
                        MobyDataObject[] recurseArray = getFlattenedPrims(localName);
                        for (int k = 0; k < recurseArray.length; k++) {
                            list.add(recurseArray[k]);
                        }
                    }
                }
            }
            for (int i = 0; i < hasa.getLength(); i++) {
                NodeList children = ((Element) hasa.item(i)).getChildNodes();
                for (int j = 0; j < children.getLength(); j++) {
                    if (children.item(j) instanceof Element) {
                        Element child = (Element) children.item(j);
                        Node articleNode = child.getElementsByTagName(
                                "moby:articleName").item(0);
                        String localName = child.getLocalName();
                        String articleName = (articleNode == null) ? ""
                                : getTextContent(articleNode);
                        //                      TODO - recurse if object is not a primitive
                        if (isPrimitive(localName)) {
                            MobyDataObject mdsi = createPrimitiveType(
                                    localName, articleName);
                            list.add(mdsi);
                        } else {
                            MobyDataObject[] recurseArray = getFlattenedPrims(localName);
                            for (int k = 0; k < recurseArray.length; k++) {
                                list.add(recurseArray[k]);
                            }
                        }
                    }
                }
            }

            for (int i = 0; i < subClassOf.getLength(); i++) {
                Element subClass = (Element) subClassOf.item(i);
                // check for attributes
                if (subClass.hasAttributes()) {
                    String _object = subClass.getAttribute("rdf:resource");
                    if (_object.indexOf("#") > 0) {
                        _object = _object.substring(_object.indexOf("#") + 1);
                    }
                    if (isPrimitive(_object) && !_object.equals("Object")) {
                        MobyDataObject mdsi = new MobyDataObject(
                                _object);
                        mdsi.setDataType(new MobyDataType(_object));
                        list.add(mdsi);
                    }
                } else {
                    String _object = ((Element) ((Element) subClass)
                            .getElementsByTagName("rdfs:Class").item(0))
                            .getAttribute("rdf:about");
                    if (_object.indexOf("#") > 0) {
                        _object = _object.substring(_object.indexOf("#") + 1);
                    }
                    if (isPrimitive(_object) && !_object.equals("Object")) {
                        MobyDataObject mdsi = new MobyDataObject(
                                _object);
                        mdsi.setDataType(new MobyDataType(_object));
                        list.add(mdsi);
                    }
                }

            }
        }

        return convertArrayListToMoby(list);
    }

    /**
     * Method to simulate DOM level 3 (Java 1.5 default) method for recursively getting text children from a Node.
     * A wrapper around getTextContent(Node, boolean) with boolean = true, since DOM 3 getTextContent() ignores 
     * things that match Text.isContentElementWhitespace().
     */
    public static String getTextContent(Node parent){
	return getTextContent(parent, true);
    }

    /**
     * Grabs the text and CDATA children of a Node (not necessairly contiguous) and returns them concatenated.
     * Added by Paul Gordon.  
     * Simulates ignoring of DOM 3 Text.isContentElementWhitespace() fully, I *think*.
     * 
     * @param parent the node who's text children should be concatenated
     * @param includeChildrensText if true, recursively appends children's text depth first
     *
     * @return non-normalized text contents of the passed-in Node, and its children if so requested
     */
    public static String getTextContent(Node parent, boolean includeChildrensText){
	if(parent.hasChildNodes()){
	    java.util.regex.Pattern nonWhitespacePattern = Pattern.compile("[^ \t\r\n]");
	    StringBuffer textbuffer = new StringBuffer();
	    
	    for(Node node = parent.getFirstChild(); node != null; node = node.getNextSibling()){
		// Append all the contiguous text data
		if(node instanceof org.w3c.dom.Text || node instanceof org.w3c.dom.CDATASection){ 
		    // Get rid of whitespace
		    String value = node.getNodeValue();
		    // Add if not just whitespace.
		    if(nonWhitespacePattern.matcher(value).find()){
			textbuffer.append(value);
		    }
		}
		// Get contents of children elements too (recursive)
		else{
		    textbuffer.append(getTextContent(node, includeChildrensText));
		}
	    }
	    return textbuffer.toString();
	}
	else{
	    return "";
	}
    }

    /* (non-Javadoc)
     * @see org.biomoby.client.gui.util.MobyObjectDecomposition#getObjectComposition(java.lang.String)
     */
    public final java.util.Map getObjectComposition(String name, String endpoint) throws MobyException {
        java.util.Map map = new java.util.HashMap();
        String parent = null;
        MobyRelationship[] relationships = null;
        StringBuffer has = null;
        StringBuffer hasa = null;
        MobyDataType data = null;
        try {
            Central central = new CentralImpl(endpoint);
            data = central.getDataType(name);
            parent = (data.getParentNames().length == 1) ? data
                    .getParentNames()[0] : null;
            relationships = data.getChildren();
        } catch (MobyException ex) {
            throw new MobyException("MobyObjectDecompositionImpl: No such object in the Moby Ontology. " + name + "\n"+ex);
        } catch (NoSuccessException ex) {
            throw new MobyException("MobyObjectDecompositionImpl: No such object in the Moby Ontology.");
        }

        // add parent
        if (parent == null)
            map.put("isa", parent);
        else
            map.put("isa", new String[] { stripURN(parent) });

        for (int x = 0; x < relationships.length; x++) {
            MobyRelationship mr = relationships[x];
            if (mr.getRelationshipType() == Central.iHAS) {
                if (has == null)
                    has = new StringBuffer();
                has.append(stripURN(mr.getDataTypeName()) + "," + mr.getName()
                        + " ");
            } else if (mr.getRelationshipType() == Central.iHASA) {
                if (hasa == null)
                    hasa = new StringBuffer();
                hasa.append(stripURN(mr.getDataTypeName()) + "," + mr.getName()
                        + " ");
            }
        }

        if (has != null)
            map.put("has", has.toString().split(" "));
        else
            map.put("has", null);

        if (hasa != null)
            map.put("hasa", hasa.toString().split(" "));
        else
            map.put("hasa", null);
        return map;
    }

    /* (non-Javadoc)
     * @see org.biomoby.client.gui.util.MobyObjectDecomposition#isPrimitive(java.lang.String)
     */
    public final boolean isPrimitive(String object) {
        for (int x = 0; x < PrimitiveTypes.PRIMS.length; x++) {
            if (PrimitiveTypes.PRIMS[x].equals(object))
                return true;
        }
        return false;
    }

    private final String getObjectRDF(String lsid) {
        
    	//TODO use LSID client instead
    	URL url;
        try {
            url = new URL(URL_OBJECT + lsid);
        } catch (MalformedURLException e) {
            System.err
                    .println("Error in getObjectRDF:1 - MalformedURLException.");
            return null;
        }
        BufferedReader in = null;
        try {
            in = new BufferedReader(new InputStreamReader(url.openStream()));
        } catch (IOException e1) {
            System.err
                    .println("Error in getObjectRDF:2 - IOException. \n- Can't read from URL");
            return null;
        }
        String input;
        StringBuffer sb = new StringBuffer();
        try {
            while ((input = in.readLine()) != null) {
                sb.append(input + System.getProperty("line.separator"));
            }
        } catch (IOException e2) {
            System.err
                    .println("Error in getObjectRDF:3 - IOException.\n-Problems reading from URL");
            return null;
        }
        return sb.toString();
    }

    /*
     * given an xml string, create a DOM document
     */
    private final Document getDOMDocument(String rdfXML) {
        try {
            // Create a builder factory
            DocumentBuilderFactory factory = DocumentBuilderFactory
                    .newInstance();
            factory.setNamespaceAware(true);
            // Create the builder and parse the file
            Document doc = factory.newDocumentBuilder().parse(
                    new InputSource(new StringReader(rdfXML)));
            return doc;
        } catch (SAXException e) {
            // A parsing error occurred; the xml input is not valid
        } catch (ParserConfigurationException e) {} catch (IOException e) {}
        return null;
    }

    /*
     * method to create default DataTypes.
     */
    public final MobyDataObject createPrimitiveType(String localName,
            String articleName) {
        if (localName.equalsIgnoreCase("Float")) {
            return new MobyDataFloat(articleName, 0.0);
        } else if (localName.equalsIgnoreCase("String")) {
            return new MobyDataString(articleName, "");
        } else if (localName.equalsIgnoreCase("Integer")) {
            return new MobyDataInt(articleName, 0);
        } else if (localName.equalsIgnoreCase("DateTime")) {
            MobyDataObject mdsi = new MobyDataObject(
                    articleName);
            mdsi.setDataType(new MobyDataType(localName));
            return mdsi;
        }
        // error
        return null;
    }

    private final MobyDataObject[] convertArrayListToMoby(ArrayList list) {
        int size = list.size();
        MobyDataObject[] mdsi = new MobyDataObject[size];
        size = 0;
        for (Iterator x = list.iterator(); x.hasNext(); size++) {
            mdsi[size] = (MobyDataObject) x.next();
        }
        return mdsi;
    }

    private final String stripURN(String urn) {
        final String lsid = "(^urn\\:lsid\\:biomoby\\.org\\:objectclass:)(\\S*$)";
        Pattern p = Pattern.compile(lsid);
        Matcher m = p.matcher(urn);
        if (m.matches()) {
            return m.group(2);
        }
        return urn;

    }

    public final Object clone() throws java.lang.CloneNotSupportedException {
        return super.clone();
    }

    private final void writeObject(java.io.ObjectOutputStream out)
            throws java.io.IOException {
        throw new java.io.IOException("Object cannot be serialized");
    }

    private final void readObject(java.io.ObjectInputStream in)
            throws java.io.IOException {
        throw new java.io.IOException("Object cannot be deserialized");
    }

    public static void main(String[] args) throws MobyException {
          }

    private final String arrayToString(Object array) {
        if (array == null) {
            return "[NULL]";
        } else {
            Object obj = null;
            if (array instanceof java.util.Hashtable) {
                array = ((java.util.Hashtable) array).entrySet().toArray();
            } else if (array instanceof java.util.HashSet) {
                array = ((java.util.HashSet) array).toArray();
            } else if (array instanceof java.util.Collection) {
                array = ((java.util.Collection) array).toArray();
            }
            int length = java.lang.reflect.Array.getLength(array);
            int lastItem = length - 1;
            StringBuffer sb = new StringBuffer("[");
            for (int i = 0; i < length; i++) {
                obj = java.lang.reflect.Array.get(array, i);
                if (obj != null) {
                    sb.append(obj);
                } else {
                    sb.append("[NULL]");
                }
                if (i < lastItem) {
                    sb.append(", ");
                }
            }
            sb.append("]");
            return sb.toString();
        }
    }
}
