// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module vweb

import os
import io
import runtime
import net
import net.http
import net.urllib
import time
import json
import encoding.html
import context
import strings

// A type which don't get filtered inside templates
pub type RawHtml = string

// A dummy structure that returns from routes to indicate that you actually sent something to a user
[noinit]
pub struct Result {}

pub const (
	methods_with_form = [http.Method.post, .put, .patch]
	headers_close     = http.new_custom_header_from_map({
		'Server':                           'VWeb'
		http.CommonHeader.connection.str(): 'close'
	}) or { panic('should never fail') }

	http_302          = http.new_response(
		status: .found
		body: '302 Found'
		header: headers_close
	)
	http_400          = http.new_response(
		status: .bad_request
		body: '400 Bad Request'
		header: http.new_header(
			key: .content_type
			value: 'text/plain'
		).join(headers_close)
	)
	http_404          = http.new_response(
		status: .not_found
		body: '404 Not Found'
		header: http.new_header(
			key: .content_type
			value: 'text/plain'
		).join(headers_close)
	)
	http_500          = http.new_response(
		status: .internal_server_error
		body: '500 Internal Server Error'
		header: http.new_header(
			key: .content_type
			value: 'text/plain'
		).join(headers_close)
	)
	mime_types        = {
		'.aac':    'audio/aac'
		'.abw':    'application/x-abiword'
		'.arc':    'application/x-freearc'
		'.avi':    'video/x-msvideo'
		'.azw':    'application/vnd.amazon.ebook'
		'.bin':    'application/octet-stream'
		'.bmp':    'image/bmp'
		'.bz':     'application/x-bzip'
		'.bz2':    'application/x-bzip2'
		'.cda':    'application/x-cdf'
		'.csh':    'application/x-csh'
		'.css':    'text/css'
		'.csv':    'text/csv'
		'.doc':    'application/msword'
		'.docx':   'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
		'.eot':    'application/vnd.ms-fontobject'
		'.epub':   'application/epub+zip'
		'.gz':     'application/gzip'
		'.gif':    'image/gif'
		'.htm':    'text/html'
		'.html':   'text/html'
		'.ico':    'image/vnd.microsoft.icon'
		'.ics':    'text/calendar'
		'.jar':    'application/java-archive'
		'.jpeg':   'image/jpeg'
		'.jpg':    'image/jpeg'
		'.js':     'text/javascript'
		'.json':   'application/json'
		'.jsonld': 'application/ld+json'
		'.md':     'text/markdown'
		'.mid':    'audio/midi audio/x-midi'
		'.midi':   'audio/midi audio/x-midi'
		'.mjs':    'text/javascript'
		'.mp3':    'audio/mpeg'
		'.mp4':    'video/mp4'
		'.mpeg':   'video/mpeg'
		'.mpkg':   'application/vnd.apple.installer+xml'
		'.odp':    'application/vnd.oasis.opendocument.presentation'
		'.ods':    'application/vnd.oasis.opendocument.spreadsheet'
		'.odt':    'application/vnd.oasis.opendocument.text'
		'.oga':    'audio/ogg'
		'.ogv':    'video/ogg'
		'.ogx':    'application/ogg'
		'.opus':   'audio/opus'
		'.otf':    'font/otf'
		'.png':    'image/png'
		'.pdf':    'application/pdf'
		'.php':    'application/x-httpd-php'
		'.ppt':    'application/vnd.ms-powerpoint'
		'.pptx':   'application/vnd.openxmlformats-officedocument.presentationml.presentation'
		'.rar':    'application/vnd.rar'
		'.rtf':    'application/rtf'
		'.sh':     'application/x-sh'
		'.svg':    'image/svg+xml'
		'.swf':    'application/x-shockwave-flash'
		'.tar':    'application/x-tar'
		'.tif':    'image/tiff'
		'.tiff':   'image/tiff'
		'.ts':     'video/mp2t'
		'.ttf':    'font/ttf'
		'.txt':    'text/plain'
		'.vsd':    'application/vnd.visio'
		'.wasm':   'application/wasm'
		'.wav':    'audio/wav'
		'.weba':   'audio/webm'
		'.webm':   'video/webm'
		'.webp':   'image/webp'
		'.woff':   'font/woff'
		'.woff2':  'font/woff2'
		'.xhtml':  'application/xhtml+xml'
		'.xls':    'application/vnd.ms-excel'
		'.xlsx':   'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
		'.xml':    'application/xml'
		'.xul':    'application/vnd.mozilla.xul+xml'
		'.zip':    'application/zip'
		'.3gp':    'video/3gpp'
		'.3g2':    'video/3gpp2'
		'.7z':     'application/x-7z-compressed'
	}
	max_http_post_size = 1024 * 1024
	default_port       = 8080
)

// The Context struct represents the Context which hold the HTTP request and response.
// It has fields for the query, form, files.
pub struct Context {
mut:
	content_type string = 'text/plain'
	status       string = '200 OK'
	ctx          context.Context = context.EmptyContext{}
pub:
	// HTTP Request
	req http.Request
	// TODO Response
pub mut:
	done bool
	// time.ticks() from start of vweb connection handle.
	// You can use it to determine how much time is spent on your request.
	page_gen_start i64
	// TCP connection to client.
	// But beware, do not store it for further use, after request processing vweb will close connection.
	conn              &net.TcpConn = unsafe { nil }
	static_files      map[string]string
	static_mime_types map[string]string
	static_hosts      map[string]string
	// Map containing query params for the route.
	// http://localhost:3000/index?q=vpm&order_by=desc => { 'q': 'vpm', 'order_by': 'desc' }
	query map[string]string
	// Multipart-form fields.
	form map[string]string
	// Files from multipart-form.
	files map[string][]http.FileData

	header http.Header // response headers
	// ? It doesn't seem to be used anywhere
	form_error                  string
	livereload_poll_interval_ms int = 250
}

struct FileData {
pub:
	filename     string
	content_type string
	data         string
}

struct Route {
	methods    []http.Method
	path       string
	path_words []string // precalculated once to avoid split() allocations in handle_conn()
	middleware string
	host       string
}

// Defining this method is optional.
// This method called at server start.
// You can use it for initializing globals.
pub fn (ctx Context) init_server() {
	eprintln('init_server() has been deprecated, please init your web app in `fn main()`')
}

// Defining this method is optional.
// This method is called before every request (aka middleware).
// You can use it for checking user session cookies or to add headers.
pub fn (ctx Context) before_request() {}

// TODO - test
// vweb intern function
[manualfree]
pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool {
	if ctx.done {
		return false
	}
	ctx.done = true
	//
	mut resp := http.Response{
		body: res
	}
	$if vweb_livereload ? {
		if mimetype == 'text/html' {
			resp.body = res.replace('</html>', '<script src="/vweb_livereload/${vweb_livereload_server_start}/script.js"></script>\n</html>')
		}
	}
	// build the header after the potential modification of resp.body from above
	header := http.new_header_from_map({
		http.CommonHeader.content_type:   mimetype
		http.CommonHeader.content_length: resp.body.len.str()
	}).join(ctx.header)
	resp.header = header.join(vweb.headers_close)
	//
	resp.set_version(.v1_1)
	resp.set_status(http.status_from_int(ctx.status.int()))
	// send_string(mut ctx.conn, resp.bytestr()) or { return false }
	fast_send_resp(mut ctx.conn, resp) or { return false }
	return true
}

// Response HTTP_OK with s as payload with content-type `text/html`
pub fn (mut ctx Context) html(s string) Result {
	ctx.send_response_to_client('text/html', s)
	return Result{}
}

// Response HTTP_OK with s as payload with content-type `text/plain`
pub fn (mut ctx Context) text(s string) Result {
	ctx.send_response_to_client('text/plain', s)
	return Result{}
}

// Response HTTP_OK with json_s as payload with content-type `application/json`
pub fn (mut ctx Context) json[T](j T) Result {
	json_s := json.encode(j)
	ctx.send_response_to_client('application/json', json_s)
	return Result{}
}

// Response HTTP_OK with a pretty-printed JSON result
pub fn (mut ctx Context) json_pretty[T](j T) Result {
	json_s := json.encode_pretty(j)
	ctx.send_response_to_client('application/json', json_s)
	return Result{}
}

// TODO - test
// Response HTTP_OK with file as payload
pub fn (mut ctx Context) file(f_path string) Result {
	if !os.exists(f_path) {
		eprintln('[vweb] file ${f_path} does not exist')
		return ctx.not_found()
	}
	ext := os.file_ext(f_path)
	data := os.read_file(f_path) or {
		eprint(err.msg())
		ctx.server_error(500)
		return Result{}
	}
	content_type := vweb.mime_types[ext]
	if content_type.len == 0 {
		eprintln('[vweb] no MIME type found for extension ${ext}')
		ctx.server_error(500)
	} else {
		ctx.send_response_to_client(content_type, data)
	}
	return Result{}
}

// Response HTTP_OK with s as payload
pub fn (mut ctx Context) ok(s string) Result {
	ctx.send_response_to_client(ctx.content_type, s)
	return Result{}
}

// TODO - test
// Response a server error
pub fn (mut ctx Context) server_error(ecode int) Result {
	$if debug {
		eprintln('> ctx.server_error ecode: ${ecode}')
	}
	if ctx.done {
		return Result{}
	}
	send_string(mut ctx.conn, vweb.http_500.bytestr()) or {}
	return Result{}
}

// Redirect to an url
pub fn (mut ctx Context) redirect(url string) Result {
	if ctx.done {
		return Result{}
	}
	ctx.done = true
	mut resp := vweb.http_302
	resp.header = resp.header.join(ctx.header)
	resp.header.add(.location, url)
	send_string(mut ctx.conn, resp.bytestr()) or { return Result{} }
	return Result{}
}

// Send an not_found response
pub fn (mut ctx Context) not_found() Result {
	// TODO add a [must_be_returned] attribute, so that the caller is forced to use `return app.not_found()`
	if ctx.done {
		return Result{}
	}
	ctx.done = true
	send_string(mut ctx.conn, vweb.http_404.bytestr()) or {}
	return Result{}
}

// TODO - test
// Sets a cookie
pub fn (mut ctx Context) set_cookie(cookie http.Cookie) {
	cookie_raw := cookie.str()
	if cookie_raw == '' {
		eprintln('[vweb] error setting cookie: name of cookie is invalid')
		return
	}
	ctx.add_header('Set-Cookie', cookie_raw)
}

// Sets the response content type
pub fn (mut ctx Context) set_content_type(typ string) {
	ctx.content_type = typ
}

// TODO - test
// Sets a cookie with a `expire_date`
pub fn (mut ctx Context) set_cookie_with_expire_date(key string, val string, expire_date time.Time) {
	cookie := http.Cookie{
		name: key
		value: val
		expires: expire_date
	}
	ctx.set_cookie(cookie)
}

// Gets a cookie by a key
pub fn (ctx &Context) get_cookie(key string) !string {
	c := ctx.req.cookie(key) or { return error('Cookie not found') }
	return c.value
	// if value := ctx.req.cookies[key] {
	// return value
	//}
}

// TODO - test
// Sets the response status
pub fn (mut ctx Context) set_status(code int, desc string) {
	if code < 100 || code > 599 {
		ctx.status = '500 Internal Server Error'
	} else {
		ctx.status = '${code} ${desc}'
	}
}

// TODO - test
// Adds an header to the response with key and val
pub fn (mut ctx Context) add_header(key string, val string) {
	ctx.header.add_custom(key, val) or {}
}

// TODO - test
// Returns the header data from the key
pub fn (ctx &Context) get_header(key string) string {
	return ctx.req.header.get_custom(key) or { '' }
}

// set_value sets a value on the context
pub fn (mut ctx Context) set_value(key context.Key, value context.Any) {
	ctx.ctx = context.with_value(ctx.ctx, key, value)
}

// get_value gets a value from the context
pub fn (ctx &Context) get_value[T](key context.Key) ?T {
	if val := ctx.ctx.value(key) {
		match val {
			T {
				// `context.value()` always returns a reference
				// if we send back `val` the returntype becomes `?&T` and this can be problematic
				// for end users since they won't be able to do something like
				// `app.get_value[string]('a') or { '' }
				// since V expects the value in the or block to be of type `&string`.
				// And if a reference was allowed it would enable mutating the context directly
				return *val
			}
			else {}
		}
	}
	return none
}

pub type DatabasePool[T] = fn (tid int) T

interface DbPoolInterface {
	db_handle voidptr
mut:
	db voidptr
}

interface DbInterface {
mut:
	db voidptr
}

pub type Middleware = fn (mut Context) bool

interface MiddlewareInterface {
	middlewares map[string][]Middleware
}

// Generate route structs for an app
fn generate_routes[T](app &T) !map[string]Route {
	// Parsing methods attributes
	mut routes := map[string]Route{}
	$for method in T.methods {
		http_methods, route_path, middleware, host := parse_attrs(method.name, method.attrs) or {
			return error('error parsing method attributes: ${err}')
		}

		routes[method.name] = Route{
			methods: http_methods
			path: route_path
			path_words: route_path.split('/').filter(it != '')
			middleware: middleware
			host: host
		}
	}
	return routes
}

type ControllerHandler = fn (ctx Context, mut url urllib.URL, host string, tid int)

pub struct ControllerPath {
pub:
	path    string
	handler ControllerHandler = unsafe { nil }
pub mut:
	host string
}

interface ControllerInterface {
	controllers []&ControllerPath
}

pub struct Controller {
pub mut:
	controllers []&ControllerPath
}

// controller generates a new Controller for the main app
pub fn controller[T](path string, global_app &T) &ControllerPath {
	routes := generate_routes(global_app) or { panic(err.msg()) }

	// generate struct with closure so the generic type is encapsulated in the closure
	// no need to type `ControllerHandler` as generic since it's not needed for closures
	return &ControllerPath{
		path: path
		handler: fn [global_app, path, routes] [T](ctx Context, mut url urllib.URL, host string, tid int) {
			// request_app is freed in `handle_route`
			mut request_app := new_request_app[T](global_app, ctx, tid)
			// transform the url
			url.path = url.path.all_after_first(path)
			handle_route[T](mut request_app, url, host, &routes, tid)
		}
	}
}

// controller_host generates a controller which only handles incoming requests from the `host` domain
pub fn controller_host[T](host string, path string, global_app &T) &ControllerPath {
	mut ctrl := controller(path, global_app)
	ctrl.host = host
	return ctrl
}

// run - start a new VWeb server, listening to all available addresses, at the specified `port`
pub fn run[T](global_app &T, port int) {
	run_at[T](global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) }
}

[params]
pub struct RunParams {
	family               net.AddrFamily = .ip6 // use `family: .ip, host: 'localhost'` when you want it to bind only to 127.0.0.1
	host                 string
	port                 int  = 8080
	nr_workers           int  = runtime.nr_jobs()
	pool_channel_slots   int  = 1000
	show_startup_message bool = true
}

// run_at - start a new VWeb server, listening only on a specific address `host`, at the specified `port`
// Example: vweb.run_at(new_app(), vweb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) }
[manualfree]
pub fn run_at[T](global_app &T, params RunParams) ! {
	if params.port <= 0 || params.port > 65535 {
		return error('invalid port number `${params.port}`, it should be between 1 and 65535')
	}
	if params.pool_channel_slots < 1 {
		return error('invalid pool_channel_slots `${params.pool_channel_slots}`, it should be above 0, preferably higher than 10 x nr_workers')
	}
	if params.nr_workers < 1 {
		return error('invalid nr_workers `${params.nr_workers}`, it should be above 0')
	}

	listen_address := '${params.host}:${params.port}'
	mut l := net.listen_tcp(params.family, listen_address) or {
		ecode := err.code()
		return error('failed to listen ${ecode} ${err}')
	}
	// eprintln('>> vweb listen_address: `${listen_address}` | params.family: ${params.family} | l.addr: ${l.addr()} | params: $params')

	routes := generate_routes(global_app)!
	controllers_sorted := check_duplicate_routes_in_controllers[T](global_app, routes)!

	host := if params.host == '' { 'localhost' } else { params.host }
	if params.show_startup_message {
		println('[Vweb] Running app on http://${host}:${params.port}/')
	}

	ch := chan &RequestParams{cap: params.pool_channel_slots}
	mut ws := []thread{cap: params.nr_workers}
	for worker_number in 0 .. params.nr_workers {
		ws << new_worker[T](ch, worker_number)
	}
	if params.show_startup_message {
		println('[Vweb] We have ${ws.len} workers')
	}
	flush_stdout()

	// Forever accept every connection that comes, and
	// pass it through the channel, to the thread pool:
	for {
		mut connection := l.accept_only() or {
			// failures should not panic
			eprintln('[vweb] accept() failed with error: ${err.msg()}')
			continue
		}
		ch <- &RequestParams{
			connection: connection
			global_app: unsafe { global_app }
			controllers: controllers_sorted
			routes: &routes
		}
	}
}

fn check_duplicate_routes_in_controllers[T](global_app &T, routes map[string]Route) ![]&ControllerPath {
	mut controllers_sorted := []&ControllerPath{}
	$if T is ControllerInterface {
		mut paths := []string{}
		controllers_sorted = global_app.controllers.clone()
		controllers_sorted.sort(a.path.len > b.path.len)
		for controller in controllers_sorted {
			if controller.host == '' {
				if controller.path in paths {
					return error('conflicting paths: duplicate controller handling the route "${controller.path}"')
				}
				paths << controller.path
			}
		}
		for method_name, route in routes {
			for controller_path in paths {
				if route.path.starts_with(controller_path) {
					return error('conflicting paths: method "${method_name}" with route "${route.path}" should be handled by the Controller of path "${controller_path}"')
				}
			}
		}
	}
	return controllers_sorted
}

fn new_request_app[T](global_app &T, ctx Context, tid int) &T {
	// Create a new app object for each connection, copy global data like db connections
	mut request_app := &T{}
	$if T is MiddlewareInterface {
		request_app = &T{
			middlewares: global_app.middlewares.clone()
		}
	}

	$if T is DbPoolInterface {
		// get database connection from the connection pool
		request_app.db = global_app.db_handle(tid)
	} $else $if T is DbInterface {
		// copy a database to a app without pooling
		request_app.db = global_app.db
	}

	$for field in T.fields {
		if field.is_shared {
			unsafe {
				// TODO: remove this horrible hack, when copying a shared field at comptime works properly!!!
				raptr := &voidptr(&request_app.$(field.name))
				gaptr := &voidptr(&global_app.$(field.name))
				*raptr = *gaptr
				_ = raptr // TODO: v produces a warning that `raptr` is unused otherwise, even though it was on the previous line
			}
		} else {
			if 'vweb_global' in field.attrs {
				request_app.$(field.name) = global_app.$(field.name)
			}
		}
	}
	request_app.Context = ctx // copy request data such as form and query etc

	// copy static files
	request_app.Context.static_files = global_app.static_files.clone()
	request_app.Context.static_mime_types = global_app.static_mime_types.clone()
	request_app.Context.static_hosts = global_app.static_hosts.clone()

	return request_app
}

[manualfree]
fn handle_conn[T](mut conn net.TcpConn, global_app &T, controllers []&ControllerPath, routes &map[string]Route, tid int) {
	conn.set_read_timeout(30 * time.second)
	conn.set_write_timeout(30 * time.second)
	defer {
		conn.close() or {}
	}

	conn.set_sock() or {
		eprintln('[vweb] tid: ${tid:03d}, error setting socket')
		return
	}

	mut reader := io.new_buffered_reader(reader: conn)
	defer {
		unsafe {
			reader.free()
		}
	}

	page_gen_start := time.ticks()

	// Request parse
	req := http.parse_request(mut reader) or {
		// Prevents errors from being thrown when BufferedReader is empty
		if '${err}' != 'none' {
			eprintln('[vweb] tid: ${tid:03d}, error parsing request: ${err}')
		}
		return
	}
	$if trace_request ? {
		dump(req)
	}
	$if trace_request_url ? {
		dump(req.url)
	}
	// URL Parse
	mut url := urllib.parse(req.url) or {
		eprintln('[vweb] tid: ${tid:03d}, error parsing path: ${err}')
		return
	}

	// Query parse
	query := parse_query_from_url(url)

	// Form parse
	form, files := parse_form_from_request(req) or {
		// Bad request
		conn.write(vweb.http_400.bytes()) or {}
		return
	}

	// remove the port from the HTTP Host header
	host_with_port := req.header.get(.host) or { '' }
	host, _ := urllib.split_host_port(host_with_port)

	// Create Context with request data
	ctx := Context{
		ctx: context.background()
		req: req
		page_gen_start: page_gen_start
		conn: conn
		query: query
		form: form
		files: files
	}

	// match controller paths
	$if T is ControllerInterface {
		for controller in controllers {
			// skip controller if the hosts don't match
			if controller.host != '' && host != controller.host {
				continue
			}
			if url.path.len >= controller.path.len && url.path.starts_with(controller.path) {
				// pass route handling to the controller
				controller.handler(ctx, mut url, host, tid)
				return
			}
		}
	}

	mut request_app := new_request_app(global_app, ctx, tid)
	handle_route(mut request_app, url, host, routes, tid)
}

[manualfree]
fn handle_route[T](mut app T, url urllib.URL, host string, routes &map[string]Route, tid int) {
	defer {
		unsafe {
			free(app)
		}
	}

	url_words := url.path.split('/').filter(it != '')

	// Calling middleware...
	app.before_request()

	$if vweb_livereload ? {
		if url.path.starts_with('/vweb_livereload/') {
			if url.path.ends_with('current') {
				app.handle_vweb_livereload_current()
				return
			}
			if url.path.ends_with('script.js') {
				app.handle_vweb_livereload_script()
				return
			}
		}
	}

	// Static handling
	if serve_if_static[T](mut app, url, host) {
		// successfully served a static file
		return
	}

	// Route matching
	$for method in T.methods {
		$if method.return_type is Result {
			route := (*routes)[method.name] or {
				eprintln('[vweb] tid: ${tid:03d}, parsed attributes for the `${method.name}` are not found, skipping...')
				Route{}
			}

			// Skip if the HTTP request method does not match the attributes
			if app.req.method in route.methods {
				// Used for route matching
				route_words := route.path_words // route.path.split('/').filter(it != '')
				// println('ROUTES ${routes}')
				// println('\nROUTE WORDS')
				// println(route_words)
				// println(route.path_words)

				// Skip if the host does not match or is empty
				if route.host == '' || route.host == host {
					// Route immediate matches first
					// For example URL `/register` matches route `/:user`, but `fn register()`
					// should be called first.
					if !route.path.contains('/:') && url_words == route_words {
						// We found a match
						$if T is MiddlewareInterface {
							if validate_middleware(mut app, url.path) == false {
								return
							}
						}

						if app.req.method == .post && method.args.len > 0 {
							// Populate method args with form values
							mut args := []string{cap: method.args.len}
							for param in method.args {
								args << app.form[param.name]
							}

							if route.middleware == '' {
								app.$method(args)
							} else if validate_app_middleware(mut app, route.middleware,
								method.name)
							{
								app.$method(args)
							}
						} else {
							if route.middleware == '' {
								app.$method()
							} else if validate_app_middleware(mut app, route.middleware,
								method.name)
							{
								app.$method()
							}
						}
						return
					}

					if url_words.len == 0 && route_words == ['index'] && method.name == 'index' {
						$if T is MiddlewareInterface {
							if validate_middleware(mut app, url.path) == false {
								return
							}
						}
						if route.middleware == '' {
							app.$method()
						} else if validate_app_middleware(mut app, route.middleware, method.name) {
							app.$method()
						}
						return
					}

					if params := route_matches(url_words, route_words) {
						method_args := params.clone()
						if method_args.len != method.args.len {
							eprintln('[vweb] tid: ${tid:03d}, warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the vweb route `${method.attrs}` (${method_args.len})')
						}

						$if T is MiddlewareInterface {
							if validate_middleware(mut app, url.path) == false {
								return
							}
						}
						if route.middleware == '' {
							app.$method(method_args)
						} else if validate_app_middleware(mut app, route.middleware, method.name) {
							app.$method(method_args)
						}
						return
					}
				}
			}
		}
	}
	// Route not found
	app.not_found()
}

// validate_middleware validates and fires all middlewares that are defined in the global app instance
fn validate_middleware[T](mut app T, full_path string) bool {
	for path, middleware_chain in app.middlewares {
		// only execute middleware if route.path starts with `path`
		if full_path.len >= path.len && full_path.starts_with(path) {
			// there is middleware for this route
			for func in middleware_chain {
				if func(mut app.Context) == false {
					return false
				}
			}
		}
	}
	// passed all middleware checks
	return true
}

// validate_app_middleware validates all middlewares as a method of `app`
fn validate_app_middleware[T](mut app T, middleware string, method_name string) bool {
	// then the middleware that is defined for this route specifically
	valid := fire_app_middleware(mut app, middleware) or {
		eprintln('[vweb] warning: middleware `${middleware}` for the `${method_name}` are not found')
		true
	}
	return valid
}

// fire_app_middleware fires all middlewares that are defined as a method of `app`
fn fire_app_middleware[T](mut app T, method_name string) ?bool {
	$for method in T.methods {
		if method_name == method.name {
			$if method.return_type is bool {
				return app.$method()
			} $else {
				eprintln('[vweb] error in `${method.name}, middleware functions must return bool')
				return none
			}
		}
	}
	// no middleware function found
	return none
}

fn route_matches(url_words []string, route_words []string) ?[]string {
	// URL path should be at least as long as the route path
	// except for the catchall route (`/:path...`)
	if route_words.len == 1 && route_words[0].starts_with(':') && route_words[0].ends_with('...') {
		return ['/' + url_words.join('/')]
	}
	if url_words.len < route_words.len {
		return none
	}

	mut params := []string{cap: url_words.len}
	if url_words.len == route_words.len {
		for i in 0 .. url_words.len {
			if route_words[i].starts_with(':') {
				// We found a path parameter
				params << url_words[i]
			} else if route_words[i] != url_words[i] {
				// This url does not match the route
				return none
			}
		}
		return params
	}

	// The last route can end with ... indicating an array
	if route_words.len == 0 || !route_words[route_words.len - 1].ends_with('...') {
		return none
	}

	for i in 0 .. route_words.len - 1 {
		if route_words[i].starts_with(':') {
			// We found a path parameter
			params << url_words[i]
		} else if route_words[i] != url_words[i] {
			// This url does not match the route
			return none
		}
	}
	params << url_words[route_words.len - 1..url_words.len].join('/')
	return params
}

// check if request is for a static file and serves it
// returns true if we served a static file, false otherwise
[manualfree]
fn serve_if_static[T](mut app T, url urllib.URL, host string) bool {
	// TODO: handle url parameters properly - for now, ignore them
	static_file := app.static_files[url.path] or { return false }
	mime_type := app.static_mime_types[url.path] or { return false }
	static_host := app.static_hosts[url.path] or { '' }
	if static_file == '' || mime_type == '' {
		return false
	}
	if static_host != '' && static_host != host {
		return false
	}
	data := os.read_file(static_file) or {
		send_string(mut app.conn, vweb.http_404.bytestr()) or {}
		return true
	}
	app.send_response_to_client(mime_type, data)
	unsafe { data.free() }
	return true
}

fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string, host string) {
	files := os.ls(directory_path) or { panic(err) }
	if files.len > 0 {
		for file in files {
			full_path := os.join_path(directory_path, file)
			if os.is_dir(full_path) {
				ctx.scan_static_directory(full_path, mount_path.trim_right('/') + '/' + file,
					host)
			} else if file.contains('.') && !file.starts_with('.') && !file.ends_with('.') {
				ext := os.file_ext(file)
				// Rudimentary guard against adding files not in mime_types.
				// Use host_serve_static directly to add non-standard mime types.
				if ext in vweb.mime_types {
					ctx.host_serve_static(host, mount_path.trim_right('/') + '/' + file,
						full_path)
				}
			}
		}
	}
}

// handle_static is used to mark a folder (relative to the current working folder)
// as one that contains only static resources (css files, images etc).
// If `root` is set the mount path for the dir will be in '/'
// Usage:
// ```v
// os.chdir( os.executable() )?
// app.handle_static('assets', true)
// ```
pub fn (mut ctx Context) handle_static(directory_path string, root bool) bool {
	return ctx.host_handle_static('', directory_path, root)
}

// host_handle_static is used to mark a folder (relative to the current working folder)
// as one that contains only static resources (css files, images etc).
// If `root` is set the mount path for the dir will be in '/'
// Usage:
// ```v
// os.chdir( os.executable() )?
// app.host_handle_static('localhost', 'assets', true)
// ```
pub fn (mut ctx Context) host_handle_static(host string, directory_path string, root bool) bool {
	if ctx.done || !os.exists(directory_path) {
		return false
	}
	dir_path := directory_path.trim_space().trim_right('/')
	mut mount_path := ''
	if dir_path != '.' && os.is_dir(dir_path) && !root {
		// Mount point hygiene, "./assets" => "/assets".
		mount_path = '/' + dir_path.trim_left('.').trim('/')
	}
	ctx.scan_static_directory(dir_path, mount_path, host)
	return true
}

// TODO - test
// mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://server/mount_path
// For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'),
// and you have a file /var/share/myassets/main.css .
// => That file will be available at URL: http://server/assets/main.css .
pub fn (mut ctx Context) mount_static_folder_at(directory_path string, mount_path string) bool {
	return ctx.host_mount_static_folder_at('', directory_path, mount_path)
}

// TODO - test
// host_mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://host/mount_path
// For example: suppose you have called .host_mount_static_folder_at('localhost', '/var/share/myassets', '/assets'),
// and you have a file /var/share/myassets/main.css .
// => That file will be available at URL: http://localhost/assets/main.css .
pub fn (mut ctx Context) host_mount_static_folder_at(host string, directory_path string, mount_path string) bool {
	if ctx.done || mount_path.len < 1 || mount_path[0] != `/` || !os.exists(directory_path) {
		return false
	}
	dir_path := directory_path.trim_right('/')

	trim_mount_path := mount_path.trim_left('/').trim_right('/')
	ctx.scan_static_directory(dir_path, '/${trim_mount_path}', host)
	return true
}

// TODO - test
// Serves a file static
// `url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the file type
pub fn (mut ctx Context) serve_static(url string, file_path string) {
	ctx.host_serve_static('', url, file_path)
}

// TODO - test
// Serves a file static
// `url` is the access path on the site, `file_path` is the real path to the file
// `mime_type` is the file type, `host` is the host to serve the file from
pub fn (mut ctx Context) host_serve_static(host string, url string, file_path string) {
	ctx.static_files[url] = file_path
	// ctx.static_mime_types[url] = mime_type
	ext := os.file_ext(file_path)
	ctx.static_mime_types[url] = vweb.mime_types[ext]
	ctx.static_hosts[url] = host
}

// user_agent returns the user-agent header for the current client
pub fn (ctx &Context) user_agent() string {
	return ctx.req.header.get(.user_agent) or { '' }
}

// Returns the ip address from the current user
pub fn (ctx &Context) ip() string {
	mut ip := ctx.req.header.get(.x_forwarded_for) or { '' }
	if ip == '' {
		ip = ctx.req.header.get_custom('X-Real-Ip') or { '' }
	}

	if ip.contains(',') {
		ip = ip.all_before(',')
	}
	if ip == '' {
		ip = ctx.conn.peer_ip() or { '' }
	}
	return ip
}

// Set s to the form error
pub fn (mut ctx Context) error(s string) {
	eprintln('[vweb] Context.error: ${s}')
	ctx.form_error = s
}

// Returns an empty result
pub fn not_found() Result {
	return Result{}
}

fn send_string(mut conn net.TcpConn, s string) ! {
	$if trace_send_string_conn ? {
		eprintln('> send_string: conn: ${ptr_str(conn)}')
	}
	$if trace_response ? {
		eprintln('> send_string:\n${s}\n')
	}
	if voidptr(conn) == unsafe { nil } {
		return error('connection was closed before send_string')
	}
	conn.write_string(s)!
}

// Formats resp to a string suitable for HTTP response transmission
// A fast version of `resp.bytestr()` used with
// `send_string(mut ctx.conn, resp.bytestr())`
fn fast_send_resp(mut conn net.TcpConn, resp http.Response) ! {
	mut sb := strings.new_builder(resp.body.len + 200)
	/*
	send_string(mut conn, 'HTTP/')!
	send_string(mut conn, resp.http_version)!
	send_string(mut conn, ' ')!
	send_string(mut conn, resp.status_code.str())!
	send_string(mut conn, ' ')!
	send_string(mut conn, resp.status_msg)!
	send_string(mut conn, '\r\n')!
	send_string(mut conn, resp.header.render(
		version: resp.version()
	))!
	send_string(mut conn, '\r\n')!
	send_string(mut conn, resp.body)!
	*/
	sb.write_string('HTTP/')
	sb.write_string(resp.http_version)
	sb.write_string(' ')
	sb.write_decimal(resp.status_code)
	sb.write_string(' ')
	sb.write_string(resp.status_msg)
	sb.write_string('\r\n')
	// sb.write_string(resp.header.render_with_sb(
	// version: resp.version()
	//))
	resp.header.render_into_sb(mut sb,
		version: resp.version()
	)
	sb.write_string('\r\n')
	sb.write_string(resp.body)
	send_string(mut conn, sb.str())!
}

// Do not delete.
// It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside vweb templates
// TODO: move it to template render
fn filter(s string) string {
	return html.escape(s)
}

// Worker functions for the thread pool:
struct RequestParams {
	global_app  voidptr
	controllers []&ControllerPath
	routes      &map[string]Route
mut:
	connection &net.TcpConn
}

struct Worker[T] {
	id int
	ch chan &RequestParams
}

fn new_worker[T](ch chan &RequestParams, id int) thread {
	mut w := &Worker[T]{
		id: id
		ch: ch
	}
	return spawn w.process_incoming_requests[T]()
}

fn (mut w Worker[T]) process_incoming_requests() {
	sid := '[vweb] tid: ${w.id:03d} received request'
	for {
		mut params := <-w.ch or { break }
		$if vweb_trace_worker_scan ? {
			eprintln(sid)
		}
		handle_conn[T](mut params.connection, params.global_app, params.controllers, params.routes,
			w.id)
	}
	$if vweb_trace_worker_scan ? {
		eprintln('[vweb] closing worker ${w.id}.')
	}
}

[params]
pub struct PoolParams[T] {
	handler    fn () T [required] = unsafe { nil }
	nr_workers int = runtime.nr_jobs()
}

// database_pool creates a pool of database connections
pub fn database_pool[T](params PoolParams[T]) DatabasePool[T] {
	mut connections := []T{}
	// create a database connection for each worker
	for _ in 0 .. params.nr_workers {
		connections << params.handler()
	}

	return fn [connections] [T](tid int) T {
		$if vweb_trace_worker_scan ? {
			eprintln('[vweb] worker ${tid} received database connection')
		}
		return connections[tid]
	}
}
