/*
 * 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.typerendering;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import java.util.Set;

import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
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.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.apache.clerezza.rdf.core.BNode;
import org.apache.clerezza.rdf.core.impl.SimpleMGraph;
import org.apache.clerezza.rdf.utils.GraphNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Generates output based on the rendering mode passed as query parameter ("mode")
 * and the rdf type of the given GraphNode.
 * 
 * @author daniel, mir, rbn
 */
@Component
@Service(Object.class)
@Property(name = "javax.ws.rs", boolValue = true)
@Reference(name = "contextProvider", referenceInterface = UserContextProvider.class,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
@Provider
@Produces({"application/xhtml+xml", "*/*"})
public class GenericGraphNodeMBW implements MessageBodyWriter<GraphNode> {

    public static final String MODE = "mode";
    
    @Reference
    RendererFactory rendererFactory;

    private static final Logger logger = LoggerFactory.getLogger(GenericGraphNodeMBW.class);
    private UriInfo uriInfo = null;
    private HttpHeaders headers = null;
    private final Set<UserContextProvider> contextProviders =
            Collections.synchronizedSet(new HashSet<UserContextProvider>());
    private final DocumentBuilder documentBuilder;
    private TransformerFactory transformerFactory = TransformerFactory.newInstance();

    {
        try {
            final DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
            builderFactory.setValidating(false);
            builderFactory.setNamespaceAware(true);
            builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            documentBuilder = builderFactory.newDocumentBuilder();
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Context
    public void setUriInfo(UriInfo uriInfo) {
        this.uriInfo = uriInfo;
    }


    @Context
    public void setHttpHeaders(HttpHeaders headers) {
        this.headers = headers;
    }

    @Override
    public long getSize(GraphNode node, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return -1;
    }

    @Override
    public boolean isWriteable(Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType) {
        return GraphNode.class.isAssignableFrom(type);
    }

    @Override
    public void writeTo(GraphNode node, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
            throws IOException, WebApplicationException {
        String mode = getRenderingMode();

        Renderer renderer = rendererFactory.createRenderer(node, mode,
                headers.getAcceptableMediaTypes());

        if (renderer == null) {
            throw new WebApplicationException(
                    Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("No suitable renderer found").build());
        }
        final MediaType rendererMediaType = renderer.getMediaType();
        Map<String, Object> sharedRenderingValues = new HashMap<String, Object>();
        ResultDocModifier.init();
        if (!(rendererMediaType.getType().equals("application") && rendererMediaType.getSubtype().equals("xhtml+xml"))) {
            httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, rendererMediaType);
            renderer.render(node, getUserContext(), mode, uriInfo, headers, httpHeaders, sharedRenderingValues, entityStream);
        } else {
            final MediaType mediaTypeWithCharset = MediaType.valueOf(MediaType.APPLICATION_XHTML_XML+";charset=UTF-8");
            httpHeaders.putSingle(HttpHeaders.CONTENT_TYPE, mediaTypeWithCharset);
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                renderer.render(node, getUserContext(), mode, uriInfo, headers, httpHeaders, sharedRenderingValues, baos);
                final byte[] bytes = baos.toByteArray();
                if (!ResultDocModifier.getInstance().isModified()) {
                    entityStream.write(bytes);
                    return;
                }
                Document document;
                try {
                    synchronized(documentBuilder) {
                        document = documentBuilder.parse(new ByteArrayInputStream(bytes));
                    }
                } catch (SAXException ex) {
                    logger.error("Error parsing XHTML", ex);
                    entityStream.write(bytes);
                    return;
                }
                ResultDocModifier.getInstance().addToDocument(document);
                Transformer transformer;
                try {
                    transformer = transformerFactory.newTransformer();
                } catch (TransformerConfigurationException ex) {
                    throw new RuntimeException(ex);
                }
                DOMSource source = new DOMSource(document);
                StreamResult result = new StreamResult(entityStream);
                try {
                    transformer.setOutputProperty("omit-xml-declaration","yes");
                    transformer.transform(source, result);
                } catch (TransformerException ex) {
                    throw new RuntimeException(ex);
                }
            } finally {
                ResultDocModifier.dispose();
            }
        }
    }

    private String getRenderingMode() {
        if (uriInfo == null) {
            return null;
        }
        MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
        List<String> modes = queryParams.get(MODE);
        if ((modes == null) || (modes.size() == 0)) {
            return null;
        }
        return modes.get(0);
    }

    protected void bindContextProvider(UserContextProvider provider) {
        contextProviders.add(provider);
    }

    protected void unbindContextProvider(UserContextProvider provider) {
        contextProviders.remove(provider);
    }

    private GraphNode getUserContext() {
        GraphNode contextNode = new GraphNode(new BNode(), new SimpleMGraph());
        synchronized(contextProviders) {
            Iterator<UserContextProvider> providersIter = contextProviders.iterator();
            while (providersIter.hasNext()) {
                UserContextProvider userContextProvider = providersIter.next();
                contextNode = userContextProvider.addUserContext(contextNode);
            }
        }
        return contextNode;
    }
}
