/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.richfaces.resource;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import javax.faces.application.ProjectStage;
import javax.faces.application.Resource;
import javax.faces.application.ResourceHandler;
import javax.faces.application.ResourceHandlerWrapper;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletResponse;

import org.richfaces.cache.Cache;
import org.richfaces.log.Logger;
import org.richfaces.log.RichfacesLogger;
import org.richfaces.renderkit.html.ResourceLibraryRenderer;
import org.richfaces.application.ServiceTracker;
import org.richfaces.util.RequestStateManager.BooleanRequestStateVariable;

/**
 * <p>RichFaces-specific {@link ResourceHandler}.</p>
 *
 * <p>It adds support for:</p>
 *
 * <ul>
 * <li>ECSS files handling</li>
 * <li>cacheable resources</li>
 * </ul>
 *
 * <p>It delegates to {@link ResourceFactory} for creating resources.</p>
 *
 * @author Nick Belaevski
 * @since 4.0
 */
// TODO extract caching
public class ResourceHandlerImpl extends ResourceHandlerWrapper {
    public static final String RICHFACES_RESOURCE_IDENTIFIER = "/rfRes/";
    public static final String RESOURCE_CACHE_NAME = "org.richfaces.ResourcesCache";
    public static final String HANDLER_START_TIME_ATTRIBUTE = ResourceHandlerImpl.class.getName() + ":StartTime";
    private static final Logger LOGGER = RichfacesLogger.RESOURCE.getLogger();

    private ResourceFactory resourceFactory;
    private ResourceHandler defaultHandler;

    public ResourceHandlerImpl(ResourceHandler defaultHandler) {
        this.defaultHandler = defaultHandler;
        this.resourceFactory = new ResourceFactoryImpl(defaultHandler);

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(MessageFormat.format("Instance of {0} resource handler created", getClass().getName()));
        }
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#isResourceRequest(javax.faces.context.FacesContext)
     */
    @Override
    public boolean isResourceRequest(FacesContext context) {
        return isThisHandlerResourceRequest(context) || defaultHandler.isResourceRequest(context);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#handleResourceRequest(javax.faces.context.FacesContext)
     */
    @Override
    public void handleResourceRequest(FacesContext context) throws IOException {
        if (isThisHandlerResourceRequest(context)) {
            ResourceCodec resourceCodec = ServiceTracker.getService(context, ResourceCodec.class);

            String resourcePath = getResourcePathFromRequest(context);

            assert (resourcePath != null) && (resourcePath.length() != 0);

            ResourceRequestData data = resourceCodec.decodeResource(context, resourcePath);
            assert (data != null);

            Cache cache = ServiceTracker.getService(context, Cache.class);
            Resource resource = lookupInCache(cache, data.getResourceKey());

            if (resource == null) {
                resource = resourceFactory.createResource(context, data);
            }

            if (resource == null) {
                sendResourceNotFound(context);
                return;
            }

            if (resource instanceof CacheableResource) {
                CacheableResource cacheableResource = (CacheableResource) resource;

                if (cacheableResource.isCacheable(context)) {

                    // TODO - we could move this part of code to ConcurrentMap so that
                    // only single thread does resource put
                    CachedResourceImpl cachedResource = new CachedResourceImpl();

                    cachedResource.initialize(resource);

                    // someone may provided this resource for us
                    // while we were reading it, check once again
                    resource = lookupInCache(cache, data.getResourceKey());

                    if (resource == null) {
                        // don't cache it on Development stage
                        if (!ProjectStage.Development.equals(context.getApplication().getProjectStage())) {
                            Date cacheExpirationDate = cachedResource.getExpired(context);
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug(new MessageFormat(
                                        "Storing {0} resource in cache until {1,date,dd MMM yyyy HH:mm:ss zzz}", Locale.US)
                                        .format(new Object[] { data.getResourceKey(), cacheExpirationDate }));
                            }
                            cache.put(data.getResourceKey(), cachedResource, cacheExpirationDate);
                        }
                        resource = cachedResource;
                    }
                }
            }

            if (resource.userAgentNeedsUpdate(context)) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("User agent needs resource update, encoding resource");
                }

                ExternalContext externalContext = context.getExternalContext();
                Map<String, String> headers = resource.getResponseHeaders();

                for (Entry<String, String> headerEntry : headers.entrySet()) {
                    String headerName = headerEntry.getKey();
                    String headerValue = headerEntry.getValue();

                    // TODO should external context handles this itself?
                    if ("content-length".equals(headerName.toLowerCase(Locale.US))) {
                        try {
                            externalContext.setResponseContentLength(Integer.parseInt(headerValue));
                        } catch (NumberFormatException e) {

                            // TODO: handle exception
                        }
                    } else {
                        externalContext.setResponseHeader(headerName, headerValue);
                    }
                }

                // TODO null content type?
                String contentType = resource.getContentType();

                if (contentType != null) {
                    externalContext.setResponseContentType(contentType);
                }

                if (resource instanceof ContentProducerResource) {
                    ContentProducerResource contentProducerResource = (ContentProducerResource) resource;
                    contentProducerResource.encode(context);
                } else {
                    // TODO setup output buffer size according to configuration parameter
                    InputStream is = resource.getInputStream();
                    OutputStream os = externalContext.getResponseOutputStream();

                    try {
                        ResourceUtils.copyStreamContent(is, os);
                    } finally {
                        if (is != null) {
                            try {
                                is.close();
                            } catch (IOException e) {
                                if (LOGGER.isDebugEnabled()) {
                                    LOGGER.debug(e.getMessage(), e);
                                }
                            }
                        }

                        // TODO flush resource
                        // TODO dispose resource
                    }
                }

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Resource succesfully encoded");
                }
            } else {
                sendNotModified(context);
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Passing request to the next resource handler in chain");
            }

            defaultHandler.handleResourceRequest(context);
        }
    }

    protected boolean isThisHandlerResourceRequest(FacesContext context) {
        Boolean resourceRequest = BooleanRequestStateVariable.ResourceRequest.get(context);

        if (resourceRequest == null) {
            String resourcePath = getResourcePathFromRequest(context);

            // TODO handle exclusions
            resourceRequest = (resourcePath != null) && (resourcePath.length() > 0);
            BooleanRequestStateVariable.ResourceRequest.set(context, resourceRequest);

            if (LOGGER.isDebugEnabled() && resourceRequest) {
                LOGGER.debug(MessageFormat.format("Resource request detected: {0}", resourcePath));
            }
        }

        return resourceRequest;
    }

    private Resource lookupInCache(Cache cache, String resourceKey) {
        if (cache == null) {
            LOGGER.debug("No cache was provided");
            return null;
        }

        Resource resource = (Resource) cache.get(resourceKey);

        if (LOGGER.isDebugEnabled()) {
            if (resource == null) {
                LOGGER.debug("Resource was not located in cache");
            } else {
                LOGGER.debug("Resource was located in cache");
            }
        }

        return resource;
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#createResource(java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public Resource createResource(String resourceName, String libraryName, String contentType) {
        Resource resource = resourceFactory.createResource(resourceName, libraryName, contentType);
        if (resource == null) {
            resource = defaultHandler.createResource(resourceName, libraryName, contentType);
        }

        return resource;
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#createResource(java.lang.String, java.lang.String)
     */
    @Override
    public Resource createResource(String resourceName, String libraryName) {
        return createResource(resourceName, libraryName, null);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#createResource(java.lang.String)
     */
    @Override
    public Resource createResource(String resourceName) {
        return createResource(resourceName, null, null);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#getRendererTypeForResourceName(java.lang.String)
     */
    @Override
    public String getRendererTypeForResourceName(String resourceName) {

        if (resourceName.endsWith(".ecss")) {
            return "javax.faces.resource.Stylesheet";
        }

        if (resourceName.endsWith(ResourceLibraryRenderer.RESOURCE_LIBRARY_EXTENSION)) {
            return ResourceLibraryRenderer.RENDERER_TYPE;
        }

        return defaultHandler.getRendererTypeForResourceName(resourceName);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#libraryExists(java.lang.String)
     */
    @Override
    public boolean libraryExists(String libraryName) {
        return defaultHandler.libraryExists(libraryName);
    }

    /*
     * (non-Javadoc)
     * @see javax.faces.application.ResourceHandlerWrapper#getWrapped()
     */
    @Override
    public ResourceHandler getWrapped() {
        return defaultHandler;
    }

    private static String getResourcePathFromRequest(FacesContext context) {
        String resourceName = ResourceUtils.decodeResourceURL(context);

        if (resourceName != null) {
            if (resourceName.startsWith(RICHFACES_RESOURCE_IDENTIFIER)) {
                return resourceName.substring(RICHFACES_RESOURCE_IDENTIFIER.length());
            } else {
                return null;
            }
        } else {
            LOGGER.warn("Resource key not found" + resourceName);
            return null;
        }
    }

    private static void sendNotModified(FacesContext context) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("User agent has actual resource copy - sending 304 status code");
        }

        // TODO send cacheable resource headers (ETag + LastModified)?
        context.getExternalContext().setResponseStatus(HttpServletResponse.SC_NOT_MODIFIED);
    }

    private static void sendResourceNotFound(FacesContext context) {
        context.getExternalContext().setResponseStatus(HttpServletResponse.SC_NOT_FOUND);
    }
}