//
//  ========================================================================
//  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.server.handler;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HandlerContainer;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/* ------------------------------------------------------------ */
/** ContextHandlerCollection.
 *
 * This {@link org.eclipse.jetty.server.handler.HandlerCollection} is creates a
 * {@link org.eclipse.jetty.http.PathMap} to it's contained handlers based
 * on the context path and virtual hosts of any contained {@link org.eclipse.jetty.server.handler.ContextHandler}s.
 * The contexts do not need to be directly contained, only children of the contained handlers.
 * Multiple contexts may have the same context path and they are called in order until one
 * handles the request.
 *
 */
@ManagedObject("Context Handler Collection")
public class ContextHandlerCollection extends HandlerCollection
{
    private static final Logger LOG = Log.getLogger(ContextHandlerCollection.class);

    private final ConcurrentMap<ContextHandler,Handler> _contextBranches = new ConcurrentHashMap<>();
    private volatile Trie<Map.Entry<String,Branch[]>> _pathBranches;
    private Class<? extends ContextHandler> _contextClass = ContextHandler.class;

    /* ------------------------------------------------------------ */
    public ContextHandlerCollection()
    {
        super(true);
    }


    /* ------------------------------------------------------------ */
    /**
     * Remap the context paths.
     */
    @ManagedOperation("update the mapping of context path to context")
    public void mapContexts()
    {
        _contextBranches.clear();
        
        if (getHandlers()==null)
        {
            _pathBranches=new ArrayTernaryTrie<>(false,16);
            return;
        }
        
        // Create map of contextPath to handler Branch
        Map<String,Branch[]> map = new HashMap<>();
        for (Handler handler:getHandlers())
        {
            Branch branch=new Branch(handler);
            for (String contextPath : branch.getContextPaths())
            {
                Branch[] branches=map.get(contextPath);
                map.put(contextPath, ArrayUtil.addToArray(branches, branch, Branch.class));
            }
            
            for (ContextHandler context : branch.getContextHandlers())
                _contextBranches.putIfAbsent(context, branch.getHandler());
        }
        
        // Sort the branches so those with virtual hosts are considered before those without
        for (Map.Entry<String,Branch[]> entry: map.entrySet())
        {
            Branch[] branches=entry.getValue();
            Branch[] sorted=new Branch[branches.length];
            int i=0;
            for (Branch branch:branches)
                if (branch.hasVirtualHost())
                    sorted[i++]=branch;
            for (Branch branch:branches)
                if (!branch.hasVirtualHost())
                    sorted[i++]=branch;
            entry.setValue(sorted);
        }
        
        // Loop until we have a big enough trie to hold all the context paths
        int capacity=512;
        Trie<Map.Entry<String,Branch[]>> trie;
        loop: while(true)
        {
            trie=new ArrayTernaryTrie<>(false,capacity);
            for (Map.Entry<String,Branch[]> entry: map.entrySet())
            {
                if (!trie.put(entry.getKey().substring(1),entry))
                {
                    capacity+=512;
                    continue loop;
                }
            }
            break loop;
        }
            
        
        if (LOG.isDebugEnabled())
        {
            for (String ctx : trie.keySet())
                LOG.debug("{}->{}",ctx,Arrays.asList(trie.get(ctx).getValue()));
        }
        _pathBranches=trie;
    }

    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.jetty.server.server.handler.HandlerCollection#setHandlers(org.eclipse.jetty.server.server.Handler[])
     */
    @Override
    public void setHandlers(Handler[] handlers)
    {
        super.setHandlers(handlers);
        if (isStarted())
            mapContexts();
    }

    /* ------------------------------------------------------------ */
    @Override
    protected void doStart() throws Exception
    {
        mapContexts();
        super.doStart();
    }


    /* ------------------------------------------------------------ */
    /*
     * @see org.eclipse.jetty.server.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
     */
    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
    {
        Handler[] handlers = getHandlers();
        if (handlers==null || handlers.length==0)
            return;

        HttpChannelState async = baseRequest.getHttpChannelState();
        if (async.isAsync())
        {
            ContextHandler context=async.getContextHandler();
            if (context!=null)
            {
                Handler branch = _contextBranches.get(context);
                
                if (branch==null)
                    context.handle(target,baseRequest,request, response);
                else
                    branch.handle(target, baseRequest, request, response);
                return;
            }
        }
        
        // data structure which maps a request to a context; first-best match wins
        // { context path => [ context ] }
        // }
        if (target.startsWith("/"))
        {
            int limit = target.length()-1;

            while (limit>=0)
            {
                // Get best match
                Map.Entry<String,Branch[]> branches = _pathBranches.getBest(target,1,limit);
                
                
                if (branches==null)
                    break;
                
                int l=branches.getKey().length();
                if (l==1 || target.length()==l || target.charAt(l)=='/')
                {
                    for (Branch branch : branches.getValue())
                    {
                        branch.getHandler().handle(target,baseRequest, request, response);
                        if (baseRequest.isHandled())
                            return;
                    }
                }
                
                limit=l-2;
            }
        }
        else
        {
            // This may not work in all circumstances... but then I think it should never be called
            for (int i=0;i<handlers.length;i++)
            {
                handlers[i].handle(target,baseRequest, request, response);
                if ( baseRequest.isHandled())
                    return;
            }
        }
    }

    /* ------------------------------------------------------------ */
    /** Add a context handler.
     * @param contextPath  The context path to add
     * @return the ContextHandler just added
     */
    public ContextHandler addContext(String contextPath,String resourceBase)
    {
        try
        {
            ContextHandler context = _contextClass.newInstance();
            context.setContextPath(contextPath);
            context.setResourceBase(resourceBase);
            addHandler(context);
            return context;
        }
        catch (Exception e)
        {
            LOG.debug(e);
            throw new Error(e);
        }
    }



    /* ------------------------------------------------------------ */
    /**
     * @return The class to use to add new Contexts
     */
    public Class<?> getContextClass()
    {
        return _contextClass;
    }


    /* ------------------------------------------------------------ */
    /**
     * @param contextClass The class to use to add new Contexts
     */
    public void setContextClass(Class<? extends ContextHandler> contextClass)
    {
        if (contextClass ==null || !(ContextHandler.class.isAssignableFrom(contextClass)))
            throw new IllegalArgumentException();
        _contextClass = contextClass;
    }

    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    /* ------------------------------------------------------------ */
    private final static class Branch
    {
        private final Handler _handler;
        private final ContextHandler[] _contexts;
        
        Branch(Handler handler)
        {
            _handler=handler;

            if (handler instanceof ContextHandler)
            {
                _contexts = new ContextHandler[]{(ContextHandler)handler};
            }
            else if (handler instanceof HandlerContainer)
            {
                Handler[] contexts=((HandlerContainer)handler).getChildHandlersByClass(ContextHandler.class);
                _contexts = new ContextHandler[contexts.length];
                System.arraycopy(contexts, 0, _contexts, 0, contexts.length);
            }
            else
                _contexts = new ContextHandler[0];
        }
        
        Set<String> getContextPaths()
        {
            Set<String> set = new HashSet<String>();
            for (ContextHandler context:_contexts)
                set.add(context.getContextPath());
            return set;
        }
        
        boolean hasVirtualHost()
        {
            for (ContextHandler context:_contexts)
                if (context.getVirtualHosts()!=null && context.getVirtualHosts().length>0)
                    return true;
            return false;
        }
        
        ContextHandler[] getContextHandlers()
        {
            return _contexts;
        }
        
        Handler getHandler()
        {
            return _handler;
        }
        
        @Override
        public String toString()
        {
            return String.format("{%s,%s}",_handler,Arrays.asList(_contexts));
        }
    }


}
