#include "Client.h"

#include <flyff.h>

#include <iostream>
#include <memory>
#include <sstream>

template<typename T>
void packet_push(std::vector<uint8_t> &packet, const T &value, std::vector<uint8_t>::iterator offset) {
    union ToBytes {
        T value;
        uint8_t bytes[sizeof(T)];
    };

    ToBytes ref = {value};

    packet.insert(offset, std::begin(ref.bytes), std::end(ref.bytes));
}

template<typename T>
void packet_push(std::vector<uint8_t> &packet, const T &value) {
    return packet_push(packet, value, std::end(packet));
}

namespace fugg {
    Client::Client() : flyff::Neuz(flyff::Neuz::instance()) {

    }

    Client &Client::instance() {
        // Constructor will be called.
        static Client instance;
        // Whole module is initialised now.
        return instance;
    }

    void Client::send_packet(const uint32_t &id, std::vector<uint8_t> payload) const {
        const uint32_t header = id < 0x400 ? id : id ^ flyff::PACKET_HEADER_HASH;
        // Add header
        packet_push(payload, header, std::begin(payload));

        struct Vector {
            uintptr_t begin;
            uintptr_t end;
            uintptr_t capacity;
        };

        auto raw_payload = reinterpret_cast<const Vector *>(&payload);
        // Copy the message into a new ptr
        auto payload_ = std::make_unique<Vector>(Vector{
                .begin = fugg::RuntimePointer<uintptr_t>(raw_payload->begin).as_raw(),
                .end = fugg::RuntimePointer<uintptr_t>(raw_payload->end).as_raw(),
                .capacity = fugg::RuntimePointer<uintptr_t>(raw_payload->capacity).as_raw()
        });

        flyff::api::finalize_packet(
                fugg::RuntimePointer<Vector>(payload_.get()).as_raw()
        );

        flyff::api::websocket_send(
                payload_->begin,
                payload_->end - payload_->begin
        );
    }

    void Client::send_logout() const {
        send_packet(0x403, {});
    }

    void Client::send_move_item_in_inventory(const uint32_t &from_slot, const uint32_t &to_slot) const {
        std::vector<uint8_t> packet;

        packet_push<uint32_t>(packet, from_slot);
        packet_push<uint32_t>(packet, to_slot);

        send_packet(0x2001, packet);
    }

    void Client::send_use_item_in_inventory(const uint32_t &slot) const {
        std::vector<uint8_t> packet;

        packet_push<uint32_t>(packet, slot);
        // Unknown yet
        packet_push<uint32_t>(packet, 0);

        send_packet(0x410, packet);
    }

    void Client::send_motion_packet(const uint32_t &action, unsigned long long param0, unsigned long long param1,
                                    unsigned long long param2, unsigned long long param3) const {
        const int32_t motion_packet_id = 0x404;
        const auto &neuz = flyff::Neuz::instance();

        if (!neuz.player) return;

        std::vector<uint8_t> packet;

        uint8_t param_flags = 0;
        if (param0 != 0) param_flags |= 1;
        if (param1 != 0) param_flags |= 2;
        if (param2 != 0) param_flags |= 4;
        if (param3 != 0) param_flags |= 8;

        packet_push<flyff::Vector3>(packet, neuz.player->position);
        packet_push<uint8_t>(packet, action);
        packet_push<uint8_t>(packet, param_flags);
        packet_push(packet, neuz.player->server_tick);

        if (param0 != 0) packet_push<unsigned long long>(packet, param0);
        if (param1 != 0) packet_push<unsigned long long>(packet, param1);
        if (param2 != 0) packet_push<unsigned long long>(packet, param2);
        if (param3 != 0) packet_push<unsigned long long>(packet, param3);

        send_packet(motion_packet_id, packet);
    }

    void Client::send_set_target(const unsigned long long &id) const {
        send_motion_packet(0x04, id);
    }

    void Client::send_clear_target() const {
        send_motion_packet(0x04, {});
    }

    void Client::send_interact_target(const unsigned long long &index) const {
        send_motion_packet(0x11, index);
    }

    void Client::send_use_skill(const flyff::SkillID &id, unsigned long long unknown) const {
        send_motion_packet(0x5, static_cast<unsigned long long>(id), unknown);
    }

    void Client::send_follow_target(const unsigned long long &id) const {
        send_motion_packet(0x10, id);
    }

    void Client::send_move_to(const double &x, const double &y, const double &z) const {
        // Mouse click
        send_motion_packet(
                0,
                *reinterpret_cast<const unsigned long long *>(&x),
                *reinterpret_cast<const unsigned long long *>(&y),
                *reinterpret_cast<const unsigned long long *>(&z)
        );
    }

    void Client::send_chat_message(const std::string &message) const {
        std::vector<uint8_t> packet;
        // Message length
        packet_push<uint32_t>(packet, message.length());
        // Write message
        for (const auto &character: message) {
            packet_push<uint8_t>(packet, character);
        }
        // Unknown 1
        packet_push<uint32_t>(packet, 0);
        // Unknown 2
        packet_push<uint32_t>(packet, 0);

        send_packet(0x2000, packet);
    }
}

#include <unordered_set>
#include <unordered_map>
#include <vector>

#include <string>
#include <memory>

#include <emscripten/html5.h>

#include <cstddef>


flyff::Pointer<flyff::Mover::Buff>
find_skill_buff(const flyff::Pointer<flyff::Mover> &mover, const flyff::SkillID &skill_index) {
    for (const auto &buff: mover->buffs) {
        if (buff->get_type() == flyff::Mover::Buff::Type::Skill) {
            auto skill_property = reinterpret_cast<const flyff::SkillProperty *>(
                    static_cast<const flyff::ObjectProperty *>(buff->property)
            );

            if (!buff->is_removed && skill_property->id == skill_index)
                return buff;
        }
    }

    return flyff::Pointer<flyff::Mover::Buff>(nullptr);
}

template<size_t size>
std::vector<flyff::SkillID>
check_rebuff(const flyff::Pointer<flyff::Mover> &buffer, const flyff::Pointer<flyff::Mover> &target,
             const flyff::SkillID (&buffs_to_check)[size]) {
    const auto &neuz = flyff::Neuz::instance();

    auto result = std::vector<flyff::SkillID>();
    for (const auto &skill_index: buffs_to_check) {
        const auto &buff = find_skill_buff(target, skill_index);
        const auto &skill_property = flyff::Neuz::get_skill_property(skill_index);
        const auto player_skill_level = buffer->get_skill_level(skill_index);
        // Only check for the buff if the buffer actually has the skill.
        if (player_skill_level <= 0) {
            continue;
        }

        // If a buff is active
        if (buff) {
            // For some reason it's one off...
            auto current_skill_level = buff->level + 1;
            auto buff_property = reinterpret_cast<const flyff::SkillProperty *>(
                    static_cast<const flyff::ObjectProperty *>(buff->property)
            );

            if (player_skill_level > current_skill_level) {
                // Always apply a higher buff.
                result.push_back(skill_index);
            } else if (buff->get_time_remaining() < 30 * 1000 && player_skill_level >= current_skill_level) {
                // Refresh the buff.
                result.push_back(skill_index);
            }
        } else {
            // Apply the buff.
            result.push_back(skill_index);
        }
    }

    return result;
}

flyff::Pointer<const flyff::InventoryItem> find_first_equippable_item(const flyff::ItemKind3 &item_kind) {
    const auto &neuz = flyff::Neuz::instance();
    const auto &player = neuz.player;

    uint32_t highest_level = 0;
    flyff::InventoryItem const *highest = nullptr;
    for (const auto &item: player->inventory) {
        const auto &prop = item.property;

        if (item.index < 168 && prop && prop->item_kind3 == item_kind && prop->required_level <= player->get_level() &&
            prop->required_level > highest_level) {
            highest = &item;
            highest_level = prop->required_level;
        }
    }

    return flyff::Pointer<const flyff::InventoryItem>(highest);
}

flyff::Vector3 start_position = {0};
// The object property id of the monster to grind.
int object_property_index = 0;

flyff::MoverID current_heal_target = 0;

enum class ScriptType {
    Off,
    Attacker,
    Healer
};

ScriptType script = ScriptType::Off;

void on_keyup_hook(int event_type, const EmscriptenKeyboardEvent *event, void *user_data) {
    const auto &client = fugg::Client::instance();

    if (event->keyCode == 220 && script == ScriptType::Off) {
        if (!client.player)
            return;

        if (client.player->selected_target == 0) {
            client.show_message("No heal target selected.", {0xFF, 0, 0});
            return;
        }

        auto target = client.get_mover(client.player->selected_target);
        if (!target) {
            client.show_message("No heal target selected.", {0xFF, 0xFF, 0});
            return;
        }

        std::string str = "I'm going to heal ";
        str.append(client.get_text(target->name));
        str.append("<Lv. ");
        str.append(std::to_string(target->get_level()));
        str.append(">");
        client.show_message(str);

        current_heal_target = client.player->selected_target;
        script = ScriptType::Healer;

        client.show_message("Bot started");
//        std::cout << "Velocity: " << client.player->delta_velocity << std::endl;


//        if (!main_hand || main_hand->item_kind3 != flyff::ItemProperty::Stick) {
//            const auto &stick = find_first_equippable_item(flyff::ItemProperty::Stick);
//
//            if (stick) {
//                std::string str = "Found valid cheerstick: ";
//                str.append(client.get_text(stick->property->name));
//                client.show_message(str, {0, 0xFF, 0});
//
//                use_item_in_inventory(stick->id);
//            }
//        }
        // std::cout << reinterpret_cast<void*>(static_cast<u32>(main_hand)) << " Weapon is required level: " << main_hand->required_level << std::endl;

        // auto length = ((uintptr_t)player->buffs.end() - (uintptr_t)player->buffs.begin()) / 4;
        // std::cout << length << " buffs!" << std::endl;;
        // for(const auto& test : player->buffs) {
        //     std::cout << "Buff with type " << test->get_type() << " and id " << reinterpret_cast<void*>(test->get_id()) << std::endl;

        //     if(test->get_type() == flyff::Mover::Buff::BUFF_SKILL) {
        //         auto skill_property = reinterpret_cast<flyff::SkillProperty*>(
        //             static_cast<flyff::ObjectProperty*>(test->property)
        //         );

        //         std::cout << "BuffSkill "
        //                   << client.get_text(skill_property->name)
        //                   << " ReqLv. " << skill_property->required_level
        //                   << " Lv." << skill_property->level
        //                   << " Id: " << reinterpret_cast<void*>(skill_property->id)
        //                   << " Remaining: " << (test->get_time_remaining() / 1000)
        //                   << std::endl;
        //     }
        // }

        //
        // send_clear_target();
        // auto items = player->inventory.begin();
        // auto length = ((uintptr_t)player->inventory.end() - (uintptr_t)player->inventory.begin()) / 0x60;

        // static constexpr int max_inventory_size = 168;
        // static constexpr int slot_weapon = max_inventory_size + 10;

        // int stick_index = -1;
        // if(items[slot_weapon].id != -1 &&
        //     items[slot_weapon].property &&
        //     items[slot_weapon].property->item_kind3 == flyff::ItemProperty::Stick) {
        //     stick_index = slot_weapon;
        // } else {
        //     for(auto i = 0; i < length; i++) {
        //         if(items[i].id != -1 && items[i].property) {
        //             // if(items[i].id < 168) InventoryItem
        //             // else Equipped

        //             // std::cout << items[i].id << std::endl;
        //             std::cout << client.get_text(items[i].property->name) << " at " << items[i].id << ": "
        //                     << "IK2=" << items[i].property->item_kind2 << ", "
        //                     << "IK3=" << items[i].property->item_kind3
        //                     << std::endl;

        //             if(items[i].property->item_kind3 == flyff::ItemProperty::Stick) {
        //                 stick_index = i;
        //                 break;
        //             }
        //         }
        //     }

        //     if(stick_index != -1) {
        //         // Switch main_hand
        //         use_item_in_inventory(stick_index);
        //     }
        // }

        // if(stick_index != -1) {
        //     // Use skill Heal
        //     send_use_skill(0x2c, 10);
        // } else {
        //     client.show_message("Could not find a stick to heal myself.", { 0xFF, 0, 0 });
        // }
    }

    // BracketLeft
    if (event->keyCode == 219 && script == ScriptType::Off) {
        if (!client.player)
            return;

        if (client.player->selected_target == 0) {
            client.show_message("No monster selected, unable to start bot.", {0xFF, 0, 0});
            return;
        }

        auto target = client.get_mover(client.player->selected_target);
        if (!target) {
            client.show_message("No monster selected, unable to start bot.", {0xFF, 0xFF, 0});
            return;
        }

        std::string str = "I'm only going to kill ";
        str.append(client.get_text(target->name));
        str.append("<Lv. ");
        str.append(std::to_string(target->get_level()));
        str.append(">");
        client.show_message(str);

        object_property_index = target->property_id;
        start_position = {client.player->position.x, client.player->position.y, client.player->position.z};
        std::cout << "Start botting at " << start_position << std::endl;

        script = ScriptType::Attacker;

        client.show_message("Bot started");
    }

    if (event->keyCode == 221) {
        if (script != ScriptType::Off) {
            script = ScriptType::Off;

            client.show_message("Bot stopped");
        } else {
            if (!client.player) return;

//            std::cout << "My job is " << client.player->get_job() << std::endl;

//            client.show_announcement("Hello world!", {0xFF, 0, 0});

            std::cout << "Player object ID: " << std::hex << client.player->object_id << std::dec << std::endl;
            auto current = client.movers.first;
            do {
                const auto &mover = current->mover;

                if (mover->type == flyff::ObjectType::Item) {
                    std::cout << "Item "
                              << std::hex << mover->object_id << std::dec
                              << " at " << mover->position
                              << " (" << std::hex << static_cast<u32>(mover) << std::dec << ")"
                              << std::endl;
                }

                current = current->next;
            } while (current);

//            auto items = client.player->inventory.begin();
//            std::cout << "Player has " << client.player->get_inventory_slots() << " slots" << std::endl;
//            for (auto i = 0; i < client.player->get_inventory_slots(); i++) {
//                if (items[i].property) {
//                    const auto &item = items[i].property;
//
//                    std::cout << client.get_text(item->name) << ": "
//                              << reinterpret_cast<const void *>(static_cast<u32>(item)) << ", "
//                              << std::endl;
//
//                }
//            }
        }
    }
}

flyff::MoverID find_closest_target() {
    auto &neuz = flyff::Neuz::instance();
    auto &client = fugg::Client::instance();

    if (!neuz.player) return 0;

    // Found in the code somewhere.
    static constexpr float max_distance = 144.;

    float lowest_distance = std::numeric_limits<float>::max();
    auto closest = flyff::Pointer<flyff::Mover>(nullptr);

    auto current = neuz.movers.first;
    do {
        const auto &mover = current->mover;

        auto dx = std::abs(mover->position.x - neuz.player->position.x);
        auto dy = std::abs(mover->position.y - neuz.player->position.y) * 5;
        auto dz = std::abs(mover->position.z - neuz.player->position.z);

        auto squared = (dx * dx) + (dy * dy) + (dz * dz);
        auto d = std::sqrt(squared);

        if (mover->type == flyff::ObjectType::Mover &&
            current->hash != neuz.player_object_id &&
            !mover->is_dead() &&
            // Only attack ground mobs.
            mover->is_grounded &&
            // Only attack same mobs.
            mover->property_id == object_property_index &&
            d <= max_distance) {

            if (d < lowest_distance) {
                closest = mover;
                lowest_distance = d;
            }
        }

        current = current->next;
    } while (current);

    if (closest) {
        return closest->object_id;
    }

    return 0;
}

flyff::MoverID find_next_item() {
    const auto &neuz = flyff::Neuz::instance();

    if (!neuz.player) return 0;

    float lowest_distance = 32.f;
    auto closest = flyff::Pointer<flyff::Mover>(nullptr);

    auto current = neuz.movers.first;
    do {
        const auto &mover = current->mover;

        auto dx = std::abs(mover->position.x - neuz.player->position.x);
        auto dz = std::abs(mover->position.z - neuz.player->position.z);

        auto squared = (dx * dx) + (dz * dz);
        auto d = std::sqrt(squared);

        if (mover->world != 0 &&
            !mover->is_despawned &&
            mover->type == flyff::ObjectType::Item &&
            d <= lowest_distance) {

            auto *prop = reinterpret_cast<const flyff::ItemProperty *>(
                    static_cast<const flyff::ObjectProperty *>(mover->property)
            );

            if (!prop->is_quest()) {
                closest = mover;
                lowest_distance = d;
            }
        }

        current = current->next;
    } while (current);

    if (closest)
        return closest->object_id;
    else
        return 0;
}

void interact(flyff::MoverID id, uint32_t action) {
    const auto &client = fugg::Client::instance();

    if (!client.player)
        return;

    client.player->selected_target = id;
    client.send_set_target(id);

    client.player->move_toward_target = id;
    client.send_interact_target(action);
}

flyff::MoverID current_target = 0;
flyff::MoverID current_pickup_item = 0;

void attacker_script() {
    const auto &neuz = flyff::Neuz::instance();
    const auto &client = fugg::Client::instance();

    if (!neuz.player)
        return;

    if (neuz.player->is_dead()) {
        neuz.show_message("Player is dead, stopping...", {0xFF, 0x00, 0x00});
        script = ScriptType::Off;
        return;
    }

    const auto &player = neuz.player;

    // if is_idle:
    //      if item_target:
    //          pickup_item()
    //      elif attacK_target:
    //          attack_target()

    if (neuz.player->get_hp_percent() != 100) {

        auto items = player->inventory.begin();
        int32_t smallest_difference = std::numeric_limits<int32_t>::max();
        auto food_item = flyff::Pointer<flyff::InventoryItem>(nullptr);
        for (auto i = 0; i < player->get_inventory_slots(); i++) {
            const auto &item_property = items[i].property;
            if (item_property &&
                player->can_use_item(item_property) &&
                item_property->is_food()) {
                auto hp = client.player->get_hp();
                auto max_hp = client.player->get_max_hp();
                // How much HP is left over after healing with this item.
                int32_t difference = max_hp - (hp + item_property->heal_hp);

                if (difference >= 0 && std::abs(difference) < std::abs(smallest_difference)) {
                    food_item = flyff::Pointer<flyff::InventoryItem>(&items[i]);
                    smallest_difference = difference;
                }
            }
        }

        if (food_item) {
            std::string str = "Eating ";
            str.append(neuz.get_text(food_item->property->name));
            str.append(" because it has the smallest difference of ");
            str.append(std::to_string(smallest_difference));

            neuz.show_message(str);

            client.send_use_item_in_inventory(food_item->index);
        }
    }

    if (neuz.player->get_move_state() == flyff::Object::MoveState::Stand &&
        !neuz.player->is_attacking()) {


        // Check buffs.
        static constexpr flyff::SkillID buffs_to_check[] = {
//                flyff::SkillID::MentalSign,
//                flyff::SkillID::HeapUp,
//                flyff::SkillID::QuickStep,
//                flyff::SkillID::Patience,
//                flyff::SkillID::Haste,
//                flyff::SkillID::BeefUp,
//                flyff::SkillID::Accuracy,
//                flyff::SkillID::CatsReflex,
//                flyff::SkillID::CannonBall,
        };

        auto item = neuz.get_mover(current_pickup_item);
        auto monster = neuz.get_mover(current_target);

        if (neuz.player->get_job() == flyff::JobID::Assist || neuz.player->get_job() == flyff::JobID::Billposter) {
            const auto &main_hand = player->get_equipped_item(flyff::Part::MainHand);
            const auto &off_hand = player->get_equipped_item(flyff::Part::Shield);

//            auto buffs = check_rebuff(neuz.player, neuz.player, buffs_to_check);
//            if (!buffs.empty() && (!item || item->is_despawned) && (!monster || monster->is_dead())) {
//                const auto skill_index = buffs.back();
//                if (!main_hand || main_hand->item_kind3 != flyff::ItemKind3::Stick) {
//                    const auto &stick = find_first_equippable_item(flyff::ItemKind3::Stick);
//
//                    if (stick) {
//                        buffs.pop_back();
//
//                        client.send_use_item_in_inventory(stick->index);
//                        client.send_use_skill(skill_index);
//                        return;
//                    } else {
//                        neuz.show_message("Unable to find a stick", {0xFF, 0, 0});
//                    }
//                } else {
//                    buffs.pop_back();
//                    client.send_use_skill(skill_index);
//                    return;
//                }
//            }
//
//            if (buffs.empty()) {
            if (!main_hand || main_hand->item_kind3 != flyff::ItemKind3::Knuckle) {
                const auto &knuckle = find_first_equippable_item(flyff::ItemKind3::Knuckle);

                if (knuckle) {
                    client.send_use_item_in_inventory(knuckle->index);
                } else {
                    neuz.show_message("Unable to find a knuckle", {0xFF, 0, 0});
                }
            }

            if (!off_hand || off_hand->item_kind3 != flyff::ItemKind3::Shield) {
                const auto &shield = find_first_equippable_item(flyff::ItemKind3::Shield);

                if (shield) {
                    client.send_use_item_in_inventory(shield->index);
                } else {
                    neuz.show_message("Unable to find a shield", {0xFF, 0, 0});
                }
            }

            if (!check_rebuff(neuz.player, neuz.player, {flyff::SkillID::Asmodeus}).empty()) {
                client.send_use_skill(flyff::SkillID::Asmodeus);
                return;
            }
//            }
        }

        if (item && !item->is_despawned && neuz.player->move_toward_target == 0) {
            // Pickup
            interact(item->object_id, 1);
        } else if (monster && !monster->is_dead() && neuz.player->move_toward_target == 0) {
            // Attack
            interact(monster->object_id, 1);
            // neuz.player->attack_target = monster->object_id;
        } else if ((!item || item->is_despawned) &&
                   (!monster || monster->is_dead())) {
            std::cout << "No item, no target. Searching..." << std::endl;

            // First check all mobs if they attacked me (aggro ones).
            for (const auto &id: client.player->enemies) {
                const auto &mover = client.get_mover(id);

                if (mover) {
                    std::string str = "Attacking ";
                    str.append(mover->name);
                    str.append(" because it is my enemy.");

                    current_target = id;
                    interact(id, 1);

                    neuz.show_message(str);
                    return;
                }
            }
            // No item target and no attack target
            current_pickup_item = find_next_item();
            item = neuz.get_mover(current_pickup_item);
            if (item) {
                auto *prop = reinterpret_cast<const flyff::ItemProperty *>(
                        static_cast<const flyff::ObjectProperty *>(item->property)
                );

                std::string str = "Picking up ";
                str.append(neuz.get_text(prop->name));

                interact(item->object_id, 1);

                neuz.show_message(str);
                return;
            } else {
                current_target = find_closest_target();

                monster = neuz.get_mover(current_target);
                if (monster) {
                    std::string str = "Attacking ";
                    str.append(monster->name);

                    interact(current_target, 1);

                    neuz.show_message(str);
                } else {
                    neuz.show_message("Unable to find target.");
                }
            }
        }
    }
}


void healer_script() {
    const auto &client = fugg::Client::instance();

    if (!client.player)
        return;

    if (client.player->is_dead()) {
        client.show_message("Player is dead, stopping...", {0xFF, 0x00, 0x00});
        script = ScriptType::Off;
        return;
    }

    if (client.player->get_move_state() == flyff::Object::MoveState::Stand &&
        !client.player->is_attacking()) {

        if (client.player->selected_target != current_heal_target) {
            client.send_set_target(current_heal_target);
            client.player->selected_target = current_heal_target;

            client.show_message("Selected target");
        }

        if (client.player->follow_target != current_heal_target) {
            client.send_follow_target(current_heal_target);
            client.player->follow_target = current_heal_target;

            client.show_message("Following target");
        }

        const auto &target = client.get_mover(current_heal_target);
        if (target) {
            // If the target has been hit, heal him.
            if (target->get_hp_percent() < 100) {
                client.show_message("Healing target");

                client.send_use_skill(flyff::SkillID::Heal);
                return;
            }
            // If I'm hit, heal myself.
            if (client.player->get_hp_percent() < 75) {
                client.send_clear_target();
                client.player->selected_target = 0;

                client.send_use_skill(flyff::SkillID::Heal);
                return;
            }
            // Check for rebuff on the target
            {
                // For target, check all buffs.
                static constexpr flyff::SkillID buffs_to_check[] = {
                        flyff::SkillID::MentalSign,
                        flyff::SkillID::HeapUp,
                        flyff::SkillID::QuickStep,
                        flyff::SkillID::Patience,
                        flyff::SkillID::Haste,
                        flyff::SkillID::BeefUp,
                        flyff::SkillID::Accuracy,
                        flyff::SkillID::CatsReflex,
                        flyff::SkillID::CannonBall,
                };

                auto buffs = check_rebuff(client.player, target, buffs_to_check);
                if (!buffs.empty()) {
                    auto skill_index = buffs.back();
                    buffs.pop_back();

                    const auto &skill = fugg::Client::get_skill_property(skill_index);
                    std::string str = "Buffing target with";
                    str.append(client.get_text(skill->name));
                    client.show_message(str);

                    client.send_use_skill(skill_index);
                    return;
                }
            }
            // Check for rebuff on yourself
            if (!target->is_in_combat(1500)) {
                // For FS itself, only these selected buffs.
                static constexpr flyff::SkillID buffs_to_check[] = {
                        flyff::SkillID::MentalSign,
                        flyff::SkillID::QuickStep,
                        flyff::SkillID::Patience,
                        flyff::SkillID::HeapUp,
                        flyff::SkillID::Haste,
                        flyff::SkillID::Prevention
                };

                auto buffs = check_rebuff(client.player, client.player, buffs_to_check);
                if (!buffs.empty()) {
                    auto skill_index = buffs.back();
                    buffs.pop_back();

                    client.send_clear_target();
                    client.player->selected_target = 0;

                    client.send_use_skill(skill_index);
                    return;
                }
            }
        } else {
            client.show_message("Can't find target!", {0xFF, 0, 0});
        }
    }
}

void before_main_loop() {
}

void after_main_loop() {
    const auto &client = fugg::Client::instance();

    // TODO: Fix player getting stuck.
    // - Detect player stuck behind tree.
    // - Try one of the following methods:
    //      - See if you can glitch through objects by moving the player forward by ~15% of the speed.
    //      - Jump

    // TODO: Fix player trying to pick up items that are someone elses.

//    if (client.player && client.player->is_moving() && !client.player->is_jumping()) {
//        std::cout << "Sending jump packet!" << std::endl;
//
//        client.send_motion_packet(0x2, *reinterpret_cast<const uint32_t*>(&client.player->rotation.y));
//        client.player->set_jump_state(flyff::Object::JumpState::Jump);
//
//        std::cout << "New state: " << reinterpret_cast<void*>(client.player->object_state) << std::endl;
//    }


//    if (client.player && client.player->is_moving()) {
////        std::cout << "Player is " << client.player->is_grounded << std::endl;
////        std::cout << client.player->get_attack_state() << std::endl;
//        std::cout << "Speed: " << client.player->get_speed() << std::endl;
//    }



    switch (script) {
        case ScriptType::Attacker:
            attacker_script();
            break;

        case ScriptType::Healer:
            healer_script();
            break;

        default:
            // Reset current target is not running.
            current_target = 0;
            current_pickup_item = 0;

            break;
    }
    // std::string test;

    // auto& music_properties = fugg::module_ref<fugg::Vector<flyff::MusicProperty>>(0x00034f7c);
    // auto& item_properties = fugg::module_ref<fugg::Vector<flyff::ItemProperty>>(0x00035194);

    // for(auto& item : item_properties) {
    //     std::cout << item.some_string->c_str() << std::endl;
    // }

    // auto& player_pointer = fugg::module_ref<uintptr_t>(0x0003546c);

    // if(player_pointer != 0) {
    //     auto& player = fugg::module_ref<flyff::Mover>(player_pointer);

    //     std::cout << "Player pos: " 
    //               << player.position.x << ", "
    //               << player.position.y << ", "
    //               << player.position.z << std::endl;

    //     // std::cout << "Current target: " << player.selected_target << std::endl;
    //     std::cout << "Current animation: " << player.current_animation << std::endl;
    //     std::cout << "Name: " << player.name << std::endl;

    //     const int bla = offsetof(Mover, name);
    //     const int test = offsetof(flyff::Mover, name);

    //     static_assert(bla == test, "OMG");

    //     // static_assert(offsetof(Mover, name) == 2020, "ASD");

    //     if(player.selected_target != 0) {
    //         struct ObjectHolder {
    //             uintptr_t next;
    //             unsigned int list_index;
    //             unsigned long long hash;
    //             uintptr_t mover;
    //         };

    //         auto& selected_target_holder = fugg::module_ref<ObjectHolder>(
    //             flyff::api::w2c_f208(player.selected_target)
    //         );

    //         auto& selected_target = fugg::module_ref<Mover>(
    //             selected_target_holder.mover
    //         );

    //         std::cout << "(" << reinterpret_cast<void*>(selected_target_holder.mover) << ") Target: " 
    //                   << selected_target.name->c_str() << " at " 
    //                   << selected_target.world_position.x << ", "
    //                   << selected_target.world_position.y << ", "
    //                   << selected_target.world_position.z << std::endl;

    //     } else {
    //         std::cout << "Got no target." << std::endl;
    //     }
    // }

    // auto& item_properties = fugg::module_ref<fugg::Vector<flyff::ItemProperty>>(0x00035194);
    // std::cout << "Expecting " << reinterpret_cast<void*>(fugg::wasm::kMemory.address_of(0x00035194)) << std::endl;

    // std::cout << "Item has " << item_properties->size() << std::endl;

    // std::cout << "There are " << item_properties->size() << " item properties." << std::endl;
    // for(const auto& item : *item_properties) {
    //     std::cout << "Item with id " << item.translation_tag.tag << std::endl;        
    // }

    // auto test = reinterpret_cast<Pointer<int>*>(0x00035194);





    // std::cout << "Test is at " << reinterpret_cast<void*>(test->to_intptr_t()) << std::endl;

    // if((void*)test == (void*)0x00035194) {
    //     std::cout << "Test is at the right address!" << std::endl;
    // }

    // auto vector_location = fugg::ModulePointer<uintptr_t> { 0x00035194 };
    // auto ptr = fugg::ModulePointer<ThisPointerTest> { 0x00035194 };

    // std::cout << "vector_location is at " << reinterpret_cast<void*>(0x00035194) << std::endl;
    // std::cout << "ptr is at " << 

    // auto raw = new RawVector {
    //     .begin          = **fugg::ModulePointer<fugg::ModulePointer<uintptr_t>> { 0x00035194 + 0 },
    //     .end            = **fugg::ModulePointer<fugg::ModulePointer<uintptr_t>> { 0x00035194 + 4 },
    //     .end_capacity   = **fugg::ModulePointer<fugg::ModulePointer<uintptr_t>> { 0x00035194 + 8 },
    // };

    // auto unique = std::unique_ptr<RawVector>(raw);
    // auto vec = reinterpret_cast<std::vector<flyff::ItemProperty>*>(raw);

    // auto unique = std::unique_ptr<std::vector<flyff::ItemProperty>>(vec);

    // std::cout << "There are " << unique->size() << " items!" << std::endl;

    // Probably deconstructed here.

    // std::cout << "Constructed raw. Call raw->temp..." << std::endl;

    // std::cout << raw->temp << std::endl;

    // std::cout << "Done calling raw->temp" << std::endl;




}