// zig type definitions for facilio lib
// or maybe let's just make it zap directly...

const std = @import("std");

/// The facilio C API. No need to use this.
pub const fio = @import("fio.zig");

/// Server-Side TLS function wrapper
pub const Tls = @import("tls.zig");

/// Endpoint and supporting types.
/// Create one and pass in your callbacks. Then,
/// pass it to a HttpListener's `register()` function to register with the
/// listener.
///
/// **NOTE**: A common endpoint pattern for zap is to create your own struct
/// that embeds an Endpoint, provides specific callbacks, and uses
/// `@fieldParentPtr` to get a reference to itself.
///
/// Example:
/// A simple endpoint listening on the /stop route that shuts down zap.
/// The main thread usually continues at the instructions after the call to zap.start().
///
/// ```zig
/// const StopEndpoint = struct {
///     ep: zap.Endpoint = undefined,
///
///     pub fn init(
///         path: []const u8,
///     ) StopEndpoint {
///         return .{
///             .ep = zap.Endpoint.init(.{
///                 .path = path,
///                 .get = get,
///             }),
///         };
///     }
///
///     // access the internal Endpoint
///     pub fn endpoint(self: *StopEndpoint) *zap.Endpoint {
///         return &self.ep;
///     }
///
///     fn get(e: *zap.Endpoint, r: zap.Request) void {
///         const self: *StopEndpoint = @fieldParentPtr(StopEndpoint, "ep", e);
///         _ = self;
///         _ = r;
///         zap.stop();
///     }
/// };
/// ```
pub const Endpoint = @import("endpoint.zig");

pub const Router = @import("router.zig");

pub usingnamespace @import("util.zig");
pub usingnamespace @import("http.zig");

/// A struct to handle Mustache templating.
///
/// This is a wrapper around fiobj's mustache template handling.
/// See http://facil.io/0.7.x/fiobj_mustache for more information.
pub const Mustache = @import("mustache.zig");

/// Authenticators
pub const Auth = @import("http_auth.zig");

/// Http request and supporting types.
pub const Request = @import("request.zig");

/// Middleware support.
/// Contains a special Listener and a Handler struct that support chaining
/// requests handlers, with an optional stop once a handler indicates it
/// processed the request. Also sports an EndpointHandler for using regular zap
/// Endpoints as Handlers.
pub const Middleware = @import("middleware.zig");

/// Websocket API
pub const WebSockets = @import("websockets.zig");

pub const Log = @import("log.zig");
const http = @import("http.zig");

const util = @import("util.zig");

// TODO: replace with comptime debug logger like in log.zig
var _debug: bool = false;

/// Start the IO reactor
///
/// Will start listeners etc.
pub fn start(args: fio.fio_start_args) void {
    fio.fio_start(args);
}

/// Stop ZAP:
///
/// 1. Stop accepting further incoming requests
/// 2. Wait for all running request handlers to return
/// 3. return from `zap.start(...)`
pub fn stop() void {
    fio.fio_stop();
}

/// Extremely simplistic zap debug function.
/// TODO: re-wwrite logging or use std.log
pub fn debug(comptime fmt: []const u8, args: anytype) void {
    if (_debug) {
        std.debug.print("[zap] - " ++ fmt, args);
    }
}

/// Enable zap debug logging
pub fn enableDebugLog() void {
    _debug = true;
}

/// start Zap with debug logging on
pub fn startWithLogging(args: fio.fio_start_args) void {
    debug = true;
    fio.fio_start(args);
}

pub const ListenError = error{
    AlreadyListening,
    ListenError,
};

pub const HttpError = error{
    HttpSendBody,
    HttpSetContentType,
    HttpSetHeader,
    HttpParseBody,
    HttpIterParams,
    SetCookie,
    SendFile,
};

/// Http Content Type enum.
/// Needs some love.
pub const ContentType = enum {
    TEXT,
    HTML,
    JSON,
    // TODO: more content types
};

/// Used internally: facilio Http request callback function type
pub const FioHttpRequestFn = *const fn (r: [*c]fio.http_s) callconv(.C) void;

/// Zap Http request callback function type.
pub const HttpRequestFn = *const fn (Request) void;

/// websocket connection upgrade callback type
/// fn(request, targetstring)
pub const HttpUpgradeFn = *const fn (r: Request, target_protocol: []const u8) void;

/// http finish, called when zap finishes. You get your udata back in the
/// HttpFinishSetting struct.
pub const HttpFinishSettings = [*c]fio.struct_http_settings_s;

/// Http finish callback type
pub const HttpFinishFn = *const fn (HttpFinishSettings) void;

/// Listener settings
pub const HttpListenerSettings = struct {
    port: usize,
    interface: [*c]const u8 = null,
    on_request: ?HttpRequestFn,
    on_response: ?HttpRequestFn = null,
    on_upgrade: ?HttpUpgradeFn = null,
    on_finish: ?HttpFinishFn = null,
    // provide any pointer in there for "user data". it will be passed pack in
    // on_finish()'s copy of the struct_http_settings_s
    udata: ?*anyopaque = null,
    public_folder: ?[]const u8 = null,
    max_clients: ?isize = null,
    max_body_size: ?usize = null,
    timeout: ?u8 = null,
    log: bool = false,
    ws_timeout: u8 = 40,
    ws_max_msg_size: usize = 262144,
    tls: ?Tls = null,
};

/// Http listener
pub const HttpListener = struct {
    settings: HttpListenerSettings,

    const Self = @This();
    var the_one_and_only_listener: ?*HttpListener = null;

    /// Create a listener
    pub fn init(settings: HttpListenerSettings) Self {
        std.debug.assert(settings.on_request != null);
        return .{
            .settings = settings,
        };
    }

    // we could make it dynamic by passing a HttpListener via udata
    /// Used internally: the listener's facilio request callback
    pub fn theOneAndOnlyRequestCallBack(r: [*c]fio.http_s) callconv(.C) void {
        if (the_one_and_only_listener) |l| {
            var req: Request = .{
                .path = util.fio2str(r.*.path),
                .query = util.fio2str(r.*.query),
                .body = util.fio2str(r.*.body),
                .method = util.fio2str(r.*.method),
                .h = r,
                ._is_finished_request_global = false,
                ._user_context = undefined,
            };
            req._is_finished = &req._is_finished_request_global;

            var user_context: Request.UserContext = .{};
            req._user_context = &user_context;

            req.markAsFinished(false);
            std.debug.assert(l.settings.on_request != null);
            if (l.settings.on_request) |on_request| {
                // l.settings.on_request.?(req);
                on_request(req);
            }
        }
    }

    /// Used internally: the listener's facilio response callback
    pub fn theOneAndOnlyResponseCallBack(r: [*c]fio.http_s) callconv(.C) void {
        if (the_one_and_only_listener) |l| {
            var req: Request = .{
                .path = util.fio2str(r.*.path),
                .query = util.fio2str(r.*.query),
                .body = util.fio2str(r.*.body),
                .method = util.fio2str(r.*.method),
                .h = r,
                ._is_finished_request_global = false,
                ._user_context = undefined,
            };
            req._is_finished = &req._is_finished_request_global;

            var user_context: Request.UserContext = .{};
            req._user_context = &user_context;

            l.settings.on_response.?(req);
        }
    }

    /// Used internally: the listener's facilio upgrade callback
    pub fn theOneAndOnlyUpgradeCallBack(r: [*c]fio.http_s, target: [*c]u8, target_len: usize) callconv(.C) void {
        if (the_one_and_only_listener) |l| {
            var req: Request = .{
                .path = util.fio2str(r.*.path),
                .query = util.fio2str(r.*.query),
                .body = util.fio2str(r.*.body),
                .method = util.fio2str(r.*.method),
                .h = r,
                ._is_finished_request_global = false,
                ._user_context = undefined,
            };
            const zigtarget: []u8 = target[0..target_len];
            req._is_finished = &req._is_finished_request_global;

            var user_context: Request.UserContext = .{};
            req._user_context = &user_context;

            l.settings.on_upgrade.?(req, zigtarget);
        }
    }

    /// Used internally: the listener's facilio finish callback
    pub fn theOneAndOnlyFinishCallBack(s: [*c]fio.struct_http_settings_s) callconv(.C) void {
        if (the_one_and_only_listener) |l| {
            l.settings.on_finish.?(s);
        }
    }

    /// Start listening
    pub fn listen(self: *Self) !void {
        var pfolder: [*c]const u8 = null;
        var pfolder_len: usize = 0;

        if (self.settings.public_folder) |pf| {
            debug("HttpListener.listen(): public folder is {s}\n", .{pf});
            pfolder_len = pf.len;
            pfolder = pf.ptr;
        }

        const x: fio.http_settings_s = .{
            .on_request = if (self.settings.on_request) |_| Self.theOneAndOnlyRequestCallBack else null,
            .on_upgrade = if (self.settings.on_upgrade) |_| Self.theOneAndOnlyUpgradeCallBack else null,
            .on_response = if (self.settings.on_response) |_| Self.theOneAndOnlyResponseCallBack else null,
            .on_finish = if (self.settings.on_finish) |_| Self.theOneAndOnlyFinishCallBack else null,
            .udata = null,
            .public_folder = pfolder,
            .public_folder_length = pfolder_len,
            .max_header_size = 32 * 1024,
            .max_body_size = self.settings.max_body_size orelse 50 * 1024 * 1024,
            // fio provides good default:
            .max_clients = self.settings.max_clients orelse 0,
            .tls = if (self.settings.tls) |tls| tls.fio_tls else null,
            .reserved1 = 0,
            .reserved2 = 0,
            .reserved3 = 0,
            .ws_max_msg_size = 0,
            .timeout = self.settings.timeout orelse 5,
            .ws_timeout = self.settings.ws_timeout,
            .log = if (self.settings.log) 1 else 0,
            .is_client = 0,
        };
        // TODO: BUG: without this print/sleep statement, -Drelease* loop forever
        // in debug2 and debug3 of hello example
        // std.debug.print("X\n", .{});
        // TODO: still happening?
        std.time.sleep(500 * std.time.ns_per_ms);

        var portbuf: [100]u8 = undefined;
        const printed_port = try std.fmt.bufPrintZ(&portbuf, "{d}", .{self.settings.port});

        const ret = fio.http_listen(printed_port.ptr, self.settings.interface, x);
        if (ret == -1) {
            return error.ListenError;
        }

        // set ourselves up to handle requests:
        // TODO: do we mind the race condition?
        // the HttpRequestFn will check if this is null and not process
        // the request if it isn't set. hence, if started under full load, the
        // first request(s) might not be serviced, as long as it takes from
        // fio.http_listen() to here
        Self.the_one_and_only_listener = self;
    }
};

/// Low-level API
pub const LowLevel = struct {
    /// lower level listening, if you don't want to use a listener but rather use
    /// the listen() function.
    pub const ListenSettings = struct {
        on_request: ?FioHttpRequestFn = null,
        on_upgrade: ?FioHttpRequestFn = null,
        on_response: ?FioHttpRequestFn = null,
        on_finish: ?FioHttpRequestFn = null,
        public_folder: ?[]const u8 = null,
        max_header_size: usize = 32 * 1024,
        max_body_size: usize = 50 * 1024 * 1024,
        max_clients: isize = 100,
        keepalive_timeout_s: u8 = 5,
        log: bool = false,

        const Self = @This();

        /// Create settings with defaults
        pub fn init() Self {
            return .{};
        }
    };

    /// Low level listen function
    pub fn listen(port: [*c]const u8, interface: [*c]const u8, settings: ListenSettings) ListenError!void {
        var pfolder: [*c]const u8 = null;
        var pfolder_len: usize = 0;

        if (settings.public_folder) |pf| {
            pfolder_len = pf.len;
            pfolder = pf.ptr;
        }
        const x: fio.http_settings_s = .{
            .on_request = settings.on_request,
            .on_upgrade = settings.on_upgrade,
            .on_response = settings.on_response,
            .on_finish = settings.on_finish,
            .udata = null,
            .public_folder = pfolder,
            .public_folder_length = pfolder_len,
            .max_header_size = settings.max_header_size,
            .max_body_size = settings.max_body_size,
            .max_clients = settings.max_clients,
            .tls = null,
            .reserved1 = 0,
            .reserved2 = 0,
            .reserved3 = 0,
            .ws_max_msg_size = settings.ws_max_msg_size,
            .timeout = settings.keepalive_timeout_s,
            .ws_timeout = 0,
            .log = if (settings.log) 1 else 0,
            .is_client = 0,
        };
        // TODO: BUG: without this print/sleep statement, -Drelease* loop forever
        // in debug2 and debug3 of hello example
        // std.debug.print("X\n", .{});
        // TODO: still happening?
        std.time.sleep(500 * std.time.ns_per_ms);

        if (fio.http_listen(port, interface, x) == -1) {
            return error.ListenError;
        }
    }

    /// lower level sendBody
    pub fn sendBody(request: [*c]fio.http_s, body: []const u8) HttpError!void {
        const ret = fio.http_send_body(request, @as(
            *anyopaque,
            @ptrFromInt(@intFromPtr(body.ptr)),
        ), body.len);
        debug("sendBody(): ret = {}\n", .{ret});
        if (ret != -1) return error.HttpSendBody;
    }
};
