#include "mesh.hpp"
#include "shader.hpp"
#include "buffer.hpp"
#include "txt.hpp"
#include "gameworld.hpp"
#include "path.hpp"
#include "transform.hpp"
#include "tile.hpp"
#include "input.hpp"
#include "ogl.hpp"
#include "common.hpp"


static bool process_inputs_and_move(Worlds& worlds, const Mesh& mesh, Input& input, const LookAt& lookat, int origin_x, int origin_y) {
    static ShortestPathFac* curr_path = NULL;
    bool moved = false;

    int poskey_x, poskey_y;
    double click_x, click_y;
    float click_z;
    if (input.getPosKey(poskey_x, poskey_y)) { // arrow keys have highest precedence, aborting paths and clearing all other pending clicks
        safe_delete(curr_path);
        while (input.getClick(click_x, click_y, click_z)) {}
        moved = worlds.world->player_step(poskey_x, poskey_y) ?: worlds.world->player_push(poskey_x, poskey_y);
    } else if (input.getClick(click_x, click_y, click_z) && worlds.world->world_get()->autoroute()) {
        GLfloat iso_x, iso_y;
        lookat.unproject(click_x, click_y, click_z, iso_x, iso_y);
        unsigned to_x = (unsigned)MAX(0, MIN((int)worlds.world->w-1, (int)mesh.iso2tile_x(iso_x) + origin_x));
        unsigned to_y = (unsigned)MAX(0, MIN((int)worlds.world->h-1, (int)mesh.iso2tile_y(iso_y) + origin_y));
        safe_delete(curr_path);
        curr_path = new ShortestPathFac(worlds.world->world_get(), to_x, to_y, worlds.world->x, worlds.world->y);
        Logger::log("%u-%u -> %u-%u", worlds.world->x, worlds.world->y, to_x, to_y);
    }

    if (curr_path) {
        unsigned to_x, to_y;
        if (!curr_path->get()) {
            // not yet calculated - noop
        } else if (curr_path->get()->step(worlds.world->x, worlds.world->y, to_x, to_y) && worlds.world->player_step(to_x-worlds.world->x, to_y-worlds.world->y)) {
            moved = true;
        } else {
            safe_delete(curr_path);
        }
    }

    if (moved) {
        const tile_t& tile = worlds.world->world_get()->at(worlds.world->x, worlds.world->y);
        switch (tile.action) {
            case tile_t::TILE_ACTION_GOTO:
                if (worlds.world->player_goto(tile.action_arg2[0], tile.action_arg2[1])) {
                    safe_delete(curr_path);
                }
                break;
            case tile_t::TILE_ACTION_FINISH:
                Logger::log("Great Success!!1!");
                input.shouldClose();
                safe_delete(curr_path);
                break;
            case tile_t::TILE_ACTION_SWITCH:
                if (worlds.all_done()) {
                    Logger::log("All Done: Great Success!!1!");
                    input.shouldClose();
                }
                if (worlds.set(tile.action_arg1)) {
                    safe_delete(curr_path);
                }
                break;
            default:
                break;
        }
    }

    return moved;
}


static void draw_scene(const World* world, const Mesh& mesh, const TileMap& tilemap, GLint uniTileSpec, GLint uniModelType, unsigned ticks, int& origin_x, int& origin_y) {
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); // depth buffer needed, color buffer only for logger. TODO: does it hurt?

    unsigned player_x=world->x, player_y=world->y;
    origin_x = player_x - ((mesh.w-1)/2); // viewing radius
    origin_y = player_y - ((mesh.h-1)/2);
    for (unsigned y=0; y<mesh.h; ++y) {
        int world_y = (int)y + origin_y;
        if (world_y < 0) continue;
        if ((unsigned)world_y >= world->h) break;
        for (unsigned x=0; x<mesh.w;) {
            int world_x = (int)x + origin_x;
            if (world_x < 0) { x++; continue; }
            if ((unsigned)world_x >= world->w) break;
            bool is_player = (unsigned)world_x==player_x && (unsigned)world_y==player_y;

            unsigned l, b;
            const tile_t& tile = is_player? tile_defaults[tile_t::TILE_PLAYER]: world->at(world_x, world_y, l, b);
            l = MIN(l, mesh.w-x);
            if (is_player) {
                l = 1;
            } else if (player_y == (unsigned)world_y && player_x > (unsigned)world_x) {
                l = MIN(l, player_x-world_x);
            }

            glUniform4fv(uniTileSpec, 1,
                tilemap.tilespec(tile.tile, ticks/2) // TODO: could do this in shader, too
            );
            glUniform2ui(uniModelType, tile.model, is_player? 0: b);

            mesh.draw(y, x, l);
            x += l;
        }
    }
}


int main(int argc, char** argv) {
    // Args
    srand(time(NULL));
    uint32_t seed = rand();
    bool fullscreen = true;
    unsigned arg_w=0, arg_h=0;
    for (int i=1; i<argc; ++i) {
        if (strcmp(argv[i], "--window") == 0) {
            fullscreen = false;
        } else if (sscanf(argv[i], "--window=%ux%u", &arg_w, &arg_h) == 2) {
            fullscreen = false;
        } else if (sscanf(argv[i], "--seed=%"SCNu32, &seed) == 1) {
        } else {
            LOG("Cannot parse arg '%s'", argv[i]);
            return 1;
        }
    }

    // initialize libs and create main window
    Window* window = Window::getInst(arg_w, arg_h, fullscreen, argv[0]);
    if (!window || glerr()) return 1;
    LOG("created %ux%u window", window->w, window->h);

    // implicitly create/bind logging/font stuff now
    Logger* logger = Logger::getInst();
    assert(logger->blocked); // do not mess with our main initialization
    LOG("loaded overlay shaders");

    // create program and attach shaders
    Program* program = Program::getInst(FILE_PREFIX "/shaders");
    if (!program) {
        return 1;
    }
    glLinkProgram(*program);
    glUseProgram(*program);
    LOG("loaded main shaders");

    // Generate our world
    Mesh mesh(window, 75);
    LOG("created %u (%ux%u) mesh (%u vertices, %zuB)", mesh.w*mesh.h, mesh.w, mesh.h, mesh.vertices_num, mesh.vertices_len);
    Worlds worlds;
    std::vector<Layout*> gworlds;
    GameWorld::gen(seed, 100, 100, 200, 200, gworlds);
    assert(!gworlds.empty());
    for (std::vector<Layout*>::iterator it=gworlds.begin(); it!=gworlds.end(); ++it) {
        worlds.push((*it)->id, new World(*it));
    }
    worlds.set(gworlds.front()->id);
    gworlds.clear();
    LOG("generated %ux%u world (seed %u)", worlds.world->w, worlds.world->h, seed);

    // Create a Vertex Buffer & Array Object first and copy the vertex data to it
    ArrayBuffer vbo(4);
    vbo.bind(mesh.vertices, mesh.vertices_num, GL_STATIC_DRAW);
    vbo.setAttrib(program, "posVI", 0, 2); // set input format acc. to GL_ARRAY_BUFFER layout
    vbo.setAttrib(program, "texPosVI", 2, 2);
    // Create an element array
    Buffer ebo(GL_ELEMENT_ARRAY_BUFFER);
    ebo.bind(mesh.elements, mesh.elements_len, GL_STATIC_DRAW);
    LOG("created buffers");

    // check for texture dimensions
    const unsigned tile_px_x = window->iso2px_x(mesh.tile2iso_x(1)) - window->iso2px_x(mesh.tile2iso_x(0));
    const unsigned tile_px_y = window->iso2px_y(mesh.tile2iso_y(1)) - window->iso2px_y(mesh.tile2iso_y(0));
    LOG("flat tiles @ ~%ux%upx", tile_px_x, tile_px_y);

    // load tilemap
    TileMap tilemap(FILE_PREFIX "/textures/tilemap.ppm", 20, 1, 1);
    GLint texloc = glGetUniformLocation(*program, "tex_tilemap");
    if (texloc != -1) {
        glUniform1i(texloc, tilemap.index());
    }
    LOG("loaded tilemap");

    // find some uniforms needed lateron
    GLint uniPlayerPos = glGetUniformLocation(*program, "playerPos");
    GLint uniTileSpec = glGetUniformLocation(*program, "tilespec");
    GLint uniTime = glGetUniformLocation(*program, "time");
    GLint uniModelType = glGetUniformLocation(*program, "modelType");

    // setup camera transformation
    GLint uniOffset = glGetUniformLocation(*program, "offset");
    GLint uniModel = glGetUniformLocation(*program, "model");
    GLint uniView = glGetUniformLocation(*program, "view");
    GLint uniProj = glGetUniformLocation(*program, "proj");
    LookAt lookat(window, uniModel, uniView, uniProj);
    // as we have fixed coordinates now, the player is always at 0,0. don't using stencils atm to save cpu time, too.
    int cam_angle = lookat.project(45); // keep looking at origin, we redraw all tiles in the mesh atm
    if (uniPlayerPos != -1) {
        assert(window->w >= window->h);
        glUniform3f(
            uniPlayerPos,
            0.0f, 0.0f,
            (float)window->w/(float)window->h // shadow factor for x-radius to avoid ellipsis
        );
    }
    LOG("initialized viewport");

    // define framerate timings etc.
    const double tick_interval = 1.0f/60.0f; // 100 fps at max (for animations)
    const double move_interval = 1.0f/10.0f; // 10 movements/s at max
    double last_tick = 0.0;
    double last_move = 0.0;
    double sleep_total=0.0, sleep_min=1.0, sleep_max=0.0;
    const bool draw_ticks = true;
    unsigned ticks=0;
    // track current animation steps in each direction (one will be 0 atm, though)
    const unsigned animation_ticks = floor(move_interval/tick_interval);
    const float animation_step_x = (mesh.tile2iso_x(1) - mesh.tile2iso_x(0)) / (float)animation_ticks;
    const float animation_step_y = (mesh.tile2iso_y(1) - mesh.tile2iso_y(0)) / (float)animation_ticks;
    int animation_x=0, animation_y=0;
    LOG("running at %.2f frames (%u animations)", round(1.0/tick_interval), animation_ticks);

    // Finish for main loop
    logger->blocked = false; // can now write to window, we will re-set active program if needed
    Input input(window);
    int origin_x=0, origin_y=0;
    LOG("starting up...");

    // main loop
    while (!window->shouldClose()) {
        // check frame timimgs
        double this_tick = glfwGetTime();
        double tick_delay = tick_interval - (this_tick - last_tick);
        if (tick_delay > 0.0f) {
            sleep_total += tick_delay;
            sleep_max = MAX(sleep_max, tick_delay);
            sleep_min = MIN(sleep_min, tick_delay);
            usleep(floor(tick_delay * 1000000.0f)); // microsecs
            this_tick = glfwGetTime();
        } else if (ticks != 0){
            LOG("losing frames!");
            sleep_min = 0.0;
        }
        if (uniTime != -1) {
            glUniform1ui(uniTime, (unsigned)floor(this_tick * 1000.0f)); // millisecs
        }
        last_tick = this_tick;
        ticks++;

        // backup current player position
        World* last_world = worlds.world;
        unsigned last_player_x=worlds.world->x, last_player_y=worlds.world->y;

        // Check for existing task or new user input
        bool moved = false;
        if (!animation_x && !animation_y && last_move <= this_tick - move_interval) {
            moved = process_inputs_and_move(worlds, mesh, input, lookat, origin_x, origin_y);
            if (moved) {
                last_move = this_tick;
            }
        }

        // check for perspective changes more often, after unproject above
        int panned = 0;
        if (input.getPanKey(panned)) {
            cam_angle = lookat.project(cam_angle + panned);
        }

        // update current position overlay if needed
        if (moved || panned) {
            Logger::log("%u-%u (%d\xb0)", worlds.world->x, worlds.world->y, cam_angle);
        }

        // setup animation if we just moved by 1 tile
        if (moved && worlds.world == last_world) {
            assert(!animation_x && !animation_y);
            int dir_x = worlds.world->x - last_player_x;
            int dir_y = worlds.world->y - last_player_y;
            if (abs(dir_x) + abs(dir_y) == 1) {
                animation_x = dir_x * animation_ticks;
                animation_y = dir_y * animation_ticks;
            }
        }

        // set camera offset acc. to a running animation
        if (animation_x || animation_y) {
            glUniform2f(uniOffset, (float)animation_x * animation_step_x, (float)animation_y * animation_step_y);
            if (animation_x < 0) { animation_x++; } else if (animation_x > 0) { animation_x--; }
            if (animation_y < 0) { animation_y++; } else if (animation_y > 0) { animation_y--; }
        } else {
            glUniform2f(uniOffset, 0.0f, 0.0f);
        }

        // draw mesh
        bool drawn = draw_ticks || moved || panned || ticks == 1;
        if (drawn) {
            draw_scene(worlds.world, mesh, tilemap, uniTileSpec, uniModelType, ticks, origin_x, origin_y);
        }

        // log new messages
        if (logger->logall(window, drawn)) { // redraw if we did sth before
            glUseProgram(*program);
            vbo.bind();
            ebo.bind();
            drawn = true;
        }

        // draw & collect new input
        if (drawn) {
            glfwSwapBuffers(window->window);
        }
        glfwPollEvents();
    }
    LOG("exiting after %u loops/draws", ticks);
    LOG("slept %f avg (%f min, %f max) of %f", sleep_total/(double)ticks, sleep_min, sleep_max, tick_interval);
    sleep(1);

    // TODO: cleanup
    delete program;
    delete window;
    return 0;
}