package restful

// Copyright 2013 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.

import (
	"net/http"
	"regexp"
	"sort"
	"strings"
	"sync"
)

// CurlyRouter expects Routes with paths that contain zero or more parameters in curly brackets.
type CurlyRouter struct{}

var (
	regexCache            sync.Map // Cache for compiled regex patterns
	pathTokenCacheEnabled = true   // Enable/disable path token regex caching
)

// SetPathTokenCacheEnabled enables or disables path token regex caching for CurlyRouter.
// When disabled, regex patterns will be compiled on every request.
// When enabled (default), compiled regex patterns are cached for better performance.
func SetPathTokenCacheEnabled(enabled bool) {
	pathTokenCacheEnabled = enabled
}

// getCachedRegexp retrieves a compiled regex from the cache if found and valid.
// Returns the regex and true if found and valid, nil and false otherwise.
func getCachedRegexp(cache *sync.Map, pattern string) (*regexp.Regexp, bool) {
	if cached, found := cache.Load(pattern); found {
		if regex, ok := cached.(*regexp.Regexp); ok {
			return regex, true
		}
	}
	return nil, false
}

// SelectRoute is part of the Router interface and returns the best match
// for the WebService and its Route for the given Request.
func (c CurlyRouter) SelectRoute(
	webServices []*WebService,
	httpRequest *http.Request) (selectedService *WebService, selected *Route, err error) {

	requestTokens := tokenizePath(httpRequest.URL.Path)

	detectedService := c.detectWebService(requestTokens, webServices)
	if detectedService == nil {
		if trace {
			traceLogger.Printf("no WebService was found to match URL path:%s\n", httpRequest.URL.Path)
		}
		return nil, nil, NewError(http.StatusNotFound, "404: Page Not Found")
	}
	candidateRoutes := c.selectRoutes(detectedService, requestTokens)
	if len(candidateRoutes) == 0 {
		if trace {
			traceLogger.Printf("no Route in WebService with path %s was found to match URL path:%s\n", detectedService.rootPath, httpRequest.URL.Path)
		}
		return detectedService, nil, NewError(http.StatusNotFound, "404: Page Not Found")
	}
	selectedRoute, err := c.detectRoute(candidateRoutes, httpRequest)
	if selectedRoute == nil {
		return detectedService, nil, err
	}
	return detectedService, selectedRoute, nil
}

// selectRoutes return a collection of Route from a WebService that matches the path tokens from the request.
func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
	candidates := make(sortableCurlyRoutes, 0, 8)
	for _, eachRoute := range ws.routes {
		matches, paramCount, staticCount := c.matchesRouteByPathTokens(eachRoute.pathParts, requestTokens, eachRoute.hasCustomVerb)
		if matches {
			candidates.add(curlyRoute{eachRoute, paramCount, staticCount}) // TODO make sure Routes() return pointers?
		}
	}
	sort.Sort(candidates)
	return candidates
}

// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string, routeHasCustomVerb bool) (matches bool, paramCount int, staticCount int) {
	if len(routeTokens) < len(requestTokens) {
		// proceed in matching only if last routeToken is wildcard
		count := len(routeTokens)
		if count == 0 || !strings.HasSuffix(routeTokens[count-1], "*}") {
			return false, 0, 0
		}
		// proceed
	}
	for i, routeToken := range routeTokens {
		if i == len(requestTokens) {
			// reached end of request path
			return false, 0, 0
		}
		requestToken := requestTokens[i]
		if routeHasCustomVerb && hasCustomVerb(routeToken) {
			if !isMatchCustomVerb(routeToken, requestToken) {
				return false, 0, 0
			}
			staticCount++
			requestToken = removeCustomVerb(requestToken)
			routeToken = removeCustomVerb(routeToken)
		}

		if strings.HasPrefix(routeToken, "{") {
			paramCount++
			if colon := strings.Index(routeToken, ":"); colon != -1 {
				// match by regex
				matchesToken, matchesRemainder := c.regularMatchesPathToken(routeToken, colon, requestToken)
				if !matchesToken {
					return false, 0, 0
				}
				if matchesRemainder {
					break
				}
			}
		} else { // no { prefix
			if requestToken != routeToken {
				return false, 0, 0
			}
			staticCount++
		}
	}
	return true, paramCount, staticCount
}

// regularMatchesPathToken tests whether the regular expression part of routeToken matches the requestToken or all remaining tokens
// format routeToken is {someVar:someExpression}, e.g. {zipcode:[\d][\d][\d][\d][A-Z][A-Z]}
func (c CurlyRouter) regularMatchesPathToken(routeToken string, colon int, requestToken string) (matchesToken bool, matchesRemainder bool) {
	regPart := routeToken[colon+1 : len(routeToken)-1]
	if regPart == "*" {
		if trace {
			traceLogger.Printf("wildcard parameter detected in route token %s that matches %s\n", routeToken, requestToken)
		}
		return true, true
	}

	// Check cache first (if enabled)
	if pathTokenCacheEnabled {
		if regex, found := getCachedRegexp(&regexCache, regPart); found {
			matched := regex.MatchString(requestToken)
			return matched, false
		}
	}

	// Compile the regex
	regex, err := regexp.Compile(regPart)
	if err != nil {
		return false, false
	}

	// Cache the regex (if enabled)
	if pathTokenCacheEnabled {
		regexCache.Store(regPart, regex)
	}

	matched := regex.MatchString(requestToken)
	return matched, false
}

var jsr311Router = RouterJSR311{}

// detectRoute selectes from a list of Route the first match by inspecting both the Accept and Content-Type
// headers of the Request. See also RouterJSR311 in jsr311.go
func (c CurlyRouter) detectRoute(candidateRoutes sortableCurlyRoutes, httpRequest *http.Request) (*Route, error) {
	// tracing is done inside detectRoute
	return jsr311Router.detectRoute(candidateRoutes.routes(), httpRequest)
}

// detectWebService returns the best matching webService given the list of path tokens.
// see also computeWebserviceScore
func (c CurlyRouter) detectWebService(requestTokens []string, webServices []*WebService) *WebService {
	var bestWs *WebService
	score := -1
	for _, eachWS := range webServices {
		matches, eachScore := c.computeWebserviceScore(requestTokens, eachWS.pathExpr.tokens)
		if matches && (eachScore > score) {
			bestWs = eachWS
			score = eachScore
		}
	}
	return bestWs
}

// computeWebserviceScore returns whether tokens match and
// the weighted score of the longest matching consecutive tokens from the beginning.
func (c CurlyRouter) computeWebserviceScore(requestTokens []string, routeTokens []string) (bool, int) {
	if len(routeTokens) > len(requestTokens) {
		return false, 0
	}
	score := 0
	for i := 0; i < len(routeTokens); i++ {
		eachRequestToken := requestTokens[i]
		eachRouteToken := routeTokens[i]
		if len(eachRequestToken) == 0 && len(eachRouteToken) == 0 {
			score++
			continue
		}
		if len(eachRouteToken) > 0 && strings.HasPrefix(eachRouteToken, "{") {
			// no empty match
			if len(eachRequestToken) == 0 {
				return false, score
			}
			score++

			if colon := strings.Index(eachRouteToken, ":"); colon != -1 {
				// match by regex
				matchesToken, _ := c.regularMatchesPathToken(eachRouteToken, colon, eachRequestToken)
				if matchesToken {
					score++ // extra score for regex match
				}
			}
		} else {
			// not a parameter
			if eachRequestToken != eachRouteToken {
				return false, score
			}
			score += (len(routeTokens) - i) * 10 //fuzzy
		}
	}
	return true, score
}
