package com.osafe.geo;

import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.GetMethod;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.cache.UtilCache;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.osafe.util.Util;

public class OsafeGeo {
    /** A <code>OsafeGeo</code> instance that represents a address appropriate with xml generated by google geocoding API. */

    protected static UtilCache<String, OsafeGeo> geoCache = UtilCache.createUtilCache("osafe.OsafeGeoCache");

    private static String GEOPT_REQUEST_URL = null;
    private static HttpClient httpClient = new HttpClient(new MultiThreadedHttpConnectionManager());

    public static final OsafeGeo EmptyOsafeGeo = new NullOsafeGeo();

    protected final String latitude;
    protected final String longitude;

    /**
     * @param latitude
     * @param longitude
     */
    public OsafeGeo(String latitude, String longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        try {
            OsafeGeo that = (OsafeGeo) obj;
            return this.latitude.equals(that.latitude) && this.longitude.equals(that.longitude);
        } catch (Exception e) {}
        return false;
    }

    /** Returns a <code>String</code> formatted as
     * latitude, longitude.
     */
    @Override
    public String toString() {
        return this.latitude + ":" + this.longitude ;
    }

    public boolean isEmpty() {
        return ((this.latitude == null) || (this.latitude.length() == 0)) && ((this.longitude == null) || (this.longitude.length() == 0));
    }

    public boolean isNotEmpty() {
        return !this.isEmpty();
    }

    /** Returns the latitude in this Geo. */
    public String latitude() {
        return this.latitude;
    }

    /** Returns the longitude in this this Geo. */
    public String longitude() {
        return this.longitude;
    }
    

    /** Returns a <code>OsafeGeo</code> instance derived from an address
     * <code>String</code> value.
     *
     * @param address 
     * @return A <code>OsafeGeo</code> instance
     */
    public static OsafeGeo getInstanceFromAddress(String address) {
        try {
            OsafeGeo osafeGeo = geoCache.get(address);
            if (osafeGeo == null)
            {
                StringBuilder urlBuilder = new StringBuilder(GEOPT_REQUEST_URL);
                urlBuilder.append("&address=").append(URLEncoder.encode(address, "UTF-8"));
                final GetMethod getMethod = new GetMethod(urlBuilder.toString());
                String latitude = null;
                String longitude = null;
                try {
                    httpClient.executeMethod(getMethod);
                    Reader reader = new InputStreamReader(getMethod.getResponseBodyAsStream(), getMethod.getResponseCharSet());
                    int data = reader.read();
                    char[] buffer = new char[1024];
                    Writer writer = new StringWriter();
                    while ((data = reader.read(buffer)) != -1) {
                            writer.write(buffer, 0, data);
                    }
                    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                    DocumentBuilder db = dbf.newDocumentBuilder();
                    InputSource is = new InputSource();
                    is.setCharacterStream(new StringReader("<"+writer.toString().trim()));
                    Document xmlDocument = db.parse(is);
    
                    Debug.logError("Status: " + getStatus(xmlDocument),"OsafeGeo");
                    if (getStatus(xmlDocument).equals("OK")) {
                        latitude = getLatitude(xmlDocument);
                        if (latitude != null) latitude = Double.valueOf(latitude).toString();
                        longitude = getlongitude(xmlDocument);
                        if (longitude != null) longitude = Double.valueOf(longitude).toString();
                    }
                } finally {
                    getMethod.releaseConnection();
                }
                osafeGeo= new OsafeGeo(latitude, longitude);
                geoCache.put(address, osafeGeo);
            }
            return osafeGeo;
        } catch (Exception e) {
            return EmptyOsafeGeo;
        }
    }

    /** Returns a <code>OsafeGeo</code> instance derived from a <code>String</code>
     * instance. If <code>address</code> is <code>null</code>,
     * returns a empty <code>OsafeGeo</code>.
     *
     * @param address A <code>String</code> instance, can not be <code>null</code>
     * @param productStoreid  <code>String</code> instance, can not be <code>null</code>
     * @return A <code>OsafeGeo</code> instance
     */
    public static OsafeGeo fromAddress(String address, String productStoreid) {
        if ((address == null) || (address.length() == 0) ||(productStoreid == null) || (productStoreid.length() == 0)) return EmptyOsafeGeo;
        GEOPT_REQUEST_URL = Util.getProductStoreParm(productStoreid, "GMAP_GEOPT_API_URL");
        return getInstanceFromAddress(address);
    }

    /** <p>"OK" indicates that no errors occurred; the address was successfully parsed and at least one geocode was returned.
     * "ZERO_RESULTS" indicates that the geocode was successful but returned no results. 
     * "OVER_QUERY_LIMIT" indicates that you are over your quota.
     * "REQUEST_DENIED" indicates that your request was denied, generally because of lack of a sensor parameter.
     * "INVALID_REQUEST" generally indicates that the query (address or latlng) is missing.</p>
     */
    private static String getStatus(Document doc) throws XPathExpressionException {
        return getXpathValue(doc, "//GeocodeResponse/status/text()");
    }

    private static String getLatitude(Document doc) throws XPathExpressionException {
        return getXpathValue(doc, "//GeocodeResponse/result[1]/geometry/location/lat/text()");
    }

    private static String getlongitude(Document doc) throws XPathExpressionException {
        return getXpathValue(doc,"//GeocodeResponse/result[1]/geometry/location/lng/text()");
    }

    private static String getXpathValue(Document doc, String strXpath) throws XPathExpressionException {
        XPath xPath = XPathFactory.newInstance().newXPath();
        XPathExpression expr = xPath.compile(strXpath);
        String resultData = null;
        Object result = expr.evaluate(doc, XPathConstants.NODESET);
        NodeList nodes = (NodeList) result;
        for (int i = 0; i < nodes.getLength(); i++) {
            resultData = nodes.item(i).getNodeValue();
        }
        return resultData;
    }

    protected static class NullOsafeGeo extends OsafeGeo {
        protected NullOsafeGeo() {
            super(null, null);
        }
        @Override
        public boolean isEmpty() {
            return true;
        }
    }
}
