package gorillamux

//  +------------------------------------------------------------+
//  | Usage                                                      |
//  +------------------------------------------------------------+
//
//
// package main
//
// import (
// 	"gopkg.in/kataras/iris.v6"
// 	"gopkg.in/kataras/iris.v6/adaptors/gorillamux"
// )
//
// func main() {
// 	app := iris.New()
//
// 	app.Adapt(gorillamux.New()) // Add this line and you're ready.
//
// 	app.Get("/api/users/{userid:[0-9]+}", func(ctx *iris.Context) {
// 		ctx.Writef("User with id: %s", ctx.Param("userid"))
// 	})
//
// 	app.Listen(":8080")
// }

import (
	"net/http"
	"strings"

	"github.com/gorilla/mux"
	"gopkg.in/kataras/iris.v6"
)

const dynamicSymbol = '{'

func staticPath(path string) string {
	i := strings.IndexByte(path, dynamicSymbol)
	if i > -1 {
		return path[0:i]
	}

	return path
}

// Name is the name of the router
//
// See $iris_instance.Config.Other for more.
const Name = "gorillamux"

// New returns a new gorilla mux router which can be plugged inside iris.
// This is magic.
func New() iris.Policies {
	var router *mux.Router

	var logger func(iris.LogMode, string)
	return iris.Policies{
		EventPolicy: iris.EventPolicy{Boot: func(s *iris.Framework) {
			logger = s.Log
			s.Set(iris.OptionOther(iris.RouterNameConfigKey, Name))
		}},
		RouterReversionPolicy: iris.RouterReversionPolicy{
			// path normalization done on iris' side
			StaticPath: staticPath,
			WildcardPath: func(path string, paramName string) string {
				// {param:.*}
				wildcardPart := "{" + paramName + ":.*}"

				if path[len(path)-1] != '/' {
					// if not ending with slash then prepend the slash to the wildcard path part
					wildcardPart = "/" + wildcardPart
				}
				// finally return the path given + the wildcard path part
				return path + wildcardPart
			},
			// 	Note: on gorilla mux the {{ url }} and {{ path}} should give the key and the value, not only the values by order.
			// 	{{ url "nameOfTheRoute" "parameterName" "parameterValue"}}.
			//
			// so: {{ url "providerLink" "facebook"}} should become
			//  {{ url "providerLink" "provider" "facebook"}}
			// 	for a path: "/auth/{provider}" with name 'providerLink'
			URLPath: func(r iris.RouteInfo, args ...string) string {
				if router == nil {
					logger(iris.ProdMode, "gorillamux' reverse routing 'URLPath' should be called after Boot/Listen/Serve")
					return ""
				}

				if r == nil {
					return ""
				}

				if gr := router.Get(r.Name()); gr != nil {
					u, err := gr.URLPath(args...)
					if err != nil {
						logger(iris.DevMode, "error on gorilla mux adaptor's URLPath(reverse routing): "+err.Error())
						return ""
					}
					return u.Path
				}
				return ""
			},
		},
		RouterBuilderPolicy: func(repo iris.RouteRepository, context iris.ContextPool) http.Handler {
			router = mux.NewRouter() // re-set the router here,
			// the RouterBuilderPolicy re-runs on every method change (route "offline/online" states mostly)

			repo.Visit(func(route iris.RouteInfo) {
				registerRoute(route, router, context)
			})

			router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				ctx := context.Acquire(w, r)
				// gorilla mux doesn't supports fire method not allowed like iris
				// so this is my hack to support it:
				if ctx.Framework().Config.FireMethodNotAllowed {
					stopVisitor := false
					repo.Visit(func(route iris.RouteInfo) {
						if stopVisitor {
							return
						}
						// this is not going to work 100% for all routes especially the coblex
						// but this is the best solution, to check via static path, subdomain and cors to find the 'correct' route to
						// compare its method in order to implement the status method not allowed in gorilla mux which doesn't support it
						// and if I edit its internal implementation it will be complicated for new releases to be updated.
						p := staticPath(route.Path())
						if route.Subdomain() == "" || route.Subdomain() == ctx.Subdomain() {
							if p == ctx.Path() {
								// we don't care about this route because it has cors and this method is options
								// or its method is equal with the requests but the router didn't select this route
								// that means that the dynamic path didn't match, so we skip it.
								if (route.HasCors() && ctx.Method() == iris.MethodOptions) || ctx.Method() == route.Method() {
									return
								}

								if ctx.Method() != route.Method() {
									// RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
									// The response MUST include an Allow header containing a list of valid methods for the requested resource.
									ctx.SetHeader("Allow", route.Method())
									ctx.EmitError(iris.StatusMethodNotAllowed)
									stopVisitor = true
									return
								}
							}
						}

					})

				} else {
					// to catch custom 404 not found http errors may registered by user
					ctx.EmitError(iris.StatusNotFound)
				}
				context.Release(ctx)
			})

			return router
		},
	}
}

// so easy:
func registerRoute(route iris.RouteInfo, gorillaRouter *mux.Router, context iris.ContextPool) {

	handler := func(w http.ResponseWriter, r *http.Request) {
		context.Run(w, r, func(ctx *iris.Context) {
			if params := mux.Vars(ctx.Request); len(params) > 0 {
				// set them with ctx.Set in order to be accesible by ctx.Param in the user's handler
				for k, v := range params {
					ctx.Set(k, v)
				}
			}
			// including the global middleware, done handlers too
			ctx.Middleware = route.Middleware()
			ctx.Do()
		})
	}

	// remember, we get a new iris.Route foreach of the HTTP Methods, so this should be work
	methods := []string{route.Method()}
	// if route has cors then we register the route with the "OPTIONS" method too
	if route.HasCors() {
		methods = append(methods, http.MethodOptions)
	}
	gorillaRoute := gorillaRouter.HandleFunc(route.Path(), handler).
		Methods(methods...).
		Name(route.Name())

	subdomain := route.Subdomain()
	if subdomain != "" {
		if subdomain == "*." {
			// it's an iris wildcard subdomain
			// so register it as wildcard on gorilla mux too
			subdomain = "{subdomain}."
		} else {
			// it's a static subdomain (which contains the dot)
		}
		// host = subdomain  + listening host
		gorillaRoute.Host(subdomain + context.Framework().Config.VHost)
	}

	// Author's notes: even if the Method is iris.MethodNone
	// the gorillamux saves the route, so we don't need to use the repo.OnMethodChanged
	// and route.IsOnline() and we don't need the RouteContextLinker, we just serve like request on Offline routes*
}