/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.clerezza.platform.content.representations.core;

import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.clerezza.jaxrs.utils.RedirectUtil;
import org.apache.clerezza.platform.config.PlatformConfig;
import org.apache.clerezza.platform.graphprovider.content.ContentGraphProvider;
import org.apache.clerezza.rdf.core.Literal;
import org.apache.clerezza.rdf.core.LiteralFactory;
import org.apache.clerezza.rdf.core.Resource;
import org.apache.clerezza.rdf.core.TypedLiteral;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.ontologies.DISCOBITS;
import org.apache.clerezza.rdf.ontologies.EXIF;
import org.apache.clerezza.rdf.utils.GraphNode;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.felix.scr.annotations.Services;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This JAX-RS resource provides a method to retrieve the uri to
 * the thumbnail or a other small representation of a InfoDiscoBit.
 *
 * @author mir
 */
@Component
@Services({
    @Service(Object.class),
    @Service(ThumbnailService.class)
})

@Property(name = "javax.ws.rs", boolValue = true)
@Path("thumbnail-service")
public class ThumbnailService implements BundleListener {

    @Reference
    ContentGraphProvider cgProvider;
    @Reference
    PlatformConfig config;
    @Reference
    AlternativeRepresentationGenerator altRepGen;
    private static final Logger log = LoggerFactory.getLogger(ThumbnailService.class);
    private BundleContext bundleContext;
    private String STATICWEB_PATH = "/CLEREZZA-INF/web-resources/style/";
    private String MEDIA_TYPE_BASE_PATH = STATICWEB_PATH + "images/icons/mediatype/";
    private Bundle cachedStyleBundle = null;
    private Map<MediaType, String> mediaTypeIconUriCache =
            Collections.synchronizedMap(new HashMap<MediaType, String>());

    protected void activate(ComponentContext context) {
        bundleContext = context.getBundleContext();
        bundleContext.addBundleListener(this);
    }

    protected void deactivate(ComponentContext context) {
        bundleContext.removeBundleListener(this);
        bundleContext = null;
        mediaTypeIconUriCache.clear();
    }

    /**
     * Returns the thumbnail uri for a InfoDiscoBit which is located at the uri
     * specified over the query parameter "uri". The thumbnails
     * maximum width and height can optionally be specified over the query parameters
     * "width" and "height". Futhermore there is the optional "exact" parameter,
     * which specifies if the thumbnail must have the exact width and height as
     * specified or not (default is "false"). If more than one acceptable thumbnail
     * is available then the thumbnail uri of the thumbnail with the highest resolution
     * (width * height) is returned. If no thumbnail is available and the logged
     * in user has the write permission for the content graph, then an attempt is
     * made to create the thumbnail on the fly. If this fails or the write permission
     * is missing, then the uri of the icon representing the media type is returned.
     * If also no media type icon is available the uri to default icon is returned.
     *
     * @param infoBitUri the uri of the infoDiscoBit of which the thumbnail uri should
     *        be returned
     * @param height the maximum height that the thumbnail has
     * @param width the maximum width that the thumbnail has
     * @param exact boolean that specifies if the return thumbnail should have
     *        the exact width and height.
     * @return
     */
    @GET
    public Response getThumbnailUri(@QueryParam("uri") UriRef infoBitUri,
            @QueryParam("width") Integer width, @QueryParam("height") Integer height,
            @DefaultValue("false") @QueryParam("exact") Boolean exact, @Context UriInfo uriInfo) {
        return RedirectUtil.createSeeOtherResponse(
                getThumbnailUri(infoBitUri, width, height, exact).getUnicodeString(), uriInfo);
    }


    /**
     * Returns the thumbnail uri for a InfoDiscoBit which is located at the uri
     * specified over the query parameter "uri". The thumbnails
     * maximum width and height can optionally be specified over the query parameters
     * "width" and "height". If more than one acceptable thumbnail is available
     * then the thumbnail uri of the thumbnail with the highest resolution
     * (width * height) is returned. If no thumbnail is available and the logged
     * in user has the write permission for the content graph, then an attempt is
     * made to create the thumbnail on the fly. If this fails or the write permission
     * is missing, then the uri of the icon representing the media type is returned.
     * If also no media type icon is available the uri to default icon is returned.
     *
     * @param infoBitUri the uri of the infoDiscoBit of which the thumbnail uri should
     *        be returned
     * @param height the maximum height that the thumbnail has
     * @param width the maximum width that the thumbnail has
     * @return
     */
    public UriRef getThumbnailUri(UriRef infoBitUri, Integer width,  Integer height) {
        return getThumbnailUri(infoBitUri, width, height, false);
    }

    /**
     * Returns the thumbnail uri for a InfoDiscoBit which is located at the uri
     * specified over the query parameter "uri". The thumbnails
     * maximum width and height can optionally be specified over the query parameters
     * "width" and "height". Futhermore there is the optional "exact" parameter,
     * which specifies if the thumbnail must have the exact width and height as
     * specified or not. If more than one acceptable thumbnail is available then
     * the thumbnail uri of the thumbnail with the highest resolution (width * height)
     * is returned. If no thumbnail is available and the logged in user has the write
     * permission for the content graph, then an attempt is made to create the
     * thumbnail on the fly. If this fails or the write permission is missing, then
     * the uri of the icon representing the media type is returned. If also no
     * media type icon is available the uri to default icon is returned.
     *
     * @param infoBitUri the uri of the infoDiscoBit of which the thumbnail uri should
     *        be returned
     * @param height the maximum height that the thumbnail has
     * @param width the maximum width that the thumbnail has
     * @param exact boolean that specifies if the return thumbnail should have
     *        the exact width and height.
     * @return
     */
    public UriRef getThumbnailUri(UriRef infoBitUri, Integer width,  Integer height,
            boolean exact) {
        if ((width == null) && (height == null)) {
            throw new IllegalArgumentException("height and/or width must be specified");
        }
        if (width == null) {
            width = Integer.MAX_VALUE;
        }
        if (height == null) {
            height = Integer.MAX_VALUE;
        }
        GraphNode infoBitNode = new GraphNode(infoBitUri, cgProvider.getContentGraph());
        UriRef thumbnailUri = getGeneratedThumbnailUri(infoBitNode, width, height, exact);
        if (thumbnailUri == null) {
            Literal mediaTypeLiteral = null;
            Lock readLock = infoBitNode.readLock();
            readLock.lock();
            try {
                Iterator<Resource> mediaTypes = infoBitNode.getObjects(DISCOBITS.mediaType);
                if (mediaTypes.hasNext()) {
                    mediaTypeLiteral = (Literal) mediaTypes.next();
                }
            } finally {
                readLock.unlock();
            }
            if (mediaTypeLiteral != null) {
                MediaType mediaType = MediaType.valueOf(mediaTypeLiteral.getLexicalForm());
                // if the infoBit is an image, create a thumbnail on the fly.
                if (mediaType.getType().startsWith("image")) {
                    try {
                        thumbnailUri = altRepGen.generateAlternativeImage(infoBitNode, width,
                                height, exact);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                        // Was worth a try. eLets go on
                    }
                }
                if (thumbnailUri == null) {
                    String iconUri = mediaTypeIconUriCache.get(mediaType);
                    if (iconUri == null) {
                        iconUri = getMediaTypeIconUri(mediaType);
                        mediaTypeIconUriCache.put(mediaType, iconUri);
                    }
                    thumbnailUri = new UriRef(iconUri);
                }
            }
        }
        if (thumbnailUri == null) {
            thumbnailUri = new UriRef(getDefaultIconUrl(getStyleBundle()));
        }
        return thumbnailUri;
    }

    private String getMediaTypeIconUri(MediaType mediaType) {
        Bundle styleBundle = getStyleBundle();
        if (styleBundle == null) {
            throw new RuntimeException("no style bundle found");
        }
        String path = MEDIA_TYPE_BASE_PATH + mediaType.getType() + "/";
        Enumeration entries = styleBundle.findEntries(path,
                mediaType.getSubtype() + ".*", false);
        String iconUri = createIconUri(entries);
        if (iconUri != null) {
            return iconUri;
        }
        entries = styleBundle.findEntries(path, "any.*", false);
        iconUri = createIconUri(entries);
        if (iconUri != null) {
            return iconUri;
        }
        return getDefaultIconUrl(styleBundle);
    }

    private String getDefaultIconUrl(Bundle bundle) {
        Enumeration entries = bundle.findEntries(MEDIA_TYPE_BASE_PATH, "any.*", false);
        String iconUri = createIconUri(entries);
        if (iconUri != null) {
            return iconUri;
        } else {
            throw new RuntimeException("No default icon found");
        }
    }

    private String createIconUri(Enumeration entries) {
        if (entries != null && entries.hasMoreElements()) {
            URL iconUrl = (URL) entries.nextElement();
            return iconUrl.getPath().replace(STATICWEB_PATH, "style/");
        }
        return null;
    }

    private UriRef getGeneratedThumbnailUri(GraphNode infoBitNode,
            Integer width, Integer height, boolean exact) {
        if (isFittingImage(infoBitNode, width, height, exact)) {
            return (UriRef) infoBitNode.getNode();
        }
        UriRef resultThumbnailUri = null;
        int pixels = 0;
        Lock readLock = infoBitNode.readLock();
        readLock.lock();
        try {
            Iterator<Resource> thumbnails = infoBitNode.getObjects(DISCOBITS.thumbnail);
            while (thumbnails.hasNext()) {
                UriRef thumbnailUri = (UriRef) thumbnails.next();
                GraphNode thumbnailNode = new GraphNode(thumbnailUri,
                        cgProvider.getContentGraph());
                int thumbnailPixels = getSurfaceSizeIfFitting(thumbnailNode, width, height, exact);
                if (thumbnailPixels > pixels) {
                    if (exact) {
                        return thumbnailUri;
                    }
                    resultThumbnailUri = thumbnailUri;
                    pixels = thumbnailPixels;
                }
            }
        } finally {
            readLock.unlock();
        }
        return resultThumbnailUri;
    }

    /**
     * returns the surface in pixel if the image fits withing width and height,
     * or -1 if it doesn't fit
     */
    private int getSurfaceSizeIfFitting(GraphNode infoBitNode, Integer width, Integer height,
            boolean exact) {
        Resource imageRes = infoBitNode.getNode();
        if (imageRes instanceof UriRef) {
            String imageUri = ((UriRef)imageRes).getUnicodeString();
            if (!exact && imageUri.contains(AlternativeRepresentationGenerator.EXACT_APPENDIX)) {
                return -1;
            }
        }        
        Iterator<Resource> exifWidths = infoBitNode.getObjects(EXIF.width);
        Iterator<Resource> exifHeights = infoBitNode.getObjects(EXIF.height);
        if (!exifWidths.hasNext() || !exifHeights.hasNext()) {
            log.warn(infoBitNode.getNode() + " doesn't have exif:width and exif:height");
            return -1;
        }
        int thumbnailWidth = LiteralFactory.getInstance().createObject(
                Integer.class, (TypedLiteral) exifWidths.next());
        int thumbnailHeight = LiteralFactory.getInstance().createObject(
                Integer.class, (TypedLiteral) exifHeights.next());
        if (exact) {
            if (thumbnailHeight == height && thumbnailWidth == width) {
                return 1;
            }
        } else {
            if (thumbnailHeight <= height && thumbnailWidth <= width) {
                return thumbnailWidth * thumbnailHeight;
            }
        }
        return -1;
    }

    /**
     * returns true if infoBitNode is an image and fits
     */
    private boolean isFittingImage(GraphNode infoBitNode, Integer width, Integer height,
            boolean exact) {
        Lock readLock = infoBitNode.readLock();
        readLock.lock();
        try {
            final Iterator<Literal> mediaTypesIter = infoBitNode.getLiterals(DISCOBITS.mediaType);
            if (!mediaTypesIter.hasNext()) {
                return false;
            }
            if (mediaTypesIter.next().getLexicalForm().startsWith("image")) {
                return getSurfaceSizeIfFitting(infoBitNode, width, height, exact) > -1;
            } else {
                return false;
            }
        } finally {
            readLock.unlock();
        }
    }

    private synchronized Bundle getStyleBundle() {
        if (cachedStyleBundle != null) {
            return cachedStyleBundle;
        }
        Bundle[] bundles = bundleContext.getBundles();
        for (Bundle bundle : bundles) {
            URL staticWebPathURL = bundle.getEntry(STATICWEB_PATH);
            if (staticWebPathURL != null) {
                cachedStyleBundle = bundle;
                return bundle;
            }
        }
        return null;
    }

    @Override
    public synchronized void bundleChanged(BundleEvent be) {
        if (be.getType() == BundleEvent.UNINSTALLED
                && be.getBundle().equals(cachedStyleBundle)) {
            cachedStyleBundle = null;
            cachedStyleBundle = getStyleBundle();
            mediaTypeIconUriCache.clear();
        }
    }
}
