/*
 * #%L
 * ACS AEM Commons Bundle
 * %%
 * Copyright (C) 2013 Adobe
 * %%
 * 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.
 * #L%
 */
package com.adobe.acs.commons.images.impl;

import com.adobe.acs.commons.dam.RenditionPatternPicker;
import com.adobe.acs.commons.images.ImageTransformer;
import com.adobe.acs.commons.images.NamedImageTransformer;
import com.adobe.acs.commons.util.PathInfoUtil;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.replacedet;
import com.day.cq.dam.api.Rendition;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.foundation.Image;
import com.day.image.Layer;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.References;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.OptingServlet;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.commons.mime.MimeTypeService;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.jcr.RepositoryException;
import javax.servlet.Servlet;
import javax.servlet.ServletException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings("serial")
@Component(
        label = "ACS AEM Commons - Named Transform Image Servlet",
        description = "Transform images programatically by applying a named transform to the requested Image.",
        metatype = true
)
@Properties({
        @Property(
                label = "Resource Types",
                description = "Resource Types and Node Types to bind this servlet to.",
                name = "sling.servlet.resourceTypes",
                value = { "nt/file", "nt/resource", "dam/replacedet", "cq/Page", "cq/PageContent", "nt/unstructured",
                        "foundation/components/image", "foundation/components/parbase", "foundation/components/page" },
                propertyPrivate = false
        ),
        @Property(
            label = "Allows Suffix Patterns",
            description = "Regex pattern to filter allowed file names. Defaults to [ "
                    + NamedTransformImageServlet.DEFAULT_FILENAME_PATTERN + " ]",
            name = NamedTransformImageServlet.NAMED_IMAGE_FILENAME_PATTERN,
            value = NamedTransformImageServlet.DEFAULT_FILENAME_PATTERN
		),
        @Property(
                label = "Extension",
                description = "",
                name = "sling.servlet.extensions",
                value = { "transform" },
                propertyPrivate = true
        ),
        @Property(
                name = "sling.servlet.methods",
                value = { "GET" },
                propertyPrivate = true
        )
})
@References({
        @Reference(
                name = "namedImageTransformers",
                referenceInterface = NamedImageTransformer.clreplaced,
                policy = ReferencePolicy.DYNAMIC,
                cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE
        ),
        @Reference(
                name = "imageTransformers",
                referenceInterface = ImageTransformer.clreplaced,
                policy = ReferencePolicy.DYNAMIC,
                cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE
        )
})
@Service(Servlet.clreplaced)
public clreplaced NamedTransformImageServlet extends SlingSafeMethodsServlet implements OptingServlet {

    private static final Logger log = LoggerFactory.getLogger(NamedTransformImageServlet.clreplaced);

    public static final String NAME_IMAGE = "image";

    public static final String NAMED_IMAGE_FILENAME_PATTERN = "acs.commons.namedimage.filename.pattern";

    public static final String DEFAULT_FILENAME_PATTERN = "(image|img)\\.(.+)";

    public static final String RT_LOCAL_SOCIAL_IMAGE = "social:asiFile";

    public static final String RT_REMOTE_SOCIAL_IMAGE = "nt:adobesocialtype";

    @Reference
    private MimeTypeService mimeTypeService;

    private static final ValueMap EMPTY_PARAMS = new ValueMapDecorator(new LinkedHashMap<String, Object>());

    private static final String MIME_TYPE_PNG = "image/png";

    private static final String TYPE_QUALITY = "quality";

    private Pattern lastSuffixPattern = Pattern.compile(DEFAULT_FILENAME_PATTERN);

    private Map<String, NamedImageTransformer> namedImageTransformers =
            new ConcurrentHashMap<String, NamedImageTransformer>();

    private Map<String, ImageTransformer> imageTransformers = new ConcurrentHashMap<String, ImageTransformer>();

    /* replacedet Rendition Pattern Picker */

    private static final String DEFAULT_replacedET_RENDITION_PICKER_REGEX = "cq5dam\\.web\\.(.*)";

    @Property(label = "replacedet Rendition Picker Regex",
            description = "Regex to select the Rendition to transform when directly transforming a DAM replacedet."
                    + " [ Default: cq5dam.web.(.*) ]",
            value = DEFAULT_replacedET_RENDITION_PICKER_REGEX)
    private static final String PROP_replacedET_RENDITION_PICKER_REGEX = "prop.replacedet-rendition-picker-regex";

    private static RenditionPatternPicker renditionPatternPicker =
            new RenditionPatternPicker(Pattern.compile(DEFAULT_replacedET_RENDITION_PICKER_REGEX));

    /**
     * Only accept requests that.
     * - Are not null
     * - Have a suffix
     * - Whose first suffix segment is a registered transform name
     * - Whose last suffix matches the image file name pattern
     *
     * @param request SlingRequest object
     * @return true if the Servlet should handle the request
     */
    @Override
    public final boolean accepts(SlingHttpServletRequest request) {
        if (request == null) {
            return false;
        }

        final String suffix = request.getRequestPathInfo().getSuffix();
        if (StringUtils.isBlank(suffix)) {
            return false;
        }

        final String transformName = PathInfoUtil.getFirstSuffixSegment(request);
        if (!this.namedImageTransformers.keySet().contains(transformName)) {
            return false;
        }

        final String lastSuffix = PathInfoUtil.getLastSuffixSegment(request);
        final Matcher matcher = lastSuffixPattern.matcher(lastSuffix);
        if (!matcher.matches()) {
            return false;
        }

        return true;
    }

    @Override
    protected final void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response) throws
            ServletException, IOException {

        // Get the transform names from the suffix
        final List<NamedImageTransformer> selectedNamedImageTransformers = getNamedImageTransformers(request);

        // Collect and combine the image transformers and their params
        final ValueMap imageTransformersWithParams = getImageTransformersWithParams(selectedNamedImageTransformers);

        final Image image = this.resolveImage(request);
        final String mimeType = this.getMimeType(request, image);
        Layer layer = this.getLayer(image);
        
        if (layer == null) {
            response.setStatus(SlingHttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        // Transform the image
        layer = this.transform(layer, imageTransformersWithParams);

        // Get the quality
        final double quality = this.getQuality(mimeType,
                imageTransformersWithParams.get(TYPE_QUALITY, EMPTY_PARAMS));

        response.setContentType(mimeType);
        layer.write(mimeType, quality, response.getOutputStream());
        response.flushBuffer();
    }

    /**
     * Execute the ImageTransformers as specified by the Request's suffix segments against the Image layer.
     *
     * @param layer the Image layer
     * @param imageTransformersWithParams the transforms and their params
     * @return the transformed Image layer
     */
    protected final Layer transform(Layer layer, final ValueMap imageTransformersWithParams) {

        for (final String type : imageTransformersWithParams.keySet()) {
            if (StringUtils.equals(TYPE_QUALITY, type)) {
                // Do not process the "quality" transform in the usual manner
                continue;
            }

            final ImageTransformer imageTransformer = this.imageTransformers.get(type);
            if (imageTransformer == null) {
                log.warn("Skipping transform. Missing ImageTransformer for type: {}");
                continue;
            }

            final ValueMap transformParams = imageTransformersWithParams.get(type, EMPTY_PARAMS);

            if (transformParams != null) {
                layer = imageTransformer.transform(layer, transformParams);
            }
        }

        return layer;
    }

    /**
     * Gets the NamedImageTransformers based on the Suffix segments in order.
     *
     * @param request the SlingHttpServletRequest object
     * @return a list of the NamedImageTransformers specified by the HTTP Request suffix segments
     */
    protected final List<NamedImageTransformer> getNamedImageTransformers(final SlingHttpServletRequest request) {
        final List<NamedImageTransformer> transformers = new ArrayList<NamedImageTransformer>();

        String[] suffixes = PathInfoUtil.getSuffixSegments(request);
        if (suffixes.length < 2) {
            log.warn("Named Transform Image Servlet requires at least one named transform");
            return transformers;
        }

        int endIndex = suffixes.length - 1;
        // Its OK to check; the above check ensures there are 2+ segments
        if (StringUtils.isNumeric(PathInfoUtil.getSuffixSegment(request, suffixes.length - 2))) {
            endIndex--;
        }

        suffixes = (String[]) ArrayUtils.subarray(suffixes, 0, endIndex);

        for (final String transformerName : suffixes) {
            final NamedImageTransformer transformer = this.namedImageTransformers.get(transformerName);
            if (transformer != null) {
                transformers.add(transformer);
            }
        }

        return transformers;
    }

    /**
     * Collect and combine the image transformers and their params.
     *
     * @param selectedNamedImageTransformers the named transformers and their params
     * @return the combined named image transformers and their params
     */
    protected final ValueMap getImageTransformersWithParams(
            final List<NamedImageTransformer> selectedNamedImageTransformers) {
        final ValueMap params = new ValueMapDecorator(new LinkedHashMap<String, Object>());

        for (final NamedImageTransformer namedImageTransformer : selectedNamedImageTransformers) {
            params.putAll(namedImageTransformer.getImageTransforms());
        }

        return params;
    }

    /**
     * Intelligently determines how to find the Image based on the replacedociated SlingRequest.
     *
     * @param request the SlingRequest Obj
     * @return the Image object configured w the info of where the image to render is stored in CRX
     */
    protected final Image resolveImage(final SlingHttpServletRequest request) {
        final Resource resource = request.getResource();
        final ResourceResolver resourceResolver = request.getResourceResolver();

        final PageManager pageManager = resourceResolver.adaptTo(PageManager.clreplaced);
        final Page page = pageManager.getContainingPage(resource);

        if (DamUtil.isreplacedet(resource)) {
            // For replacedets, pick the configured rendition if it exists
            // If rendition does not exist, use original

            final replacedet replacedet = DamUtil.resolveToreplacedet(resource);
            Rendition rendition = replacedet.getRendition(renditionPatternPicker);

            if (rendition == null) {
                log.warn("Could not find rendition [ {} ] for [ {} ]", renditionPatternPicker.toString(),
                        resource.getPath());
                rendition = replacedet.getOriginal();
            }

            final Resource renditionResource = request.getResourceResolver().getResource(rendition.getPath());

            final Image image = new Image(resource);
            image.set(Image.PN_REFERENCE, renditionResource.getPath());
            return image;

        } else if (DamUtil.isRendition(resource)
                || resourceResolver.isResourceType(resource, JcrConstants.NT_FILE)
                || resourceResolver.isResourceType(resource, JcrConstants.NT_RESOURCE)) {
            // For renditions; use the requested rendition
            final Image image = new Image(resource);
            image.set(Image.PN_REFERENCE, resource.getPath());
            return image;

        } else if (page != null) {
            if (resourceResolver.isResourceType(resource, NameConstants.NT_PAGE)
                    || StringUtils.equals(resource.getPath(), page.getContentResource().getPath())) {
                // Is a Page or Page's Content Resource; use the Page's image resource
                return new Image(page.getContentResource(), NAME_IMAGE);
            } else {
                return new Image(resource);
            }
        } else {
        	if (resourceResolver.isResourceType(resource, RT_LOCAL_SOCIAL_IMAGE)
                    && resource.getValueMap().get("mimetype", StringUtils.EMPTY).startsWith("image/")) {
                // Is a UGC image
                return new SocialImageImpl(resource, NAME_IMAGE);
            } else if (resourceResolver.isResourceType(resource, RT_REMOTE_SOCIAL_IMAGE)) {
                // Is a UGC image
                return new SocialRemoteImageImpl(resource, NAME_IMAGE);
            }
        }

        return new Image(resource);
    }

    /**
     * Gets the mimeType of the image.
     * - The last segments suffix is looked at first and used
     * - if the last suffix segment's "extension" is .orig or .original then use the underlying resources mimeType
     * - else look up the mimeType to use based on this "extension"
     * - default to the resource's mimeType if the requested mimeType by extension is not supported.
     *
     * @param image the image to get the mimeType for
     * @return the string representation of the image's mimeType
     */
    private String getMimeType(final SlingHttpServletRequest request, final Image image) {
        final String lastSuffix = PathInfoUtil.getLastSuffixSegment(request);

        final String mimeType = mimeTypeService.getMimeType(lastSuffix);

        if (!StringUtils.endsWithIgnoreCase(lastSuffix, ".orig")
            && !StringUtils.endsWithIgnoreCase(lastSuffix, ".original")
            && (ImageIO.getImageWritersByMIMEType(mimeType).hasNext())) {
            return mimeType;
        } else {
            try {
                return image.getMimeType();
            } catch (final RepositoryException e) {
                return MIME_TYPE_PNG;
            }
        }
    }

    /**
     * Gets the Image layer.
     *
     * @param image The Image to get the layer from
     * @return the image's Layer
     * @throws IOException
     */
    private Layer getLayer(final Image image) throws IOException {
        Layer layer = null;

        try {
            layer = image.getLayer(false, false, false);
        } catch (RepositoryException ex) {
            log.error("Could not create layer");
        }

        if (layer == null) {
            log.error("Could not create layer - layer is null;");
        } else {
            image.crop(layer);
            image.rotate(layer);
        }

        return layer;
    }


    /**
     * Computes the quality based on the "synthetic" Image Quality transform params
     *
     * Image Quality does not "transform" in the usual manner (it is not a simple layer manipulation)
     * thus this ad-hoc method is required to handle quality manipulation transformations.
     *
     * If "quality" key is no available in "transforms" the default of 82 is used (magic AEM Product quality setting)
     *
     * @param mimeType the desired image mimeType
     * @param transforms the map of image transform params
     * @return
     */
    protected final double getQuality(final String mimeType, final ValueMap transforms) {
        final String key = "quality";
        final int defaultQuality = 82;
        final int maxQuality = 100;
        final int minQuality = 0;
        final int maxQualityGIF = 255;
        final double oneHundred = 100D;

        log.debug("Transforming with [ quality ]");

        double quality = transforms.get(key, defaultQuality);

        if (quality > maxQuality || quality < minQuality) {
            quality = defaultQuality;
        }

        quality = quality / oneHundred;

        if (StringUtils.equals("image/gif", mimeType)) {
            quality = quality * maxQualityGIF;
        }

        return quality;
    }

    @Activate
    protected final void activate(final Map<String, String> properties) throws Exception {
        final String regex = PropertiesUtil.toString(properties.get(PROP_replacedET_RENDITION_PICKER_REGEX),
                DEFAULT_replacedET_RENDITION_PICKER_REGEX);
	    final String fileNameRegex = PropertiesUtil.toString(properties.get(NAMED_IMAGE_FILENAME_PATTERN),
			    DEFAULT_FILENAME_PATTERN);
	    if(StringUtils.isNotEmpty(fileNameRegex)) {
		    lastSuffixPattern = Pattern.compile(fileNameRegex);
	    }
        try {
            renditionPatternPicker = new RenditionPatternPicker(regex);
            log.info("replacedet Rendition Pattern Picker: {}", regex);
        } catch (Exception ex) {
            log.error("Error creating RenditionPatternPicker with regex [ {} ], defaulting to [ {} ]", regex,
                    DEFAULT_replacedET_RENDITION_PICKER_REGEX);
            renditionPatternPicker = new RenditionPatternPicker(DEFAULT_replacedET_RENDITION_PICKER_REGEX);
        }
    }

    protected final void bindNamedImageTransformers(final NamedImageTransformer service,
                                                    final Map<Object, Object> props) {
        final String type = PropertiesUtil.toString(props.get(NamedImageTransformer.PROP_NAME), null);
        if (type != null) {
            this.namedImageTransformers.put(type, service);
        }
    }

    protected final void unbindNamedImageTransformers(final NamedImageTransformer service,
                                                      final Map<Object, Object> props) {
        final String type = PropertiesUtil.toString(props.get(NamedImageTransformer.PROP_NAME), null);
        if (type != null) {
            this.namedImageTransformers.remove(type);
        }
    }

    protected final void bindImageTransformers(final ImageTransformer service, final Map<Object, Object> props) {
        final String type = PropertiesUtil.toString(props.get(ImageTransformer.PROP_TYPE), null);
        if (type != null) {
            imageTransformers.put(type, service);
        }
    }

    protected final void unbindImageTransformers(final ImageTransformer service, final Map<Object, Object> props) {
        final String type = PropertiesUtil.toString(props.get(ImageTransformer.PROP_TYPE), null);
        if (type != null) {
            imageTransformers.remove(type);
        }
    }
}