const std = @import("std");
const zap = @import("zap.zig");

/// Your middleware components need to contain a handler.
///
/// A Handler is one element in the chain of request handlers that will be tried
/// by the listener when a request arrives. Handlers indicate to the previous
/// handler whether they processed a request by returning `true` from their
/// `on_request` function, in which case a typical request handler would stop
/// trying to pass the request on to the next handler in the chain. See
/// the `handle_other` function in this struct.
pub fn Handler(comptime ContextType: anytype) type {
    return struct {
        other_handler: ?*Self = null,
        on_request: ?RequestFn = null,

        // will be set
        allocator: ?std.mem.Allocator = null,

        pub const RequestFn = *const fn (*Self, zap.Request, *ContextType) bool;
        const Self = @This();

        pub fn init(on_request: RequestFn, other: ?*Self) Self {
            return .{
                .other_handler = other,
                .on_request = on_request,
            };
        }

        // example for handling a request request
        // which you can use in your components, e.g.:
        // return self.handler.handleOther(r, context);
        pub fn handleOther(self: *Self, r: zap.Request, context: *ContextType) bool {
            // in structs embedding a handler, we'd @fieldParentPtr the first
            // param to get to the real self

            // First, do our pre-other stuff
            // ..

            // then call the wrapped thing
            var other_handler_finished = false;
            if (self.other_handler) |other_handler| {
                if (other_handler.on_request) |on_request| {
                    other_handler_finished = on_request(other_handler, r, context);
                }
            }

            // now do our post stuff
            return other_handler_finished;
        }
    };
}

/// A convenience handler for artibrary zap.Endpoint
pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anytype) type {
    return struct {
        handler: HandlerType,
        endpoint: *zap.Endpoint,
        breakOnFinish: bool,

        const Self = @This();

        /// Create an endpointhandler from an endpoint and pass in the next (other) handler in the chain.
        /// If `breakOnFinish` is `true`, the handler will stop handing requests down the chain if
        /// the endpoint processed the request.
        pub fn init(endpoint: *zap.Endpoint, other: ?*HandlerType, breakOnFinish: bool) Self {
            return .{
                .handler = HandlerType.init(onRequest, other),
                .endpoint = endpoint,
                .breakOnFinish = breakOnFinish,
            };
        }

        /// Provides the handler as a common interface to chain stuff
        pub fn getHandler(self: *Self) *HandlerType {
            return &self.handler;
        }

        /// The Handler's request handling function. Gets called from the listener
        /// with the request and a context instance. Calls the endpoint.
        ///
        /// If `breakOnFinish` is `true`, the handler will stop handing requests down the chain if
        /// the endpoint processed the request.
        pub fn onRequest(handler: *HandlerType, r: zap.Request, context: *ContextType) bool {
            var self = @fieldParentPtr(Self, "handler", handler);
            r.setUserContext(context);
            self.endpoint.onRequest(r);

            // if the request was handled by the endpoint, we may break the chain here
            if (r.isFinished() and self.breakOnFinish) {
                return true;
            }
            return self.handler.handleOther(r, context);
        }
    };
}

pub const Error = error{
    /// The listener could not be created because the settings provided to its
    /// init() function contained an `on_request` callback that was not null.
    InitOnRequestIsNotNull,
};

pub const RequestAllocatorFn = *const fn () std.mem.Allocator;

/// Special Listener that supports chaining request handlers.
pub fn Listener(comptime ContextType: anytype) type {
    return struct {
        listener: zap.HttpListener = undefined,
        settings: zap.HttpListenerSettings,

        // static initial handler
        var handler: ?*Handler(ContextType) = undefined;
        // static allocator getter
        var requestAllocator: ?RequestAllocatorFn = null;

        const Self = @This();

        /// Construct and initialize a middleware handler.
        /// The passed in settings must have on_request set to null! If that is
        /// not the case, an InitOnRequestIsNotNull error will be returned.
        pub fn init(settings: zap.HttpListenerSettings, initial_handler: *Handler(ContextType), request_alloc: ?RequestAllocatorFn) Error!Self {
            // override on_request with ourselves
            if (settings.on_request != null) {
                return Error.InitOnRequestIsNotNull;
            }
            requestAllocator = request_alloc;
            std.debug.assert(requestAllocator != null);

            var ret: Self = .{
                .settings = settings,
            };

            ret.settings.on_request = onRequest;
            ret.listener = zap.HttpListener.init(ret.settings);
            handler = initial_handler;
            return ret;
        }

        /// Start listening.
        pub fn listen(self: *Self) !void {
            try self.listener.listen();
        }

        /// The listener's request handler, stepping through the chain of Handlers
        /// by calling the initial one which takes it from there.
        ///
        /// This is just a reference implementation that you can use by default.
        /// Create your own listener if you want different behavior.
        /// (Didn't want to make this a callback. Submit an issue if you really
        /// think that's an issue).
        pub fn onRequest(r: zap.Request) void {
            // we are the 1st handler in the chain, so we create a context
            var context: ContextType = .{};

            // handlers might need an allocator
            // we CAN provide an allocator getter
            var allocator: ?std.mem.Allocator = null;
            if (requestAllocator) |foo| {
                allocator = foo();
            }

            if (handler) |initial_handler| {
                initial_handler.allocator = allocator;
                if (initial_handler.on_request) |on_request| {
                    // we don't care about the return value at the top level
                    _ = on_request(initial_handler, r, &context);
                }
            }
        }
    };
}
