/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2014 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.jersey.server.wadl.internal.generators.resourcedoc;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;

import javax.inject.Provider;
import javax.xml.parsers.SAXParserFactory;

import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.wadl.WadlGenerator;
import org.glassfish.jersey.server.wadl.internal.ApplicationDescription;
import org.glassfish.jersey.server.wadl.internal.WadlUtils;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ClassDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.MethodDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ParamDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.RepresentationDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ResourceDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.ResponseDocType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.model.WadlParamType;
import org.glassfish.jersey.server.wadl.internal.generators.resourcedoc.xhtml.Elements;

import com.sun.research.ws.wadl.Application;
import com.sun.research.ws.wadl.Doc;
import com.sun.research.ws.wadl.Method;
import com.sun.research.ws.wadl.Param;
import com.sun.research.ws.wadl.ParamStyle;
import com.sun.research.ws.wadl.Representation;
import com.sun.research.ws.wadl.Request;
import com.sun.research.ws.wadl.Resource;
import com.sun.research.ws.wadl.Resources;
import com.sun.research.ws.wadl.Response;

/**
 * A {@link org.glassfish.jersey.server.wadl.WadlGenerator} implementation that enhances the generated wadl by
 * information read from a resourcedoc (containing javadoc information about resource
 * classes).
 * <p>
 * The resourcedoc information can either be provided via a {@link File} ({@link #setResourceDocFile(File)}) reference or
 * via an {@link InputStream} ({@link #setResourceDocStream(InputStream)}).
 * </p>
 * <p>
 * The {@link File} should be used when using the maven-wadl-plugin for generating wadl offline,
 * the {@link InputStream} should be used when the extended wadl is generated by jersey at runtime, e.g.
 * using the {@link org.glassfish.jersey.server.wadl.config.WadlGeneratorConfig} for configuration.
 * </p>
 *
 * @author Martin Grotzke (martin.grotzke at freiheit.com)
 * @author Miroslav Fuksa (miroslav.fuksa at oracle.com)
 */
public class WadlGeneratorResourceDocSupport implements WadlGenerator {

    private WadlGenerator delegate;
    private File resourceDocFile;
    private InputStream resourceDocStream;
    private ResourceDocAccessor resourceDoc;
    @Context
    private Provider<SAXParserFactory> saxFactoryProvider;


    public WadlGeneratorResourceDocSupport() {
    }

    public WadlGeneratorResourceDocSupport(WadlGenerator wadlGenerator, ResourceDocType resourceDoc) {
        delegate = wadlGenerator;
        this.resourceDoc = new ResourceDocAccessor(resourceDoc);
    }

    public void setWadlGeneratorDelegate(WadlGenerator delegate) {
        this.delegate = delegate;
    }

    /**
     * Set the <code>resourceDocFile</code> to the given file. Invoking this method is only allowed, as long as
     * the <code>resourceDocStream</code> is not set, otherwise an {@link IllegalStateException} will be thrown.
     * @param resourceDocFile the resourcedoc file to set.
     */
    public void setResourceDocFile(File resourceDocFile) {
        if (resourceDocStream != null) {
            throw new IllegalStateException("The resourceDocStream property is already set," +
                    " therefore you cannot set the resourceDocFile property. Only one of both can be set at a time.");
        }
        this.resourceDocFile = resourceDocFile;
    }

    /**
     * Set the <code>resourceDocStream</code> to the given file. Invoking this method is only allowed, as long as
     * the <code>resourceDocFile</code> is not set, otherwise an {@link IllegalStateException} will be thrown.
     * <p>
     * The resourcedoc stream must be closed by the client providing the stream.
     * </p>
     * @param resourceDocStream the resourcedoc stream to set.
     */
    public void setResourceDocStream(InputStream resourceDocStream) {
        if (this.resourceDocStream != null) {
            throw new IllegalStateException("The resourceDocFile property is already set," +
                    " therefore you cannot set the resourceDocStream property. Only one of both can be set at a time.");
        }
        this.resourceDocStream = resourceDocStream;
    }

    public void init() throws Exception {
        if (resourceDocFile == null && resourceDocStream == null) {
            throw new IllegalStateException("Neither the resourceDocFile nor the resourceDocStream" +
                    " is set, one of both is required.");
        }
        delegate.init();

        try (final InputStream inputStream = resourceDocFile != null ? new FileInputStream(resourceDocFile) : resourceDocStream) {
            final ResourceDocType resourceDocType = WadlUtils.unmarshall(inputStream, saxFactoryProvider.get(), ResourceDocType.class);
            resourceDoc = new ResourceDocAccessor(resourceDocType);
        } finally {
            resourceDocFile = null;
        }
    }

    public String getRequiredJaxbContextPath() {
        String name = Elements.class.getName();
        name = name.substring(0, name.lastIndexOf('.'));

        return delegate.getRequiredJaxbContextPath() == null
                ? name
                : delegate.getRequiredJaxbContextPath() + ":" + name;
    }

    /**
     * @return the {@link com.sun.research.ws.wadl.Application} created by the delegate
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createApplication()
     */
    public Application createApplication() {
        return delegate.createApplication();
    }

    /**
     * @param r
     * @param path
     * @return the enhanced {@link com.sun.research.ws.wadl.Resource}
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createResource(org.glassfish.jersey.server.model.Resource, String)
     */
    public Resource createResource(org.glassfish.jersey.server.model.Resource r, String path) {
        final Resource result = delegate.createResource(r, path);
        for (Class<?> resourceClass : r.getHandlerClasses()) {
            final ClassDocType classDoc = resourceDoc.getClassDoc(resourceClass);
            if (classDoc != null && !isEmpty(classDoc.getCommentText())) {
                final Doc doc = new Doc();
                doc.getContent().add(classDoc.getCommentText());
                result.getDoc().add(doc);
            }
        }
        return result;
    }

    /**
     * @param resource
     * @param resourceMethod
     * @return the enhanced {@link com.sun.research.ws.wadl.Method}
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createMethod(org.glassfish.jersey.server.model.Resource,
     * org.glassfish.jersey.server.model.ResourceMethod)
     */
    public Method createMethod(org.glassfish.jersey.server.model.Resource resource,
                               ResourceMethod resourceMethod) {
        final Method result = delegate.createMethod(resource, resourceMethod);
        final java.lang.reflect.Method method = resourceMethod.getInvocable().getDefinitionMethod();
        final MethodDocType methodDoc = resourceDoc.getMethodDoc(method.getDeclaringClass(), method);
        if (methodDoc != null && !isEmpty(methodDoc.getCommentText())) {
            final Doc doc = new Doc();
            doc.getContent().add(methodDoc.getCommentText());
            // doc.getOtherAttributes().put( new QName( "xmlns" ), "http://www.w3.org/1999/xhtml" );
            result.getDoc().add(doc);
        }

        return result;
    }

    /**
     * @param r
     * @param m
     * @param mediaType
     * @return the enhanced {@link com.sun.research.ws.wadl.Representation}
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#
     * createRequestRepresentation(org.glassfish.jersey.server.model.Resource,
     * org.glassfish.jersey.server.model.ResourceMethod, javax.ws.rs.core.MediaType)
     */
    public Representation createRequestRepresentation(org.glassfish.jersey.server.model.Resource r,
                                                      org.glassfish.jersey.server.model.ResourceMethod m,
                                                      MediaType mediaType) {
        final Representation result = delegate.createRequestRepresentation(r, m, mediaType);
        final RepresentationDocType requestRepresentation = resourceDoc.getRequestRepresentation(m.getInvocable()
                        .getDefinitionMethod().getDeclaringClass(),
                m.getInvocable().getDefinitionMethod(), result.getMediaType()
        );
        if (requestRepresentation != null) {
            result.setElement(requestRepresentation.getElement());
            addDocForExample(result.getDoc(), requestRepresentation.getExample());
        }
        return result;
    }

    /**
     * @param r
     * @param m
     * @return the enhanced {@link com.sun.research.ws.wadl.Request}
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createRequest(org.glassfish.jersey.server.model.Resource,
     * org.glassfish.jersey.server.model.ResourceMethod)
     */
    public Request createRequest(org.glassfish.jersey.server.model.Resource r, org.glassfish.jersey.server.model.ResourceMethod
            m) {
        return delegate.createRequest(r, m);
    }

    /**
     * @param r
     * @param m
     * @return the enhanced {@link com.sun.research.ws.wadl.Response}
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createResponses(org.glassfish.jersey.server.model.Resource,
     * org.glassfish.jersey.server.model.ResourceMethod)
     */
    public List<Response> createResponses(org.glassfish.jersey.server.model.Resource r,
                                          org.glassfish.jersey.server.model.ResourceMethod m) {
        final ResponseDocType responseDoc = resourceDoc.getResponse(m.getInvocable().getDefinitionMethod().getDeclaringClass(),
                m.getInvocable().getDefinitionMethod());
        List<Response> responses = new ArrayList<Response>();
        if (responseDoc != null && responseDoc.hasRepresentations()) {
            for (RepresentationDocType representationDoc : responseDoc.getRepresentations()) {
                Response response = new Response();

                final Representation wadlRepresentation = new Representation();
                wadlRepresentation.setElement(representationDoc.getElement());
                wadlRepresentation.setMediaType(representationDoc.getMediaType());
                addDocForExample(wadlRepresentation.getDoc(), representationDoc.getExample());
                addDoc(wadlRepresentation.getDoc(), representationDoc.getDoc());

                response.getStatus().add(representationDoc.getStatus());
                response.getRepresentation().add(wadlRepresentation);

                responses.add(response);
            }

            if (!responseDoc.getWadlParams().isEmpty()) {
                for (WadlParamType wadlParamType : responseDoc.getWadlParams()) {
                    final Param param = new Param();
                    param.setName(wadlParamType.getName());
                    param.setStyle(ParamStyle.fromValue(wadlParamType.getStyle()));
                    param.setType(wadlParamType.getType());
                    addDoc(param.getDoc(), wadlParamType.getDoc());
                    for (Response response : responses) {
                        response.getParam().add(param);
                    }
                }
            }

            if (!isEmpty(responseDoc.getReturnDoc())) {
                for (Response response : responses) {
                    addDoc(response.getDoc(), responseDoc.getReturnDoc());
                }
            }

        } else {
            responses = delegate.createResponses(r, m);
        }

        return responses;
    }

    private void addDocForExample(final List<Doc> docs, final String example) {
        if (!isEmpty(example)) {
            final Doc doc = new Doc();

            final Elements pElement = Elements.el("p")
                    .add(Elements.val("h6", "Example"))
                    .add(Elements.el("pre").add(Elements.val("code", example)));

            doc.getContent().add(pElement);
            docs.add(doc);
        }
    }

    private void addDoc(final List<Doc> docs, final String text) {
        if (!isEmpty(text)) {
            final Doc doc = new Doc();
            doc.getContent().add(text);
            docs.add(doc);
        }
    }

    /**
     * @param r
     * @param m
     * @param p
     * @return the enhanced {@link Param}
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createParam(org.glassfish.jersey.server.model.Resource,
     * org.glassfish.jersey.server.model.ResourceMethod, org.glassfish.jersey.server.model.Parameter)
     */
    public Param createParam(org.glassfish.jersey.server.model.Resource r,
                             org.glassfish.jersey.server.model.ResourceMethod m, Parameter p) {
        final Param result = delegate.createParam(r, m, p);
        if (result != null) {
            final ParamDocType paramDoc = resourceDoc.getParamDoc(m.getInvocable().getDefinitionMethod().getDeclaringClass(),
                    m.getInvocable().getDefinitionMethod(), p);
            if (paramDoc != null && !isEmpty(paramDoc.getCommentText())) {
                final Doc doc = new Doc();
                doc.getContent().add(paramDoc.getCommentText());
                result.getDoc().add(doc);
            }
        }
        return result;
    }

    /**
     * @return the {@link com.sun.research.ws.wadl.Resources} created by the delegate
     * @see org.glassfish.jersey.server.wadl.WadlGenerator#createResources()
     */
    public Resources createResources() {
        return delegate.createResources();
    }

    private boolean isEmpty(String text) {
        return text == null || text.length() == 0 || "".equals(text.trim());
    }

    // ================ methods for post build actions =======================

    @Override
    public ExternalGrammarDefinition createExternalGrammar() {
        return delegate.createExternalGrammar();
    }

    @Override
    public void attachTypes(ApplicationDescription egd) {
        delegate.attachTypes(egd);
    }

}
