#include "Client.hpp"

#include "render/index.hpp"
#include "render/UI.hpp"

#include "InputMap.hpp"
#include "world/index.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include "../core/data/math.hpp"
#include <Tracy.hpp>

Client::Client(config::client::options& options): options(options) { }
Client::~Client() { }

void Client::run(server_handle* const localHandle) {
    if (!render::Load(window, options.preferVulkan, options.renderer, options.window))
        return;

    InputMap inputs(window.getPtr());
    Controllable player(window.getPtr(), inputs, options.control);
    Camera camera(&player, options.camera);

    auto pipeline = render::Renderer::Get();
    pipeline->LightInvDir = glm::normalize(glm::vec3(-.5f, 2, -2));
    render::Renderer::Get()->loadUI(window);

    auto world = world::client::Load(options.connection, localHandle, options.world, options.contouring);
    state.contouring = world->getContouring();
    world->onTeleport = [&](voxel_pos pos) {
        state.position = player.position = pos;
    };
    world->onMessage = [&](const std::string &text) {
        // MAYBE: rolling buffer
        const auto yellow = std::make_optional(0xFF43F5F5);
        state.console.lines.push_back(state::state::line{text, text[0] == '>' ? yellow : std::nullopt});
    };

    do {
        window.startFrame();
        FrameMark;
        { // Update
            ZoneScopedN("Update");
            static double lastTime = window.getTime();
            const double partTime = window.getTime();
            const float deltaTime = partTime - lastTime;
            inputs.toggle(state.capture_mouse, Input::Mouse);
            inputs.toggle(options.debugMenu.bar, Input::Debug);

            player.capture(state.capture_mouse, !render::UI::IsFocus(), deltaTime);
            if(player.velocity != glm::vec3(0) && (
                !options.control.collide ||
                !world->collide(player.position, player.velocity, options.voxel_density, options.voxel_density))
            ) {
                player.position += player.velocity;
                const auto pos = player.position.as_voxel(options.voxel_density);
                if(pos != state.position.as_voxel(options.voxel_density)) {
                    world->emit(world::action::Move(pos));
                }
                state.position = pos;
            }
            camera.update();
            pipeline->lookFrom(camera);
            pipeline->LightInvDir = glm::vec3(glm::rotate(glm::mat4(1), deltaTime * .1f, glm::vec3(1, .5, .1)) * glm::vec4(pipeline->LightInvDir, 0));

            {
                const auto ray_result = world->raycast(camera.getRay() * options.voxel_density);
                if(auto target = std::get_if<world::Universe::ray_target>(&ray_result)) {
                    state.look_at = *target;
                    const auto &tool = options.editor.tool;
                    state.can_fill = world->isAreaFree(target->pos, world::action::ToGeometry(tool.shape), tool.radius);
                } else {
                    state.look_at = {};
                }
            }
            if (state.capture_mouse) {
                if (state.look_at.has_value()) {
                    ZoneScopedN("Edit");
                    const auto &tool = options.editor.tool;
                    if (inputs.isPressing(Mouse::Left))
                        world->emit(world::action::FillShape(
                            state.look_at.value().pos, world::Voxel(world::materials::AIR, tool.emptyAir * world::Voxel::DENSITY_MAX), tool.shape, tool.radius));
                    else if (const auto voxel = world::Voxel(tool.material, world::Voxel::DENSITY_MAX);
                        inputs.isPressing(Mouse::Right) && (state.can_fill || !voxel.is_solid()))
                        world->emit(world::action::FillShape(
                            state.look_at.value().pos, voxel, tool.shape, tool.radius));
                }
                if (inputs.isDown(Input::Throw)) {
                    //FIXME: register entity type world->addEntity(entity_id(0), {state.position * options.voxel_density, glm::vec3(10, 0, 0)});
                }
            }
            world->update(state.position.as_voxel(options.voxel_density), deltaTime);
            inputs.saveKeys();
            lastTime = partTime;
        }

        {
            ZoneScopedN("UI");
            const auto actions = render::UI::Get()->draw(options, state, reports);
            if (actions && render::UI::Actions::FPS) {
                window.setTargetFPS(options.window.targetFPS);
                pipeline->setVSync(options.window.targetFPS < Window::MIN_FPS);
            }
            if (actions && render::UI::Actions::FullScreen) {
                window.setFullscreen(options.window.fullscreen);
            }
            if(actions && render::UI::Actions::ClearColor) {
                pipeline->setClearColor(options.renderer.clear_color);
            }
            if(actions && render::UI::Actions::RendererSharders) {
                pipeline->reloadShaders(options.renderer.voxel);
            }
            if(actions && render::UI::Actions::RendererTextures) {
                pipeline->reloadTextures(options.renderer.textures, options.renderer.getMipmapLodBias(), options.renderer.getAnisotropy());
            }
            if(actions && render::UI::Actions::World) {
                //FIXME: server options world->setOptions(options.world);
            }
            if(actions && render::UI::Actions::Camera) {
                camera.setOptions(options.camera);
            }
            if(actions && render::UI::Actions::Control) {
                player.setOptions(options.control);
            }
            if(actions && render::UI::Actions::FillMode) {
                pipeline->setFillMode(options.renderer.wireframe);
            }
            if(actions && render::UI::Actions::Message) {
                char *s = state.console.buffer.data();
                //Strtrim(s);
                //if (s[0])
                //    ExecCommand(s);
                world->emit(world::action::Message(s));
                strcpy(s, "");
            }
        }
        { // Rendering
            ZoneScopedN("Render");
            pipeline->beginFrame();

            reports.models_count = 0;
            reports.tris_count = 0;
            std::optional<geometry::Frustum> frustum;
            if(options.culling <= 0) {
                frustum = {camera.getFrustum()};
            }
            const auto offset = state.position.raw_as_long();
            std::vector<glm::vec3> occlusion;
            if (options.culling > 0) {
                const auto ratio = options.culling * 2;
                occlusion.reserve(glm::pow2(ratio * 2 - 1));
                const auto [ch, cv] = player.getAngles();
                const auto max_v = tan(options.camera.fov / 2.), max_h = Window::RATIO * max_v;
                for(int iv = -ratio + 1; iv < ratio; iv++) {
                    const auto v = cv + max_v * iv / ratio;
                    for(int ih = -ratio + 1; ih < ratio; ih++) {
                        const auto h = ch + max_h * ih / ratio;
                        occlusion.emplace_back(cos(v) * sin(h), sin(v), cos(v) * cos(h));
                    }
                }
            }
            { // Solid
                const auto pass = pipeline->beginWorldPass(true);
                const auto draw = [&](glm::mat4 model, render::LodModel *const buffer, const contouring::Abstract::area_info &area, const voxel_pos &pos) {
                    reports.models_count++;
                    reports.tris_count += pass(buffer, model, glm::vec4(pos, std::get<1>(area)), std::get<2>(area));
                };
                if (options.culling > 0) {
                    state.contouring->getModels(draw, player.position, options.camera.far_dist, occlusion, offset, options.voxel_density, true);
                } else {
                    state.contouring->getModels(draw, frustum, offset, options.voxel_density, true);
                }
            }
            { // Entities
                const auto pass = pipeline->beginEntityPass();
                const auto draw = [&](size_t idx, const std::vector<glm::mat4> &models) {
                    if(const auto buffer = state.contouring->getEntityModel(idx)) {
                        reports.models_count += models.size();
                        reports.tris_count += pass(buffer, models);
                    }
                };
                world->getEntitiesModels(draw, frustum, offset, options.voxel_density);
            }
            {
                const auto pass = pipeline->beginIndicatorPass();
                if(state.look_at.has_value()) { // Indicator
                    const auto model = glm::scale(glm::translate(glm::scale(glm::mat4(1), 1.f / glm::vec3(options.voxel_density)), glm::vec3(state.look_at.value().pos.second + state.look_at.value().offset - offset * glm::llvec3(options.voxel_density)) - glm::vec3(.5 + options.editor.tool.radius)), glm::vec3(1 + options.editor.tool.radius * 2));
                    const auto color = state.can_fill ? glm::vec4(1, 1, 1, .5) : (world::materials::solidity[options.editor.tool.material] ? glm::vec4(1, 0, 0, .5) : glm::vec4(1, .5, 0, .5));
                    reports.models_count++;
                    reports.tris_count += pass(model, options.editor.tool.shape, color);
                }
            }
            if (options.renderer.voxel.transparency) {
                const auto pass = pipeline->beginWorldPass(false);
                const auto draw = [&](glm::mat4 model, render::LodModel *const buffer, const contouring::Abstract::area_info &area, const voxel_pos &pos) {
                    reports.models_count++;
                    reports.tris_count += pass(buffer, model, glm::vec4(pos, std::get<1>(area)), std::get<2>(area));
                };
                if (options.culling > 0) {
                    state.contouring->getModels(draw, player.position, options.camera.far_dist, occlusion, offset, options.voxel_density, false);
                } else {
                    state.contouring->getModels(draw, frustum, offset, options.voxel_density, false);
                }
            }
            pipeline->postProcess();
            render::UI::Get()->render();
            pipeline->endFrame();
        }

        { // Swap buffers
            ZoneScopedN("Swap");
            pipeline->swapBuffer(window);
            inputs.poll();
        }

        window.waitTargetFPS();
    } while (!(inputs.isDown(Input::Quit) || window.shouldClose() || !world->isRunning()));

    options.contouring = state.contouring->getOptions();
    world.reset();

    render::Renderer::Unload();
    window.destroy();
}