/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */
package ch.boye.httpclientandroidlib.impl.client.cache;

import java.util.Date;

import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HeaderElement;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.annotation.Immutable;
import ch.boye.httpclientandroidlib.client.cache.HeaderConstants;
import ch.boye.httpclientandroidlib.client.cache.HttpCacheEntry;
import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
import ch.boye.httpclientandroidlib.protocol.HTTP;

/**
 * @since 4.1
 */
@Immutable
class CacheValidityPolicy {

    public static final long MAX_AGE = 2147483648L;

    CacheValidityPolicy() {
        super();
    }

    public long getCurrentAgeSecs(final HttpCacheEntry entry, Date now) {
        return getCorrectedInitialAgeSecs(entry) + getResidentTimeSecs(entry, now);
    }

    public long getFreshnessLifetimeSecs(final HttpCacheEntry entry) {
        long maxage = getMaxAge(entry);
        if (maxage > -1)
            return maxage;

        Date dateValue = getDateValue(entry);
        if (dateValue == null)
            return 0L;

        Date expiry = getExpirationDate(entry);
        if (expiry == null)
            return 0;
        long diff = expiry.getTime() - dateValue.getTime();
        return (diff / 1000);
    }

    public boolean isResponseFresh(final HttpCacheEntry entry, Date now) {
        return (getCurrentAgeSecs(entry, now) < getFreshnessLifetimeSecs(entry));
    }

    /**
     * Decides if this response is fresh enough based Last-Modified and Date, if available.
     * This entry is meant to be used when isResponseFresh returns false.  The algorithm is as follows:
     *
     * if last-modified and date are defined, freshness lifetime is coefficient*(date-lastModified),
     * else freshness lifetime is defaultLifetime
     *
     * @param entry the cache entry
     * @param now what time is it currently (When is right NOW)
     * @param coefficient Part of the heuristic for cache entry freshness
     * @param defaultLifetime How long can I assume a cache entry is default TTL
     * @return {@code true} if the response is fresh
     */
    public boolean isResponseHeuristicallyFresh(final HttpCacheEntry entry,
            Date now, float coefficient, long defaultLifetime) {
        return (getCurrentAgeSecs(entry, now) < getHeuristicFreshnessLifetimeSecs(entry, coefficient, defaultLifetime));
    }

    public long getHeuristicFreshnessLifetimeSecs(HttpCacheEntry entry,
            float coefficient, long defaultLifetime) {
        Date dateValue = getDateValue(entry);
        Date lastModifiedValue = getLastModifiedValue(entry);

        if (dateValue != null && lastModifiedValue != null) {
            long diff = dateValue.getTime() - lastModifiedValue.getTime();
            if (diff < 0)
                return 0;
            return (long)(coefficient * (diff / 1000));
        }

        return defaultLifetime;
    }

    public boolean isRevalidatable(final HttpCacheEntry entry) {
        return entry.getFirstHeader(HeaderConstants.ETAG) != null
                || entry.getFirstHeader(HeaderConstants.LAST_MODIFIED) != null;
    }

    public boolean mustRevalidate(final HttpCacheEntry entry) {
        return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE);
    }

    public boolean proxyRevalidate(final HttpCacheEntry entry) {
        return hasCacheControlDirective(entry, HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE);
    }

    public boolean mayReturnStaleWhileRevalidating(final HttpCacheEntry entry, Date now) {
        for (Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
            for(HeaderElement elt : h.getElements()) {
                if (HeaderConstants.STALE_WHILE_REVALIDATE.equalsIgnoreCase(elt.getName())) {
                    try {
                        int allowedStalenessLifetime = Integer.parseInt(elt.getValue());
                        if (getStalenessSecs(entry, now) <= allowedStalenessLifetime) {
                            return true;
                        }
                    } catch (NumberFormatException nfe) {
                        // skip malformed directive
                    }
                }
            }
        }

        return false;
    }

    public boolean mayReturnStaleIfError(HttpRequest request,
            HttpCacheEntry entry, Date now) {
        long stalenessSecs = getStalenessSecs(entry, now);
        return mayReturnStaleIfError(request.getHeaders(HeaderConstants.CACHE_CONTROL),
                                     stalenessSecs)
                || mayReturnStaleIfError(entry.getHeaders(HeaderConstants.CACHE_CONTROL),
                                         stalenessSecs);
    }

    private boolean mayReturnStaleIfError(Header[] headers, long stalenessSecs) {
        boolean result = false;
        for(Header h : headers) {
            for(HeaderElement elt : h.getElements()) {
                if (HeaderConstants.STALE_IF_ERROR.equals(elt.getName())) {
                    try {
                        int staleIfErrorSecs = Integer.parseInt(elt.getValue());
                        if (stalenessSecs <= staleIfErrorSecs) {
                            result = true;
                            break;
                        }
                    } catch (NumberFormatException nfe) {
                        // skip malformed directive
                    }
                }
            }
        }
        return result;
    }

    protected Date getDateValue(final HttpCacheEntry entry) {
        Header dateHdr = entry.getFirstHeader(HTTP.DATE_HEADER);
        if (dateHdr == null)
            return null;
        try {
            return DateUtils.parseDate(dateHdr.getValue());
        } catch (DateParseException dpe) {
            // ignore malformed date
        }
        return null;
    }

    protected Date getLastModifiedValue(final HttpCacheEntry entry) {
        Header dateHdr = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
        if (dateHdr == null)
            return null;
        try {
            return DateUtils.parseDate(dateHdr.getValue());
        } catch (DateParseException dpe) {
            // ignore malformed date
        }
        return null;
    }

    protected long getContentLengthValue(final HttpCacheEntry entry) {
        Header cl = entry.getFirstHeader(HTTP.CONTENT_LEN);
        if (cl == null)
            return -1;

        try {
            return Long.parseLong(cl.getValue());
        } catch (NumberFormatException ex) {
            return -1;
        }
    }

    protected boolean hasContentLengthHeader(HttpCacheEntry entry) {
        return null != entry.getFirstHeader(HTTP.CONTENT_LEN);
    }

    /**
     * This matters for deciding whether the cache entry is valid to serve as a
     * response. If these values do not match, we might have a partial response
     *
     * @param entry The cache entry we are currently working with
     * @return boolean indicating whether actual length matches Content-Length
     */
    protected boolean contentLengthHeaderMatchesActualLength(final HttpCacheEntry entry) {
        return !hasContentLengthHeader(entry) || getContentLengthValue(entry) == entry.getResource().length();
    }

    protected long getApparentAgeSecs(final HttpCacheEntry entry) {
        Date dateValue = getDateValue(entry);
        if (dateValue == null)
            return MAX_AGE;
        long diff = entry.getResponseDate().getTime() - dateValue.getTime();
        if (diff < 0L)
            return 0;
        return (diff / 1000);
    }

    protected long getAgeValue(final HttpCacheEntry entry) {
        long ageValue = 0;
        for (Header hdr : entry.getHeaders(HeaderConstants.AGE)) {
            long hdrAge;
            try {
                hdrAge = Long.parseLong(hdr.getValue());
                if (hdrAge < 0) {
                    hdrAge = MAX_AGE;
                }
            } catch (NumberFormatException nfe) {
                hdrAge = MAX_AGE;
            }
            ageValue = (hdrAge > ageValue) ? hdrAge : ageValue;
        }
        return ageValue;
    }

    protected long getCorrectedReceivedAgeSecs(final HttpCacheEntry entry) {
        long apparentAge = getApparentAgeSecs(entry);
        long ageValue = getAgeValue(entry);
        return (apparentAge > ageValue) ? apparentAge : ageValue;
    }

    protected long getResponseDelaySecs(final HttpCacheEntry entry) {
        long diff = entry.getResponseDate().getTime() - entry.getRequestDate().getTime();
        return (diff / 1000L);
    }

    protected long getCorrectedInitialAgeSecs(final HttpCacheEntry entry) {
        return getCorrectedReceivedAgeSecs(entry) + getResponseDelaySecs(entry);
    }

    protected long getResidentTimeSecs(HttpCacheEntry entry, Date now) {
        long diff = now.getTime() - entry.getResponseDate().getTime();
        return (diff / 1000L);
    }

    protected long getMaxAge(final HttpCacheEntry entry) {
        long maxage = -1;
        for (Header hdr : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
            for (HeaderElement elt : hdr.getElements()) {
                if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())
                        || "s-maxage".equals(elt.getName())) {
                    try {
                        long currMaxAge = Long.parseLong(elt.getValue());
                        if (maxage == -1 || currMaxAge < maxage) {
                            maxage = currMaxAge;
                        }
                    } catch (NumberFormatException nfe) {
                        // be conservative if can't parse
                        maxage = 0;
                    }
                }
            }
        }
        return maxage;
    }

    protected Date getExpirationDate(final HttpCacheEntry entry) {
        Header expiresHeader = entry.getFirstHeader(HeaderConstants.EXPIRES);
        if (expiresHeader == null)
            return null;
        try {
            return DateUtils.parseDate(expiresHeader.getValue());
        } catch (DateParseException dpe) {
            // malformed expires header
        }
        return null;
    }

    public boolean hasCacheControlDirective(final HttpCacheEntry entry,
            final String directive) {
        for (Header h : entry.getHeaders(HeaderConstants.CACHE_CONTROL)) {
            for(HeaderElement elt : h.getElements()) {
                if (directive.equalsIgnoreCase(elt.getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    public long getStalenessSecs(HttpCacheEntry entry, Date now) {
        long age = getCurrentAgeSecs(entry, now);
        long freshness = getFreshnessLifetimeSecs(entry);
        if (age <= freshness) return 0L;
        return (age - freshness);
    }


}
