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

import org.apache.clerezza.rdf.utils.graphnodeprovider.GraphNodeProvider;
import org.apache.clerezza.rdf.metadata.MetaDataGenerator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.ArrayList;
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 java.util.concurrent.locks.Lock;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.PUT;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Response.Status;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import org.apache.clerezza.jaxrs.utils.RedirectUtil;
import org.apache.clerezza.platform.Constants;
import org.apache.clerezza.platform.content.WebDavUtils.PropertyMap;
import org.apache.clerezza.platform.content.collections.CollectionCreator;
import org.apache.clerezza.platform.content.webdav.MKCOL;
import org.apache.clerezza.platform.content.webdav.MOVE;
import org.apache.clerezza.platform.content.webdav.PROPFIND;
import org.apache.clerezza.platform.content.webdav.PROPPATCH;

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.felix.scr.annotations.Services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.clerezza.platform.graphprovider.content.ContentGraphProvider;
import org.apache.clerezza.platform.typehandlerspace.OPTIONS;
import org.apache.clerezza.platform.typehandlerspace.SupportedTypes;
import org.apache.clerezza.rdf.core.MGraph;
import org.apache.clerezza.rdf.core.Triple;
import org.apache.clerezza.rdf.core.TripleCollection;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.core.access.LockableMGraph;
import org.apache.clerezza.rdf.core.impl.TripleImpl;
import org.apache.clerezza.rdf.ontologies.HIERARCHY;
import org.apache.clerezza.rdf.ontologies.RDF;
import org.apache.clerezza.rdf.utils.GraphNode;
import org.apache.clerezza.rdf.utils.UnionMGraph;
import org.apache.clerezza.rdf.utils.UriMutatingTripleCollection;
import org.apache.clerezza.web.fileserver.util.MediaTypeGuesser;
import org.osgi.service.component.ComponentContext;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This Class allows getting and putting content structured using the
 * Discobits ontology.
 *
 * Is an implementation of DiscobitsHandler and additionally registers as
 * TypeHanlder to allow HTTP GET and PUT.
 *
 * @author reto, tho, agron, mir
 */
@Component(metatype=true)
@Service({
    Object.class,
    DiscobitsHandler.class
})
@Property(name="org.apache.clerezza.platform.typehandler", boolValue=true)
@Reference(name="metaDataGenerator",
    policy=ReferencePolicy.DYNAMIC,
    cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE,
    referenceInterface=MetaDataGenerator.class
)
@SupportedTypes(types = { "http://www.w3.org/2000/01/rdf-schema#Resource" }, prioritize = false)
public class DiscobitsTypeHandler extends AbstractDiscobitsHandler
        implements DiscobitsHandler {
@Property(value="600", label="Max-Age", description="Specifies the value of the max-age field"
        + "set in the cache-control header of InfoDiscoBit-Responses, as per RFC 2616 this is a number of "
        + "seconds")
    public static final String MAX_AGE = "max-age";

    @Reference
    protected ContentGraphProvider cgProvider;

    @Reference
    PageNotFoundService notFoundPageService;

    @Reference
    GraphNodeProvider graphNodeProvider;

    private static final Logger logger = LoggerFactory.getLogger(DiscobitsTypeHandler.class);

    private final Set<MetaDataGenerator> metaDataGenerators =
            Collections.synchronizedSet(new HashSet<MetaDataGenerator>());

    private String cacheControlHeaderValue;

    protected void activate(ComponentContext context) {
        cacheControlHeaderValue = "max-age="+(String) context.getProperties().get(MAX_AGE);
    }

    /**
     * TypeHandle method for rdf types "TitledContext", "InfoDiscoBit",
     * "OrderedContent" and "XHTMLInfoDiscoBit".
     * 
     * @param uriInfo
     * @return
     */
    @GET
    @Produces({"*/*"})
    public Object getResource(@Context UriInfo uriInfo) {
        final UriRef uri = new UriRef(uriInfo.getAbsolutePath().toString());
            final GraphNode graphNode = getResourceAsGraphNode(uriInfo);
        if (graphNode == null) {
            return resourceUnavailable(uri, uriInfo);
        }
        InfoDiscobit infoDiscobit = InfoDiscobit.createInstance(graphNode);
        if (infoDiscobit != null) {
            Response response = Response.ok().
                    header(HttpHeaders.CACHE_CONTROL, cacheControlHeaderValue).
                    entity(infoDiscobit).
                    build();
            return response;
        } else {
            return graphNode;
        }
    }

    private GraphNode getResourceAsGraphNode(UriInfo uriInfo) {
        final UriRef uri = new UriRef(uriInfo.getAbsolutePath().toString());
        GraphNode result = graphNodeProvider.getLocal(uri);
        //could chck if nodeContext > 0, but this would be less efficient
        TripleCollection tc = result.getGraph();
        if (tc.filter(uri, null, null).hasNext()) {
            return result;
        }
        if (tc.filter(null, null, uri).hasNext()) {
            return result;
        }
        return null;
    }


    
    /**
     * Creates an <code>InfoDiscoBit</code> at the specified location
     *
     * @param uriInfo the uri of the InforDiscoBit to be created
     * @param data the content of the upload
     */
    @PUT
    public Response putInfoDiscobit(@Context UriInfo uriInfo, @Context HttpHeaders headers, byte[] data) {
        final String contentType;
        {
            final List<String> contentTypeHeaders = headers.getRequestHeader(HttpHeaders.CONTENT_TYPE);
            if (contentTypeHeaders == null) {
                logger.warn("Content-Type not specified");
                final MediaType guessTypeForName = MediaTypeGuesser.getInstance().
                        guessTypeForName(uriInfo.getAbsolutePath().toString());
                contentType = guessTypeForName == null ?
                    MediaType.APPLICATION_OCTET_STREAM : guessTypeForName.toString();
            } else {
                contentType = contentTypeHeaders.get(0);
            }
            
        }    
        final UriRef infoDiscoBitUri = new UriRef(uriInfo.getAbsolutePath().toString());
        put(infoDiscoBitUri, MediaType.valueOf(contentType), data);
        return Response.status(Status.CREATED).build();
    }

    /**
     * Creates a new collection at the specified uri
     *
     * @param uriInfo
     * @return
     * <ul>
     *    <li>201 "Created" response if method succeeded
     *    <li>405 "Method Not Allowed" response if collection already exists
     * </ul>
     */
    @MKCOL
    public Object mkcol(@Context UriInfo uriInfo) {
        String uriString = uriInfo.getAbsolutePath().toString();
        if (uriString.charAt(uriString.length()-1) != '/') {
            uriString += '/';
        }
        UriRef nodeUri = new UriRef(uriString);
        final MGraph mGraph = cgProvider.getContentGraph();
        Triple typeTriple = new TripleImpl(nodeUri, RDF.type, HIERARCHY.Collection);
        if (mGraph.contains(typeTriple)) {
            return Response.status(405) // Method Not Allowed
                    .entity("Collection \"" + nodeUri.getUnicodeString()
                    + "\" already exists.").build();
        }
        new CollectionCreator(mGraph).createContainingCollections(nodeUri);
        return Response.created(uriInfo.getAbsolutePath()).build();
    }

    /**
     * Finds all properties of a hierarchy node and returns them in a
     * {@link DOMSource}
     *
     * @param uriInfo
     * @param headers {@link HttpHeaders}
     * @param body {@link DOMSource} containing the requested properties, can be null
     * @return
     * <ul>
     *    <li>207 "Multistatus" response if method succeeded
     *    <li>404 "Not Found" response if the hierarchy node was not found
     *    <li>400 "Bad Request" response if the body is malformed
     * </ul>
     */
    @PROPFIND
    @Consumes({"application/xml", "text/xml", "*/*"})
    @Produces({"application/xml", "text/xml", "*/*"})
    public Response propfind(@Context UriInfo uriInfo,
            @Context HttpHeaders headers, DOMSource body) {
        final UriRef nodeUri = new UriRef(uriInfo.getAbsolutePath().toString());
        if (!nodeAtUriExists(nodeUri)) {
            return resourceUnavailable(nodeUri, uriInfo);
        }
            Map<UriRef, PropertyMap> result;
        try {
            String depthHeader = WebDavUtils.getHeaderAsString(headers, "Depth");
            if (depthHeader == null) {
                depthHeader = WebDavUtils.infinite;
            }
            final MGraph mGraph = cgProvider.getContentGraph();
            GraphNode node = new GraphNode(nodeUri, mGraph);
            if (body != null) {
                Document requestDoc = WebDavUtils.sourceToDocument(body);
                Node propfindNode = WebDavUtils.getNode(requestDoc, WebDavUtils.propfind);
                Node requestNode = WebDavUtils.getFirstChild(propfindNode);
                String requestType = requestNode.getLocalName();
                if (requestType.equalsIgnoreCase(WebDavUtils.allprop)) {
                    result = getAllProps(node, depthHeader);
                } else if (requestType.equalsIgnoreCase(WebDavUtils.prop)) {
                    result = getPropsByName(requestNode, node, depthHeader);
                } else if (requestType.equalsIgnoreCase(WebDavUtils.propname)) {
                    result = getPropNames(node, depthHeader);
                } else {
                    return Response.status(Status.BAD_REQUEST).build();
                }
            } else {
                // returns all properties
                result = getAllProps(node, depthHeader);
            }
            Document responseDoc = WebDavUtils.createResponseDoc(result);
            return Response.status(207).entity(new DOMSource(responseDoc)).type(
                    MediaType.APPLICATION_XML_TYPE).build();
        } catch (TransformerFactoryConfigurationError e) {
            return Response.status(Status.BAD_REQUEST).build();
        } catch (TransformerException e) {
            return Response.status(Status.BAD_REQUEST).build();
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    Map<UriRef, PropertyMap> getPropNames(GraphNode node, String depthHeader) {
        Map<UriRef, PropertyMap> result = new HashMap<UriRef, PropertyMap>();
        WebDavUtils.addNodeProperties(result, null, null, node, false);
        return result;
    }

    Map<UriRef, PropertyMap> getPropsByName(Node requestNode, GraphNode node,
            String depthHeader) {
        Map<UriRef, PropertyMap> result;
        NodeList children = requestNode.getChildNodes();
        result = WebDavUtils.getPropsByName(children, node, "0", true);
        return result;
    }

    Map<UriRef, PropertyMap> getAllProps(GraphNode node, String depthHeader) {
        HashMap<UriRef, PropertyMap> result = new HashMap<UriRef, PropertyMap>();
        WebDavUtils.addNodeProperties(result, null, null, node, true);
        return result;
    }

    /**
     * @param uriInfo
     * @param body {@link DOMSource} containing properties which should be set
     * or deleted
     * @return
     * <ul>
     *    <li>207 "Multistatus" response if method succeeded
     *    <li>404 "Not Found" response if the hierarchy node was not found
     *    <li>400 "Bad Request" response if the body is malformed
     * </ul>
     */
    @PROPPATCH
    @Consumes({"application/xml", "text/xml", "*/*"})
    @Produces({"application/xml", "text/xml", "*/*"})
    public Response proppatch(@Context UriInfo uriInfo, DOMSource body) {
        UriRef nodeUri = new UriRef(uriInfo.getAbsolutePath().toString());
        if (!nodeAtUriExists(nodeUri)) {
            return resourceUnavailable(nodeUri, uriInfo);
        }
        try {
            Document requestDoc = WebDavUtils.sourceToDocument(body);
            final MGraph mGraph = cgProvider.getContentGraph();
            GraphNode node = new GraphNode(nodeUri, mGraph);
            NodeList propsToSet = null;
            NodeList propsToRemove = null;
            Node proppatchNode = WebDavUtils.getNode(requestDoc, WebDavUtils.proppatch);
            NodeList childNodes = proppatchNode.getChildNodes();
            for (int i = 0; i < childNodes.getLength(); i++) {
                Node child = childNodes.item(i);
                String localName = child.getLocalName();
                if (localName != null) {
                    if (localName.equals(WebDavUtils.set)) {
                        propsToSet = child.getFirstChild().getChildNodes();
                    } else if (localName.equals(WebDavUtils.remove)) {
                        propsToRemove = child.getFirstChild().getChildNodes();
                    }
                }
            }
            Document responseDoc = WebDavUtils.modifyProperties(node, propsToSet, propsToRemove);
            return Response.status(207).entity(new DOMSource(responseDoc)).type(
                    MediaType.APPLICATION_XML_TYPE).build();
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException(ex);
        } catch (TransformerFactoryConfigurationError ex) {
            return Response.status(Status.BAD_REQUEST).build();
        } catch (TransformerException ex) {
            return Response.status(Status.BAD_REQUEST).build();
        } 
    }

    /**
     * Moves or renames a hierarchy node
     *
     * @param uriInfo
     * @param headers
     * @return
     * <ul>
     *  <li>201 "Created" response if method succeeded
     *  <li>412 "Precondition Failed" response if the destination URL is already
     *    mapped to a resource
     *  <li>404 "Not Found" response if the hierarchy node was not found
     *  <li>403 "Forbidden" response if the source and destination resources are the same
     *  <li>400 "Bad Request" if no "Destination" header was found
     * </ul>
     */
    @MOVE
    public Response move(@Context UriInfo uriInfo, @Context HttpHeaders headers) {
        UriRef nodeUri = new UriRef(uriInfo.getAbsolutePath().toString());
        final LockableMGraph mGraph = cgProvider.getContentGraph();
        GraphNode node = new GraphNode(nodeUri, mGraph);
        String targetString = WebDavUtils.getHeaderAsString(headers,
                    "Destination");
        UriRef targetUri = new UriRef(targetString);
        String overwriteHeader = WebDavUtils.getHeaderAsString(headers, "Overwrite");
        boolean overwriteTarget = "T".equalsIgnoreCase(overwriteHeader);
        if (nodeAtUriExists(targetUri)) {
            if (overwriteTarget) {
                new GraphNode(targetUri, mGraph).deleteNodeContext();
            } else {
                return Response.status(Status.PRECONDITION_FAILED).build();
            }
        }
        Lock l = mGraph.getLock().writeLock();
        l.lock();
        try {
            Iterator<Triple> oldParentTripleIter
                    = mGraph.filter(nodeUri, HIERARCHY.parent, null);
            if (oldParentTripleIter.hasNext()) {
                oldParentTripleIter.next();
                oldParentTripleIter.remove();
            }
            while (oldParentTripleIter.hasNext()) {
                logger.error("more than one parent statement: "+oldParentTripleIter.next());
                oldParentTripleIter.remove();
            }
            Lock writeLock = node.writeLock();
            writeLock.lock();
            try {
                node.replaceWith(targetUri);
            } finally {
                writeLock.unlock();
            }
            new CollectionCreator(mGraph).createContainingCollections(targetUri);
            try {
                return Response.created(new java.net.URI(targetUri.getUnicodeString())).build();
            } catch (URISyntaxException ex) {
                throw new IllegalArgumentException(ex);
            }
        } finally {
            l.unlock();
        }
    }

    /**
     * Deletes a hierarchy node
     *
     * @param uriInfo
     * @return
     * <ul>
     *    <li>200 "OK" response if method succeeded
     *    <li>404 "Not Found" response if the hierarchy node was not found
     * </ul>
     */
    @DELETE
    public Response delete(@Context UriInfo uriInfo) {
        UriRef nodeUri = new UriRef(uriInfo.getAbsolutePath().toString());
        if (!nodeAtUriExists(nodeUri)) {
            return Response.status(Status.NOT_FOUND).entity(
                    uriInfo.getAbsolutePath()).type(MediaType.TEXT_PLAIN).build();
        }
        final LockableMGraph mGraph = cgProvider.getContentGraph();
        GraphNode node = new GraphNode(nodeUri, mGraph);
        node.deleteNodeContext();
        return Response.ok().build();
    }

    /**
     * @param uriInfo
     * @return
     * <ul>
     *    <li>200 "OK" response with an "Allow" and a "DAV" header. The "Allow"
     * header contains all the possible HTTP methods that can be executed on the
     * resource and the "DAV" header shows if the resource is WebDAV enabled
     *    <li>404 "Not Found" response if the resource was not found
     * </ul>
     */
    @OPTIONS
    public Response options(@Context UriInfo uriInfo) {
        final UriRef nodeUri = new UriRef(uriInfo.getAbsolutePath().toString());
        if (!nodeAtUriExists(nodeUri)) {
            return resourceUnavailable(nodeUri, uriInfo);
        }
            Response.ResponseBuilder builder = Response.ok();
            builder.header("Dav", "1");
            Set<String> allow = new HashSet<String>();
            Method[] methods = this.getClass().getMethods();
            for (Method method : methods) {
                for (Annotation annotation : method.getAnnotations()) {
                    HttpMethod httpMethod = annotation.annotationType().getAnnotation(HttpMethod.class);
                    if (httpMethod != null) {
                        allow.add(httpMethod.value());
                    }
                }
            }
            if (allow.isEmpty()) {
                builder.header("Allow", "");
            } else {
                final Iterator<String> iterator = allow.iterator();
                final StringBuffer buffer = new StringBuffer(iterator.next());
                while (iterator.hasNext()) {
                    buffer.append(", ");
                    buffer.append(iterator.next());
                }
                builder.header("Allow", buffer.toString());
            }
        return builder.build();
    }

    protected void bindMetaDataGenerator(MetaDataGenerator generator) {
        metaDataGenerators.add(generator);
    }

    protected void unbindMetaDataGenerator(MetaDataGenerator generator) {
        metaDataGenerators.remove(generator);
    }

    @Override
    protected MGraph getMGraph() {
        return cgProvider.getContentGraph();
    }

    @Override
    protected Set<MetaDataGenerator> getMetaDataGenerators() {
        return metaDataGenerators;
    }

    

    private boolean nodeAtUriExists(UriRef nodeUri) {
        LockableMGraph mGraph = (LockableMGraph) getMGraph();
        Lock readLock = mGraph.getLock().readLock();
        readLock.lock();
        try {
            return mGraph.filter(nodeUri, null, null).hasNext()
                    || mGraph.filter(null, null, nodeUri).hasNext();
        } finally {
            readLock.unlock();
        }
    }

    private Response resourceUnavailable(UriRef nodeUri,
            UriInfo uriInfo) {
        UriRef oppositUri = makeOppositeUriRef(nodeUri);
        if (nodeAtUriExists(oppositUri)) {
            return RedirectUtil.createSeeOtherResponse(
                    oppositUri.getUnicodeString(), uriInfo);
        } else {
            return notFoundPageService.createResponse(uriInfo);
        }
        //return Response.status(Status.NOT_FOUND).build();
    }
    /**
     * add trailing slash if none present, remove otherwise
     *
     * @param uri
     * @return
     */
    private static UriRef makeOppositeUriRef(UriRef uri) {
        String uriString = uri.getUnicodeString();
        if (uriString.endsWith("/")) {
            return new UriRef(uriString.substring(0, uriString.length() - 1));
        } else {
            return new UriRef(uriString + "/");
        }
    }

    private UriRef createAnyHostUri(UriInfo uriInfo) {
        return new UriRef(Constants.ALL_HOSTS_URI_PREFIX+uriInfo.getPath());
    }
}
