package ginext

import (
	"github.com/gin-gonic/gin"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"net/http"
	"path"
	"reflect"
	"runtime"
	"strings"
)

var anyMethods = []string{
	http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
	http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
	http.MethodTrace,
}

type GinRoutesWrapper struct {
	wrapper        *GinWrapper
	routes         gin.IRouter
	absPath        string
	defaultHandler []gin.HandlerFunc
}

type GinRouteBuilder struct {
	routes *GinRoutesWrapper

	method   string
	relPath  string
	absPath  string
	handlers []gin.HandlerFunc
}

func (w *GinWrapper) Routes() *GinRoutesWrapper {
	return &GinRoutesWrapper{
		wrapper:        w,
		routes:         w.engine,
		absPath:        "",
		defaultHandler: make([]gin.HandlerFunc, 0),
	}
}

func (w *GinRoutesWrapper) Group(relativePath string) *GinRoutesWrapper {
	return &GinRoutesWrapper{
		wrapper:        w.wrapper,
		routes:         w.routes.Group(relativePath),
		defaultHandler: langext.ArrCopy(w.defaultHandler),
		absPath:        joinPaths(w.absPath, relativePath),
	}
}

func (w *GinRoutesWrapper) Use(middleware ...gin.HandlerFunc) *GinRoutesWrapper {
	defHandler := langext.ArrCopy(w.defaultHandler)
	defHandler = append(defHandler, middleware...)
	return &GinRoutesWrapper{wrapper: w.wrapper, routes: w.routes, defaultHandler: defHandler, absPath: w.absPath}
}

func (w *GinRoutesWrapper) WithJSONFilter(filter string) *GinRoutesWrapper {
	return w.Use(func(g *gin.Context) { g.Set(jsonFilterKey, filter) })
}

func (w *GinRoutesWrapper) GET(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodGet, relativePath)
}

func (w *GinRoutesWrapper) POST(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodPost, relativePath)
}

func (w *GinRoutesWrapper) DELETE(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodDelete, relativePath)
}

func (w *GinRoutesWrapper) PATCH(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodPatch, relativePath)
}

func (w *GinRoutesWrapper) PUT(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodPut, relativePath)
}

func (w *GinRoutesWrapper) OPTIONS(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodOptions, relativePath)
}

func (w *GinRoutesWrapper) HEAD(relativePath string) *GinRouteBuilder {
	return w._route(http.MethodHead, relativePath)
}

func (w *GinRoutesWrapper) COUNT(relativePath string) *GinRouteBuilder {
	return w._route("COUNT", relativePath)
}

func (w *GinRoutesWrapper) Any(relativePath string) *GinRouteBuilder {
	return w._route("*", relativePath)
}

func (w *GinRoutesWrapper) _route(method string, relativePath string) *GinRouteBuilder {
	return &GinRouteBuilder{
		routes:   w,
		method:   method,
		relPath:  relativePath,
		absPath:  joinPaths(w.absPath, relativePath),
		handlers: langext.ArrCopy(w.defaultHandler),
	}
}

func (w *GinRouteBuilder) Use(middleware ...gin.HandlerFunc) *GinRouteBuilder {
	w.handlers = append(w.handlers, middleware...)
	return w
}

func (w *GinRouteBuilder) WithJSONFilter(filter string) *GinRouteBuilder {
	return w.Use(func(g *gin.Context) { g.Set(jsonFilterKey, filter) })
}

func (w *GinRouteBuilder) Handle(handler WHandlerFunc) {

	if w.routes.wrapper.bufferBody {
		arr := make([]gin.HandlerFunc, 0, len(w.handlers)+1)
		arr = append(arr, BodyBuffer)
		arr = append(arr, w.handlers...)
		w.handlers = arr
	}

	middlewareNames := langext.ArrMap(w.handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
	handlerName := nameOfFunction(handler)

	w.handlers = append(w.handlers, Wrap(w.routes.wrapper, handler))

	methodName := w.method

	if w.method == "*" {
		methodName = "ANY"
		for _, method := range anyMethods {
			w.routes.routes.Handle(method, w.relPath, w.handlers...)
		}
	} else {
		w.routes.routes.Handle(w.method, w.relPath, w.handlers...)
	}

	w.routes.wrapper.routeSpecs = append(w.routes.wrapper.routeSpecs, ginRouteSpec{
		Method:      methodName,
		URL:         w.absPath,
		Middlewares: middlewareNames,
		Handler:     handlerName,
	})
}

func (w *GinWrapper) NoRoute(handler WHandlerFunc) {

	handlers := make([]gin.HandlerFunc, 0)

	if w.bufferBody {
		handlers = append(handlers, BodyBuffer)
	}

	middlewareNames := langext.ArrMap(handlers, func(v gin.HandlerFunc) string { return nameOfFunction(v) })
	handlerName := nameOfFunction(handler)

	handlers = append(handlers, Wrap(w, handler))

	w.engine.NoRoute(handlers...)

	w.routeSpecs = append(w.routeSpecs, ginRouteSpec{
		Method:      "ANY",
		URL:         "[NO_ROUTE]",
		Middlewares: middlewareNames,
		Handler:     handlerName,
	})
}

func nameOfFunction(f any) string {

	fname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()

	split := strings.Split(fname, "/")
	if len(split) == 0 {
		return ""
	}

	fname = split[len(split)-1]

	// https://stackoverflow.com/a/32925345/1761622
	if strings.HasSuffix(fname, "-fm") {
		fname = fname[:len(fname)-len("-fm")]
	}

	return fname
}

// joinPaths is copied verbatim from gin@v1.9.1/gin.go
func joinPaths(absolutePath, relativePath string) string {
	if relativePath == "" {
		return absolutePath
	}

	finalPath := path.Join(absolutePath, relativePath)
	if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
		return finalPath + "/"
	}
	return finalPath
}

func lastChar(str string) uint8 {
	if str == "" {
		panic("The length of the string can't be 0")
	}
	return str[len(str)-1]
}