package com.limegroup.gnutella.uploader;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Locale;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.nio.entity.ConsumingNHttpEntity;
import org.apache.http.nio.protocol.SimpleNHttpRequestHandler;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.limewire.core.settings.SharingSettings;
import org.limewire.http.BasicHeaderProcessor;
import org.limewire.http.MalformedHeaderException;
import org.limewire.http.RangeHeaderInterceptor;
import org.limewire.http.RangeHeaderInterceptor.Range;

import com.google.inject.Provider;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.PushEndpointFactory;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.Uploader.UploadStatus;
import com.limegroup.gnutella.altlocs.AltLocManager;
import com.limegroup.gnutella.altlocs.AlternateLocationFactory;
import com.limegroup.gnutella.http.AltLocHeaderInterceptor;
import com.limegroup.gnutella.http.FWNodeInfoInterceptor;
import com.limegroup.gnutella.http.FeatureHeaderInterceptor;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HTTPUtils;
import com.limegroup.gnutella.http.ProblemReadingHeaderException;
import com.limegroup.gnutella.http.UserAgentHeaderInterceptor;
import com.limegroup.gnutella.library.CreationTimeCache;
import com.limegroup.gnutella.library.FileDesc;
import com.limegroup.gnutella.library.IncompleteFileDesc;
import com.limegroup.gnutella.library.Library;
import com.limegroup.gnutella.library.LibraryUtils;
import com.limegroup.gnutella.tigertree.HashTree;
import com.limegroup.gnutella.tigertree.HashTreeCache;
import com.limegroup.gnutella.tigertree.HashTreeWriteHandler;
import com.limegroup.gnutella.tigertree.HashTreeWriteHandlerFactory;
import com.limegroup.gnutella.uploader.FileRequestParser.FileRequest;
import com.limegroup.gnutella.uploader.HTTPUploadSessionManager.QueueStatus;
import com.limegroup.gnutella.uploader.authentication.HttpRequestFileViewProvider;

/**
 * Handles upload requests for files and THEX trees.
 * 
 * @see FileResponseEntity
 * @see THEXResponseEntity
 */
public class FileRequestHandler extends SimpleNHttpRequestHandler {

    private static final Log LOG = LogFactory.getLog(FileRequestHandler.class);

    /**
     * Constant for the amount of time to wait before retrying if we are not
     * actively downloading this file. (1 hour)
     * <p>
     * The value is meant to be used only as a suggestion to when newer ranges
     * may be available if we do not have any ranges that the downloader may
     * want.
     */
    private static final String INACTIVE_RETRY_AFTER = "" + (60 * 60);

    private final HTTPUploadSessionManager sessionManager;

    private final Library library;

    private final HTTPHeaderUtils httpHeaderUtils;

    private final HttpRequestHandlerFactory httpRequestHandlerFactory;

    private final Provider<CreationTimeCache> creationTimeCache;

    private final FileResponseEntityFactory fileResponseEntityFactory;

    private final AltLocManager altLocManager;

    private final AlternateLocationFactory alternateLocationFactory;

    private final Provider<DownloadManager> downloadManager;

    private final Provider<HashTreeCache> tigerTreeCache;

    private final PushEndpointFactory pushEndpointFactory;

    private final HashTreeWriteHandlerFactory tigerWriteHandlerFactory;

    private final HttpRequestFileViewProvider fileListProvider;

    FileRequestHandler(HTTPUploadSessionManager sessionManager, Library library,
            HTTPHeaderUtils httpHeaderUtils, HttpRequestHandlerFactory httpRequestHandlerFactory,
            Provider<CreationTimeCache> creationTimeCache,
            FileResponseEntityFactory fileResponseEntityFactory, AltLocManager altLocManager,
            AlternateLocationFactory alternateLocationFactory,
            Provider<DownloadManager> downloadManager, Provider<HashTreeCache> tigerTreeCache,
            PushEndpointFactory pushEndpointFactory,
            HashTreeWriteHandlerFactory tigerWriteHandlerFactory, 
            HttpRequestFileViewProvider fileListProvider) {
        this.sessionManager = sessionManager;
        this.library = library;
        this.httpHeaderUtils = httpHeaderUtils;
        this.httpRequestHandlerFactory = httpRequestHandlerFactory;
        this.creationTimeCache = creationTimeCache;
        this.fileResponseEntityFactory = fileResponseEntityFactory;
        this.altLocManager = altLocManager;
        this.alternateLocationFactory = alternateLocationFactory;
        this.downloadManager = downloadManager;
        this.tigerTreeCache = tigerTreeCache;
        this.pushEndpointFactory = pushEndpointFactory;
        this.tigerWriteHandlerFactory = tigerWriteHandlerFactory;
        this.fileListProvider = fileListProvider;
    }
    
    public ConsumingNHttpEntity entityRequest(HttpEntityEnclosingRequest request,
            HttpContext context) throws HttpException, IOException {
        return null;
    }

    @Override
    public void handle(HttpRequest request, HttpResponse response, HttpContext context)
            throws HttpException, IOException {
        if (LOG.isDebugEnabled())
            LOG.debug("Handling upload request: " + request.getRequestLine().getUri());

        FileRequest fileRequest = null;
        HTTPUploader uploader = null;

        try {
            fileRequest = FileRequestParser.parseRequest(fileListProvider, request.getRequestLine().getUri(), context);
        } catch (IOException e) {
            uploader = sessionManager.getOrCreateUploader(request, context,
                    UploadType.MALFORMED_REQUEST, "Malformed Request");
            handleMalformedRequest(response, uploader);
        }
        
        if(fileRequest != null) {
            assert uploader == null;
            uploader = findFileAndProcessHeaders(request, response, context, fileRequest);
        } else if(uploader == null) {
            uploader = sessionManager.getOrCreateUploader(request, context,
                    UploadType.FILE_NOT_FOUND, "Unknown Query");
            uploader.setState(UploadStatus.FILE_NOT_FOUND);
            response.setStatusCode(HttpStatus.SC_NOT_FOUND);
        }
        
        assert uploader != null;

        sessionManager.sendResponse(uploader, response);
    }

    /**
     * Looks up file and processes request headers.
     */
    private HTTPUploader findFileAndProcessHeaders(HttpRequest request, HttpResponse response,
            HttpContext context, FileRequest fileRequest) throws IOException, HttpException {
        FileDesc fileDesc = fileRequest.getFileDesc();
        // create uploader
        UploadType type = (LibraryUtils.isForcedShare(fileDesc)) ? UploadType.FORCED_SHARE
                : UploadType.SHARED_FILE;
        HTTPUploader uploader = sessionManager.getOrCreateUploader(request, context, type, fileDesc.getFileName(), fileRequest.getFriendID());
        uploader.setFileDesc(fileDesc);

        // process headers
        BasicHeaderProcessor processor = new BasicHeaderProcessor();
        RangeHeaderInterceptor rangeHeaderInterceptor = new RangeHeaderInterceptor();
        processor.addInterceptor(rangeHeaderInterceptor);
        processor.addInterceptor(new FeatureHeaderInterceptor(uploader));
        processor.addInterceptor(new AltLocHeaderInterceptor(uploader, altLocManager,
                alternateLocationFactory));
        processor.addInterceptor(new FWNodeInfoInterceptor(uploader, pushEndpointFactory));
        if (!uploader.getFileName().toUpperCase(Locale.US).startsWith("WIRESHARE")) {
            processor.addInterceptor(new UserAgentHeaderInterceptor(uploader));
        }
        try {
            processor.process(request, context);
        } catch (ProblemReadingHeaderException e) {
            handleMalformedRequest(response, uploader);
            return uploader;
        } catch (MalformedHeaderException e) {
            handleMalformedRequest(response, uploader);
            return uploader;
        }

        if (UserAgentHeaderInterceptor.isFreeloader(uploader.getUserAgent())) {
            sessionManager.handleFreeLoader(request, response, context, uploader);
            return uploader;
        }

        if (!validateHeaders(uploader, fileRequest.isThexRequest())) {
            uploader.setState(UploadStatus.FILE_NOT_FOUND);
            response.setStatusCode(HttpStatus.SC_NOT_FOUND);
            return uploader;
        }

        if (!fileRequest.isThexRequest()) {
            if (rangeHeaderInterceptor.hasRequestedRanges()) {
                Range[] ranges = rangeHeaderInterceptor.getRequestedRanges();
                if (ranges.length > 1) {
                    handleInvalidRange(response, uploader, fileDesc);
                    return uploader;
                }

                uploader.setUploadBegin(ranges[0].getStartOffset(uploader.getFileSize()));
                uploader.setUploadEnd(ranges[0].getEndOffset(uploader.getFileSize()) + 1);
                uploader.setContainedRangeRequest(true);
            }

            if (!uploader.validateRange()) {
                handleInvalidRange(response, uploader, fileDesc);
                return uploader;
            }
        }

        // start upload
        if (fileRequest.isThexRequest()) {
            handleTHEXRequest(request, response, context, uploader, fileDesc);
        } else {
            handleFileUpload(context, request, response, uploader, fileDesc, fileRequest.getFriendID());
        }

        return uploader;
    }

    private void handleMalformedRequest(HttpResponse response, HTTPUploader uploader) {
        uploader.setState(UploadStatus.MALFORMED_REQUEST);
        response.setStatusCode(HttpStatus.SC_BAD_REQUEST);
        response.setReasonPhrase("Malformed Request");
    }

    /**
     * Enqueues <code>request</code> and handles <code>uploader</code> in
     * respect to the returned queue status.
     * 
     * @param friendId can be null if not a friend upload
     */
    private void handleFileUpload(HttpContext context, HttpRequest request, HttpResponse response,
            HTTPUploader uploader, FileDesc fd, String friendId) throws HttpException, IOException {
        if (!uploader.getSession().isAccepted()) {
            QueueStatus queued = sessionManager.enqueue(context, request);
            switch (queued) {
            case REJECTED:
                httpRequestHandlerFactory.createLimitReachedRequestHandler(uploader).handle(
                        request, response, context);
                break;
            case BANNED:
                uploader.setState(UploadStatus.BANNED_GREEDY);
                response.setStatusCode(HttpStatus.SC_FORBIDDEN);
                response.setReasonPhrase("Banned");
                break;
            case QUEUED:
                handleQueued(context, request, response, uploader, fd, friendId);
                break;
            case ACCEPTED:
                sessionManager.addAcceptedUploader(uploader, context);
                break;
            case BYPASS: // ignore
            }
        }

        if (uploader.getSession().canUpload()) {
            handleAccept(context, request, response, uploader, fd, friendId);
        }
    }

    /**
     * Processes an accepted file upload by adding headers and setting the
     * entity.
     * 
     * @param friendId can be null if not a friend upload
     */
    protected void handleAccept(HttpContext context, HttpRequest request, HttpResponse response,
            HTTPUploader uploader, FileDesc fd, String friendId) throws IOException, HttpException {

        assert fd != null;

        response.addHeader(HTTPHeaderName.DATE.create(HTTPUtils.getDateValue()));
        response.addHeader(HTTPHeaderName.CONTENT_DISPOSITION.create("attachment; filename=\""
                + HTTPUtils.encode(uploader.getFileName(), "US-ASCII") + "\""));

        if (uploader.containedRangeRequest()) {
            // uploadEnd is an EXCLUSIVE index internally, but HTTP uses an
            // INCLUSIVE index.
            String value = "bytes " + uploader.getUploadBegin() + "-"
                    + (uploader.getUploadEnd() - 1) + "/" + uploader.getFileSize();
            response.addHeader(HTTPHeaderName.CONTENT_RANGE.create(value));
        }

        httpHeaderUtils.addAltLocationsHeader(response, uploader.getAltLocTracker(), altLocManager);
        httpHeaderUtils.addRangeHeader(response, uploader, fd);
        httpHeaderUtils.addProxyHeader(response);

        URN urn = fd.getSHA1Urn();
        if (uploader.isFirstReply()) {
            // write the creation time if this is the first reply.
            // if this is just a continuation, we don't need to send
            // this information again.
            // it's possible t do that because we don't use the same
            // uploader for different files
            if (creationTimeCache.get().getCreationTime(urn) != null) {
                response.addHeader(HTTPHeaderName.CREATION_TIME.create(creationTimeCache.get()
                        .getCreationTime(urn).toString()));
            }
        }

        // write x-features header once because the downloader is
        // supposed to cache that information anyway
        if (uploader.isFirstReply()) {
            httpHeaderUtils.addFeatures(response);
        }

        addThexUriHeader(response, fd, friendId);

        response.setEntity(fileResponseEntityFactory.createFileResponseEntity(uploader, fd
                .getFile()));
        uploader.setState(UploadStatus.UPLOADING);

        if (uploader.isPartial()) {
            response.setStatusCode(HttpStatus.SC_PARTIAL_CONTENT);
        } else {
            response.setStatusCode(HttpStatus.SC_OK);
        }
    }
    
    /**
     * Adds the X-Thex-URI header to the http response.
     * 
     * @param friendId can be null if not a friend upload, otherwise used to
     * create the correct path for the Thex uri
     */
    private void addThexUriHeader(HttpResponse response, FileDesc fileDesc, String friendId) {
        // We do not support serving TigerTrees for incomplete files,
        // so sending the THEX-URI makes no sense for them.
        if (fileDesc instanceof IncompleteFileDesc) {
            return;
        }
        
        // write X-Thex-URI header with root hash if we have already
        // calculated the tigertree
        HashTree tree = tigerTreeCache.get().getHashTree(fileDesc);
        if (tree != null) {
            if (friendId != null) {
                // TODO: breaking dependencies: prefix same as in CoreGlueFriendService
                try {
                    String uri = "/friend/download/" + URLEncoder.encode(friendId, "UTF-8")+ tree.httpStringValue();
                    response.addHeader(HTTPHeaderName.THEX_URI.create(uri));
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            } else {
                response.addHeader(HTTPHeaderName.THEX_URI.create(tree));
            }
        }

    }

    /**
     * Processes an accepted THEX tree upload by adding headers and setting the
     * entity.
     */
    private void handleTHEXRequest(HttpRequest request, HttpResponse response, HttpContext context,
            HTTPUploader uploader, FileDesc fd) {
        // reset the poll interval to allow subsequent requests right away
        uploader.getSession().updatePollTime(QueueStatus.BYPASS);

        // do not count THEX transfers towards the total amount
        uploader.setIgnoreTotalAmountUploaded(true);

        HashTree tree = tigerTreeCache.get().getHashTree(fd);
        if (tree == null) {
            // tree was requested before hashing completed
            uploader.setState(UploadStatus.FILE_NOT_FOUND);
            response.setStatusCode(HttpStatus.SC_NOT_FOUND);
            return;
        }

        HashTreeWriteHandler tigerWriteHandler = tigerWriteHandlerFactory
                .createTigerWriteHandler(tree);

        // XXX reset range to size of THEX tree
        int outputLength = tigerWriteHandler.getOutputLength();
        uploader.setFileSize(outputLength);
        uploader.setUploadBegin(0);
        uploader.setUploadEnd(outputLength);

        // see CORE-174
        // response.addHeader(HTTPHeaderName.GNUTELLA_CONTENT_URN.create(fd.getSHA1Urn()));

        uploader.setState(UploadStatus.THEX_REQUEST);
        response.setEntity(new THEXResponseEntity(uploader, tigerWriteHandler, uploader
                .getFileSize()));
        response.setStatusCode(HttpStatus.SC_OK);
    }

    /**
     * Processes a request for an invalid range.
     */
    private void handleInvalidRange(HttpResponse response, HTTPUploader uploader, FileDesc fd) {
        httpHeaderUtils.addAltLocationsHeader(response, uploader.getAltLocTracker(), altLocManager);
        httpHeaderUtils.addRangeHeader(response, uploader, fd);
        httpHeaderUtils.addProxyHeader(response);

        if (fd instanceof IncompleteFileDesc) {
            if (!downloadManager.get().isActivelyDownloading(fd.getSHA1Urn())) {
                response.addHeader(HTTPHeaderName.RETRY_AFTER.create(INACTIVE_RETRY_AFTER));
            }
        }

        uploader.setState(UploadStatus.UNAVAILABLE_RANGE);
        response.setStatusCode(HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        response.setReasonPhrase("Requested Range Unavailable");
    }

    /**
     * Processes a queued file upload by adding headers.
     * 
     * @param friendId can be null if not a friend upload
     */
    private void handleQueued(HttpContext context, HttpRequest request, HttpResponse response,
            HTTPUploader uploader, FileDesc fd, String friendId)  {
        // if not queued, this should never be the state
        int position = uploader.getSession().positionInQueue();
        assert (position != -1);

        String value = "position=" + (position + 1) + ", pollMin="
                + (HTTPUploadSession.MIN_POLL_TIME / 1000) + /* mS to S */
                ", pollMax=" + (HTTPUploadSession.MAX_POLL_TIME / 1000) /*
                                                                         * mS to
                                                                         * S
                                                                         */;
        response.addHeader(HTTPHeaderName.QUEUE.create(value));

        httpHeaderUtils.addAltLocationsHeader(response, uploader.getAltLocTracker(), altLocManager);
        httpHeaderUtils.addRangeHeader(response, uploader, fd);

        if (uploader.isFirstReply()) {
            httpHeaderUtils.addFeatures(response);
        }

        addThexUriHeader(response, fd, friendId);
        
        response.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);

        uploader.setState(UploadStatus.QUEUED);
        response.setStatusCode(HttpStatus.SC_SERVICE_UNAVAILABLE);
    }

    private boolean validateHeaders(HTTPUploader uploader, boolean thexRequest) {
        final FileDesc fd = uploader.getFileDesc();
        assert fd != null;

        // If it's the wrong URN, File Not Found it.
        URN urn = uploader.getRequestedURN();
        if (urn != null && !fd.containsUrn(urn)) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Invalid content urn: " + uploader);
            }
            return false;
        }

        // handling THEX Requests
        if (thexRequest && tigerTreeCache.get().getHashTree(fd) == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Requested thex tree is not available: " + uploader);
            }
            return false;
        }

        // special handling for incomplete files
        if (fd instanceof IncompleteFileDesc) {
            // Check to see if we're allowing PFSP.
            if (!SharingSettings.ALLOW_PARTIAL_SHARING.getValue()) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Sharing of partial files is diabled: " + uploader);
                }
                return false;
            }

            // cannot service THEX requests for partial files
            if (thexRequest) {
                return false;
            }
        } else {
            // check if fd is up-to-date
            File file = fd.getFile();
            if (file.lastModified() != fd.lastModified()) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("File has changed on disk, resharing: " + file);
                }
                library.fileChanged(file, fd.getLimeXMLDocuments());
                return false;
            }
        }

        return true;
    }

}
