//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  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.server;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Locale;

import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.server.handler.ContextHandler.ContextScopeListener;
import org.eclipse.jetty.util.DateCache;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;


/** A Context Listener that produces additional debug.
 * This listener if added to a ContextHandler, will produce additional debug information to
 * either/or a specific log stream or the standard debug log.
 * The events produced by {@link ServletContextListener}, {@link ServletRequestListener}, 
 * {@link AsyncListener} and {@link ContextScopeListener} are logged.
 */
@ManagedObject("Debug Listener")
public class DebugListener extends AbstractLifeCycle implements ServletContextListener
{
    private static final Logger LOG = Log.getLogger(DebugListener.class);
    private static final DateCache __date=new DateCache("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
    
    private final String _attr = String.format("__R%s@%x",this.getClass().getSimpleName(),System.identityHashCode(this));

    private final PrintStream _out;
    private boolean _renameThread;
    private boolean _showHeaders;
    private boolean _dumpContext;

    public DebugListener()
    {
        this(null,false,false,false);
    }
    
    public DebugListener(@Name("renameThread") boolean renameThread, @Name("showHeaders") boolean showHeaders, @Name("dumpContext") boolean dumpContext)
    {
        this(null,renameThread,showHeaders,dumpContext);
    }
    
    public DebugListener(@Name("outputStream") OutputStream out, @Name("renameThread") boolean renameThread, @Name("showHeaders") boolean showHeaders, @Name("dumpContext") boolean dumpContext)
    {
        _out=out==null?null:new PrintStream(out);
        _renameThread=renameThread;
        _showHeaders=showHeaders;
        _dumpContext=dumpContext;
    }
    
    @ManagedAttribute("Rename thread within context scope")
    public boolean isRenameThread()
    {
        return _renameThread;
    }

    public void setRenameThread(boolean renameThread)
    {
        _renameThread = renameThread;
    }

    @ManagedAttribute("Show request headers")
    public boolean isShowHeaders()
    {
        return _showHeaders;
    }

    public void setShowHeaders(boolean showHeaders)
    {
        _showHeaders = showHeaders;
    }

    @ManagedAttribute("Dump contexts at start")
    public boolean isDumpContext()
    {
        return _dumpContext;
    }

    public void setDumpContext(boolean dumpContext)
    {
        _dumpContext = dumpContext;
    }

    @Override
    public void contextInitialized(ServletContextEvent sce)
    {
        sce.getServletContext().addListener(_servletRequestListener);        
        ContextHandler handler =  ContextHandler.getContextHandler(sce.getServletContext());
        handler.addEventListener(_contextScopeListener);
        String cname=findContextName(sce.getServletContext());
        log("^  ctx=%s %s",cname,sce.getServletContext());
        if (_dumpContext)
        {
            if (_out==null)
            {
                handler.dumpStdErr();
                System.err.println(Dumpable.KEY);
            }
            else
            {
                try
                {
                    handler.dump(_out);
                    _out.println(Dumpable.KEY);
                }
                catch(Exception e)
                {
                    LOG.warn(e);
                }
            }
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce)
    {
        String cname=findContextName(sce.getServletContext());
        log("v  ctx=%s %s",cname,sce.getServletContext());
    }
    
    protected String findContextName(ServletContext context)
    {
        if (context==null)
            return null;
        String n = (String)context.getAttribute(_attr);
        if (n==null)
        {
            n=String.format("%s@%x",context.getContextPath(),context.hashCode());
            context.setAttribute(_attr,n);
        }
        return n;
    }

    protected String findRequestName(ServletRequest request)
    {
        if (request==null)
            return null;
        HttpServletRequest r = (HttpServletRequest)request;
        String n = (String)request.getAttribute(_attr);
        if (n==null)
        {
            n=String.format("%s@%x",r.getRequestURI(),request.hashCode());
            request.setAttribute(_attr,n);
        }
        return n;
    }
    
    protected void log(String format, Object... arg)
    {
        if (!isRunning())
            return;
        
        String s=String.format(format,arg);
        
        long now = System.currentTimeMillis();
        long ms = now%1000;
        if (_out!=null)
            _out.printf("%s.%03d:%s%n",__date.formatNow(now),ms,s);
        if (LOG.isDebugEnabled())
            LOG.info(s);
    }
    
    final AsyncListener _asyncListener = new AsyncListener()
    { 
        @Override
        public void onTimeout(AsyncEvent event) throws IOException
        {
            String cname=findContextName(((AsyncContextEvent)event).getServletContext());
            String rname=findRequestName(event.getAsyncContext().getRequest());
            log("!  ctx=%s r=%s onTimeout %s",cname,rname,((AsyncContextEvent)event).getHttpChannelState());
        }
        
        @Override
        public void onStartAsync(AsyncEvent event) throws IOException
        {
            String cname=findContextName(((AsyncContextEvent)event).getServletContext());
            String rname=findRequestName(event.getAsyncContext().getRequest());
            log("!  ctx=%s r=%s onStartAsync %s",cname,rname,((AsyncContextEvent)event).getHttpChannelState());
        }
        
        @Override
        public void onError(AsyncEvent event) throws IOException
        {
            String cname=findContextName(((AsyncContextEvent)event).getServletContext());
            String rname=findRequestName(event.getAsyncContext().getRequest());
            log("!! ctx=%s r=%s onError %s %s",cname,rname,event.getThrowable(),((AsyncContextEvent)event).getHttpChannelState());
        }
        
        @Override
        public void onComplete(AsyncEvent event) throws IOException
        {
            AsyncContextEvent ace=(AsyncContextEvent)event;
            String cname=findContextName(ace.getServletContext());
            String rname=findRequestName(ace.getAsyncContext().getRequest());
            
            Request br=Request.getBaseRequest(ace.getAsyncContext().getRequest());
            Response response = br.getResponse();
            String headers=_showHeaders?("\n"+response.getHttpFields().toString()):"";
            
            log("!  ctx=%s r=%s onComplete %s %d%s",cname,rname,ace.getHttpChannelState(),response.getStatus(),headers);
        }
    };
    
    final ServletRequestListener _servletRequestListener = new ServletRequestListener()
    {
        @Override
        public void requestInitialized(ServletRequestEvent sre)
        {
            String cname=findContextName(sre.getServletContext());
            HttpServletRequest r = (HttpServletRequest)sre.getServletRequest();
           
            String rname=findRequestName(r);
            DispatcherType d = r.getDispatcherType();
            if (d==DispatcherType.REQUEST)
            {
                Request br=Request.getBaseRequest(r);

                String headers=_showHeaders?("\n"+br.getMetaData().getFields().toString()):"";
                
                
                StringBuffer url=r.getRequestURL();
                if (r.getQueryString()!=null)
                    url.append('?').append(r.getQueryString());
                log(">> %s ctx=%s r=%s %s %s %s %s %s%s",d,
                        cname,
                        rname,
                        d,
                        r.getMethod(),
                        url.toString(),
                        r.getProtocol(),
                        br.getHttpChannel(),
                        headers);
            }
            else
                log(">> %s ctx=%s r=%s",d,cname,rname);
        }
        
        @Override
        public void requestDestroyed(ServletRequestEvent sre)
        {
            String cname=findContextName(sre.getServletContext());
            HttpServletRequest r = (HttpServletRequest)sre.getServletRequest();
            String rname=findRequestName(r);
            DispatcherType d = r.getDispatcherType();
            if (sre.getServletRequest().isAsyncStarted())
            {
                sre.getServletRequest().getAsyncContext().addListener(_asyncListener);
                log("<< %s ctx=%s r=%s async=true",d,cname,rname);
            }
            else
            {
                Request br=Request.getBaseRequest(r);
                String headers=_showHeaders?("\n"+br.getResponse().getHttpFields().toString()):"";
                log("<< %s ctx=%s r=%s async=false %d%s",d,cname,rname,Request.getBaseRequest(r).getResponse().getStatus(),headers);
            }
        }
    };
    
    final ContextHandler.ContextScopeListener _contextScopeListener = new ContextHandler.ContextScopeListener()
    {
        @Override
        public void enterScope(Context context, Request request, Object reason)
        {
            String cname=findContextName(context);
            if (request==null)
                log(">  ctx=%s %s",cname,reason);
            else
            {
                String rname=findRequestName(request);

                if (_renameThread)
                {
                    Thread thread=Thread.currentThread();
                    thread.setName(String.format("%s#%s",thread.getName(),rname));
                }
            
                log(">  ctx=%s r=%s %s",cname,rname,reason);
            }
        }
        

        @Override
        public void exitScope(Context context, Request request)
        {
            String cname=findContextName(context);
            if (request==null)
                log("<  ctx=%s",cname);
            else
            {
                String rname=findRequestName(request);

                log("<  ctx=%s r=%s",cname,rname);
                if (_renameThread)
                {
                    Thread thread=Thread.currentThread();
                    if (thread.getName().endsWith(rname))
                        thread.setName(thread.getName().substring(0,thread.getName().length()-rname.length()-1));
                }
            }
        }   
    };
}
