/**
 * 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.cxf.jaxrs.microprofile.openapi;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.ws.rs.core.Application;

import org.apache.cxf.Bus;
import org.apache.cxf.annotations.Provider;
import org.apache.cxf.annotations.Provider.Scope;
import org.apache.cxf.annotations.Provider.Type;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.feature.AbstractPortableFeature;
import org.apache.cxf.feature.DelegatingFeature;
import org.apache.cxf.jaxrs.JAXRSServiceFactoryBean;
import org.apache.cxf.jaxrs.common.openapi.DefaultApplicationFactory;
import org.apache.cxf.jaxrs.common.openapi.SwaggerProperties;
import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
import org.apache.cxf.jaxrs.swagger.ui.SwaggerUiConfig;
import org.apache.cxf.jaxrs.swagger.ui.SwaggerUiSupport;
import org.apache.geronimo.microprofile.openapi.config.GeronimoOpenAPIConfig;
import org.apache.geronimo.microprofile.openapi.impl.model.ContactImpl;
import org.apache.geronimo.microprofile.openapi.impl.model.InfoImpl;
import org.apache.geronimo.microprofile.openapi.impl.model.LicenseImpl;
import org.apache.geronimo.microprofile.openapi.impl.model.OpenAPIImpl;
import org.apache.geronimo.microprofile.openapi.impl.processor.AnnotationProcessor;
import org.apache.geronimo.microprofile.openapi.impl.processor.reflect.ClassElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.reflect.MethodElement;
import org.apache.geronimo.microprofile.openapi.impl.processor.spi.NamingStrategy;
import org.eclipse.microprofile.openapi.models.OpenAPI;


@Provider(value = Type.Feature, scope = Scope.Server)
public class OpenApiFeature extends DelegatingFeature<OpenApiFeature.Portable>
        implements SwaggerUiSupport, SwaggerProperties {

    public OpenApiFeature() {
        super(new Portable());
    }

    public boolean isScan() {
        return delegate.isScan();
    }

    public void setScan(boolean scan) {
        delegate.setScan(scan);
    }

    public String getFilterClass() {
        return delegate.getFilterClass();
    }

    public void setFilterClass(String filterClass) {
        delegate.setFilterClass(filterClass);
    }
    
    public Set<String> getResourcePackages() {
        return delegate.getResourcePackages();
    }
    
    public void setResourcePackages(Set<String> resourcePackages) {
        delegate.setResourcePackages(resourcePackages);
    }

    public String getVersion() {
        return delegate.getVersion();
    }

    public void setVersion(String version) {
        delegate.setVersion(version);
    }

    public String getTitle() {
        return delegate.getTitle();
    }

    public void setTitle(String title) {
        delegate.setTitle(title);
    }

    public String getDescription() {
        return delegate.getDescription();
    }

    public void setDescription(String description) {
        delegate.setDescription(description);
    }

    public String getContactName() {
        return delegate.getContactName();
    }

    public void setContactName(String contactName) {
        delegate.setContactName(contactName);
    }

    public String getContactEmail() {
        return delegate.getContactEmail();
    }

    public void setContactEmail(String contactEmail) {
        delegate.setContactEmail(contactEmail);
    }

    public String getContactUrl() {
        return delegate.getContactUrl();
    }

    public void setContactUrl(String contactUrl) {
        delegate.setContactUrl(contactUrl);
    }

    public String getLicense() {
        return delegate.getLicense();
    }

    public void setLicense(String license) {
        delegate.setLicense(license);
    }

    public String getLicenseUrl() {
        return delegate.getLicenseUrl();
    }

    public void setLicenseUrl(String licenseUrl) {
        delegate.setLicenseUrl(licenseUrl);
    }

    public String getTermsOfServiceUrl() {
        return delegate.getTermsOfServiceUrl();
    }

    public void setTermsOfServiceUrl(String termsOfServiceUrl) {
        delegate.setTermsOfServiceUrl(termsOfServiceUrl);
    }

    public boolean isReadAllResources() {
        return delegate.isReadAllResources();
    }

    public void setReadAllResources(boolean readAllResources) {
        delegate.setReadAllResources(readAllResources);
    }

    public Set<String> getResourceClasses() {
        return delegate.getResourceClasses();
    }

    public void setResourceClasses(Set<String> resourceClasses) {
        delegate.setResourceClasses(resourceClasses);
    }

    public Collection<String> getIgnoredRoutes() {
        return delegate.getIgnoredRoutes();
    }

    public void setIgnoredRoutes(Collection<String> ignoredRoutes) {
        delegate.setIgnoredRoutes(ignoredRoutes);
    }

    public boolean isPrettyPrint() {
        return delegate.isPrettyPrint();
    }

    public void setPrettyPrint(boolean prettyPrint) {
        delegate.setPrettyPrint(prettyPrint);
    }
    
    public boolean isRunAsFilter() {
        return delegate.isRunAsFilter();
    }
    
    @Override
    public Boolean isSupportSwaggerUi() {
        return delegate.isSupportSwaggerUi();
    }

    public void setSupportSwaggerUi(Boolean supportSwaggerUi) {
        delegate.setSupportSwaggerUi(supportSwaggerUi);
    }

    public String getSwaggerUiVersion() {
        return delegate.getSwaggerUiVersion();
    }

    public void setSwaggerUiVersion(String swaggerUiVersion) {
        delegate.setSwaggerUiVersion(swaggerUiVersion);
    }

    public String getSwaggerUiMavenGroupAndArtifact() {
        return delegate.getSwaggerUiMavenGroupAndArtifact();
    }

    public void setSwaggerUiMavenGroupAndArtifact(
            String swaggerUiMavenGroupAndArtifact) {
        delegate.setSwaggerUiMavenGroupAndArtifact(swaggerUiMavenGroupAndArtifact);
    }

    @Override
    public Map<String, String> getSwaggerUiMediaTypes() {
        return delegate.getSwaggerUiMediaTypes();
    }

    public void setSwaggerUiMediaTypes(Map<String, String> swaggerUiMediaTypes) {
        delegate.setSwaggerUiMediaTypes(swaggerUiMediaTypes);
    }

    public String getConfigLocation() {
        return delegate.getConfigLocation();
    }

    public void setConfigLocation(String configLocation) {
        delegate.setConfigLocation(configLocation);
    }

    public String getPropertiesLocation() {
        return delegate.getPropertiesLocation();
    }

    public void setPropertiesLocation(String propertiesLocation) {
        delegate.setPropertiesLocation(propertiesLocation);
    }

    public void setRunAsFilter(boolean runAsFilter) {
        delegate.setRunAsFilter(runAsFilter);
    }

    public void setScanKnownConfigLocations(boolean scanKnownConfigLocations) {
        delegate.setScanKnownConfigLocations(scanKnownConfigLocations);
    }
    
    public boolean isScanKnownConfigLocations() {
        return delegate.isScanKnownConfigLocations();
    }
    
    public void setSwaggerUiConfig(final SwaggerUiConfig swaggerUiConfig) {
        delegate.setSwaggerUiConfig(swaggerUiConfig);
    }
    
    @Override
    public SwaggerUiConfig getSwaggerUiConfig() {
        return delegate.getSwaggerUiConfig();
    }

    @Override
    public String findSwaggerUiRoot() {
        return delegate.findSwaggerUiRoot();
    }
    
    protected Properties getUserProperties(final Map<String, Object> userDefinedOptions) {
        return delegate.getUserProperties(userDefinedOptions);
    }

    protected void registerOpenApiResources(
            final JAXRSServiceFactoryBean sfb,
            final OpenAPI openApiDefinition) {

        delegate.registerOpenApiResources(sfb, openApiDefinition);
    }

    protected void registerSwaggerUiResources(JAXRSServiceFactoryBean sfb, Properties properties,
            ServerProviderFactory factory, Bus bus) {
        
        delegate.registerSwaggerUiResources(sfb, properties, factory, bus);
    }

    public static class Portable implements AbstractPortableFeature, SwaggerUiSupport, SwaggerProperties {
        private static final Logger LOG = LogUtils.getL7dLogger(OpenApiFeature.class);

        private String version;
        private String title;
        private String description;
        private String contactName;
        private String contactEmail;
        private String contactUrl;
        private String license;
        private String licenseUrl;
        private String termsOfServiceUrl;
        // Read all operations also with no @Operation
        private boolean readAllResources = true;
        // Scan all JAX-RS resources automatically
        private boolean scan = true;
        private boolean prettyPrint = true;
        private boolean runAsFilter;
        private Collection<String> ignoredRoutes;
        private Set<String> resourcePackages;
        private Set<String> resourceClasses;
        private String filterClass;

        private Boolean supportSwaggerUi;
        private String swaggerUiVersion;
        private String swaggerUiMavenGroupAndArtifact;
        private Map<String, String> swaggerUiMediaTypes;

        // Allows to pass the configuration location, usually openapi-configuration.json
        // or openapi-configuration.yml file.
        private String configLocation;
        // Allows to pass the properties location, by default swagger.properties
        private String propertiesLocation = DEFAULT_PROPS_LOCATION;
        // Allows to disable automatic scan of known configuration locations (enabled by default)
        private boolean scanKnownConfigLocations = true;
        // Swagger UI configuration parameters (to be passed as query string).
        private SwaggerUiConfig swaggerUiConfig;

        @Override
        public void initialize(Server server, Bus bus) {
            final JAXRSServiceFactoryBean sfb = (JAXRSServiceFactoryBean)server
                    .getEndpoint()
                    .get(JAXRSServiceFactoryBean.class.getName());

            final ServerProviderFactory factory = (ServerProviderFactory)server
                    .getEndpoint()
                    .get(ServerProviderFactory.class.getName());

            final Set<String> packages = new HashSet<>();
            if (resourcePackages != null) {
                packages.addAll(resourcePackages);
            }

            final Application application = DefaultApplicationFactory.createApplicationOrDefault(server, factory,
                    sfb, bus, resourcePackages, isScan());

            final AnnotationProcessor processor = new AnnotationProcessor(GeronimoOpenAPIConfig.create(),
                    new NamingStrategy.Http(), null /* default JsonReaderFactory */);

            final OpenAPIImpl api = new OpenAPIImpl();

            if (isScan()) {
                packages.addAll(scanResourcePackages(sfb));
            }

            final Set<Class<?>> resources = new HashSet<>();
            if (application != null) {
                processor.processApplication(api, new ClassElement(application.getClass()));
                LOG.fine("Processed application " + application);

                if (application.getClasses() != null) {
                    resources.addAll(application.getClasses());
                }
            }

            resources.addAll(sfb
                    .getClassResourceInfo()
                    .stream()
                    .map(AbstractResourceInfo::getServiceClass)
                    .filter(cls -> filterByPackage(cls, packages))
                    .filter(cls -> filterByClassName(cls, resourceClasses))
                    .collect(Collectors.toSet()));

            if (!resources.isEmpty()) {
                final String binding = (application == null) ? ""
                        : processor.getApplicationBinding(application.getClass());

                resources
                        .stream()
                        .peek(c -> LOG.info("Processing class " + c.getName()))
                        .forEach(c -> processor.processClass(binding, api, new ClassElement(c),
                                Stream.of(c.getMethods()).map(MethodElement::new)));
            } else {
                LOG.warning("No resource classes registered, the OpenAPI will not contain any endpoints.");
            }

            Properties swaggerProps = getSwaggerProperties(propertiesLocation, bus);
            if (api.getInfo() == null) {
                api.setInfo(getInfo(swaggerProps));
            }

            registerOpenApiResources(sfb, api);
            registerSwaggerUiResources(sfb, swaggerProps, factory, bus);
        }

        public boolean isScan() {
            return scan;
        }

        public void setScan(boolean scan) {
            this.scan = scan;
        }

        public String getFilterClass() {
            return filterClass;
        }

        public void setFilterClass(String filterClass) {
            this.filterClass = filterClass;
        }

        public Set<String> getResourcePackages() {
            return resourcePackages;
        }

        public void setResourcePackages(Set<String> resourcePackages) {
            this.resourcePackages = (resourcePackages == null) ? null : new HashSet<>(resourcePackages);
        }

        public String getVersion() {
            return version;
        }

        public void setVersion(String version) {
            this.version = version;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }

        public String getContactName() {
            return contactName;
        }

        public void setContactName(String contactName) {
            this.contactName = contactName;
        }

        public String getContactEmail() {
            return contactEmail;
        }

        public void setContactEmail(String contactEmail) {
            this.contactEmail = contactEmail;
        }

        public String getContactUrl() {
            return contactUrl;
        }

        public void setContactUrl(String contactUrl) {
            this.contactUrl = contactUrl;
        }

        public String getLicense() {
            return license;
        }

        public void setLicense(String license) {
            this.license = license;
        }

        public String getLicenseUrl() {
            return licenseUrl;
        }

        public void setLicenseUrl(String licenseUrl) {
            this.licenseUrl = licenseUrl;
        }

        public String getTermsOfServiceUrl() {
            return termsOfServiceUrl;
        }

        public void setTermsOfServiceUrl(String termsOfServiceUrl) {
            this.termsOfServiceUrl = termsOfServiceUrl;
        }

        public boolean isReadAllResources() {
            return readAllResources;
        }

        public void setReadAllResources(boolean readAllResources) {
            this.readAllResources = readAllResources;
        }

        public Set<String> getResourceClasses() {
            return resourceClasses;
        }

        public void setResourceClasses(Set<String> resourceClasses) {
            this.resourceClasses = (resourceClasses == null) ? null : new HashSet<>(resourceClasses);
        }

        public Collection<String> getIgnoredRoutes() {
            return ignoredRoutes;
        }

        public void setIgnoredRoutes(Collection<String> ignoredRoutes) {
            this.ignoredRoutes = (ignoredRoutes == null) ? null : new HashSet<>(ignoredRoutes);
        }

        public boolean isPrettyPrint() {
            return prettyPrint;
        }

        public void setPrettyPrint(boolean prettyPrint) {
            this.prettyPrint = prettyPrint;
        }

        public boolean isRunAsFilter() {
            return runAsFilter;
        }

        @Override
        public Boolean isSupportSwaggerUi() {
            return supportSwaggerUi;
        }

        public void setSupportSwaggerUi(Boolean supportSwaggerUi) {
            this.supportSwaggerUi = supportSwaggerUi;
        }

        public String getSwaggerUiVersion() {
            return swaggerUiVersion;
        }

        public void setSwaggerUiVersion(String swaggerUiVersion) {
            this.swaggerUiVersion = swaggerUiVersion;
        }

        public String getSwaggerUiMavenGroupAndArtifact() {
            return swaggerUiMavenGroupAndArtifact;
        }

        public void setSwaggerUiMavenGroupAndArtifact(
                String swaggerUiMavenGroupAndArtifact) {
            this.swaggerUiMavenGroupAndArtifact = swaggerUiMavenGroupAndArtifact;
        }

        @Override
        public Map<String, String> getSwaggerUiMediaTypes() {
            return swaggerUiMediaTypes;
        }

        public void setSwaggerUiMediaTypes(Map<String, String> swaggerUiMediaTypes) {
            this.swaggerUiMediaTypes = swaggerUiMediaTypes;
        }

        public String getConfigLocation() {
            return configLocation;
        }

        public void setConfigLocation(String configLocation) {
            this.configLocation = configLocation;
        }

        public String getPropertiesLocation() {
            return propertiesLocation;
        }

        public void setPropertiesLocation(String propertiesLocation) {
            this.propertiesLocation = propertiesLocation;
        }

        public void setRunAsFilter(boolean runAsFilter) {
            this.runAsFilter = runAsFilter;
        }

        public void setScanKnownConfigLocations(boolean scanKnownConfigLocations) {
            this.scanKnownConfigLocations = scanKnownConfigLocations;
        }

        public boolean isScanKnownConfigLocations() {
            return scanKnownConfigLocations;
        }

        public void setSwaggerUiConfig(final SwaggerUiConfig swaggerUiConfig) {
            this.swaggerUiConfig = swaggerUiConfig;
        }

        @Override
        public SwaggerUiConfig getSwaggerUiConfig() {
            return swaggerUiConfig;
        }

        @Override
        public String findSwaggerUiRoot() {
            return SwaggerUi.findSwaggerUiRoot(swaggerUiMavenGroupAndArtifact, swaggerUiVersion);
        }

        protected Properties getUserProperties(final Map<String, Object> userDefinedOptions) {
            final Properties properties = new Properties();

            if (userDefinedOptions != null) {
                userDefinedOptions
                        .entrySet()
                        .stream()
                        .filter(entry -> entry.getValue() != null)
                        .forEach(entry -> properties.setProperty(entry.getKey(), entry.getValue().toString()));
            }

            return properties;
        }

        protected void registerOpenApiResources(
                final JAXRSServiceFactoryBean sfb,
                final OpenAPI openApiDefinition) {

            sfb.setResourceClassesFromBeans(Collections.singletonList(new OpenApiEndpoint(openApiDefinition)));
        }

        protected void registerSwaggerUiResources(JAXRSServiceFactoryBean sfb, Properties properties,
                                                  ServerProviderFactory factory, Bus bus) {

            final Registration swaggerUiRegistration = getSwaggerUi(bus, properties, isRunAsFilter());

            if (!isRunAsFilter()) {
                sfb.setResourceClassesFromBeans(swaggerUiRegistration.getResources());
            }

            factory.setUserProviders(swaggerUiRegistration.getProviders());
        }

        /**
         * The info will be used only if there is no @OpenAPIDefinition annotation is present.
         */
        private org.eclipse.microprofile.openapi.models.info.Info getInfo(final Properties properties) {
            org.eclipse.microprofile.openapi.models.info.Info info = new InfoImpl()
                    .title(getOrFallback(getTitle(), properties, TITLE_PROPERTY))
                    .version(getOrFallback(getVersion(), properties, VERSION_PROPERTY))
                    .description(getOrFallback(getDescription(), properties, DESCRIPTION_PROPERTY))
                    .termsOfService(getOrFallback(getTermsOfServiceUrl(), properties, TERMS_URL_PROPERTY))
                    .contact(new ContactImpl()
                            .name(getOrFallback(getContactName(), properties, CONTACT_PROPERTY))
                            .email(getContactEmail())
                            .url(getContactUrl()));

            String licenseName = getOrFallback(getLicense(), properties, LICENSE_PROPERTY);
            if (licenseName != null) {
                info = info.license(new LicenseImpl()
                        .name(getOrFallback(getLicense(), properties, LICENSE_PROPERTY))
                        .url(getOrFallback(getLicenseUrl(), properties, LICENSE_URL_PROPERTY)));
            }
            return info;
        }

        private String getOrFallback(String value, Properties properties, String property) {
            if (value == null && properties != null) {
                return properties.getProperty(property);
            } else {
                return value;
            }
        }

        private Collection<String> scanResourcePackages(JAXRSServiceFactoryBean sfb) {
            return sfb
                    .getClassResourceInfo()
                    .stream()
                    .map(cri -> cri.getServiceClass().getPackage().getName())
                    .collect(Collectors.toSet());
        }

        private static boolean filterByPackage(final Class<?> cls, final Set<String> packages) {
            return (packages == null || packages.isEmpty())
                    || packages.stream().anyMatch(pkg -> cls.getPackage().getName().startsWith(pkg));
        }

        private static boolean filterByClassName(final Class<?> cls, final Set<String> classes) {
            return (classes == null || classes.isEmpty())
                    || classes.stream().anyMatch(cls.getName()::equalsIgnoreCase);
        }
    }
}
