const std = @import("std");
const pb = @import("proto").pb;
const logic = @import("../../logic.zig");
const Session = @import("../../Session.zig");
const Assets = @import("../../Assets.zig");

const Level = logic.Level;
const Player = logic.Player;
const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator;

pub fn enterSceneOnLogin(
    rx: logic.event.Receiver(.login),
    tx: logic.event.Sender(.change_scene_begin),
) !void {
    _ = rx;

    try tx.send(.{});
}

fn respawnCharTeam(
    gpa: Allocator,
    char_bag: *const Player.CharBag,
    cur_scene: *const Player.Scene.Current,
    level: *Level,
) Level.SpawnError!void {
    // Despawn previous team
    var team = level.team();
    var char_net_id = team.next();
    while (char_net_id != .none) : (char_net_id = team.next()) {
        const handle = level.getObjectByNetId(char_net_id).?;
        level.despawn(handle);
    }

    // Spawn new team
    const position: Level.Object.Vector = .{
        .x = cur_scene.position[0],
        .y = cur_scene.position[1],
        .z = cur_scene.position[2],
    };

    const rotation: Level.Object.Vector = .{
        .x = cur_scene.rotation[0],
        .y = cur_scene.rotation[1],
        .z = cur_scene.rotation[2],
    };

    const team_index = char_bag.meta.curr_team_index;
    const chars = char_bag.chars.slice();

    for (char_bag.teams.items(.char_team)[team_index]) |slot| {
        const char_index = slot.charIndex() orelse continue;
        const i = @intFromEnum(char_index);

        const char_template_id_num = chars.items(.template_id)[i];

        _ = try level.spawn(
            gpa,
            .{
                .template_id = char_template_id_num,
                .position = position,
                .rotation = rotation,
                .hp = chars.items(.hp)[i],
            },
            .{ .character = .{
                .level = chars.items(.level)[i],
                .char_index = char_index,
            } },
        );
    }
}

pub fn beginChangingScene(
    rx: logic.event.Receiver(.change_scene_begin),
    gpa: logic.Resource.Allocator(.gpa),
    session: *Session,
    base: Player.Component(.base),
    scene: Player.Component(.scene),
    char_bag: Player.Component(.char_bag),
    level: *Level,
) !void {
    _ = rx;

    level.reset();
    try respawnCharTeam(gpa.interface, char_bag.data, &scene.data.current, level);

    const position: pb.VECTOR = .{
        .X = scene.data.current.position[0],
        .Y = scene.data.current.position[1],
        .Z = scene.data.current.position[2],
    };

    try session.send(pb.SC_CHANGE_SCENE_BEGIN_NOTIFY{
        .scene_num_id = scene.data.current.level_id,
        .position = position,
        .pass_through_data = .init,
    });

    try session.send(pb.SC_ENTER_SCENE_NOTIFY{
        .role_id = base.data.role_id,
        .scene_num_id = scene.data.current.level_id,
        .position = position,
        .pass_through_data = .init,
    });
}

pub fn refreshCharTeam(
    rx: logic.event.Receiver(.char_bag_team_modified),
    gpa: logic.Resource.Allocator(.gpa),
    char_bag: Player.Component(.char_bag),
    scene: Player.Component(.scene),
    level: *Level,
    sync_tx: logic.event.Sender(.sync_self_scene),
) !void {
    switch (rx.payload.modification) {
        .set_leader => return, // Doesn't require any action from server.
        .set_char_team => if (rx.payload.team_index == char_bag.data.meta.curr_team_index) {
            // If the current active team has been modified, it has to be re-spawned.
            try respawnCharTeam(gpa.interface, char_bag.data, &scene.data.current, level);
            try sync_tx.send(.{ .reason = .team_modified });
        },
    }
}

pub fn syncSelfScene(
    rx: logic.event.Receiver(.sync_self_scene),
    session: *Session,
    arena: logic.Resource.Allocator(.arena),
    char_bag: Player.Component(.char_bag),
    scene: Player.Component(.scene),
    level: *Level,
    assets: *const Assets,
) !void {
    const reason: pb.SELF_INFO_REASON_TYPE = switch (rx.payload.reason) {
        .entrance => .SLR_ENTER_SCENE,
        .team_modified => .SLR_CHANGE_TEAM,
    };

    const team_index = char_bag.data.meta.curr_team_index;
    const leader_index = char_bag.data.teams.items(.leader_index)[team_index];

    var self_scene_info: pb.SC_SELF_SCENE_INFO = .{
        .scene_num_id = scene.data.current.level_id,
        .self_info_reason = @intFromEnum(reason),
        .teamInfo = .{
            .team_type = .CHAR_BAG_TEAM_TYPE_MAIN,
            .team_index = @intCast(team_index),
            .cur_leader_id = leader_index.objectId(),
            .team_change_token = 0,
        },
        .scene_impl = .{ .empty = .{} },
        .detail = .{},
    };

    const objects = level.objects.slice();
    for (0..objects.len) |i| {
        const position = objects.items(.position)[i];
        const rotation = objects.items(.rotation)[i];
        const net_id = @intFromEnum(objects.items(.net_id)[i]);

        var common_info: pb.SCENE_OBJECT_COMMON_INFO = .{
            .id = net_id,
            .position = .{ .X = position.x, .Y = position.y, .Z = position.z },
            .rotation = .{ .X = rotation.x, .Y = rotation.y, .Z = rotation.z },
            .scene_num_id = scene.data.current.level_id,
            .hp = objects.items(.hp)[i],
        };

        switch (objects.items(.extra)[i]) {
            .character => |extra| {
                const template_id_num = objects.items(.template_id)[i];
                const template_id = assets.numToStr(.char_id, template_id_num).?;

                common_info.type = 0;
                common_info.templateid = template_id;

                var scene_char: pb.SCENE_CHARACTER = .{
                    .level = extra.level,
                    .common_info = common_info,
                    .battle_info = .{
                        .msg_generation = @intCast(net_id),
                        .battle_inst_id = @intCast(net_id),
                        .part_inst_info = .init,
                        .skill_list = try packCharacterSkills(arena.interface, assets, template_id),
                    },
                };

                const char_data = assets.table(.character).getPtr(template_id).?;
                for (char_data.attributes[0].Attribute.attrs) |attr| {
                    try scene_char.attrs.append(arena.interface, .{
                        .attr_type = @intFromEnum(attr.attrType),
                        .basic_value = attr.attrValue,
                        .value = attr.attrValue,
                    });
                }

                try self_scene_info.detail.?.char_list.append(arena.interface, scene_char);
            },
        }
    }

    try session.send(self_scene_info);
}

fn packCharacterSkills(
    arena: Allocator,
    assets: *const Assets,
    template_id: []const u8,
) Allocator.Error!ArrayList(pb.SERVER_SKILL) {
    const char_skills = assets.char_skill_map.map.getPtr(template_id).?.all_skills;
    var list: ArrayList(pb.SERVER_SKILL) = try .initCapacity(
        arena,
        char_skills.len + assets.common_skill_config.config.Character.skillConfigs.len,
    );

    errdefer comptime unreachable;

    for (char_skills, 1..) |name, i| {
        list.appendAssumeCapacity(.{
            .skill_id = .{
                .id_impl = .{ .str_id = name },
                .type = .BATTLE_ACTION_OWNER_TYPE_SKILL,
            },
            .blackboard = .{},
            .inst_id = (100 + i),
            .level = 1,
            .source = .BATTLE_SKILL_SOURCE_DEFAULT,
            .potential_lv = 1,
            .is_enable = true,
        });
    }

    for (assets.common_skill_config.config.Character.skillConfigs, char_skills.len + 1..) |config, i| {
        list.appendAssumeCapacity(.{
            .skill_id = .{
                .id_impl = .{ .str_id = config.skillId },
                .type = .BATTLE_ACTION_OWNER_TYPE_SKILL,
            },
            .blackboard = .{},
            .inst_id = (100 + i),
            .level = 1,
            .source = .BATTLE_SKILL_SOURCE_DEFAULT,
            .potential_lv = 1,
            .is_enable = true,
        });
    }

    return list;
}
