/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002-2006, GeoTools Project Managment Committee (PMC)
 *    
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library 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.
 *
 *    Created on 03 July 2002, 10:21
 */
package org.geotools.filter;

import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Logger;

import org.geotools.feature.FeatureType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.TopologyException;


/**
 * parsez short sections of gml for use in expressions and filters Hopefully we
 * can get away without a full parser here.
 *
 * @author iant
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/filter/ExpressionDOMParser.java $
 */
public final class ExpressionDOMParser {
    /** The logger for the filter module. */
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.filter");

    /** Factory for creating filters. */
    private FilterFactory ff;

    /** Factory for creating geometry objects */
    private static GeometryFactory gfac = new GeometryFactory();

    /** int representation of a box */
    private static final int GML_BOX = 1;

    /** int representation of a polygon */
    private static final int GML_POLYGON = 2;

    /** int representation of a linestring */
    private static final int GML_LINESTRING = 3;

    /** int representation of a point */
    private static final int GML_POINT = 4;

    /** number of coordinates in a box */
    private static final int NUM_BOX_COORDS = 5;

    /**
     * Creates a new instance of ExpressionXmlParser
     */
    private ExpressionDOMParser() {
    	this( FilterFactoryFinder.createFilterFactory() );    	
    }
    /** Constructor injection */
    public ExpressionDOMParser( FilterFactory factory ){
    	ff = factory;
    	LOGGER.finer("made new logic factory");
    }
    /** Setter injection */
    public void setFilterFactory( FilterFactory factory ){
    	ff = factory;
    }
    
    /**
     * 
     * @deprecated Please use ExpressionDOMParser.expression
     * @param root
     */
    public static Expression parseExpression( Node root ){
    	ExpressionDOMParser parser = new ExpressionDOMParser();
    	return parser.expression( root );
    }
    /**
     * parses an expression for a filter.
     *
     * @param root the root node to parse, should be an filter expression.
     *
     * @return the geotools representation of the expression held in the node.
     */
    public Expression expression(Node root) {
        LOGGER.finer("parsingExpression " + root.getLocalName());

        //NodeList children = root.getChildNodes();
        //LOGGER.finest("children "+children);
        if ((root == null) || (root.getNodeType() != Node.ELEMENT_NODE)) {
            LOGGER.finer("bad node input ");

            return null;
        }

        LOGGER.finer("processing root " + root.getLocalName());

        Node child = root;

        String childName = (child.getLocalName() != null)
            ? child.getLocalName() : child.getNodeName();
        
        if (childName.indexOf(':') != -1)
        {
        	//the DOM parser wasnt properly set to handle namespaces...
        	childName = childName.substring(childName.indexOf(':')+1);
        }

        if (childName.equalsIgnoreCase("Literal")) {
            LOGGER.finer("processing literal " + child);

            NodeList kidList = child.getChildNodes();
            LOGGER.finest("literal elements (" + kidList.getLength() + ") "
                + kidList.toString());

            for (int i = 0; i < kidList.getLength(); i++) {
                Node kid = kidList.item(i);
                LOGGER.finest("kid " + i + " " + kid);

                if (kid == null) {
                    LOGGER.finest("Skipping ");

                    continue;
                }

                if (kid.getNodeValue() == null) {
                    /* it might be a gml string so we need to convert it into
                     * a geometry this is a bit tricky since our standard
                     * gml parser is SAX based and we're a DOM here.
                     */
                    LOGGER.finer("node " + kid.getNodeValue() + " namespace "
                        + kid.getNamespaceURI());
                    LOGGER.fine("a literal gml string?");

                    try {
                        Geometry geom = parseGML(kid);

                        if (geom != null) {
                            LOGGER.finer("built a " + geom.getGeometryType()
                                + " from gml");
                            LOGGER.finer("\tpoints: " + geom.getNumPoints());
                        } else {
                            LOGGER.finer(
                                "got a null geometry back from gml parser");
                        }

                        return ff.createLiteralExpression(geom);
                    } catch (IllegalFilterException ife) {
                        LOGGER.warning("Problem building GML/JTS object: "
                            + ife);
                    }

                    return null;
                }

                if (kid.getNodeValue().trim().length() == 0) {
                    LOGGER.finest("empty text element");

                    continue;
                }

                // debuging only

                /*switch(kid.getNodeType()){
                   case Node.ELEMENT_NODE:
                       LOGGER.finer("element :"+kid);
                       break;
                   case Node.TEXT_NODE:
                       LOGGER.finer("text :"+kid);
                       break;
                   case Node.ATTRIBUTE_NODE:
                       LOGGER.finer("Attribute :"+kid);
                       break;
                   case Node.CDATA_SECTION_NODE:
                       LOGGER.finer("Cdata :"+kid);
                       break;
                   case Node.COMMENT_NODE:
                       LOGGER.finer("comment :"+kid);
                       break;
                   } */
                String nodeValue = kid.getNodeValue();
                LOGGER.finer("processing " + nodeValue);

                try {
                    //always store internal values as strings.  We might lose info otherwise.
                    return ff.createLiteralExpression(nodeValue);
                } catch (IllegalFilterException ife) {
                    LOGGER.finer("Unable to build expression " + ife);

                    return null;
                }
            }
        }

        if (childName.equalsIgnoreCase("add")) {
            try {
                LOGGER.fine("processing an Add");

                //Node left = null;
                //Node right = null;
                MathExpression math = ff.createMathExpression(DefaultExpression.MATH_ADD);
                Node value = child.getFirstChild();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add left value -> " + value + "<-");
                math.addLeftValue(parseExpression(value));
                value = value.getNextSibling();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add right value -> " + value + "<-");
                math.addRightValue(parseExpression(value));

                return math;
            } catch (IllegalFilterException ife) {
                LOGGER.warning("Unable to build expression " + ife);

                return null;
            }
        }

        if (childName.equalsIgnoreCase("sub")) {
            try {
                //NodeList kids = child.getChildNodes();
                MathExpression math;
                math = ff.createMathExpression(DefaultExpression.MATH_SUBTRACT);

                Node value = child.getFirstChild();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add left value -> " + value + "<-");
                math.addLeftValue(parseExpression(value));
                value = value.getNextSibling();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add right value -> " + value + "<-");
                math.addRightValue(parseExpression(value));

                return math;
            } catch (IllegalFilterException ife) {
                LOGGER.warning("Unable to build expression " + ife);

                return null;
            }
        }

        if (childName.equalsIgnoreCase("mul")) {
            try {
                //NodeList kids = child.getChildNodes();
                MathExpression math;
                math = ff.createMathExpression(DefaultExpression.MATH_MULTIPLY);

                Node value = child.getFirstChild();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add left value -> " + value + "<-");
                math.addLeftValue(parseExpression(value));
                value = value.getNextSibling();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add right value -> " + value + "<-");
                math.addRightValue(parseExpression(value));

                return math;
            } catch (IllegalFilterException ife) {
                LOGGER.warning("Unable to build expression " + ife);

                return null;
            }
        }

        if (childName.equalsIgnoreCase("div")) {
            try {
                MathExpression math;
                math = ff.createMathExpression(DefaultExpression.MATH_DIVIDE);

                Node value = child.getFirstChild();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add left value -> " + value + "<-");
                math.addLeftValue(parseExpression(value));
                value = value.getNextSibling();

                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                LOGGER.finer("add right value -> " + value + "<-");
                math.addRightValue(parseExpression(value));

                return math;
            } catch (IllegalFilterException ife) {
                LOGGER.warning("Unable to build expression " + ife);

                return null;
            }
        }

        if (childName.equalsIgnoreCase("PropertyName")) {
            try {
            	//JD: trim whitespace here
            	String value = child.getFirstChild().getNodeValue();
            	value = value != null ? value.trim() : value;
                AttributeExpression attribute = ff
                    .createAttributeExpression((FeatureType) null, value);

                //                attribute.setAttributePath(child.getFirstChild().getNodeValue());
                return attribute;
            } catch (IllegalFilterException ife) {
                LOGGER.warning("Unable to build expression: " + ife);

                return null;
            }
        }

        if (childName.equalsIgnoreCase("Function")) {
            FunctionExpression func = null;
            Element param = (Element) child;

            NamedNodeMap map = param.getAttributes();
            String funcName = null;

            for (int k = 0; k < map.getLength(); k++) {
                String res = map.item(k).getNodeValue();
                String name = map.item(k).getLocalName();

                if (name == null) {
                    name = map.item(k).getNodeName();
                }
                if (name.indexOf(':') != -1)
                {
                	//the DOM parser wasnt properly set to handle namespaces...
                	name = name.substring(name.indexOf(':')+1);
                }

                LOGGER.fine("attribute " + name + " with value of " + res);

                if (name.equalsIgnoreCase("name")) {
                    funcName = res;
                    func = ff.createFunctionExpression(res);
                }
            }

            if (func == null) {
                if (funcName != null) {
                    LOGGER.severe("failed to create instance of function "
                        + funcName);
                } else {
                    LOGGER.severe("failed to find a function name in " + child);
                }

                return null;
            }

            int argCount = func.getArgCount();
            Expression[] args = new Expression[argCount];
            Node value = child.getFirstChild();

            for (int i = 0; i < argCount; i++) {
                while (value.getNodeType() != Node.ELEMENT_NODE) {
                    value = value.getNextSibling();
                }

                args[i] = parseExpression(value);
                value = value.getNextSibling();
            }

            func.setArgs(args);

            return func;
        }

        if (child.getNodeType() == Node.TEXT_NODE) {
            LOGGER.finer("processing a text node " + root.getNodeValue());

            String nodeValue = root.getNodeValue();
            LOGGER.finer("Text name " + nodeValue);

            // see if it's an int
            try {
                try {
                    Integer intLiteral = new Integer(nodeValue);

                    return ff.createLiteralExpression(intLiteral);
                } catch (NumberFormatException e) {
                    /* really empty */
                }

                try {
                    Double doubleLit = new Double(nodeValue);

                    return ff.createLiteralExpression(doubleLit);
                } catch (NumberFormatException e) {
                    /* really empty */
                }

                return ff.createLiteralExpression(nodeValue);
            } catch (IllegalFilterException ife) {
                LOGGER.finer("Unable to build expression " + ife);
            }
        }

        return null;
    }

    /**
     * @deprecated Please use ExpressionDOMParser.gml
     * @param root
     * @return the java representation of the geometry contained in root.
     */
    public static Geometry parseGML(Node root) {
    	ExpressionDOMParser parser = new ExpressionDOMParser();
    	return parser.gml( root );
    }
    /**
     * Parses the gml of this node to jts.
     *
     * @param root the parent node of the gml to parse.
     * @return the java representation of the geometry contained in root.
     */
    public Geometry gml(Node root) {
    	LOGGER.finer("processing gml " + root);

        List coordList;
        int type = 0;
        Node child = root;

        //Jesus I hate DOM.  I have no idea why this was checking for localname
        //and then nodename - lead to non-deterministic behavior, that for
        //some reason only failed if the filter parser was used within the
        //SLDparser.  I really would like that class redone, so we don't have
        //to use this crappy DOM GML parser.
        String childName = child.getNodeName();
            if(childName == null)
            {
                childName = child.getLocalName();
            }
     		if(!childName.startsWith("gml:"))
     		{
                    childName = "gml:" + childName;
            }

        if (childName.equalsIgnoreCase("gml:box")) {
            type = GML_BOX;
            coordList = parseCoords(child);

            com.vividsolutions.jts.geom.Envelope env = new com.vividsolutions.jts.geom.Envelope();

            for (int i = 0; i < coordList.size(); i++) {
                env.expandToInclude((Coordinate) coordList.get(i));
            }

            Coordinate[] coords = new Coordinate[NUM_BOX_COORDS];
            coords[0] = new Coordinate(env.getMinX(), env.getMinY());
            coords[1] = new Coordinate(env.getMinX(), env.getMaxY());
            coords[2] = new Coordinate(env.getMaxX(), env.getMaxY());
            coords[3] = new Coordinate(env.getMaxX(), env.getMinY());
            coords[4] = new Coordinate(env.getMinX(), env.getMinY());

            com.vividsolutions.jts.geom.LinearRing ring = null;

            try {
                ring = gfac.createLinearRing(coords);
            } catch (com.vividsolutions.jts.geom.TopologyException tope) {
                LOGGER.fine("Topology Exception in GMLBox" + tope);

                return null;
            }

            return gfac.createPolygon(ring, null);
        }

        //check for geometry properties
        if (childName.equalsIgnoreCase("gml:polygonmember") || 
        		childName.equalsIgnoreCase( "gml:pointmember") || 
        		childName.equalsIgnoreCase( "gml:linestringmember") || 
        		childName.equalsIgnoreCase( "gml:linearringmember" ) ) {
        	
        	for ( int i = 0; i < child.getChildNodes().getLength(); i++ ) {
        		Node newChild = child.getChildNodes().item( i );
        		if ( newChild.getNodeType() == Node.ELEMENT_NODE ) {
        			childName = newChild.getNodeName();
        			if ( !childName.startsWith("gml:" ) ) {
        				childName = "gml:" + childName;
        			}
        			root = newChild;
        			child = newChild;
        			break;
        		}
        	}
        }
        
        if (childName.equalsIgnoreCase("gml:polygon")) {
            LOGGER.finer("polygon");
            type = GML_POLYGON;

            LinearRing outer = null;
            List inner = new ArrayList();
            NodeList kids = root.getChildNodes();

            for (int i = 0; i < kids.getLength(); i++) {
                Node kid = kids.item(i);
                LOGGER.finer("doing " + kid);

                String kidName = kid.getNodeName();
                if(kidName == null)
                {
                    kidName = child.getLocalName();
                }
                if(!kidName.startsWith("gml:"))
                {
                	kidName = "gml:" + kidName;
                }       	              
       	
		
                if (kidName.equalsIgnoreCase("gml:outerBoundaryIs")) {
                    outer = (LinearRing) parseGML(kid);
        	 }
        	
                if (kidName.equalsIgnoreCase("gml:innerBoundaryIs")) {
                    inner.add((LinearRing) parseGML(kid));
                }
            }

            if (inner.size() > 0) {
                return gfac.createPolygon(outer,
                    (LinearRing[]) inner.toArray(new LinearRing[0]));
            } else {
                return gfac.createPolygon(outer, null);
            }
        }

        if (childName.equalsIgnoreCase("gml:outerBoundaryIs")
                || childName.equalsIgnoreCase("gml:innerBoundaryIs")) {
            LOGGER.finer("Boundary layer");

            NodeList kids = ((Element) child).getElementsByTagName(
                    "gml:LinearRing");
            if (kids.getLength() ==0)
            	kids = ((Element) child).getElementsByTagName("LinearRing");
            return parseGML(kids.item(0));
        }

        if (childName.equalsIgnoreCase("gml:linearRing")) {
            LOGGER.finer("LinearRing");
            coordList = parseCoords(child);

            com.vividsolutions.jts.geom.LinearRing ring = null;

            try {
                ring = gfac.createLinearRing((Coordinate[]) coordList.toArray(
                            new Coordinate[] {  }));
            } catch (TopologyException te) {
                LOGGER.finer("Topology Exception build linear ring: " + te);

                return null;
            }

            return ring;
        }
        
        if (childName.equalsIgnoreCase("gml:linestring")) {
            LOGGER.finer("linestring");
            type = GML_LINESTRING;
            coordList = parseCoords(child);

            com.vividsolutions.jts.geom.LineString line = null;
            line = gfac.createLineString((Coordinate[]) coordList.toArray(
                        new Coordinate[] {  }));

            return line;
        }

        if (childName.equalsIgnoreCase("gml:point")) {
            LOGGER.finer("point");
            type = GML_POINT;
            coordList = parseCoords(child);

            com.vividsolutions.jts.geom.Point point = null;
            point = gfac.createPoint((Coordinate) coordList.get(0));

            return point;
        }

        if (childName.toLowerCase().startsWith("gml:multipolygon") 
    		|| childName.toLowerCase().startsWith("gml:multilinestring") 
    		|| childName.toLowerCase().startsWith("gml:multipoint")) {
        	
        	List multi = new ArrayList();

            // parse all children thru parseGML
            NodeList kids = child.getChildNodes();

            for (int i = 0; i < kids.getLength(); i++) {
            	if ( kids.item(i).getNodeType() == Node.ELEMENT_NODE ) {
            		multi.add(parseGML(kids.item(i)));	
            	}
            }
            
            if ( childName.toLowerCase().startsWith("gml:multipolygon") ) {
            	LOGGER.finer( "MultiPolygon" );
            	return gfac.createMultiPolygon((Polygon[]) multi.toArray(
                        new Polygon[0]));
            }
            else if ( childName.toLowerCase().startsWith("gml:multilinestring") ) {
            	LOGGER.finer( "MultiLineString" );
            	return gfac.createMultiLineString((LineString[]) multi.toArray(
                        new LineString[0]));
            }
            else {
            	LOGGER.finer( "MultiPoint" );
            	return gfac.createMultiPoint((Point[])multi.toArray(new Point[0]) );
            }
            

        }

        return null;
    }

    /**
     * Parse a DOM node into a coordiante list.
     * 
     * @deprecated please use ExpressionDOMParser.coords()
     * @param root the root node representation of gml:coordinates.
     * @return the coordinates in a list.
     */
    public static java.util.List parseCoords(Node root) {
    	ExpressionDOMParser parser = new ExpressionDOMParser();
    	return parser.coords( root );
    }
    /**
     * Parses a dom node into a coordinate list.
     *
     * @param root the root node representation of gml:coordinates.
     * @return the coordinates in a list.
     */
    public java.util.List coords(Node root) {
        LOGGER.finer("parsing coordinate(s) " + root);

        List clist = new ArrayList();
        NodeList kids = root.getChildNodes();

        for (int i = 0; i < kids.getLength(); i++) {
            Node child = kids.item(i);
            LOGGER.finer("doing " + child);

            //if (child.getLocalName().equalsIgnoreCase("gml:coordinate")) {
            //  String internal = child.getNodeValue();
            //}
            String childName = child.getNodeName();
            if(childName == null)
            {
                childName = child.getLocalName();
            }
     		if(!childName.startsWith("gml:"))
     		{
                    childName = "gml:" + childName;
            }
            if (childName.equalsIgnoreCase("gml:coord"))
            {
            	//DJB: adding support for:
            	//<gml:coord><gml:X>-180.0</gml:X><gml:Y>-90.0</gml:Y></gml:coord>
				//<gml:coord><gml:X>180.0</gml:X><gml:Y>90.0</gml:Y></gml:coord>
            	Coordinate c = new Coordinate();
            	NodeList grandChildren = child.getChildNodes();

                for (int t = 0; t < grandChildren.getLength(); t++) 
                {
                	Node grandChild = grandChildren.item(t);
                	String grandChildName = grandChild.getNodeName();
                    if(grandChildName == null)
                    	grandChildName = grandChild.getLocalName();
             		if(!grandChildName.startsWith("gml:"))
             			grandChildName = "gml:" + grandChildName;
                    
             	    if (grandChildName.equalsIgnoreCase("gml:x")) 
             	    {
             	    	c.x = Double.parseDouble(grandChild.getChildNodes().item(0).getNodeValue().trim());
             	    }
             	    else if (grandChildName.equalsIgnoreCase("gml:y")) 
             	    {
             	    	c.y = Double.parseDouble(grandChild.getChildNodes().item(0).getNodeValue().trim());
             	    }
             	   else if (grandChildName.equalsIgnoreCase("gml:z")) 
            	    {
            	    	c.z = Double.parseDouble(grandChild.getChildNodes().item(0).getNodeValue().trim());
            	    }
                }
                clist.add(c);
            }
           

            if (childName.equalsIgnoreCase("gml:coordinates")) {
                LOGGER.finer("coordinates "
                    + child.getFirstChild().getNodeValue());

                NodeList grandKids = child.getChildNodes();

                for (int k = 0; k < grandKids.getLength(); k++) {
                    Node grandKid = grandKids.item(k);

                    if (grandKid.getNodeValue() == null) {
                        continue;
                    }

                    if (grandKid.getNodeValue().trim().length() == 0) {
                        continue;
                    }

                    String outer = grandKid.getNodeValue().trim();
                    //handle newline and tab whitespace along with space
                    StringTokenizer ost = new StringTokenizer(outer, " \n\t");

                    while (ost.hasMoreTokens()) {
                        String internal = ost.nextToken();
                        StringTokenizer ist = new StringTokenizer(internal, ",");
                        double xCoord = Double.parseDouble(ist.nextToken());
                        double yCoord = Double.parseDouble(ist.nextToken());
                        double zCoord = Double.NaN;

                        if (ist.hasMoreTokens()) {
                            zCoord = Double.parseDouble(ist.nextToken());
                        }

                        clist.add(new Coordinate(xCoord, yCoord, zCoord));
                    }
                }
            }
        }

        return clist;
    }
}
