/*
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 1792 2007-05-08 21:28:37Z 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 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;
    }

    /**
     * @param connectTimeout
     * @throws IllegalArgumentException if <code>connectTimeOut</code> is less than zero
     */
    public final void setConnectTimeout(int connectTimeout)
    {
        if (connectTimeout < 0)
        {
            String message = WorldWind.retrieveErrMsg("URLRetriever.ConnectTimeoutLessThanZero");
            WorldWind.logger().log(java.util.logging.Level.FINE, message);
            throw new IllegalArgumentException(message);
        }

        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
        {
            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.
            if (connection.getContentType().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.equals(that.url) : 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();
    }
}
