/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed 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.
 */

package org.springframework.security.web.savedrequest;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.TimeZone;


/**
 * Utility class to generate HTTP dates.
 * <p>
 * This class is based on code in Apache Tomcat.
 *
 * @author Remy Maucherat
 * @author Andrey Grebnev
 */
public class FastHttpDateFormat {
    //~ Static fields/initializers =====================================================================================

    /** HTTP date format. */
    protected static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);

    /** The set of SimpleDateFormat formats to use in <code>getDateHeader()</code>. */
    protected static final SimpleDateFormat[] formats = {
            new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
            new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
            new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
        };

    /** GMT time zone - all HTTP dates are on GMT */
    protected static final TimeZone gmtZone = TimeZone.getTimeZone("GMT");

    static {
        format.setTimeZone(gmtZone);

        formats[0].setTimeZone(gmtZone);
        formats[1].setTimeZone(gmtZone);
        formats[2].setTimeZone(gmtZone);
    }

    /** Instant on which the currentDate object was generated. */
    protected static long currentDateGenerated = 0L;

    /** Current formatted date. */
    protected static String currentDate = null;

    /** Formatter cache. */
    protected static final HashMap<Long,String> formatCache = new HashMap<Long,String>();

    /** Parser cache. */
    protected static final HashMap<String,Long> parseCache = new HashMap<String,Long>();

    //~ Methods ========================================================================================================

    /**
     * Formats a specified date to HTTP format. If local format is not <code>null</code>, it's used instead.
     *
     * @param value Date value to format
     * @param threadLocalformat The format to use (or <code>null</code> -- then HTTP format will be used)
     *
     * @return Formatted date
     */
    public static String formatDate(long value, DateFormat threadLocalformat) {
        String cachedDate = null;
        Long longValue = Long.valueOf(value);

        try {
            cachedDate = formatCache.get(longValue);
        } catch (Exception ignored) {}

        if (cachedDate != null) {
            return cachedDate;
        }

        String newDate;
        Date dateValue = new Date(value);

        if (threadLocalformat != null) {
            newDate = threadLocalformat.format(dateValue);

            synchronized (formatCache) {
                updateCache(formatCache, longValue, newDate);
            }
        } else {
            synchronized (formatCache) {
                newDate = format.format(dateValue);
                updateCache(formatCache, longValue, newDate);
            }
        }

        return newDate;
    }

    /**
     * Gets the current date in HTTP format.
     *
     * @return Current date in HTTP format
     */
    public static String getCurrentDate() {
        long now = System.currentTimeMillis();

        if ((now - currentDateGenerated) > 1000) {
            synchronized (format) {
                if ((now - currentDateGenerated) > 1000) {
                    currentDateGenerated = now;
                    currentDate = format.format(new Date(now));
                }
            }
        }

        return currentDate;
    }

    /**
     * Parses date with given formatters.
     *
     * @param value The string to parse
     * @param formats Array of formats to use
     *
     * @return Parsed date (or <code>null</code> if no formatter mached)
     */
    private static Long internalParseDate(String value, DateFormat[] formats) {
        Date date = null;

        for (int i = 0; (date == null) && (i < formats.length); i++) {
            try {
                date = formats[i].parse(value);
            } catch (ParseException ignored) {
            }
        }

        if (date == null) {
            return null;
        }

        return new Long(date.getTime());
    }

    /**
     * Tries to parse the given date as an HTTP date. If local format list is not <code>null</code>, it's used
     * instead.
     *
     * @param value The string to parse
     * @param threadLocalformats Array of formats to use for parsing. If <code>null</code>, HTTP formats are used.
     *
     * @return Parsed date (or -1 if error occurred)
     */
    public static long parseDate(String value, DateFormat[] threadLocalformats) {
        Long cachedDate = null;

        try {
            cachedDate = (Long) parseCache.get(value);
        } catch (Exception ignored) {}

        if (cachedDate != null) {
            return cachedDate.longValue();
        }

        Long date;

        if (threadLocalformats != null) {
            date = internalParseDate(value, threadLocalformats);

            synchronized (parseCache) {
                updateCache(parseCache, value, date);
            }
        } else {
            synchronized (parseCache) {
                date = internalParseDate(value, formats);
                updateCache(parseCache, value, date);
            }
        }

        if (date == null) {
            return (-1L);
        } else {
            return date.longValue();
        }
    }

    /**
     * Updates cache.
     *
     * @param cache Cache to be updated
     * @param key Key to be updated
     * @param value New value
     */
    @SuppressWarnings("unchecked")
    private static void updateCache(HashMap cache, Object key, Object value) {
        if (value == null) {
            return;
        }

        if (cache.size() > 1000) {
            cache.clear();
        }

        cache.put(key, value);
    }
}
