/*
 * Copyright (C) 2001, 2006 United States Government as represented by the
 * Administrator of the National Aeronautics and Space Administration. All
 * Rights Reserved.
 */

package gov.nasa.worldwind;

import java.util.concurrent.atomic.*;
import java.util.zip.*;
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.io.*;

/**
 * @author Tom Gaskins
 * @version $Id: URLRetriever.java 2228 2007-07-06 22:18:04Z tgaskins $
 */
public abstract class URLRetriever extends WWObjectImpl implements Retriever
{
   private volatile String state = RETRIEVER_STATE_NOT_STARTED;

   private volatile int contentLength = 0;

   private AtomicInteger contentLengthRead = new AtomicInteger(0);

   private volatile String contentType;

   private volatile ByteBuffer byteBuffer;

   private volatile URLConnection connection;

   private final URL url;

   private final RetrievalPostProcessor postProcessor;

   private int connectTimeout = Configuration.getIntegerValue(
      AVKey.URL_CONNECT_TIMEOUT,
      8000);

   private int readTimeout = Configuration.getIntegerValue(
      AVKey.URL_READ_TIMEOUT,
      5000);

   private String URLProxyHost = Configuration.getStringValue(AVKey.URL_PROXY_HOST);
   
   private String URLProxyType = Configuration.getStringValue(AVKey.URL_PROXY_TYPE);
   
   private int    URLProxyPort = Configuration.getIntegerValue(AVKey.URL_PROXY_PORT);
   
   private int staleRequestLimit = -1;

   private long submitTime;

   private long beginTime;

   private long endTime;

   /**
    * @param url
    * @param postProcessor
    * @throws IllegalArgumentException if <code>url</code> or
    *            <code>postProcessor</code> is null
    */
   public URLRetriever(URL url, RetrievalPostProcessor postProcessor)
   {
      if (url == null)
      {
         String message = WorldWind.retrieveErrMsg("nullValue.URLIsNull");
         WorldWind.logger().log(java.util.logging.Level.FINE, message);
         throw new IllegalArgumentException(message);
      }
      if (postProcessor == null)
      {
         String message = WorldWind
               .retrieveErrMsg("nullValue.PostProcessorIsNull");
         WorldWind.logger().log(java.util.logging.Level.FINE, message);
         throw new IllegalArgumentException(message);
      }

      this.url = url;
      this.postProcessor = postProcessor;
   }

   public final URL getUrl()
   {
      return url;
   }

   public final int getContentLength()
   {
      return this.contentLength;
   }

   protected void setContentLengthRead(int length)
   {
      this.contentLengthRead.set(length);
   }

   public final int getContentLengthRead()
   {
      return this.contentLengthRead.get();
   }

   public final String getContentType()
   {
      return this.contentType;
   }

   public final ByteBuffer getBuffer()
   {
      return this.byteBuffer;
   }

   public final String getName()
   {
      return this.url.toString();
   }

   public final String getState()
   {
      return this.state;
   }

   protected final URLConnection getConnection()
   {
      return this.connection;
   }

   public final RetrievalPostProcessor getPostProcessor()
   {
      return postProcessor;
   }

   public final int getConnectTimeout()
   {
      return connectTimeout;
   }

   public int getReadTimeout()
   {
      return readTimeout;
   }

   public void setReadTimeout(int readTimeout)
   {
      this.readTimeout = readTimeout;
   }

   public int getStaleRequestLimit()
   {
      return staleRequestLimit;
   }

   public void setStaleRequestLimit(int staleRequestLimit)
   {
      this.staleRequestLimit = staleRequestLimit;
   }

   public final void setConnectTimeout(int connectTimeout)
   {
      this.connectTimeout = connectTimeout;
   }

   public long getSubmitTime()
   {
      return submitTime;
   }

   public void setSubmitTime(long submitTime)
   {
      this.submitTime = submitTime;
   }

   public long getBeginTime()
   {
      return beginTime;
   }

   public void setBeginTime(long beginTime)
   {
      this.beginTime = beginTime;
   }

   public long getEndTime()
   {
      return endTime;
   }

   public void setEndTime(long endTime)
   {
      this.endTime = endTime;
   }

   public final Retriever call() throws Exception
   {
      if (this.interrupted())
         return this;

      try
      {
         this.setState(RETRIEVER_STATE_STARTED);

         if (!this.interrupted())
         {
            this.setState(RETRIEVER_STATE_CONNECTING);
            this.connection = this.openConnection();
         }

         if (!this.interrupted())
         {
            this.setState(RETRIEVER_STATE_READING);
            this.byteBuffer = this.read();
         }

         if (!this.interrupted())
            this.setState(RETRIEVER_STATE_SUCCESSFUL);
      }
      catch (Exception e)
      {
         this.setState(RETRIEVER_STATE_ERROR);
         if (!(e instanceof java.net.SocketTimeoutException))
         {
            String message = WorldWind
                  .retrieveErrMsg("URLRetriever.ErrorAttemptingToRetrieve")
                  + this.url.toString();
            WorldWind.logger().log(java.util.logging.Level.FINE, message, e);
         }
         throw e;
      }
      finally
      {
         this.end();
      }

      return this;
   }

   private void setState(String state)
   {
      String oldState = this.state;
      this.state = state;
      this.firePropertyChange(AVKey.RETRIEVER_STATE, oldState, this.state);
   }

   private boolean interrupted()
   {
      if (Thread.currentThread().isInterrupted())
      {
         this.setState(RETRIEVER_STATE_INTERRUPTED);
         String message = WorldWind
               .retrieveErrMsg("URLRetriever.RetrievalInterruptedFor")
               + this.url.toString();
         WorldWind.logger().log(java.util.logging.Level.FINER, message);
         return true;
      }
      return false;
   }

   private URLConnection openConnection() throws IOException
   {

      try
      {
        
         if (URLProxyHost != null)
         { 
            SocketAddress addr = new InetSocketAddress(URLProxyHost, URLProxyPort);

            Proxy proxy = null;
            if (URLProxyType.equals("Proxy.Type.Http"))
            {
               proxy = new Proxy(Proxy.Type.HTTP, addr);
            }
            else if (URLProxyType.equals("Proxy.Type.SOCKS"))
            {
               proxy = new Proxy(Proxy.Type.SOCKS, addr);
            }
            
            this.connection = this.url.openConnection(proxy);
         }
         else
         {
            this.connection = this.url.openConnection();
         }
      }
      catch (java.io.IOException e)
      {
         String message = WorldWind
               .retrieveErrMsg("URLRetriever.ErrorOpeningConnection")
               + this.url.toString() + " " + e.getLocalizedMessage();
         WorldWind.logger().log(java.util.logging.Level.FINE, message, e);
         throw e;
      }

      if (this.connection == null) // java.net.URL docs imply that this
      // won't happen. We check anyway.
      {
         String message = WorldWind
               .retrieveErrMsg("URLRetriever.NullReturnedFromOpenConnection")
               + this.url;
         WorldWind.logger().log(java.util.logging.Level.FINE, message);
         throw new IllegalStateException(message);
      }

      this.connection.setConnectTimeout(this.connectTimeout);
      this.connection.setReadTimeout(this.readTimeout);

      return connection;
   }

   private void end() throws Exception
   {
      try
      {
         if (this.postProcessor != null)
         {
            this.byteBuffer = this.postProcessor.run(this);
         }
      }
      catch (Exception e)
      {
         this.setState(RETRIEVER_STATE_ERROR);
         String message = WorldWind
               .retrieveErrMsg("URLRetriever.ErrorPostProcessing")
               + this.url.toString();
         WorldWind.logger().log(
            java.util.logging.Level.FINE,
            message + " " + e.getLocalizedMessage());
         throw e;
      }
   }

   private ByteBuffer read() throws Exception
   {
      try
      {
         ByteBuffer buffer = this.doRead(this.connection);
         if (buffer == null)
            this.contentLength = 0;
         return buffer;
      }
      catch (Exception e)
      {
         if (!(e instanceof java.net.SocketTimeoutException))
         {
            String message = WorldWind
                  .retrieveErrMsg("URLRetriever.ErrorReadingFromConnection")
                  + this.url.toString() + e.getLocalizedMessage();
            WorldWind.logger().log(java.util.logging.Level.FINE, message, e);
         }
         throw e;
      }
   }

   /**
    * @param connection
    * @return
    * @throws Exception
    * @throws IllegalArgumentException if <code>connection</code> is null
    */
   protected ByteBuffer doRead(URLConnection connection) throws Exception
   {
      if (connection == null)
      {
         String msg = WorldWind.retrieveErrMsg("nullValue.ConnectionIsNull");
         WorldWind.logger().log(java.util.logging.Level.FINE, msg);
         throw new IllegalArgumentException(msg);
      }

      this.contentLength = this.connection.getContentLength();

      ByteBuffer buffer;
      InputStream inputStream = null;
      try
      {
         inputStream = this.connection.getInputStream();
         if (inputStream == null)
         {
            WorldWind
                  .logger()
                  .log(
                     java.util.logging.Level.FINE,
                     WorldWind
                           .retrieveErrMsg("URLRetriever.InputStreamFromConnectionNull")
                           + connection.getURL());
            return null;
         }

         // TODO: Make decompression of zip file configurable
         // TODO: Make this more generally configurable based on content type
         // todo: make a zip reader that handles streams of unknown length
         // TODO: add a gzip reader
         // TODO: this code is checking content type for compression when it
         // should be checking content encoding,
         // but the WW servers are sending application/zip as the content type,
         // and null for the content encoding.
         this.contentType = connection.getContentType();
         if (this.contentType != null
               && this.contentType.equalsIgnoreCase("application/zip"))
            buffer = this.readZipStream(inputStream, connection); // assume
         // single file
         // in zip and
         // decompress
         // it
         else
            buffer = this.readNonSpecificStream(inputStream, connection);

         this.contentType = connection.getContentType();
      }
      finally
      {
         if (inputStream != null)
            try
            {
               inputStream.close();
            }
            catch (IOException e)
            {
               WorldWind
                     .logger()
                     .log(
                        java.util.logging.Level.FINE,
                        WorldWind
                              .retrieveErrMsg("URLRetriever.ExceptionClosingInputStreamToConnection")
                              + connection.getURL());
            }
      }

      return buffer;
   }

   private ByteBuffer readNonSpecificStream(InputStream inputStream,
                                            URLConnection connection)
         throws IOException
   {
      if (inputStream == null)
      {
         String message = WorldWind
               .retrieveErrMsg("URLRetriever.InputStreamNullFor")
               + connection.getURL();
         WorldWind.logger().log(java.util.logging.Level.FINE, message);
         throw new IllegalArgumentException(message);
      }

      if (this.contentLength < 1)
      {
         return readNonSpecificStreamUnknownLength(inputStream);
      }

      ReadableByteChannel channel = Channels.newChannel(inputStream);
      ByteBuffer buffer = ByteBuffer.allocate(this.contentLength);

      int numBytesRead = 0;
      while (!this.interrupted() && numBytesRead >= 0
            && numBytesRead < buffer.limit())
      {
         int count = channel.read(buffer);
         if (count > 0)
            this.contentLengthRead.getAndAdd(numBytesRead += count);
      }

      if (buffer != null)
         buffer.flip();

      return buffer;
   }

   private ByteBuffer readNonSpecificStreamUnknownLength(InputStream inputStream)
         throws IOException
   {
      final int PAGE_SIZE = 4096;

      ReadableByteChannel channel = Channels.newChannel(inputStream);
      ByteBuffer buffer = ByteBuffer.allocate(PAGE_SIZE);

      int count = 0;
      int numBytesRead = 0;
      while (!this.interrupted() && count >= 0)
      {
         count = channel.read(buffer);
         if (count > 0)
            this.contentLengthRead.getAndAdd(numBytesRead += count);

         if (count > 0 && !buffer.hasRemaining())
         {
            ByteBuffer biggerBuffer = ByteBuffer.allocate(buffer.limit()
                  + PAGE_SIZE);
            biggerBuffer.put((ByteBuffer)buffer.rewind());
            buffer = biggerBuffer;
         }
      }

      if (buffer != null)
         buffer.flip();

      return buffer;
   }

   /**
    * @param inputStream
    * @param connection
    * @return
    * @throws java.io.IOException
    * @throws IllegalArgumentException if <code>inputStream</code> is null
    */
   private ByteBuffer readZipStream(InputStream inputStream,
                                    URLConnection connection)
         throws IOException
   {
      ZipInputStream zis = new ZipInputStream(inputStream);
      ZipEntry ze = zis.getNextEntry();
      if (ze == null)
      {
         WorldWind.logger().log(
            java.util.logging.Level.FINE,
            WorldWind.retrieveErrMsg("URLRetriever.NoZipEntryFor")
                  + connection.getURL());
         return null;
      }

      ByteBuffer buffer = null;
      if (ze.getSize() > 0)
      {
         buffer = ByteBuffer.allocate((int)ze.getSize());

         byte[] inputBuffer = new byte[8192];
         while (buffer.hasRemaining())
         {
            int count = zis.read(inputBuffer);
            if (count > 0)
            {
               buffer.put(inputBuffer, 0, count);
               this.contentLengthRead.getAndAdd(buffer.position() + 1);
            }
         }
      }
      if (buffer != null)
         buffer.flip();

      return buffer;
   }

   @Override
   public boolean equals(Object o)
   {
      if (this == o)
         return true;
      if (o == null || getClass() != o.getClass())
         return false;

      final URLRetriever that = (URLRetriever)o;

      // Retrievers are considered identical if they are for the same URL. This
      // convention is used by the
      // retrieval service to filter out duplicate retreival requests.
      return !(url != null
            ? !url.toString().contentEquals(that.url.toString())
            : that.url != null);
   }

   @Override
   public int hashCode()
   {
      int result;
      result = (url != null ? url.hashCode() : 0);
      return result;
   }

   @Override
   public String toString()
   {
      return this.getName() != null ? this.getName() : super.toString();
   }
}