/*
 * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.jersey.server.wadl.internal.generators.resourcedoc;

import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;

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

import jakarta.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
 */
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(final WadlGenerator wadlGenerator, final ResourceDocType resourceDoc) {
        delegate = wadlGenerator;
        this.resourceDoc = new ResourceDocAccessor(resourceDoc);
    }

    public void setWadlGeneratorDelegate(final 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(final 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(final 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 ? Files.newInputStream(resourceDocFile.toPath())
                : 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 Jersey resource component for which the WADL reource is to be created.
     * @param path path where the resource is exposed.
     * @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(final org.glassfish.jersey.server.model.Resource r, final String path) {
        final Resource result = delegate.createResource(r, path);
        for (final 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 Jersey resource component.
     * @param resourceMethod resource method.
     * @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(final org.glassfish.jersey.server.model.Resource resource,
                               final 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 Jersey resource component.
     * @param m resource method.
     * @param mediaType media type.
     * @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, jakarta.ws.rs.core.MediaType)
     */
    public Representation createRequestRepresentation(final org.glassfish.jersey.server.model.Resource r,
                                                      final org.glassfish.jersey.server.model.ResourceMethod m,
                                                      final 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 Jersey resource component.
     * @param m resource method.
     * @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(final org.glassfish.jersey.server.model.Resource r,
                                 final org.glassfish.jersey.server.model.ResourceMethod m) {
        return delegate.createRequest(r, m);
    }

    /**
     * @param r Jersey resource component.
     * @param m resource method.
     * @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(final org.glassfish.jersey.server.model.Resource r,
                                          final 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 (final RepresentationDocType representationDoc : responseDoc.getRepresentations()) {
                final 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 (final 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 (final Response response : responses) {
                        response.getParam().add(param);
                    }
                }
            }

            if (!isEmpty(responseDoc.getReturnDoc())) {
                for (final 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 Jersey resource component.
     * @param m resource method.
     * @param p method parameter.
     * @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(final org.glassfish.jersey.server.model.Resource r,
                             final org.glassfish.jersey.server.model.ResourceMethod m, final 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(final String text) {
        return text == null || text.isEmpty() || "".equals(text.trim());
    }

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

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

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

}
