package org.jboss.resteasy.core.registry;

import org.jboss.resteasy.core.ResourceInvoker;
import org.jboss.resteasy.core.ResourceLocator;
import org.jboss.resteasy.core.ResourceMethod;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.Failure;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.LoggableFailure;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.util.IsHttpMethod;
import org.jboss.resteasy.util.PathHelper;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class RootSegment extends Segment
{
   protected Map<String, SimpleSegment> simpleSegments = new HashMap<String, SimpleSegment>();
   protected Map<String, PathParamSegment> resourceExpressions = new HashMap<String, PathParamSegment>();
   protected List<PathParamSegment> sortedResourceExpressions = new ArrayList<PathParamSegment>();
   protected Map<String, PathParamSegment> locatorExpressions = new HashMap<String, PathParamSegment>();
   protected List<PathParamSegment> sortedLocatorExpressions = new ArrayList<PathParamSegment>();
   protected Map<String, List<ResourceInvoker>> bounded = new LinkedHashMap<String, List<ResourceInvoker>>();

   /**
    * Return a map of paths and what resource methods they are bound to
    *
    * @return
    */
   public Map<String, List<ResourceInvoker>> getBounded()
   {
      return bounded;
   }

   @Override
   protected boolean isEmpty()
   {
      return super.isEmpty() && simpleSegments.size() == 0 && resourceExpressions.size() == 0 && locatorExpressions.size() == 0;
   }

   protected void addPath(String[] segments, int index, ResourceInvoker invoker)
   {
      String segment = segments[index];
      // Regular expressions can have '{' and '}' characters.  Replace them to do match
      String replacedCurlySegment = PathHelper.replaceEnclosedCurlyBraces(segment);
      Matcher withPathParam = PathHelper.URI_PARAM_PATTERN.matcher(replacedCurlySegment);
      if (withPathParam.find())
      {
         String expression = recombineSegments(segments, index);

         if (invoker instanceof ResourceLocator)
         {
            PathParamSegment segmentNode = locatorExpressions.get(expression);
            if (segmentNode != null)
               throw new LoggableFailure("You cannot have 2 locators for same path: " + expression);
            segmentNode = new PathParamSegment(expression);
            segmentNode.locator = (ResourceLocator) invoker;
            locatorExpressions.put(segmentNode.getPathExpression(), segmentNode);
            sortedLocatorExpressions.add(segmentNode);
            Collections.sort(sortedLocatorExpressions);
         }
         else
         {
            PathParamSegment segmentNode = resourceExpressions.get(expression);
            if (segmentNode == null)
            {
               segmentNode = new PathParamSegment(expression);
               resourceExpressions.put(segmentNode.getPathExpression(), segmentNode);
               sortedResourceExpressions.add(segmentNode);
               Collections.sort(sortedResourceExpressions);
            }
            segmentNode.methods.add((ResourceMethod) invoker);
         }
      }
      else
      {
         SimpleSegment segmentNode = simpleSegments.get(segment);
         if (segmentNode == null)
         {
            segmentNode = new SimpleSegment(segment);
            simpleSegments.put(segment, segmentNode);
         }
         if (segments.length > index + 1)
         {
            segmentNode.addPath(segments, index + 1, invoker);
         }
         else
         {
            if (invoker instanceof ResourceLocator)
            {
               segmentNode.locator = (ResourceLocator) invoker;
            }
            else
            {
               segmentNode.methods.add((ResourceMethod) invoker);
            }
         }
      }

   }

   private String recombineSegments(String[] segments, int index)
   {
      String expression = "";
      boolean first = true;
      for (int i = index; i < segments.length; i++)
      {
         if (first)
         {
            first = false;
         }
         else
         {
            expression += "/";
         }
         expression += segments[i];
      }
      return expression;
   }

   protected boolean isLocator(Method method)
   {
      return IsHttpMethod.getHttpMethods(method) == null;
   }


   protected ResourceInvoker removePath(String[] segments, int index, Method method)
   {
      String segment = segments[index];
      Matcher withPathParam = PathHelper.URI_PARAM_PATTERN.matcher(segment);
      if (withPathParam.find())
      {
         String expression = recombineSegments(segments, index);
         if (isLocator(method))
         {
            PathParamSegment rtn = locatorExpressions.remove(expression);
            if (rtn != null)
            {
               sortedLocatorExpressions.remove(rtn);
               return rtn.locator;
            }
            else return null;
         }
         else
         {
            PathParamSegment node = resourceExpressions.get(expression);
            if (node == null) return null;
            Iterator<ResourceMethod> it = node.methods.iterator();
            try
            {
               while (it.hasNext())
               {
                  ResourceMethod invoker = it.next();
                  if (invoker.getMethod().equals(method))
                  {
                     it.remove();
                     return invoker;
                  }

               }
            }
            finally
            {
               if (node.isEmpty())
               {
                  PathParamSegment seg = resourceExpressions.remove(expression);
                  if (seg != null) sortedResourceExpressions.remove(seg);
               }
            }
         }
      }
      else
      {
         SimpleSegment segmentNode = simpleSegments.get(segment);
         if (segmentNode == null)
         {
            return null;
         }
         if (segments.length > index + 1)
         {
            try
            {
               return segmentNode.removePath(segments, index + 1, method);
            }
            finally
            {
               if (segmentNode.isEmpty()) simpleSegments.remove(segment);
            }
         }
         else
         {
            try
            {
               if (isLocator(method))
               {
                  ResourceLocator loc = segmentNode.locator;
                  segmentNode.locator = null;
                  return loc;
               }
               else
               {
                  Iterator<ResourceMethod> it = segmentNode.methods.iterator();
                  while (it.hasNext())
                  {
                     ResourceMethod invoker = it.next();
                     if (invoker.getMethod().equals(method))
                     {
                        it.remove();
                        return invoker;
                     }

                  }
               }
            }
            finally
            {
               if (segmentNode.isEmpty()) simpleSegments.remove(segment);
            }
         }
      }
      return null;

   }


   public void addPath(String path, ResourceInvoker invoker)
   {
      List<ResourceInvoker> list = bounded.get(path);
      if (list == null)
      {
         list = new ArrayList<ResourceInvoker>();
         bounded.put(path, list);
      }
      list.add(invoker);
      if (path.startsWith("/")) path = path.substring(1);

      MultivaluedMapImpl<String, String> pathParamExpr = new MultivaluedMapImpl<String, String>();
      StringBuffer newPath = pullPathParamExpressions(path, pathParamExpr);
      path = newPath.toString();
      String[] segments = path.split("/");

      for (int i = 0; i < segments.length; i++)
      {
         segments[i] = putBackPathParamExpressions(segments[i], pathParamExpr);
      }
      addPath(segments, 0, invoker);
   }

   public ResourceInvoker removePath(String path, Method method)
   {
      List<ResourceInvoker> list = bounded.get(path);
      if (list != null)
      {
         ResourceInvoker removed = null;
         for (ResourceInvoker invoker : list)
         {
            if (invoker instanceof ResourceMethod)
            {
               ResourceMethod rm = (ResourceMethod) invoker;
               if (rm.getMethod().equals(method))
               {
                  removed = rm;
                  break;
               }
            }
            else
            {
               ResourceLocator locator = (ResourceLocator) invoker;
               if (locator.getMethod().equals(method))
               {
                  removed = locator;
                  break;
               }

            }
         }
         list.remove(removed);
         if (list.size() == 0) bounded.remove(path);
      }
      if (path.startsWith("/")) path = path.substring(1);

      MultivaluedMapImpl<String, String> pathParamExpr = new MultivaluedMapImpl<String, String>();
      StringBuffer newPath = pullPathParamExpressions(path, pathParamExpr);
      path = newPath.toString();
      String[] segments = path.split("/");

      for (int i = 0; i < segments.length; i++)
      {
         segments[i] = putBackPathParamExpressions(segments[i], pathParamExpr);
      }
      return removePath(segments, 0, method);
   }

   protected ResourceInvoker matchChildren(HttpRequest request, String path, int start)
   {
      String simpleSegment = null;
      if (start == path.length())
      {
         simpleSegment = "";
      }
      else
      {
         int endOfSegmentIndex = path.indexOf('/', start);
         if (endOfSegmentIndex > -1) simpleSegment = path.substring(start, endOfSegmentIndex);
         else simpleSegment = path.substring(start);
      }

      Failure lastFailure = null;

      SimpleSegment segment = simpleSegments.get(simpleSegment);
      if (segment != null)
      {
         try
         {
            return segment.matchSimple(request, path, start);
         }
         catch (Failure e)
         {
            lastFailure = e;
         }
      }

      for (PathParamSegment pathParamSegment : sortedResourceExpressions)
      {
         try
         {
            return pathParamSegment.matchPattern(request, path, start);
         }
         catch (Failure e)
         {
            // try and propagate matched path that threw non-404 responses, i.e. MethodNotAllowed, etc.
            if (lastFailure == null || lastFailure instanceof NotFoundException) lastFailure = e;
         }
      }
      for (PathParamSegment pathParamSegment : sortedLocatorExpressions)
      {
         try
         {
            return pathParamSegment.matchPattern(request, path, start);
         }
         catch (Failure e)
         {
            // try and propagate matched path that threw non-404 responses, i.e. MethodNotAllowed, etc.
            if (lastFailure == null || lastFailure instanceof NotFoundException) lastFailure = e;
         }
      }
      if (lastFailure != null) throw lastFailure;
      throw new NotFoundException("Could not find resource for relative : " + path + " of full path: " + request.getUri().getRequestUri());
   }


   public ResourceInvoker matchRoot(HttpRequest request)
   {
      int start = 0;
      return matchRoot(request, start);
   }

   public ResourceInvoker matchRoot(HttpRequest request, int start)
   {
      String path = request.getPreprocessedPath();
      if (start < path.length() && path.charAt(start) == '/') start++;
      return matchChildren(request, path, start);
   }


   private static StringBuffer pullPathParamExpressions(String path, MultivaluedMapImpl<String, String> pathParamExpr)
   {
      // Regular expressions can have '{' and '}' characters.  Replace them to do match
      path = PathHelper.replaceEnclosedCurlyBraces(path);

      Matcher matcher = PathHelper.URI_PARAM_WITH_REGEX_PATTERN.matcher(path);
      StringBuffer newPath = new StringBuffer();
      while (matcher.find())
      {
         String name = matcher.group(1);
         String regex = matcher.group(3);
         // Regular expressions can have '{' and '}' characters.  Recover original replacement
         pathParamExpr.add(name, PathHelper.recoverEnclosedCurlyBraces(regex));
         matcher.appendReplacement(newPath, "{$1:x}");
      }
      matcher.appendTail(newPath);
      return newPath;
   }

   private static String putBackPathParamExpressions(String path, MultivaluedMapImpl<String, String> pathParamExpr)
   {
      Matcher matcher = PathHelper.URI_PARAM_WITH_REGEX_PATTERN.matcher(path);
      StringBuffer newPath = new StringBuffer();
      while (matcher.find())
      {
         String name = matcher.group(1);
         String val = pathParamExpr.get(name).remove(0);
         // double encode slashes, so that slashes stay where they are 
         val = val.replace("\\", "\\\\");
         matcher.appendReplacement(newPath, "{$1:" + val + "}");
      }
      matcher.appendTail(newPath);
      return newPath.toString();
   }

}
