const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("build_config.zig");
const build_options = @import("build_options");
const cli = @import("cli.zig");
const internal_os = @import("os/main.zig");
const fontconfig = @import("fontconfig");
const glslang = @import("glslang");
const harfbuzz = @import("harfbuzz");
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const apprt = @import("apprt.zig");
const assert = @import("quirks.zig").inlineAssert;
const allocTmpDir = @import("os/file.zig").allocTmpDir;
const freeTmpDir = @import("os/file.zig").freeTmpDir;

// This file should only be imported for certain platforms.
comptime {
    switch (@import("terminal_options").artifact) {
        .ghostty => {},
        // This file is not allowed to be included in libghostty-vt
        .lib => @compileError("global state cannot be used in libghostty-vt"),
    }
}

/// We export the xev backend we want to use so that the rest of
/// Ghostty can import this once and have access to the proper
/// backend.
pub const xev = @import("xev").Dynamic;

/// Global process state. This is initialized in main() for exe artifacts and
/// by ghostty_init() for lib artifacts. Most other methods in this file will
/// retrieve items stored in this state.
var state: ?GlobalState = null;

pub const InitOpts = union(enum) {
    main: std.process.Init.Minimal,

    /// Same as `main` but for auxiliary tool binaries (e.g. ghostty-bench
    /// and ghostty-gen) that have their own CLI action namespace. This
    /// skips detection of ghostty app CLI actions, since tool actions
    /// (e.g. `+terminal-stream`) are not valid app actions and would
    /// otherwise cause init to fail with InvalidAction.
    tool: std.process.Init.Minimal,

    c: struct {
        argc: usize,
        argv: [*][*:0]u8,
        environ: std.process.Environ,
    },
};

/// Initialize the global state.
pub fn init(opts: InitOpts) !void {
    // Initialize ourself to nothing so we don't have any extra state.
    // IMPORTANT: this MUST be initialized before any log output because
    // the log function uses the global state.
    state = .{
        .io_impl = undefined,
        .gpa = null,
        .alloc = undefined,
        .environ = switch (opts) {
            .main, .tool => |m| m.environ,
            .c => |c| c.environ,
        },
        .args = switch (opts) {
            .main, .tool => |m| m.args,
            // TODO: Using the C API from Windows is unsupported at this time.
            //
            // When do we plan on supporting Windows, it's recommended to
            // ensure that the C API can take a UNICODE_STRING (aka []16, a
            // WTF-16 string) so that it can just be passed into
            // std.process.Args.Vector directly.
            .c => |c| .{ .vector = if (builtin.os.tag == .windows)
                return error.UnsupportedOSForCApi
            else
                c.argv[0..c.argc] },
        },
        .tmp_dir_path = null,
        .action = null,
        .logging = .{},
        .rlimits = .{},
        .resources_dir = .{},
    };
    const self = &state.?;
    errdefer deinit();

    self.gpa = gpa: {
        // Use the libc allocator if it is available because it is WAY
        // faster than GPA. We only do this in release modes so that we
        // can get easy memory leak detection in debug modes.
        if (builtin.link_libc) {
            if (switch (builtin.mode) {
                .ReleaseSafe, .ReleaseFast => true,

                // We also use it if we can detect we're running under
                // Valgrind since Valgrind only instruments the C allocator
                else => std.valgrind.runningOnValgrind() > 0,
            }) break :gpa null;
        }

        break :gpa .init;
    };

    self.alloc = if (self.gpa) |*value|
        value.allocator()
    else if (builtin.link_libc)
        std.heap.c_allocator
    else
        unreachable;

    // Set up our main I/O implementation (fully threaded w/allocator). Note
    // that we cannot use any implementation supplied from main at this point,
    // because there are some later initialization steps that depend on us
    // mutating the environment, and thus it needs to be re-synced farther
    // down. For that, we need a stable implementation that allows us to do so.
    self.io_impl = .init(self.alloc, .{
        .argv0 = .init(self.args),
        .environ = self.environ,
    });

    // Discover and save the temporary directory path
    self.tmp_dir_path = try allocTmpDir(self.alloc, self.environ);

    // We first try to parse any action that we may be executing.
    // Tool binaries (ghostty-bench, ghostty-gen) have their own action
    // namespace and detect their own actions, so we skip detection here.
    self.action = switch (opts) {
        .main, .c => try cli.action.detectArgs(
            cli.ghostty.Action,
            self.alloc,
            self.args,
        ),
        .tool => null,
    };

    // If we have an action executing, we disable logging by default
    // since we write to stderr we don't want logs messing up our
    // output.
    if (self.action != null) self.logging.stderr = false;

    // I don't love the env var name but I don't have it in my heart
    // to parse CLI args 3 times (once for actions, once for config,
    // maybe once for logging) so for now this is an easy way to do
    // this. Env vars are useful for logging too because they are
    // easy to set.
    logging: {
        const v = self.environ.getAlloc(self.alloc, "GHOSTTY_LOG") catch |err| switch (err) {
            error.EnvironmentVariableMissing => break :logging,
            else => return err,
        };
        defer self.alloc.free(v);
        self.logging = cli.args.parsePackedStruct(GlobalState.Logging, v) catch .{};
    }

    // Setup our signal handlers before logging
    GlobalState.initSignals();

    // Setup our Xev backend if we're dynamic
    if (comptime xev.dynamic) xev.detect() catch |err| {
        std.log.warn("failed to detect xev backend, falling back to " ++
            "most compatible backend err={}", .{err});
    };

    // Output some debug information right away
    std.log.info("ghostty version={s}", .{build_config.version_string});
    std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
    std.log.info("runtime={}", .{build_config.app_runtime});
    std.log.info("font_backend={}", .{build_config.font_backend});
    if (comptime build_config.font_backend.hasHarfbuzz()) {
        std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
    }
    if (comptime build_config.font_backend.hasFontconfig()) {
        std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
    }
    std.log.info("renderer={}", .{renderer.Renderer});
    std.log.info("libxev default backend={t}", .{xev.backend});

    // As early as possible, initialize our resource limits.
    self.rlimits = .init();

    if (build_options.sentry) {
        // Initialize our crash reporting. The environ map snapshot is
        // owned by crash.init (it is freed by the init thread).
        const environ_map = try self.environ.createMap(self.alloc);
        crash.init(self.alloc, environ_map) catch |err| {
            std.log.warn(
                "sentry init failed, no crash capture available err={}",
                .{err},
            );
        };
    }

    // const sentrylib = @import("sentry");
    // if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent(
    //     .info,
    //     null,
    //     "hello, world",
    // ))) |uuid| {
    //     std.log.warn("uuid={s}", .{uuid.string()});
    // } else std.log.warn("failed to capture event", .{});

    // We need to make sure the process locale is set properly. Locale
    // affects a lot of behaviors in a shell.
    //
    // We need to re-sync the environment after this completes.
    try internal_os.ensureLocale();
    syncEnviron();

    // Initialize glslang for shader compilation
    try glslang.init();

    // Initialize oniguruma for regex
    try oni.init(&.{oni.Encoding.utf8});

    // Find our resources directory once for the app so every launch
    // hereafter can use this cached value.
    self.resources_dir = try apprt.runtime.resourcesDir(self.alloc);
    errdefer self.resources_dir.deinit(self.alloc);

    // Setup i18n
    if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| {
        std.log.warn("failed to init i18n, translations will not be available err={}", .{err});
    };
}

/// Cleans up the global state. This doesn't _need_ to be called but
/// doing so in dev modes will check for memory leaks.
///
/// Asserts that the state exists.
pub fn deinit() void {
    const self = &state.?;

    self.resources_dir.deinit(self.alloc);

    // Flush our crash logs
    crash.deinit();

    // Release our tmp_dir_path if needed
    if (self.tmp_dir_path) |td| freeTmpDir(self.alloc, td);

    // Release our I/O instance
    self.io_impl.deinit();

    if (self.gpa) |*value| {
        // We want to ensure that we deinit the GPA because this is
        // the point at which it will output if there were safety violations.
        _ = value.deinit();
    }
}

/// Helper to return either the state's I/O instance, or one from testing.
///
/// Asserts that the global state is initialized when not running as as test.
pub fn io() std.Io {
    if (builtin.is_test) return std.testing.io;

    return state.?.io();
}

/// Helper to return either the state's I/O instance, or one from testing.
///
/// Asserts that the global state is initialized when not running as as test.
pub fn alloc() std.mem.Allocator {
    if (builtin.is_test) return std.testing.allocator;

    return state.?.alloc;
}

/// Helper to return either the state's environment, or one from testing.
///
/// Asserts that the global state is initialized when not running as a test.
pub fn environ() std.process.Environ {
    if (builtin.is_test) return std.testing.environ;

    return state.?.environ;
}

/// Helper to create an environment map off of the state's environment, or one
/// from testing. The map is created off of the state allocator.
///
/// Asserts that the global state is initialized when not running as a test.
pub fn environMap() !std.process.Environ.Map {
    if (builtin.is_test) return std.testing.environ.createMap(std.testing.allocator);

    return state.?.environ.createMap(state.?.alloc);
}

/// Re-synchronizes the global Environ (both the higher-level and I/O versions)
/// from the process. No-op on Windows, asserts libc and an initialized global
/// state on everything else.
///
/// It is not valid to run this within any code that needs to be run through
/// tests. For any of these, re-factor the code to take an environment map
/// instead, where you can modify the environment as needed.
///
/// NOTE: Be cognizant of where you are calling this! While the only real
/// difference between the POSIX environment and higher-level Zig `PosixBlock`
/// struct is that the latter has a length versus the former's many-item
/// pointer state, there is no concurrency control on this function (or for
/// that matter, `environ` or `environMap`. Direct modification of the system
/// environment is becoming more discouraged in the standard library as well,
/// and this should be kept in mind when resorting to lower-level `setenv` or
/// `unsetenv` - as a rule, beyond initialization, favor
/// `std.process.Environ.Map` whenever possible.
pub fn syncEnviron() void {
    switch (builtin.os.tag) {
        .windows => {},
        else => {
            assert(builtin.link_libc);
            assert(!builtin.is_test);
            const new_environ: std.process.Environ = .{ .block = .{ .slice = std.c.environ[0..env_len: {
                var len: usize = 0;
                while (std.c.environ[len]) |_| : (len += 1) {}
                break :env_len len;
            } :null] } };
            state.?.environ = new_environ;
            state.?.io_impl.environ = .{ .process_environ = new_environ };
        },
    }
}

/// Helper to return either the state's args, or one from testing.
///
/// Asserts that the global state is initialized when not running as a test.
pub fn args() std.process.Args {
    if (builtin.is_test) return .{ .vector = &.{} };

    return state.?.args;
}

/// Returns the temporary directory discovered from the global environment
/// (saves allocation of the temporary directory where you can get at global
/// state).
///
/// Asserts that the global state is initialized.
pub fn tmpDirPath() []const u8 {
    return state.?.tmp_dir_path.?;
}

/// Returns the global state resources_dir, or an empty one when testing.
///
/// Asserts that the global state is initialized when not running as a test.
pub fn resourcesDir() internal_os.ResourcesDir {
    if (builtin.is_test) return .{};

    return state.?.resources_dir;
}

/// Returns the global state rlimits, or an empty one when testing.
///
/// Asserts that the global state is initialized when not running as a test.
pub fn rlimits() ResourceLimits {
    if (builtin.is_test) return .{};

    return state.?.rlimits;
}

/// Returns the global state logging configuration.
///
/// Asserts that the global state is initialized.
pub fn logging() GlobalState.Logging {
    return state.?.logging;
}

/// Returns the global state action.
///
/// Asserts that the global state is initialized.
pub fn action() ?cli.ghostty.Action {
    return state.?.action;
}

/// This represents the global process state. There should only
/// be one of these at any given moment. This is extracted into a dedicated
/// struct because it is reused by main and the static C lib.
pub const GlobalState = struct {
    const GPA = std.heap.DebugAllocator(.{});

    io_impl: std.Io.Threaded,
    gpa: ?GPA,
    alloc: std.mem.Allocator,
    environ: std.process.Environ,
    args: std.process.Args,
    tmp_dir_path: ?[]const u8,
    action: ?cli.ghostty.Action,
    logging: Logging,
    rlimits: ResourceLimits = .{},

    /// The app resources directory, equivalent to zig-out/share when we build
    /// from source. This is null if we can't detect it.
    resources_dir: internal_os.ResourcesDir,

    /// Where logging should go
    pub const Logging = packed struct {
        /// Whether to log to stderr. For lib mode we always disable stderr
        /// logging by default. Otherwise it's enabled by default.
        stderr: bool = build_config.app_runtime != .none,
        /// Whether to log to macOS's unified logging. Enabled by default
        /// on macOS.
        macos: bool = builtin.os.tag.isDarwin(),
    };

    /// Asserts that `self.io_impl` has been initialized.
    pub fn io(self: *GlobalState) std.Io {
        return self.io_impl.io();
    }

    fn initSignals() void {
        // Only posix systems.
        if (comptime builtin.os.tag == .windows) return;

        const p = std.posix;

        var sa: p.Sigaction = .{
            .handler = .{ .handler = p.SIG.IGN },
            .mask = p.sigemptyset(),
            .flags = 0,
        };

        // We ignore SIGPIPE because it is a common signal we may get
        // due to how we implement termio. When a terminal is closed we
        // often write to a broken pipe to exit the read thread. This should
        // be fixed one day but for now this helps make this a bit more
        // robust.
        p.sigaction(p.SIG.PIPE, &sa, null);
    }
};

/// Maintains the Unix resource limits that we set for our process. This
/// can be used to restore the limits to their original values.
pub const ResourceLimits = struct {
    nofile: ?internal_os.rlimit = null,

    pub fn init() ResourceLimits {
        return .{
            // Maximize the number of file descriptors we can have open
            // because we can consume a lot of them if we make many terminals.
            .nofile = internal_os.fixMaxFiles(),
        };
    }

    pub fn restore(self: *const ResourceLimits) void {
        if (self.nofile) |lim| internal_os.restoreMaxFiles(lim);
    }
};
