/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.servlet.cache;

import com.twelvemonkeys.lang.Validate;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * CachedEntity
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @version $Id: CachedEntityImpl.java#3 $
 */
class CachedEntityImpl implements CachedEntity {
    private String cacheURI;
    private HTTPCache cache;

    CachedEntityImpl(String pCacheURI, HTTPCache pCache) {
        cacheURI = Validate.notNull(pCacheURI, "cacheURI");
        cache = pCache;
    }

    public void render(CacheRequest pRequest, CacheResponse pResponse) throws IOException {
        // Get cached content
        CachedResponse cached = cache.getContent(cacheURI, pRequest);

        // Sanity check
        if (cached == null) {
            throw new IllegalStateException("Tried to render non-cached response (cache == null).");
        }

        // If the cached entity is not modified since the date of the browsers
        // version, then simply send a "304 Not Modified" response
        // Otherwise send the full response.

        // TODO: WHY DID I COMMENT OUT THIS LINE AND REPLACE IT WITH THE ONE BELOW??
        //long lastModified = HTTPCache.getDateHeader(cached.getHeaderValue(HTTPCache.HEADER_LAST_MODIFIED));
        long lastModified = HTTPCache.getDateHeader(cached.getHeaderValue(HTTPCache.HEADER_CACHED_TIME));

        // TODO: Consider handling time skews between server "now" and client "now"?
        // NOTE: The If-Modified-Since is probably right according to the server
        // even in a time skew situation, as the client should use either the
        // Date or Last-Modifed dates from the response headers (server generated)
        long ifModifiedSince = -1L;
        try {
            List<String> ifmh = pRequest.getHeaders().get(HTTPCache.HEADER_IF_MODIFIED_SINCE);
            ifModifiedSince = ifmh != null ? HTTPCache.getDateHeader(ifmh.get(0)) : -1L;
            if (ifModifiedSince != -1L) {
                /*
                long serverTime = DateUtil.currentTimeMinute();
                long clientTime = DateUtil.roundToMinute(pRequest.getDateHeader(HTTPCache.HEADER_DATE));

                // Test if time skew is greater than time skew threshold (currently 1 minute)
                if (Math.abs(serverTime - clientTime) > 1) {
                    // TODO: Correct error in ifModifiedSince?
                }
                */

                // System.out.println(" << CachedEntity >> If-Modified-Since present: " + ifModifiedSince + " --> " + NetUtil.formatHTTPDate(ifModifiedSince) + "==" + pRequest.getHeader(HTTPCache.HEADER_IF_MODIFIED_SINCE));
                // System.out.println(" << CachedEntity >> Last-Modified for entity: " + lastModified + " --> " + NetUtil.formatHTTPDate(lastModified));
            }
        }
        catch (IllegalArgumentException e) {
            // Seems to be a bug in FireFox 1.0.2..?!
            cache.log("Error in date header from user-agent. User-Agent: " + pRequest.getHeaders().get("User-Agent"), e);
        }

        if (lastModified == -1L || (ifModifiedSince < (lastModified / 1000L) * 1000L)) {
            pResponse.setStatus(cached.getStatus());
            cached.writeHeadersTo(pResponse);
            if (isStale(pRequest)) {
                // Add warning header
                // Warning: 110 <server>:<port> Content is stale
                pResponse.addHeader(HTTPCache.HEADER_WARNING, "110 " + getHost(pRequest) + " Content is stale.");
            }

            // NOTE: At the moment we only ever try to cache HEAD and GET requests
            if (!"HEAD".equals(pRequest.getMethod())) {
                cached.writeContentsTo(pResponse.getOutputStream());
            }
        }
        else {
            pResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            // System.out.println(" << CachedEntity >> Not modified: " + toString());
            if (isStale(pRequest)) {
                // Add warning header
                // Warning: 110 <server>:<port> Content is stale
                pResponse.addHeader(HTTPCache.HEADER_WARNING, "110 " + getHost(pRequest) + " Content is stale.");
            }
        }
    }

    /* Utility method to get Host header */
    private static String getHost(CacheRequest pRequest) {
        return pRequest.getServerName() + ":" + pRequest.getServerPort();
    }

    public void capture(CacheRequest pRequest, CachedResponse pResponse) throws IOException {
//        if (!(pResponse instanceof CacheResponseWrapper)) {
//            throw new IllegalArgumentException("Response must be created by CachedEntity.createResponseWrapper()");
//        }
//
//        CacheResponseWrapper response = (CacheResponseWrapper) pResponse;

//        if (response.isCacheable()) {
            cache.registerContent(
                    cacheURI,
                    pRequest,
                    pResponse instanceof WritableCachedResponse ? ((WritableCachedResponse) pResponse).getCachedResponse() : pResponse
            );
//        }
//        else {
            // Else store that the response for this request is not cachable
//            pRequest.setAttribute(ATTRIB_NOT_CACHEABLE, Boolean.TRUE);

            // TODO: Store this in HTTPCache, for subsequent requests to same resource?
//        }
    }

    public boolean isStale(CacheRequest pRequest) {
        return cache.isContentStale(cacheURI, pRequest);
    }

    public WritableCachedResponse createCachedResponse() {
        return new WritableCachedResponseImpl();
    }

    public int hashCode() {
        return (cacheURI != null ? cacheURI.hashCode() : 0) + 1397;
    }

    public boolean equals(Object pOther) {
        return pOther instanceof CachedEntityImpl &&
                ((cacheURI == null && ((CachedEntityImpl) pOther).cacheURI == null) ||
                  cacheURI != null && cacheURI.equals(((CachedEntityImpl) pOther).cacheURI));
    }

    public String toString() {
        return "CachedEntity[URI=" + cacheURI + "]";
    }
}