/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-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.
 */
package org.geotools.data.ows;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

import org.geotools.ows.ServiceException;

/**
 * This abstract class provides a building block for one to implement an 
 * Open Web Service (OWS) client. Each OWS is usually defined by an OGC 
 * specification, available at <a href="http://www.opengeospatial.org">http://www.opengeospatial.org</a>.
 * 
 * This class provides version negotiation, Capabilities document retrieval,
 * and a request/response infrastructure. Implementing subclasses need to
 * provide their own Specifications (representing versions of the OWS to be
 * implemented) and their own request/response instances.
 * 
 * @author Richard Gould
 *
 */
public abstract class AbstractOpenWebService {
    protected final URL serverURL;
    protected Capabilities capabilities;

    /** Contains the specifications that are to be used with this service */
    protected Specification[] specs;
    protected Specification specification;
    
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.ows");

    /**
     * Set up the specifications used and retrieve the Capabilities document
     * given by serverURL.
     * 
     * @param serverURL a URL that points to the capabilities document of a server
     * @throws IOException if there is an error communicating with the server
     * @throws ServiceException if the server responds with an error
     */
    public AbstractOpenWebService( final URL serverURL ) throws IOException, ServiceException {
    	if (serverURL == null) {
			throw new NullPointerException("ServerURL cannot be null");
		}
    	
    	this.serverURL = serverURL;

        setupSpecifications();
        
        capabilities = negotiateVersion();
        if (capabilities == null) {
        	throw new ServiceException("Unable to retrieve or parse Capabilities document.");
        }
    }

    public AbstractOpenWebService(Capabilities capabilties, URL serverURL) {
		if (capabilties == null) {
			throw new NullPointerException("Capabilities cannot be null.");
		}
		
		if (serverURL == null) {
			throw new NullPointerException("ServerURL cannot be null");
		}
		
		setupSpecifications();
		
		for (int i = 0; i < specs.length; i++) {
			if (specs[i].getVersion().equals(capabilties.getVersion())) {
				specification = specs[i];
				break;
			}
		}
		
		if (specification == null) {
			specification = specs[specs.length-1];
			LOGGER.warning("Unable to choose a specification based on cached capabilities. "
					+"Arbitrarily choosing spec '"+specification.getVersion()+"'.");
		}
		
		this.serverURL = serverURL;
		this.capabilities = capabilties;
	}

	/**
     * Sets up the specifications/versions that this server is capable of
     * communicating with.
     */
    protected abstract void setupSpecifications();

    /**
     * <p>
     * Version number negotiation occurs as follows (credit OGC):
     * <ul>
     * <li><b>1) </b> If the server implements the requested version number, the server shall send that version.</li>
     * <li><b>2a) </b> If a version unknown to the server is requested, the server shall send the highest version less
     * than the requested version.</li>
     * <li><b>2b) </b> If the client request is for a version lower than any of those known to the server, then the
     * server shall send the lowest version it knows.</li>
     * <li><b>3a) </b> If the client does not understand the new version number sent by the server, it may either cease
     * communicating with the server or send a new request with a new version number that the client does understand but
     * which is less than that sent by the server (if the server had responded with a lower version).</li>
     * <li><b>3b) </b> If the server had responded with a higher version (because the request was for a version lower
     * than any known to the server), and the client does not understand the proposed higher version, then the client
     * may send a new request with a version number higher than that sent by the server.</li>
     * </ul>
     * </p>
     * <p>
     * The OGC tells us to repeat this process (or give up). This means we are 
     * actually going to come up with a bit of setup cost in figuring out our 
     * GetCapabilities request. This means that it is possible that we may make
     * multiple requests before being satisfied with a response. 
     * 
     * Also, if we are unable to parse a given version for some reason, 
     * for example, malformed XML, we will request a lower version until
     * we have run out of versions to request with. Thus, a server that does
     * not play nicely may take some time to parse and might not even 
     * succeed.
     * 
     * @return a capabilities object that represents the Capabilities on the server
     * @throws IOException if there is an error communicating with the server, or the XML cannot be parsed
     * @throws ServiceException if the server returns a ServiceException
     */
    protected Capabilities negotiateVersion() throws IOException, ServiceException {
        List versions = new ArrayList(specs.length);
        Exception exception = null;

        for( int i = 0; i < specs.length; i++ ) {
            versions.add(i, specs[i].getVersion());
        }

        int minClient = 0;
        int maxClient = specs.length - 1;

        int test = maxClient;

        while( (minClient <= test) && (test <= maxClient) ) {
            Specification tempSpecification = specs[test];
            String clientVersion = tempSpecification.getVersion();

            GetCapabilitiesRequest request = tempSpecification.createGetCapabilitiesRequest(serverURL);

            //Grab document
            Capabilities tempCapabilities;
            try {
                tempCapabilities = issueRequest(request).getCapabilities();
            } catch (ServiceException e) {
            	tempCapabilities = null;
            	exception = e;
            }

            int compare = -1;
            String serverVersion = clientVersion; //Ignored if caps is null

            if (tempCapabilities != null) {

                serverVersion = tempCapabilities.getVersion();

                compare = serverVersion.compareTo(clientVersion);
            }

            if (compare == 0) {
                //we have an exact match and have capabilities as well!
                this.specification = tempSpecification;

                return tempCapabilities;
            }

            if (tempCapabilities != null && versions.contains(serverVersion)) {
                // we can communicate with this server
                int index = versions.indexOf(serverVersion);
                this.specification = specs[index];

                return tempCapabilities;

            } else if (compare < 0) {
                // server responded lower then we asked - and we don't understand.
                maxClient = test - 1; // set current version as limit

                // lets try and go one lower?
                //	           
                clientVersion = before(versions, serverVersion);

                if (clientVersion == null) {
                    if (exception != null) {
                    	if (exception instanceof ServiceException) {
                    		throw (ServiceException) exception;
                    	}
                        IOException e = new IOException(exception.getMessage());
                        throw e;
                    }
                    return null; // do not know any lower version numbers
                }

                test = versions.indexOf(clientVersion);
            } else {
                // server responsed higher than we asked - and we don't understand
                minClient = test + 1; // set current version as lower limit

                // lets try and go one higher
                clientVersion = after(versions, serverVersion);

                if (clientVersion == null) {
                    if (exception != null) {
                    	if (exception instanceof ServiceException) {
                    		throw (ServiceException) exception;
                    	}
                        IOException e = new IOException(exception.getMessage());
                        throw e;
                    }
                    return null; // do not know any lower version numbers
                }

                test = versions.indexOf(clientVersion);
            }
        }

        // could not talk to this server
        if (exception != null) {
            IOException e = new IOException(exception.getMessage());
            throw e;
        }
        return null;
    }

    /**
     * Utility method returning the known version, just before the provided version
     * 
     * @param known List<String> of all known versions
     * @param version the boundary condition
     * @return the version just below the provided boundary version
     */
    String before( List known, String version ) {
        if (known.isEmpty()) {
            return null;
        }

        String before = null;

        for( Iterator i = known.iterator(); i.hasNext(); ) {
            String test = (String) i.next();

            if (test.compareTo(version) < 0) {

                if ((before == null) || (before.compareTo(test) < 0)) {
                    before = test;
                }
            }
        }

        return before;
    }

    /**
     * Utility method returning the known version, just after the provided version
     * 
     * @param known a List<String> of all known versions
     * @param version the boundary condition
     * @return a version just after the provided boundary condition
     */
    String after( List known, String version ) {
        if (known.isEmpty()) {
            return null;
        }

        String after = null;

        for( Iterator i = known.iterator(); i.hasNext(); ) {
            String test = (String) i.next();

            if (test.compareTo(version) > 0) {
                if ((after == null) || (after.compareTo(test) < 0)) {
                    after = test;
                }
            }
        }

        return after;
    }

    /**
     * Issues a request to the server and returns that server's response. It
     * asks the server to send the response gzipped to provide a faster transfer
     * time. 
     * 
     * @param request the request to be issued
     * @return a response from the server, which is created according to the specific Request
     * @throws IOException if there was a problem communicating with the server
     * @throws ServiceException if the server responds with an exception or returns bad content
     */
    protected Response internalIssueRequest( Request request ) throws IOException, ServiceException {
        URL finalURL = request.getFinalURL();

        HttpURLConnection connection = (HttpURLConnection) finalURL.openConnection();
        
        connection.addRequestProperty("Accept-Encoding", "gzip");
        
        if (request.requiresPost()) {
        	connection.setRequestMethod("POST");
        	connection.setDoOutput(true);
        	connection.setRequestProperty("Content-type", request.getPostContentType());

        	OutputStream outputStream = connection.getOutputStream();

        	if (LOGGER.isLoggable(Level.FINE)) {
        		ByteArrayOutputStream out = new ByteArrayOutputStream();
        		request.performPostOutput(out);
        		
        		InputStream in = new ByteArrayInputStream(out.toByteArray());
        		BufferedReader reader = new BufferedReader(new InputStreamReader(in));

        		PrintStream stream = new PrintStream(outputStream);
        		
        		String postText = "";
        		
        		while (reader.ready()) {
        			String input = reader.readLine();
        			postText = postText + input;
        			stream.println(input);
        		}
        		LOGGER.fine(postText);
        		System.out.println(postText);
        		
        		out.close();
        		in.close();
        	} else {
        		request.performPostOutput(outputStream);
        	}
        	
        	outputStream.flush();
        	outputStream.close();
        } else {
        	connection.setRequestMethod("GET");
        }
        

        InputStream inputStream = connection.getInputStream();
        
        if (connection.getContentEncoding() != null && connection.getContentEncoding().indexOf("gzip") != -1) { //$NON-NLS-1$
            inputStream = new GZIPInputStream(inputStream);
        }

        String contentType = connection.getContentType();
        
        return request.createResponse(contentType, inputStream);
    }
    
    public GetCapabilitiesResponse issueRequest(GetCapabilitiesRequest request) throws IOException, ServiceException {
    	return (GetCapabilitiesResponse) internalIssueRequest(request);
    }
    
    public void setLoggingLevel(Level newLevel) {
    	LOGGER.setLevel(newLevel);
    }
}
