/*
 * Copyright 2021 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package descriptor

import (
	"fmt"
	"sync"
)

// Router http router for bam annotations
type Router interface {
	// Handle register Route to Router
	Handle(rt Route)
	// Lookup FunctionDescriptor from HTTPRequest
	Lookup(req *HTTPRequest) (*FunctionDescriptor, error)
}

type router struct {
	trees      map[string]*node
	maxParams  uint16
	paramsPool sync.Pool
}

// NewRouter ...
func NewRouter() Router {
	return &router{}
}

func (r *router) getParams() *Params {
	ps, _ := r.paramsPool.Get().(*Params)
	ps.params = ps.params[0:0] // reset slice
	return ps
}

func (r *router) putParams(ps *Params) {
	if ps != nil {
		r.paramsPool.Put(ps)
	}
}

func (r *router) Handle(rt Route) {
	method := rt.Method()
	path := rt.Path()
	function := rt.Function()
	if method == "" {
		panic("method must not be empty")
	}
	if len(path) < 1 || path[0] != '/' {
		panic("path must begin with '/' in path '" + path + "'")
	}
	if function == nil {
		panic("function descriptor must not be nil")
	}

	if r.trees == nil {
		r.trees = make(map[string]*node)
	}

	root := r.trees[method]
	if root == nil {
		root = new(node)
		r.trees[method] = root
	}

	root.addRoute(path, function)

	if paramsCount := countParams(path); paramsCount > r.maxParams {
		r.maxParams = paramsCount
	}

	// Lazy-init paramsPool alloc func
	if r.paramsPool.New == nil && r.maxParams > 0 {
		r.paramsPool.New = func() interface{} {
			ps := Params{
				params:  make([]Param, 0, r.maxParams),
				recycle: r.putParams,
			}
			return &ps
		}
	}
}

func (r *router) Lookup(req *HTTPRequest) (*FunctionDescriptor, error) {
	root, ok := r.trees[req.GetMethod()]
	if !ok {
		return nil, fmt.Errorf("function lookup failed, no root with method=%s", req.GetMethod())
	}
	fn, ps, _ := root.getValue(req.GetPath(), r.getParams, false)
	if fn == nil {
		r.putParams(ps)
		return nil, fmt.Errorf("function lookup failed, path=%s", req.GetPath())
	}
	req.Params = ps
	return fn, nil
}
