package ro.isdc.wro.http.support;

import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.util.WroUtil;


/**
 * Responsible for configuring response headers. The configured headers contains the default headers overridden by those
 * set through "headers" property.
 *
 * @author Alex Objelean
 * @since 1.4.9
 * @created 27 Aug 2012
 */
public class ResponseHeadersConfigurer {
  private static final Logger LOG = LoggerFactory.getLogger(ResponseHeadersConfigurer.class);
  /**
   * Default value used by Cache-control header.
   */
  private static final String DEFAULT_CACHE_CONTROL_VALUE = "public, max-age=315360000";
  /**
   * String representation of headers to set. Each header is separated by a | character.
   */
  private final String headersAsString;
  private final Long lastModifiedTimestamp = initTimestamp();

  /**
   * Map containing header values used to control caching. The keys from this values are trimmed and lower-cased when
   * put, in order to avoid duplicate keys. This is done, because according to RFC 2616 Message Headers field names are
   * case-insensitive.
   */
  @SuppressWarnings("serial")
  private final Map<String, String> headersMap = new LinkedHashMap<String, String>() {
    @Override
    public String put(final String key, final String value) {
      return super.put(key.trim().toLowerCase(), value);
    }

    @Override
    public String get(final Object key) {
      return super.get(((String) key).toLowerCase());
    }
  };

  /**
   * Factory method which creates a {@link ResponseHeadersConfigurer} containing headers used to disable cache.
   */
  public static ResponseHeadersConfigurer noCache() {
    return new ResponseHeadersConfigurer() {
      @Override
      public void configureDefaultHeaders(final Map<String, String> map) {
        addNoCacheHeaders(map);
      }
    };
  }

  /**
   * Factory method which creates a {@link ResponseHeadersConfigurer} containing no headers set.
   */
  public static ResponseHeadersConfigurer emptyHeaders() {
    return new ResponseHeadersConfigurer();
  }

  /**
   * Factory method which creates a {@link ResponseHeadersConfigurer} containing headers used to disable cache in debug
   * mode.
   */
  public static ResponseHeadersConfigurer fromConfig(final WroConfiguration config) {
    return new ResponseHeadersConfigurer(config.getHeader()) {
      @Override
      public void configureDefaultHeaders(final Map<String, String> map) {
        if (config.isDebug()) {
          // prevent caching when in development mode
          addNoCacheHeaders(map);
        } else {
          final Calendar cal = Calendar.getInstance();
          cal.roll(Calendar.YEAR, 1);
          map.put(HttpHeader.CACHE_CONTROL.toString(), DEFAULT_CACHE_CONTROL_VALUE);
          map.put(HttpHeader.EXPIRES.toString(), WroUtil.toDateAsString(cal.getTimeInMillis()));
          // TODO probably this is not a good idea to set this field which will have a different value when there will be
          // more than one instance of wro4j.
          map.put(HttpHeader.LAST_MODIFIED.toString(), WroUtil.toDateAsString(getLastModifiedTimestamp()));
        }
      };
    };
  }

  /**
   * Factory method which creates a {@link ResponseHeadersConfigurer} containing headers provided as string (separated
   * by | character).
   */
  public static ResponseHeadersConfigurer withHeadersSet(final String headersAsString) {
    return new ResponseHeadersConfigurer(headersAsString);
  }

  public ResponseHeadersConfigurer() {
    this(null);
  }

  /**
   * @param headersAsString
   *          string representation of the headers to add separated by | character.
   */
  public ResponseHeadersConfigurer(final String headersAsString) {
    this.headersAsString = headersAsString;
    headersMap.clear();
    initHeaderValues();
  }

  /**
   * Initialize header values.
   */
  private void initHeaderValues() {
    configureDefaultHeaders(headersMap);
    configureFromHeadersAsString();
    LOG.debug("Header Values: {}", headersMap);
  }

  private void configureFromHeadersAsString() {
    if (!StringUtils.isEmpty(headersAsString)) {
      try {
        if (headersAsString.contains("|")) {
          final String[] headerAsArray = headersAsString.split("[|]");
          for (final String header : headerAsArray) {
            parseHeader(header);
          }
        } else {
          parseHeader(headersAsString);
        }
      } catch (final Exception e) {
        throw new WroRuntimeException("Invalid header init-param value: " + headersAsString
            + ". A correct value should have the following format: "
            + "<HEADER_NAME1>: <VALUE1> | <HEADER_NAME2>: <VALUE2>. " + "Ex: <look like this: "
            + "Expires: Thu, 15 Apr 2010 20:00:00 GMT | cache-control: public", e);
      }
    }
  }

  /**
   * Parse header value & puts the found values in headersMap field.
   *
   * @param header
   *          value to parse.
   */
  private void parseHeader(final String header) {
    LOG.debug("parseHeader: {}", header);
    final String headerName = header.substring(0, header.indexOf(":"));
    if (!headersMap.containsKey(headerName)) {
      final String value = header.substring(header.indexOf(":") + 1);
      headersMap.put(headerName, StringUtils.trim(value));
    }
  }

  /**
   * Allow configuration of default headers. This is useful when you need to set custom expires headers.
   *
   * @param map
   *          the {@link Map} where key represents the header name, and value - header value.
   */
  public void configureDefaultHeaders(final Map<String, String> map) {
  }

  /**
   * Populates the map with headers used to disable cache.
   */
  private static void addNoCacheHeaders(final Map<String, String> map) {
    map.put(HttpHeader.PRAGMA.toString(), "no-cache");
    map.put(HttpHeader.CACHE_CONTROL.toString(), "no-cache");
    map.put(HttpHeader.EXPIRES.toString(), "0");
  }

  /**
   * Method called for each request and responsible for setting response headers, used mostly for cache control.
   * Override this method if you want to change the way headers are set.<br>
   *
   * @param response
   *          {@link HttpServletResponse} object.
   */
  public void setHeaders(final HttpServletResponse response) {
    // Force resource caching as best as possible
    for (final Map.Entry<String, String> entry : headersMap.entrySet()) {
      response.setHeader(entry.getKey(), entry.getValue());
    }
  }

  /**
   * @VisibleForTesting
   */
  final Map<String, String> getHeadersMap() {
    return Collections.unmodifiableMap(headersMap);
  }

  /**
   * @return the timestamp of the last modification.
   */
  public Long getLastModifiedTimestamp() {
    return lastModifiedTimestamp;
  }

  /**
   * @return the timestamp value milliseconds stripped. Strip operation is important, because when timestamp is
   *         extracted from response header, it ends with 000 (milliseconds are not applied).
   */
  private long initTimestamp() {
    long timestamp = new Date().getTime();
    timestamp = timestamp - (timestamp % 1000);
    return timestamp;
  }
}
