🍯 Glaze

package http

import "core:fmt"
import "core:net"
import "core:os"
import "core:slice"
import "core:strings"
import "core:sync"
import "core:sys/posix"
import "core:thread"
import "core:time"

Method :: enum {
	Get,
	Post,
	Put,
	Delete,
	Patch,
	Head,
	Options,
}

Status_Code :: enum {
	Ok                  = 200,
	BadRequest          = 400,
	NotFound            = 404,
	RequestTimeout      = 408,
	InternalServerError = 500,
	NotImplemented      = 501,
	ServiceUnavailable  = 503,
}

Status_Message :: #sparse[Status_Code]string {
	.Ok                  = "200 OK",
	.BadRequest          = "400 Bad Request",
	.NotFound            = "404 Not Found",
	.RequestTimeout      = "408 Request Timeout",
	.InternalServerError = "500 Internal Server Error",
	.NotImplemented      = "501 Not Implemented",
	.ServiceUnavailable  = "503 Service Unavailable",
}

Request :: struct {
	_raw_head:      []u8,
	host:           string,
	method:         Method,
	target:         string, // TODO: Implement URI type
	version:        string,
	headers:        [dynamic]Header,
	content_length: int,
	body:           []u8,
}

Response :: struct {
	code:    Status_Code,
	headers: [dynamic]Header,
	body:    []u8,
}

Handler :: proc(req: Request, res: ^Response, user_data: rawptr)

// TODO: stream support when body grows very large?
respond :: proc {
	respond_empty,
	respond_string,
	respond_bytes,
//respond_stream,
}

respond_empty :: proc(res: ^Response, code: Status_Code) {
	res.code = code
	res.body = nil
}

respond_string :: proc(res: ^Response, code: Status_Code, str: string) {
	res.code = code
	res.body = slice.clone(transmute([]u8)str[:])
}

respond_bytes :: proc(res: ^Response, code: Status_Code, buf: []u8) {
	res.code = code
	res.body = slice.clone(buf)
}

/*respond_stream :: proc(res: ^Response, code: Status_Code, ...) {
}*/

MAX_CONNECTIONS :: 128

serve :: proc(
	endpoint: net.Endpoint,
	h: proc(req: Request, res: ^Response, user_data: rawptr),
	data: rawptr,
) {
	posix.signal(.SIGINT, should_quit_handler)
	posix.signal(.SIGTERM, should_quit_handler)

	listener, err := net.listen_tcp(endpoint)
	if err != nil {
		fmt.eprintf("listen_tcp failed: %v", err)
		os.exit(1)
	}
	defer net.close(listener)

	net.set_option(listener, .Receive_Timeout, time.Millisecond * 10)

	fmt.printf("Glaze listening on %s\n", net.to_string(endpoint))
	free_all(context.temp_allocator)

	active_connections: int
	for !SHOULD_QUIT {
		client, _, err := net.accept_tcp(listener)
		if err != nil {
			if err == .Would_Block || err == .Interrupted {
				continue
			}

			fmt.eprintf("accept_tcp failed: %v\n", err)
			os.exit(1)
			// continue
		}
		if active_connections >= MAX_CONNECTIONS {
			send_response_error(client, .ServiceUnavailable)
			net.close(client)
			continue
		}
		sync.atomic_add(&active_connections, 1)

		cj := new(Conn_Job)
		cj.conn = {
			sock = client,
		}
		cj.handler = h
		cj.user_data = data
		cj.active_connections = &active_connections

		thread.create_and_start_with_poly_data(cj, handle_conn_job, context, self_cleanup = true)
	}
}

// allocates only if escapeable character found
escape_html :: proc(s: string) -> (string, bool) {
	escape_with := make(map[rune]string)
	defer delete(escape_with)
	escape_with['&'] = "&"
	escape_with['<'] = "<"
	escape_with['>'] = ">"
	escape_with['"'] = """
	escape_with['\''] = "'"

	needs_escaping: bool
	for c in s {
		if c in escape_with {
			needs_escaping = true
		}
	}

	if needs_escaping {
		b: strings.Builder
		strings.builder_init(&b)

		for c in s {
			if c in escape_with {
				strings.write_string(&b, escape_with[c])
			} else {
				strings.write_rune(&b, c)
			}
		}

		return strings.to_string(b), true
	}

	return s, false
}