//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.websocket.server;

import java.io.IOException;
import java.util.EnumSet;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

/**
 * Inline Servlet Filter to capture WebSocket upgrade requests and perform path mappings to {@link WebSocketCreator} objects.
 */
@ManagedObject("WebSocket Upgrade Filter")
public class WebSocketUpgradeFilter extends AbstractLifeCycle implements Filter, MappedWebSocketCreator, Dumpable
{
    public static final String CONTEXT_ATTRIBUTE_KEY = "contextAttributeKey";
    public static final String CONFIG_ATTRIBUTE_KEY = "configAttributeKey";
    private static final Logger LOG = Log.getLogger(WebSocketUpgradeFilter.class);
    
    public static WebSocketUpgradeFilter configureContext(ServletContextHandler context) throws ServletException
    {
        // Prevent double configure
        WebSocketUpgradeFilter filter = (WebSocketUpgradeFilter) context.getAttribute(WebSocketUpgradeFilter.class.getName());
        if (filter != null)
        {
            return filter;
        }
        
        // Dynamically add filter
        NativeWebSocketConfiguration configuration = NativeWebSocketServletContainerInitializer.getDefaultFrom(context.getServletContext());
        filter = new WebSocketUpgradeFilter(configuration);
        filter.setToAttribute(context, WebSocketUpgradeFilter.class.getName());
        
        String name = "Jetty_WebSocketUpgradeFilter";
        String pathSpec = "/*";
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
        
        FilterHolder fholder = new FilterHolder(filter);
        fholder.setName(name);
        fholder.setAsyncSupported(true);
        fholder.setInitParameter(CONTEXT_ATTRIBUTE_KEY, WebSocketUpgradeFilter.class.getName());
        context.addFilter(fholder, pathSpec, dispatcherTypes);
        
        if (LOG.isDebugEnabled())
        {
            LOG.debug("Adding [{}] {} mapped to {} to {}", name, filter, pathSpec, context);
        }
        
        return filter;
    }
    
    /**
     * @deprecated use {@link #configureContext(ServletContextHandler)} instead
     */
    @Deprecated
    public static WebSocketUpgradeFilter configureContext(ServletContext context) throws ServletException
    {
        ContextHandler handler = ContextHandler.getContextHandler(context);
        
        if (handler == null)
        {
            throw new ServletException("Not running on Jetty, WebSocket support unavailable");
        }
        
        if (!(handler instanceof ServletContextHandler))
        {
            throw new ServletException("Not running in Jetty ServletContextHandler, WebSocket support via " + WebSocketUpgradeFilter.class.getName() + " unavailable");
        }
        
        return configureContext((ServletContextHandler) handler);
    }
    
    private NativeWebSocketConfiguration configuration;
    private boolean alreadySetToAttribute = false;
    
    public WebSocketUpgradeFilter()
    {
        // do nothing
    }
    
    public WebSocketUpgradeFilter(WebSocketPolicy policy, ByteBufferPool bufferPool)
    {
        this(new NativeWebSocketConfiguration(new WebSocketServerFactory(policy, bufferPool)));
    }
    
    public WebSocketUpgradeFilter(NativeWebSocketConfiguration configuration)
    {
        this.configuration = configuration;
    }
    
    @Override
    public void addMapping(PathSpec spec, WebSocketCreator creator)
    {
        configuration.addMapping(spec, creator);
    }
    
    @Deprecated
    @Override
    public void addMapping(org.eclipse.jetty.websocket.server.pathmap.PathSpec spec, WebSocketCreator creator)
    {
        configuration.addMapping(spec, creator);
    }

    @Override
    public void addMapping(String spec, WebSocketCreator creator)
    {
        configuration.addMapping(spec, creator);
    }

    @Override
    public boolean removeMapping(String spec)
    {
        return configuration.removeMapping(spec);
    }

    @Override
    public void destroy()
    {
        try
        {
            alreadySetToAttribute = false;
            configuration.stop();
        }
        catch (Exception e)
        {
            LOG.ignore(e);
        }
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
    {
        if (configuration == null)
        {
            // no configuration, cannot operate
            LOG.debug("WebSocketUpgradeFilter is not operational - missing " + NativeWebSocketConfiguration.class.getName());
            chain.doFilter(request, response);
            return;
        }
        
        WebSocketServletFactory factory = configuration.getFactory();
        
        if (factory == null)
        {
            // no factory, cannot operate
            LOG.debug("WebSocketUpgradeFilter is not operational - no WebSocketServletFactory configured");
            chain.doFilter(request, response);
            return;
        }
        
        try
        {
            HttpServletRequest httpreq = (HttpServletRequest) request;
            HttpServletResponse httpresp = (HttpServletResponse) response;
            
            if (!factory.isUpgradeRequest(httpreq, httpresp))
            {
                // Not an upgrade request, skip it
                chain.doFilter(request, response);
                return;
            }
            
            // Since this is a filter, we need to be smart about determining the target path
            String contextPath = httpreq.getContextPath();
            String target = httpreq.getRequestURI();
            if (target.startsWith(contextPath))
            {
                target = target.substring(contextPath.length());
            }
            
            MappedResource<WebSocketCreator> resource = configuration.getMatch(target);
            if (resource == null)
            {
                // no match.
                chain.doFilter(request, response);
                return;
            }
            
            if (LOG.isDebugEnabled())
            {
                LOG.debug("WebSocket Upgrade detected on {} for endpoint {}", target, resource);
            }
            
            WebSocketCreator creator = resource.getResource();
            
            // Store PathSpec resource mapping as request attribute
            httpreq.setAttribute(PathSpec.class.getName(), resource.getPathSpec());
            
            // We have an upgrade request
            if (factory.acceptWebSocket(creator, httpreq, httpresp))
            {
                // We have a socket instance created
                return;
            }
            
            // If we reach this point, it means we had an incoming request to upgrade
            // but it was either not a proper websocket upgrade, or it was possibly rejected
            // due to incoming request constraints (controlled by WebSocketCreator)
            if (response.isCommitted())
            {
                // not much we can do at this point.
                return;
            }
        }
        catch (ClassCastException e)
        {
            // We are in some kind of funky non-http environment.
            if (LOG.isDebugEnabled())
            {
                LOG.debug("Not a HttpServletRequest, skipping WebSocketUpgradeFilter");
            }
        }
        
        // not an Upgrade request
        chain.doFilter(request, response);
    }
    
    @Override
    public String dump()
    {
        return ContainerLifeCycle.dump(this);
    }
    
    @Override
    public void dump(Appendable out, String indent) throws IOException
    {
        out.append(indent).append(" +- configuration=").append(configuration.toString()).append("\n");
        configuration.dump(out, indent);
    }
    
    public WebSocketServletFactory getFactory()
    {
        return configuration.getFactory();
    }
    
    @ManagedAttribute(value = "configuration", readonly = true)
    public NativeWebSocketConfiguration getConfiguration()
    {
        if (configuration == null)
        {
            throw new IllegalStateException(this.getClass().getName() + " not initialized yet");
        }
        return configuration;
    }
    
    @Override
    public WebSocketCreator getMapping(String target)
    {
        return getConfiguration().getMapping(target);
    }
    
    @Override
    @Deprecated
    public org.eclipse.jetty.websocket.server.pathmap.PathMappings<WebSocketCreator> getMappings()
    {
        throw new IllegalStateException("Access to PathMappings cannot be supported. See alternative API in javadoc for "
                + MappedWebSocketCreator.class.getName());
    }
    
    @Override
    public void init(FilterConfig config) throws ServletException
    {
        try
        {
            String configurationKey = config.getInitParameter(CONFIG_ATTRIBUTE_KEY);
            if (configurationKey == null)
            {
                configurationKey = NativeWebSocketConfiguration.class.getName();
            }
            
            if (configuration == null)
            {
                this.configuration = (NativeWebSocketConfiguration) config.getServletContext().getAttribute(configurationKey);
                if (this.configuration == null)
                {
                    // The NativeWebSocketConfiguration should have arrived from the NativeWebSocketServletContainerInitializer
                    throw new ServletException("Unable to find required instance of " +
                            NativeWebSocketConfiguration.class.getName() + " at ServletContext attribute '" + configurationKey + "'");
                }
            }
            else
            {
                // We have a NativeWebSocketConfiguration already present, make sure it exists on the ServletContext
                if (config.getServletContext().getAttribute(configurationKey) == null)
                {
                    config.getServletContext().setAttribute(configurationKey, this.configuration);
                }
            }
            
            this.configuration.start();
            
            String max = config.getInitParameter("maxIdleTime");
            if (max != null)
            {
                getFactory().getPolicy().setIdleTimeout(Long.parseLong(max));
            }
            
            max = config.getInitParameter("maxTextMessageSize");
            if (max != null)
            {
                getFactory().getPolicy().setMaxTextMessageSize(Integer.parseInt(max));
            }
            
            max = config.getInitParameter("maxBinaryMessageSize");
            if (max != null)
            {
                getFactory().getPolicy().setMaxBinaryMessageSize(Integer.parseInt(max));
            }
            
            max = config.getInitParameter("inputBufferSize");
            if (max != null)
            {
                getFactory().getPolicy().setInputBufferSize(Integer.parseInt(max));
            }
            
            String key = config.getInitParameter(CONTEXT_ATTRIBUTE_KEY);
            if (key == null)
            {
                // assume default
                key = WebSocketUpgradeFilter.class.getName();
            }
            
            // Set instance of this filter to context attribute
            setToAttribute(config.getServletContext(), key);
        }
        catch (ServletException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            throw new ServletException(t);
        }
    }
    
    private void setToAttribute(ServletContextHandler context, String key) throws ServletException
    {
        setToAttribute(context.getServletContext(), key);
    }
    
    public void setToAttribute(ServletContext context, String key) throws ServletException
    {
        if (alreadySetToAttribute)
        {
            return;
        }
        
        if (context.getAttribute(key) != null)
        {
            throw new ServletException(WebSocketUpgradeFilter.class.getName() +
                    " is defined twice for the same context attribute key '" + key
                    + "'.  Make sure you have different init-param '" +
                    CONTEXT_ATTRIBUTE_KEY + "' values set");
        }
        context.setAttribute(key, this);
        
        alreadySetToAttribute = true;
    }
    
    @Override
    public String toString()
    {
        return String.format("%s[configuration=%s]", this.getClass().getSimpleName(), configuration);
    }
}
