/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.exoplatform.portal.application;

import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import javax.imageio.ImageIO;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.gatein.portal.controller.resource.ResourceRequestHandler;

import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.web.AbstractFilter;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

public class ResourceRequestFilter extends AbstractFilter {

    protected static Log log = ExoLogger.getLogger(ResourceRequestFilter.class);

    // private static final Charset UTF_8 = Charset.forName("UTF-8");

    private FilterConfig cfg;

    private ImageType[] imageTypes = ImageType.values();

    private ConcurrentMap<String, FutureTask<Image>> mirroredImageCache = new ConcurrentHashMap<String, FutureTask<Image>>();

    public static final String IF_MODIFIED_SINCE = "If-Modified-Since";

    public static final String LAST_MODIFIED = "Last-Modified";
  
    public static final String EXPIRES = "Expires";

    public void afterInit(FilterConfig filterConfig) {
        cfg = filterConfig;
        log.info("Cache eXo Resource at client: " + !PropertyManager.isDevelopping());
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        final String uri = URLDecoder.decode(httpRequest.getRequestURI(), "UTF-8");
        final HttpServletResponse httpResponse = (HttpServletResponse) response;

        // Fast matching
        final int len = uri.length();
        if (len >= 7 && uri.charAt(len - 7) == '-' && uri.charAt(len - 6) == 'r' && uri.charAt(len - 5) == 't') {
            for (final ImageType imageType : imageTypes) {
                if (imageType.matches(uri)) {
                    final String resource = uri.substring(httpRequest.getContextPath().length(), len - 7)
                            + uri.substring(len - 4);
                    FutureTask<Image> futureImg = mirroredImageCache.get(resource);
                    if (futureImg == null) {
                        FutureTask<Image> tmp = new FutureTask<Image>(new Callable<Image>() {
                            public Image call() throws Exception {
                                InputStream in = cfg.getServletContext().getResourceAsStream(resource);
                                if (in == null) {
                                    return null;
                                }

                                //
                                BufferedImage img = ImageIO.read(in);
                                log.debug("Read image " + uri + " (" + img.getWidth() + "," + img.getHeight() + ")");
                                AffineTransform tx = AffineTransform.getScaleInstance(-1, 1);
                                tx.translate(-img.getWidth(null), 0);
                                AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
                                img = op.filter(img, null);
                                log.debug("Mirrored image " + uri + " (" + img.getWidth() + "," + img.getHeight() + ")");
                                ByteArrayOutputStream baos = new ByteArrayOutputStream(1000);
                                ImageIO.write(img, imageType.getFormat(), baos);
                                baos.close();
                                return new Image(imageType, baos.toByteArray());
                            }
                        });

                        //
                        futureImg = mirroredImageCache.putIfAbsent(resource, tmp);
                        if (futureImg == null) {
                            futureImg = tmp;
                            futureImg.run();
                        }
                    }

                    //
                    try {
                        Image img = futureImg.get();
                        if (img != null) {
                            // Check if cached resource has not been modifed, return 304 code
                            String ifModifiedSinceString = httpRequest.getHeader(IF_MODIFIED_SINCE);
                            long ifModifiedSince = ifModifiedSinceString == null ? 0 : new Date(ifModifiedSinceString).getTime();
                            long imgLastModified = img.getLastModified();
                            if (isNotModified(ifModifiedSince, imgLastModified)) {
                                httpResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                                return;
                            }
                            httpResponse.setContentType(img.type.getMimeType());
                            httpResponse.setContentLength(img.bytes.length);
                            httpResponse.setHeader("Cache-Control", "public,max-age=2592000");
                            processIfModified(imgLastModified, httpResponse);

                            OutputStream out = httpResponse.getOutputStream();
                            out.write(img.bytes);
                            out.close();
                        } else {
                            mirroredImageCache.remove(resource);
                            httpResponse.setStatus(HttpServletResponse.SC_NOT_FOUND);
                        }
                        return;
                    } catch (InterruptedException e) {
                        // Find out what is relevant to do
                        log.error(e.getMessage(), e);
                    } catch (ExecutionException e) {
                        // Cleanup
                        log.error(e.getMessage(), e);
                        mirroredImageCache.remove(resource);
                    }
                }
            }
        }

        //
        httpResponse.addHeader("Cache-Control", "max-age=2592000");
        httpResponse.setDateHeader(ResourceRequestFilter.EXPIRES, System.currentTimeMillis() + ResourceRequestHandler.MAX_AGE * 1000);
        chain.doFilter(request, response);
    }

    /**
     * Add Last-Modified Http header to HttpServetResponse
     */
    public void processIfModified(long lastModified, HttpServletResponse httpResponse) {
        httpResponse.setDateHeader(ResourceRequestFilter.LAST_MODIFIED, lastModified);
    }

    /**
     * If cached resource has not changed since date in http header (If_Modified_Since), return true Else return false;
     *
     * @param ifModifedSince - String, and HttpHeader element
     * @param lastModified
     * @param httpResponse
     * @return
     */
    private boolean isNotModified(long ifModifedSince, long lastModified) {
      return Math.abs(ifModifedSince - lastModified) < 1000;
    }

    public void destroy() {
    }
}
