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

import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.annotation.Priority;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.UriInfo;

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.util.StringUtils;
import org.apache.cxf.endpoint.Server;
import org.apache.cxf.jaxrs.JAXRSServiceFactoryBean;
import org.apache.cxf.jaxrs.ext.ContextProvider;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.model.ApplicationInfo;
import org.apache.cxf.jaxrs.model.ClassResourceInfo;
import org.apache.cxf.jaxrs.provider.ServerProviderFactory;
import org.apache.cxf.jaxrs.utils.InjectionUtils;
import org.apache.cxf.message.Message;

import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.config.DefaultReaderConfig;
import io.swagger.jaxrs.config.ReaderConfig;
import io.swagger.jaxrs.config.SwaggerContextService;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.models.Swagger;
import io.swagger.models.auth.SecuritySchemeDefinition;

@Provider(value = Type.Feature, scope = Scope.Server)
public class Swagger2Feature extends AbstractSwaggerFeature {
    
    private String host;

    private String[] schemes;

    private boolean prettyPrint;

    private boolean scanAllResources;

    private String ignoreRoutes;
    
    private Swagger2Serializers swagger2Serializers;

    private boolean supportSwaggerUi = true;

    private String swaggerUiVersion;
    
    private String swaggerUiMavenGroupAndArtifact;

    private Map<String, String> swaggerUiMediaTypes;

    private boolean usePathBasedConfig;
    
    private boolean dynamicBasePath;
    
    private Map<String, SecuritySchemeDefinition> securityDefinitions;
    
    private Swagger2Customizer customizer;

    @Override
    protected void calculateDefaultBasePath(Server server) {
        dynamicBasePath = true;
        super.calculateDefaultBasePath(server);
    }
    
    @Override
    protected void addSwaggerResource(Server server, Bus bus) {
        JAXRSServiceFactoryBean sfb =
            (JAXRSServiceFactoryBean) server.getEndpoint().get(JAXRSServiceFactoryBean.class.getName());
        
        ApplicationInfo appInfo = null;
        if (!isScan()) {
            ServerProviderFactory factory = 
                (ServerProviderFactory)server.getEndpoint().get(ServerProviderFactory.class.getName());
            appInfo = factory.getApplicationProvider();
            if (appInfo == null) {
                Set<Class<?>> serviceClasses = new HashSet<>();
                for (ClassResourceInfo cri : sfb.getClassResourceInfo()) {
                    serviceClasses.add(cri.getServiceClass());
                }
                appInfo = new ApplicationInfo(new DefaultApplication(serviceClasses), bus);
                server.getEndpoint().put(Application.class.getName(), appInfo);
            }
        } 
        
        List<Object> swaggerResources = new LinkedList<>();
        ApiListingResource apiListingResource = new Swagger2ApiListingResource(customizer);
        swaggerResources.add(apiListingResource);
        
        List<Object> providers = new ArrayList<>();
        if (runAsFilter) {
            providers.add(new SwaggerContainerRequestFilter(appInfo == null ? null : appInfo.getProvider()));
        }
        
        if (supportSwaggerUi) {
            String swaggerUiRoot = SwaggerUiResolver.findSwaggerUiRoot(swaggerUiMavenGroupAndArtifact, 
                                                                       swaggerUiVersion);
            if (swaggerUiRoot != null) {
                SwaggerUIService swaggerUiService = new SwaggerUIService(swaggerUiRoot, swaggerUiMediaTypes);
                if (!runAsFilter) {
                    swaggerResources.add(swaggerUiService);
                } else {
                    providers.add(new SwaggerUIServiceFilter(swaggerUiService));
                }
                providers.add(new SwaggerUIResourceFilter());
                
                bus.setProperty("swagger.service.ui.available", "true");
            }
        }
        
        sfb.setResourceClassesFromBeans(swaggerResources);
        
        List<ClassResourceInfo> cris = sfb.getClassResourceInfo();
        if (!runAsFilter) {
            for (ClassResourceInfo cri : cris) {
                if (ApiListingResource.class.isAssignableFrom(cri.getResourceClass())) {
                    InjectionUtils.injectContextProxies(cri, apiListingResource);
                }
            }
        }
        if (customizer == null) {
            if (swagger2Serializers == null) {
                swagger2Serializers = new DefaultSwagger2Serializers();
            }
            swagger2Serializers.setClassResourceInfos(cris);
            swagger2Serializers.setDynamicBasePath(dynamicBasePath);
            providers.add(swagger2Serializers);
        } else {
            customizer.setClassResourceInfos(cris);
            customizer.setDynamicBasePath(dynamicBasePath);
        }

        providers.add(new ReaderConfigFilter());
        
        if (usePathBasedConfig) {
            providers.add(new ServletConfigProvider());
        }

        ((ServerProviderFactory) server.getEndpoint().get(
                ServerProviderFactory.class.getName())).setUserProviders(providers);
        BeanConfig beanConfig = appInfo == null 
            ? new BeanConfig() 
            : new ApplicationBeanConfig(appInfo.getProvider());
        beanConfig.setResourcePackage(getResourcePackage());
        beanConfig.setUsePathBasedConfig(isUsePathBasedConfig());
        beanConfig.setVersion(getVersion());
        String basePath = getBasePath();
        beanConfig.setBasePath(basePath);
        beanConfig.setHost(getHost());
        beanConfig.setSchemes(getSchemes());
        beanConfig.setTitle(getTitle());
        beanConfig.setDescription(getDescription());
        beanConfig.setContact(getContact());
        beanConfig.setLicense(getLicense());
        beanConfig.setLicenseUrl(getLicenseUrl());
        beanConfig.setTermsOfServiceUrl(getTermsOfServiceUrl());
        beanConfig.setScan(isScan());
        beanConfig.setPrettyPrint(isPrettyPrint());
        beanConfig.setFilterClass(getFilterClass());
        
        Swagger swagger = beanConfig.getSwagger();
        if (swagger != null && securityDefinitions != null) {
            swagger.setSecurityDefinitions(securityDefinitions);
        }
        
        if (customizer == null) {
            swagger2Serializers.setBeanConfig(beanConfig);
        } else {
            customizer.setBeanConfig(beanConfig);
        }
    }

    public boolean isUsePathBasedConfig() {
        return usePathBasedConfig;
    }
    
    public void setUsePathBasedConfig(boolean usePathBasedConfig) {
        this.usePathBasedConfig = usePathBasedConfig;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String[] getSchemes() {
        return schemes;
    }
    
    public Swagger2Customizer getCustomizer() {
        return customizer;
    }

    public void setCustomizer(Swagger2Customizer customizer) {
        this.customizer = customizer;
    }

    public void setSchemes(String[] schemes) {
        this.schemes = schemes;
    }

    public boolean isPrettyPrint() {
        return prettyPrint;
    }

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

    public boolean isScanAllResources() {
        return scanAllResources;
    }

    public void setScanAllResources(boolean scanAllResources) {
        this.scanAllResources = scanAllResources;
    }

    public String getIgnoreRoutes() {
        return ignoreRoutes;
    }

    public void setIgnoreRoutes(String ignoreRoutes) {
        this.ignoreRoutes = ignoreRoutes;
    }

    public void setSwagger2Serializers(final Swagger2Serializers swagger2Serializers) {
        this.swagger2Serializers = swagger2Serializers;
    }
        
    @Override
    protected void setBasePathByAddress(String address) {
        if (!address.startsWith("/")) {
            // get the path part
            URI u = URI.create(address);
            setBasePath(u.getPath());
            setHost(u.getPort() < 0 ? u.getHost() : u.getHost() + ":" + u.getPort());
        } else {
            setBasePath(address);
        }
    }

    /**
     * Set SwaggerUI Maven group and artifact using the "groupId/artifactId" format. 
     * @param swaggerUiMavenGroupAndArtifact
     */
    public void setSwaggerUiMavenGroupAndArtifact(String swaggerUiMavenGroupAndArtifact) {
        this.swaggerUiMavenGroupAndArtifact = swaggerUiMavenGroupAndArtifact;
    }
    
    public void setSwaggerUiVersion(String swaggerUiVersion) {
        this.swaggerUiVersion = swaggerUiVersion;
    }

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

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

    public void setSecurityDefinitions(Map<String, SecuritySchemeDefinition> securityDefinitions) {
        this.securityDefinitions = securityDefinitions;
    }

    private class ServletConfigProvider implements ContextProvider<ServletConfig> {

        @Override
        public ServletConfig createContext(Message message) {
            final ServletConfig sc = (ServletConfig)message.get("HTTP.CONFIG");
            
            // When deploying into OSGi container, it is possible to use embedded Jetty
            // transport. In this case, the ServletConfig is not available and Swagger 
            // does not take into account certain configuration parameters. To overcome 
            // that, the ServletConfig is synthesized from ServletContext instance.
            if (sc == null) {
                final ServletContext context = (ServletContext)message.get("HTTP.CONTEXT");
                if (context != null) {
                    return new SyntheticServletConfig(context);
                }
            } else if (sc != null && sc.getInitParameter(SwaggerContextService.USE_PATH_BASED_CONFIG) == null) {
                return new DelegatingServletConfig(sc);
            }
            
            return sc;
        }
    }
    
    @PreMatching
    protected static class SwaggerContainerRequestFilter extends ApiListingResource implements ContainerRequestFilter {

        protected static final String APIDOCS_LISTING_PATH_JSON = "swagger.json";
        protected static final String APIDOCS_LISTING_PATH_YAML = "swagger.yaml";

        @Context
        protected MessageContext mc;

        private Application app;
        public SwaggerContainerRequestFilter(Application app) {
            this.app = app;
        }

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            UriInfo ui = mc.getUriInfo();

            Response response = null;
            if (ui.getPath().endsWith(APIDOCS_LISTING_PATH_JSON)) {

                response = getListingJsonResponse(
                        app, mc.getServletContext(), mc.getServletConfig(), mc.getHttpHeaders(), ui);
            } else if (ui.getPath().endsWith(APIDOCS_LISTING_PATH_YAML)) {

                response = getListingYamlResponse(
                        app, mc.getServletContext(), mc.getServletConfig(), mc.getHttpHeaders(), ui);
            }

            if (response != null) {
                requestContext.abortWith(response);
            }
        }
    }

    protected class ReaderConfigFilter implements ContainerRequestFilter {

        @Context
        protected MessageContext mc;

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            ServletContext servletContext = mc.getServletContext();
            if (servletContext != null && servletContext.getAttribute(ReaderConfig.class.getName()) == null) {
                if (mc.getServletConfig() != null
                        && Boolean.valueOf(mc.getServletConfig().getInitParameter("scan.all.resources"))) {
                    addReaderConfig(mc.getServletConfig().getInitParameter("ignore.routes"));
                } else if (isScanAllResources()) {
                    addReaderConfig(getIgnoreRoutes());
                }
            }
        }

        protected void addReaderConfig(String ignoreRoutesParam) {
            DefaultReaderConfig rc = new DefaultReaderConfig();
            rc.setScanAllResources(true);
            if (ignoreRoutesParam != null) {
                Set<String> routes = new LinkedHashSet<>();
                for (String route : StringUtils.split(ignoreRoutesParam, ",")) {
                    routes.add(route.trim());
                }
                rc.setIgnoredRoutes(routes);
            }
            mc.getServletContext().setAttribute(ReaderConfig.class.getName(), rc);
        }
    }
    
    @Path("api-docs")
    public static class SwaggerUIService {
        private static final String FAVICON = "favicon";
        private static final Map<String, String> DEFAULT_MEDIA_TYPES;
        
        static {
            DEFAULT_MEDIA_TYPES = new HashMap<>();
            DEFAULT_MEDIA_TYPES.put("html", "text/html");
            DEFAULT_MEDIA_TYPES.put("png", "image/png");
            DEFAULT_MEDIA_TYPES.put("gif", "image/gif");
            DEFAULT_MEDIA_TYPES.put("css", "text/css");
            DEFAULT_MEDIA_TYPES.put("js", "application/javascript");
            DEFAULT_MEDIA_TYPES.put("eot", "application/vnd.ms-fontobject");
            DEFAULT_MEDIA_TYPES.put("ttf", "application/font-sfnt");
            DEFAULT_MEDIA_TYPES.put("svg", "image/svg+xml");
            DEFAULT_MEDIA_TYPES.put("woff", "application/font-woff");
            DEFAULT_MEDIA_TYPES.put("woff2", "application/font-woff2");
        }

        private final String swaggerUiRoot;

        private final Map<String, String> mediaTypes;

        public SwaggerUIService(String swaggerUiRoot, Map<String, String> mediaTypes) {
            this.swaggerUiRoot = swaggerUiRoot;
            this.mediaTypes = mediaTypes;
        }
        
        @GET
        @Path("{resource:.*}")
        public Response getResource(@Context UriInfo uriInfo, @PathParam("resource") String resourcePath) {
            if (StringUtils.isEmpty(resourcePath) || "/".equals(resourcePath)) {        
                resourcePath = "index.html";
            }
            if (resourcePath.contains(FAVICON)) {        
                return Response.status(404).build();
            }
            if (resourcePath.startsWith("/")) {
                resourcePath = resourcePath.substring(1);
            }
            
            try {
                URL resourceURL = URI.create(swaggerUiRoot + resourcePath).toURL();
                
                String mediaType = null;
                int ind = resourcePath.lastIndexOf('.');
                if (ind != -1 && ind < resourcePath.length()) {
                    String resourceExt = resourcePath.substring(ind + 1);
                    if (mediaTypes != null && mediaTypes.containsKey(resourceExt)) {
                        mediaType = mediaTypes.get(resourceExt);
                    } else {
                        mediaType = DEFAULT_MEDIA_TYPES.get(resourceExt);
                    }
                }
                
                ResponseBuilder rb = Response.ok(resourceURL.openStream());
                if (mediaType != null) {
                    rb.type(mediaType);
                }
                return rb.build();
            } catch (IOException ex) {
                throw new NotFoundException(ex);
            }
        }
        
    }
    @PreMatching
    @Priority(Priorities.USER)
    protected static class SwaggerUIResourceFilter implements ContainerRequestFilter {
        private static final Pattern PATTERN =
            Pattern.compile(
                  ".*[.]js|.*[.]gz|.*[.]map|oauth2*[.]html|.*[.]png|.*[.]css|.*[.]ico|"
                  + "/css/.*|/images/.*|/lib/.*|/fonts/.*"
            );

        @Override
        public void filter(ContainerRequestContext rc) throws IOException {
            if (HttpMethod.GET.equals(rc.getRequest().getMethod())) {
                UriInfo ui = rc.getUriInfo();
                String path = "/" + ui.getPath();
                if (PATTERN.matcher(path).matches()) {
                    rc.setRequestUri(URI.create("api-docs" + path));
                }
            }
        }
    }
    @PreMatching
    @Priority(Priorities.USER + 1)
    protected static class SwaggerUIServiceFilter implements ContainerRequestFilter {
        private SwaggerUIService uiService;
        SwaggerUIServiceFilter(SwaggerUIService uiService) {
            this.uiService = uiService;
        }
        @Override
        public void filter(ContainerRequestContext rc) throws IOException {
            if (HttpMethod.GET.equals(rc.getRequest().getMethod())) {
                UriInfo ui = rc.getUriInfo();
                String path = ui.getPath();
                int uiPathIndex = path.lastIndexOf("api-docs");
                if (uiPathIndex >= 0) {
                    String resourcePath = uiPathIndex + 8 < path.length() 
                        ? path.substring(uiPathIndex + 8) : "";
                    rc.abortWith(uiService.getResource(ui, resourcePath));    
                }
            }
            
        }
    }
    protected static class DefaultApplication extends Application {
        Set<Class<?>> serviceClasses;
        DefaultApplication(Set<Class<?>> serviceClasses) {
            this.serviceClasses = serviceClasses;
        }
        @Override
        public Set<Class<?>> getClasses() {
            return serviceClasses;
        }
    }
}
