// zig fmt: off
const std = @import("std");
const Lib = @import("lib");
const UI = @import("./ui.zig");
const RectUtils = @import("./rect-utils.zig");
const rl = @import("raylib");
const FontFace = @import("./font-face.zig");
const srcery = @import("./srcery.zig");
const rlgl_h = @cImport({
    @cInclude("rlgl.h");
});

const assert = std.debug.assert;
const log = std.log.scoped(.app);
const Api = Lib.Api;

const App = @This();

const MapTexture = struct {
    name: Api.Map.Skin,
    texture: rl.Texture2D,
};

ui: UI,
server: *Api.Server,
store: *Api.Store,
map_textures: std.ArrayList(MapTexture),
map_texture_indexes: std.ArrayList(usize),
map_position_min: Api.Position,
map_position_max: Api.Position,
character_skin_textures: std.EnumArray(Api.Character.Skin, rl.Texture2D),
camera: rl.Camera2D,
font_face: FontFace,

blur_texture_original: ?rl.RenderTexture = null,
blur_texture_horizontal: ?rl.RenderTexture = null,
blur_texture_both: ?rl.RenderTexture = null,
blur_shader: rl.Shader,

simulation: bool = false,

system_clock: Lib.SystemClock,
started_at: i128,
artificer: Lib.ArtificerApi,
sim_artificer: Lib.ArtificerSim,
sim_server: Lib.SimServer,
sim_started_at: i128,
last_sim_timestamp: i128 = 0,

thread_running: bool = true,
artificer_thread: std.Thread,

pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !*App {
    var map_textures = std.ArrayList(MapTexture).init(allocator);
    errdefer map_textures.deinit();
    errdefer {
        for (map_textures.items) |map_texture| {
            map_texture.texture.unload();
        }
    }

    var map_texture_indexes = std.ArrayList(usize).init(allocator);
    errdefer map_texture_indexes.deinit();

    // Load all map textures from api store
    {
        const map_image_ids = store.images.category_mapping.get(.map).items;
        try map_textures.ensureTotalCapacity(map_image_ids.len);

        for (map_image_ids) |image_id| {
            const image = store.images.get(image_id).?;

            map_textures.appendAssumeCapacity(MapTexture{
                .name = try Api.Map.Skin.fromSlice(image.code.slice()),
                .texture = try loadTextureFromStore(store, image_id)
            });
        }
    }

    var map_position_max = Api.Position.zero();
    var map_position_min = Api.Position.zero();
    for (store.maps.items) |map| {
        map_position_min.x = @min(map_position_min.x, map.position.x);
        map_position_min.y = @min(map_position_min.y, map.position.y);

        map_position_max.x = @max(map_position_max.x, map.position.x);
        map_position_max.y = @max(map_position_max.y, map.position.y);
    }
    const map_size = map_position_max.subtract(map_position_min);

    try map_texture_indexes.ensureTotalCapacity(@intCast(map_size.x * map_size.y));

    for (0..@intCast(map_size.y)) |oy| {
        for (0..@intCast(map_size.x)) |ox| {
            const x = map_position_min.x + @as(i64, @intCast(ox));
            const y = map_position_min.y + @as(i64, @intCast(oy));
            const map = store.getMap(.{ .x = x, .y = y }).?;

            var found_texture = false;
            const map_skin = map.skin.slice();
            for (0.., map_textures.items) |i, map_texture| {
                if (std.mem.eql(u8, map_skin, map_texture.name.slice())) {
                    map_texture_indexes.appendAssumeCapacity(i);
                    found_texture = true;
                    break;
                }
            }

            if (!found_texture) {
                return error.MapImageNotFound;
            }
        }
    }

    var character_skin_textures = std.EnumArray(Api.Character.Skin, rl.Texture2D).initUndefined();

    inline for (std.meta.fields(Api.Character.Skin)) |field| {
        const skin: Api.Character.Skin = @enumFromInt(field.value);
        const skin_image_id = store.images.getId(.character, skin.toString()).?;
        const texture = try loadTextureFromStore(store, skin_image_id);
        character_skin_textures.set(skin, texture);
    }

    const blur_shader = rl.loadShaderFromMemory(
        @embedFile("./base.vsh"),
        @embedFile("./blur.fsh"),
    );
    if (!rl.isShaderReady(blur_shader)) {
        return error.LoadShaderFromMemory;
    }

    var fontChars: [95]i32 = undefined;
    for (0..fontChars.len) |i| {
        fontChars[i] = 32 + @as(i32, @intCast(i));
    }
    var font = rl.loadFontFromMemory(".ttf", @embedFile("./roboto-font/Roboto-Medium.ttf"), 16, &fontChars);
    if (!font.isReady()) {
        return error.LoadFontFromMemory;
    }

    const font_face = FontFace{ .font = font };

    const ui = UI.init(font_face);

    const character_id = store.characters.getId("Blondie").?;

    const sim_server = Lib.SimServer.init(0, store);

    var app = try allocator.create(App);
    errdefer allocator.destroy(app);

    app.* = App{
        .store = store,
        .server = server,
        .system_clock = .{},
        .sim_server = sim_server,
        .started_at = 0,
        .sim_started_at = sim_server.clock.nanoTimestamp(),
        .ui = ui,
        .map_textures = map_textures,
        .map_texture_indexes = map_texture_indexes,
        .map_position_max = map_position_max,
        .map_position_min = map_position_min,
        .character_skin_textures = character_skin_textures,
        .blur_shader = blur_shader,
        .font_face = font_face,
        .camera = rl.Camera2D{
            .offset = rl.Vector2.zero(),
            .target = rl.Vector2.zero(),
            .rotation = 0,
            .zoom = 1,
        },

        .artificer = undefined,
        .sim_artificer = undefined,
        .artificer_thread = undefined,
    };
    app.started_at = app.system_clock.nanoTimestamp();

    app.sim_artificer = try Lib.ArtificerSim.init(allocator, store, &app.sim_server.clock, &app.sim_server, character_id);
    errdefer app.sim_artificer.deinit(allocator);

    app.artificer = try Lib.ArtificerApi.init(allocator, store, &app.system_clock, server, character_id);
    errdefer app.artificer.deinit(allocator);

    app.artificer_thread = try std.Thread.spawn(.{ .allocator = allocator }, artificer_thread_cb, .{ app });
    errdefer {
        app.thread_running = false;
        app.artificer_thread.join();
    }

    return app;
}

pub fn deinit(self: *App) void {
    const allocator = self.map_textures.allocator;

    self.thread_running = false;
    self.artificer_thread.join();

    self.artificer.deinit(allocator);
    self.sim_artificer.deinit(allocator);

    for (self.map_textures.items) |map_texture| {
        map_texture.texture.unload();
    }
    for (self.character_skin_textures.values) |texture| {
        texture.unload();
    }

    self.map_textures.deinit();
    self.map_texture_indexes.deinit();
    self.font_face.font.unload();

    if (self.blur_texture_horizontal) |render_texture| {
        render_texture.unload();
    }
    if (self.blur_texture_both) |render_texture| {
        render_texture.unload();
    }
    if (self.blur_texture_original) |render_texture| {
        render_texture.unload();
    }

    allocator.destroy(self);
}

fn artificer_thread_cb(app: *App) void {
    while (app.thread_running) {

        if (app.simulation) {
            const artificer = &app.sim_artificer;

            const expires_in = artificer.timeUntilCooldownExpires();
            if (expires_in > 0) {
                artificer.clock.sleep(expires_in);
            }

            artificer.tick() catch |e| {
                log.err("Error in .tick in thread: {}", .{e});
                app.thread_running = false;
            };
        } else {
            const artificer = &app.artificer;

            const expires_in = artificer.timeUntilCooldownExpires();
            if (expires_in > 0) {
                artificer.clock.sleep(expires_in);
            }

            artificer.tick() catch |e| {
                log.err("Error in .tick in thread: {}", .{e});
                app.thread_running = false;
            };
        }
    }
}

fn loadTextureFromStore(store: *Api.Store, image_id: Api.Store.Id) !rl.Texture2D {
    const image = store.images.get(image_id).?;
    const texture = rl.loadTextureFromImage(rl.Image{
        .width = @intCast(image.width),
        .height = @intCast(image.height),
        .data = store.images.getRGBA(image_id).?.ptr,
        .mipmaps = 1,
        .format = rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8
    });
    if (!rl.isTextureReady(texture)) {
        return error.LoadMapTextureFromImage;
    }

    return texture;
}

fn cameraControls(camera: *rl.Camera2D) void {
    if (rl.isMouseButtonDown(.mouse_button_left)) {
        const mouse_delta = rl.getMouseDelta();
        camera.target.x -= mouse_delta.x / camera.zoom;
        camera.target.y -= mouse_delta.y / camera.zoom;
    }

    const zoom_speed = 0.2;
    const min_zoom = 0.1;
    const max_zoom = 2;
    const zoom_delta = rl.getMouseWheelMove();
    if (zoom_delta != 0) {
        const mouse_screen = rl.getMousePosition();

        // Get the world point that is under the mouse
        const mouse_world = rl.getScreenToWorld2D(mouse_screen, camera.*);

        // Set the offset to where the mouse is
        camera.offset = mouse_screen;

        // Set the target to match, so that the camera maps the world space point 
        // under the cursor to the screen space point under the cursor at any zoom
        camera.target = mouse_world;

        // Zoom increment
        camera.zoom *= (1 + zoom_delta * zoom_speed);
        camera.zoom = std.math.clamp(camera.zoom, min_zoom, max_zoom);
    }
}

// fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void {
//     var buffer: [256]u8 = undefined;
//
//     const name_height = 20;
//     UI.drawTextCentered(ui.font, brain.name, .{ .x = RectUtils.center(rect).x, .y = rect.y + name_height / 2 }, 20, 2, srcery.bright_white);
//
//     var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom);
//     label_stack.gap = 4;
//
//     const now = std.time.milliTimestamp();
//     const cooldown = brain.cooldown(&artificer.server);
//     const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s;
//     const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{seconds_left});
//     UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
//
//     var task_label: []u8 = undefined;
//     if (brain.task) |task| {
//         task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{@tagName(task)});
//     } else {
//         task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{});
//     }
//     UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
//
//     const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{brain.action_queue.items.len});
//     UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
// }

fn createOrGetRenderTexture(maybe_render_texture: *?rl.RenderTexture) !rl.RenderTexture {
    const screen_width = rl.getScreenWidth();
    const screen_height = rl.getScreenHeight();

    if (maybe_render_texture.*) |render_texture| {
        if (render_texture.texture.width != screen_width or render_texture.texture.height != screen_height) {
            render_texture.unload();
            maybe_render_texture.* = null;
        }
    }

    if (maybe_render_texture.* == null) {
        const render_texture = rl.loadRenderTexture(screen_width, screen_height);
        if (!rl.isRenderTextureReady(render_texture)) {
            return error.LoadRenderTexture;
        }

        maybe_render_texture.* = render_texture;
    }

    return maybe_render_texture.*.?;
}

pub fn drawWorld(self: *App) void {
    rl.clearBackground(srcery.black);

    const tile_size = rl.Vector2.init(224, 224);

    const map_size = self.map_position_max.subtract(self.map_position_min);
    for (0..@intCast(map_size.y)) |oy| {
        for (0..@intCast(map_size.x)) |ox| {
            const map_index = @as(usize, @intCast(map_size.x)) * oy + ox;
            const x = self.map_position_min.x + @as(i64, @intCast(ox));
            const y = self.map_position_min.y + @as(i64, @intCast(oy));
            const texture_index = self.map_texture_indexes.items[map_index];
            const texture = self.map_textures.items[texture_index].texture;

            const position = rl.Vector2.init(@floatFromInt(x), @floatFromInt(y)).multiply(tile_size);
            rl.drawTextureV(texture, position, rl.Color.white);
        }
    }

    for (self.store.characters.objects.items) |optional_character| {
        if (optional_character != .object) continue;
        const character = optional_character.object;
        const skin_texture = self.character_skin_textures.get(character.skin);

        const position = rl.Vector2{
            .x = @floatFromInt(character.position.x),
            .y = @floatFromInt(character.position.y),
        };

        const skin_size = rl.Vector2{
            .x = @floatFromInt(skin_texture.width),
            .y = @floatFromInt(skin_texture.height),
        };

        rl.drawTextureV(
            skin_texture,
            position.addValue(0.5).multiply(tile_size).subtract(skin_size.divide(.{ .x = 2, .y = 2 })),
            rl.Color.white
        );
    }
}

pub fn drawWorldAndBlur(self: *App) !void {
    const screen_width = rl.getScreenWidth();
    const screen_height = rl.getScreenHeight();
    const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height));

    const blur_original = try createOrGetRenderTexture(&self.blur_texture_original);
    const blur_horizontal = try createOrGetRenderTexture(&self.blur_texture_horizontal);
    const blur_both = try createOrGetRenderTexture(&self.blur_texture_both);

    // 1 pass. Draw the all of the sprites  
    {
        blur_original.begin();
        defer blur_original.end();

        self.camera.begin();
        defer self.camera.end();

        self.drawWorld();
    }

    // 2 pass. Apply horizontal blur
    const kernel_radius: i32 = 16;
    var kernel_coeffs: [kernel_radius * 2 + 1]f32 = undefined;
    {
        const sigma = 10;

        for (0..kernel_coeffs.len) |i| {
            const i_i32: i32 = @intCast(i);
            const io: f32 = @floatFromInt(i_i32 - kernel_radius);
            kernel_coeffs[i] = @exp(-(io * io) / (sigma * sigma));
        }

        var kernel_sum: f32 = 0;
        for (kernel_coeffs) |coeff| {
            kernel_sum += coeff;
        }

        for (&kernel_coeffs) |*coeff| {
            coeff.* /= kernel_sum;
        }
    }

    {
        const texture_size_loc = rl.getShaderLocation(self.blur_shader, "textureSize");
        assert(texture_size_loc != -1);

        rl.setShaderValue(self.blur_shader, texture_size_loc, &screen_size, .shader_uniform_vec2);
    }

    {
        const kernel_radius_loc = rl.getShaderLocation(self.blur_shader, "kernelRadius");
        assert(kernel_radius_loc != -1);

        rl.setShaderValue(self.blur_shader, kernel_radius_loc, &kernel_radius, .shader_uniform_int);
    }

    {
        const coeffs_loc = rl.getShaderLocation(self.blur_shader, "coeffs");
        assert(coeffs_loc != -1);

        rl.setShaderValueV(self.blur_shader, coeffs_loc, &kernel_coeffs, .shader_uniform_float, kernel_coeffs.len);
    }

    {
        const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection");
        assert(kernel_direction_loc != -1);

        const kernel_direction = rl.Vector2.init(1, 0);
        rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2);
    }

    {
        blur_horizontal.begin();
        defer blur_horizontal.end();

        rl.clearBackground(rl.Color.black.alpha(0));

        self.blur_shader.activate();
        defer self.blur_shader.deactivate();

        rl.drawTextureRec(
            blur_original.texture,
            .{
                .x = 0,
                .y = 0,
                .width = screen_size.x,
                .height = -screen_size.y,
            },
            rl.Vector2.zero(),
            rl.Color.white
        );
    }

    // 3 pass. Apply vertical blur
    {
        const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection");
        assert(kernel_direction_loc != -1);

        const kernel_direction = rl.Vector2.init(0, 1);
        rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2);
    }

    {
        blur_both.begin();
        defer blur_both.end();

        self.blur_shader.activate();
        defer self.blur_shader.deactivate();

        rl.drawTextureRec(
            blur_horizontal.texture,
            .{
                .x = 0,
                .y = 0,
                .width = screen_size.x,
                .height = -screen_size.y,
            },
            rl.Vector2.zero(),
            rl.Color.white
        );
    }

    // Last thing, draw world without blur
    rl.drawTextureRec(
        blur_original.texture,
        .{
            .x = 0,
            .y = 0,
            .width = @floatFromInt(blur_original.texture.width),
            .height = @floatFromInt(-blur_original.texture.height),
        },
        rl.Vector2.zero(),
        rl.Color.white
    );
}

fn drawRatelimits(self: *App, box: rl.Rectangle) void {
    const Category = Api.RateLimit.Category;
    const ratelimits = self.server.ratelimits;

    const padding = 16;
    var stack = UI.Stack.init(RectUtils.shrink(box, padding, padding), .top_to_bottom);
    stack.gap = 8;

    inline for (.{
        .{ "Account creation", Category.account_creation },
        .{ "Token", Category.token },
        .{ "Data", Category.data },
        .{ "Actions", Category.actions },
    }) |ratelimit_bar| {
        const title = ratelimit_bar[0];
        const category = ratelimit_bar[1];
        const ratelimit = ratelimits.get(category);

        const ratelimit_box = stack.next(24);
        rl.drawRectangleRec(ratelimit_box, rl.Color.white);

        inline for (.{
            .{ ratelimit.hours  , std.time.ms_per_hour, srcery.red },
            .{ ratelimit.minutes, std.time.ms_per_min , srcery.blue },
            .{ ratelimit.seconds, std.time.ms_per_s   , srcery.green },
        }) |limit_spec| {
            const maybe_timespan = limit_spec[0];
            const timespan_size = limit_spec[1];
            const color = limit_spec[2];

            if (maybe_timespan) |timespan| {
                const limit_f32: f32 = @floatFromInt(timespan.limit);
                const counter_f32: f32 = @floatFromInt(timespan.counter);
                const timer_f32: f32 = @floatFromInt(timespan.timer_ms);

                const ms_per_request = timespan_size / limit_f32;
                const progress = std.math.clamp((counter_f32 - timer_f32 / ms_per_request) / limit_f32, 0, 1);

                var progress_bar = ratelimit_box;
                progress_bar.width *= progress;
                rl.drawRectangleRec(progress_bar, color);
            }
        }

        if (self.ui.isMouseInside(ratelimit_box)) {
            // TODO: Draw more detailed info about rate limits.
            // Show how many requests have occured
        } else {
            const title_size = self.font_face.measureText(title);
            self.font_face.drawText(
                title,
                .{
                    .x = ratelimit_box.x + 8,
                    .y = ratelimit_box.y + title_size.y/2,
                },
                srcery.white
            );
        }
    }
}

fn showButton(self: *App, text: []const u8) UI.Interaction {
    const key_hash: u16 = @truncate(@intFromPtr(text.ptr));
    var button = self.ui.getOrAppendWidget(key_hash);
    button.padding.vertical(8);
    button.padding.horizontal(16);

    button.flags.insert(.clickable);
    button.size = .{
        .x = .{ .text = {} },
        .y = .{ .text = {} },
    };

    const interaction = self.ui.getInteraction(button);
    var text_color: rl.Color = undefined;
    if (interaction.held_down) {
        button.background = .{ .color = srcery.hard_black };
        text_color = srcery.white;
    } else if (interaction.hovering) {
        button.background = .{ .color = srcery.bright_black };
        text_color = srcery.bright_white;
    } else {
        button.background = .{ .color = srcery.black };
        text_color = srcery.bright_white;
    }

    button.text = .{
        .content = text,
        .color = text_color
    };

    return interaction;
}

fn showLabel(self: *App, text: []const u8) UI.Widget.Key {
    const key_hash: u16 = @truncate(@intFromPtr(text.ptr));
    var label = self.ui.getOrAppendWidget(key_hash);

    label.text = .{
        .content = text
    };
    label.size = .{
        .x = .{ .text = {} },
        .y = .{ .text = {} }
    };
    return label.key;
}

pub fn toggleSimulationMode(self: *App) void {
    self.simulation = !self.simulation;

    if (self.simulation) {
        const system_clock = self.system_clock;
        const time_passed = system_clock.nanoTimestamp() - self.started_at;

        const sim_clock = &self.sim_server.clock;
        sim_clock.timestamp_limit = self.sim_started_at + time_passed;
        sim_clock.timestamp = sim_clock.timestamp_limit.?;
        self.last_sim_timestamp = std.time.nanoTimestamp();
    }
}

pub fn tick(self: *App) !void {
    for (&self.server.ratelimits.values) |*ratelimit| {
        ratelimit.update_timers(std.time.milliTimestamp());
    }

    if (self.simulation) {
        const now = std.time.nanoTimestamp();
        const time_passed = now - self.last_sim_timestamp;
        self.last_sim_timestamp = now;


        const sim_clock = &self.sim_server.clock;
        sim_clock.timestamp_limit.? += time_passed;
    }

    const screen_width = rl.getScreenWidth();
    const screen_height = rl.getScreenHeight();
    const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height));

    if (!self.ui.isHoveringAnything()) {
        cameraControls(&self.camera);
        self.ui.disable_mouse_interaction = rl.isMouseButtonDown(.mouse_button_left);
    } else {
        self.ui.disable_mouse_interaction = false;
    }

    rl.beginDrawing();
    defer rl.endDrawing();

    rl.clearBackground(srcery.black);

    try self.drawWorldAndBlur();

    const ui = &self.ui;
    if (self.blur_texture_both) |blur_texture_both| {
        ui.blur_background = blur_texture_both.texture;
    }
    ui.begin();

    ui.topWidget().padding.all(16);
    ui.layout().gap = 8;

    {
        const panel = ui.getOrAppendWidget(ui.randomWidgetHash());
        panel.size = .{ .x = .fit_children, .y = .fit_children };
        panel.padding.all(16);
        panel.layout.gap = 8;
        panel.background = .blur_world;
        ui.pushWidget(panel.key);
        defer ui.popWidget();

        if (self.simulation) {
            const clock = self.sim_server.clock;
            const started_at = self.sim_started_at;
            const time_passed: f32 = @floatFromInt(clock.nanoTimestamp() - started_at);
            var time_passed_buff: [128]u8 = undefined;
            const time_passed_str = try std.fmt.bufPrint(&time_passed_buff, "Time passed: {d:.1}s", .{ time_passed / std.time.ns_per_s });

            _ = self.showLabel(time_passed_str);
            if (self.showButton("Turn off simulation").clicked) {
                self.toggleSimulationMode();
            }
        } else {
            const clock = self.system_clock;
            const started_at = self.started_at;
            const time_passed: f32 = @floatFromInt(clock.nanoTimestamp() - started_at);
            var time_passed_buff: [128]u8 = undefined;
            const time_passed_str = try std.fmt.bufPrint(&time_passed_buff, "Time passed: {d:.1}s", .{ time_passed / std.time.ns_per_s });

            _ = self.showLabel(time_passed_str);
            if (self.showButton("Turn on simulation").clicked) {
                self.toggleSimulationMode();
            }
        }

    }

    ui.end();

    // self.drawRatelimits(
    //     .{ .x = 20, .y = 20, .width = 200, .height = 200 },
    // );

    rl.drawFPS(
        @as(i32, @intFromFloat(screen_size.x)) - 100,
        @as(i32, @intFromFloat(screen_size.y)) - 24
    );

    // var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
    // for (artificer.characters.items) |*brain| {
    //     const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len));
    //     try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
    // }
}
