#include <errno.h>
#include <unistd.h>
#include "maze.hpp"
#include "state.hpp"


/** @brief Main event loop, waiting for input and update state for a new frame. */
static int run(State&& state) {
    Renderer::input_t in{};
    float curr_delay = ((1000.0F / min_fps) - (1000.0F / max_fps)) / 2.0F;

    // poll duration of pre-flight frame for seeding frame delay
    (void)state.tick(in);
    while (true) {
        usleep(10 * 1000);
        if (state.duration_hint().get_size() > 0) {
            curr_delay = std::min(curr_delay, state.duration_hint().get_sliding_avg());
            break;
        } else if (state.congestion_hint() > 0) {
            break;
        }
    }

    while (true) {
        msec_t now = Timer::now();
        const msec_t next_draw = now + (msec_t)std::floor(curr_delay);

        if (!state.tick(in)) {
            LOG("Great success!");
            return 0;
        } else if (in.action.quit) {
            LOG("Exit");
            return EINTR;
        } else if (in.action.restart) {
            LOG("Restart");
            return EAGAIN;
        } else if (in.action.regen) {
            LOG("Re-generate");
            return ERESTART;
        }

        now = Timer::now();
        if (now < next_draw) {
            usleep(static_cast<unsigned>((next_draw - now) * 1000));
        }

        do {
            in = state.get_input();
        } while (in.actions == 0);

        curr_delay = state.duration_hint().get_sliding_avg() > curr_delay ? curr_delay * 1.05F : curr_delay * 0.95F;
        curr_delay = state.congestion_hint() > 0 ? curr_delay * 1.25F : curr_delay;
        curr_delay = std::min(1000.0F / min_fps, std::max(1000.0F / max_fps, SQUEEZE_FACTOR * curr_delay));
#if USE_LOG_STATS > 1
        LOG("Delay for %4u ms (%.1f fps)", (unsigned)std::floor(curr_delay), 1000.0F / curr_delay);
#endif
    }
}


/**
 * @brief As the main loop, but do so without processing input for benchmarking.
 *
 * Walk the given level waypoints for comparable performance comparisons.
 * Can be done without actually drawing images to or reading keys from an X window.
 */
static int run(State&& state, const std::vector<vertex_t>& waypoints) {
    const unsigned max_delay = std::ceil(1000.0F / (float)min_fps);
    const unsigned min_delay = std::floor(1000.0F / (float)max_fps);
    unsigned curr_delay = min_delay;

#if 1
    const msec_t min_runtime = Timer::now() + 10000;
    const size_t waypoint_samples = 3;
#else
    const msec_t min_runtime = 0;
    const size_t waypoint_samples = 1;
#endif

    do {
        for (const auto& waypoint : waypoints) {
            Renderer::input_t in{};
            in.offset = waypoint;
            in.action.beam = 1;

            for (size_t i = 0; i < waypoint_samples; ++i) {
                in.offset += micro_dist;
                (void)state.tick(in);

                usleep(curr_delay * 1000);
                curr_delay = (unsigned)std::floor(state.duration_hint().get_sliding_avg());
                curr_delay += (state.congestion_hint() * min_delay);
                curr_delay = std::min(max_delay, std::max(min_delay, curr_delay));
            }
        }
    } while (0 < min_runtime && min_runtime > Timer::now());

    return EINTR;
}


/** @brief Given a level seed, generate a maze and run the main loop. */
static int run(PRNG::result_type seed, res_t resolution, bool subsample, bool benchmark) {
    using namespace level_config;
    const vertex_t raster = vertex_t::setted(raster_base).with_z(1.0F);
    std::unique_ptr<const Level> scene = generate(seed, {level_dim[0], level_dim[0]}, {level_dim[1], level_dim[1]},
                                                  {level_dim[2], level_dim[2]}, {level_max[0], level_max[1]}, raster);

    const fcoord_t sensitivity = raster_base * raster_per_second / 1000.0F; // per msec
    const vertex_t camera = vertex_t{scene->start.x + MICRO_DIST, scene->start.y + MICRO_DIST, camera_z} * raster;
    const vertex_t viewport =
        vertex_t{(fcoord_t)level_dim[2] / 2.0F, (fcoord_t)level_dim[2] / 2.0F, viewport_z} * raster;

    if (benchmark) {
        return run(State(scene->objects, scene->lights, raster, viewport, camera, resolution, subsample, sensitivity),
                   scene->rooms);
    }

    while (true) {
        int rv =
            run(State(scene->objects, scene->lights, raster, viewport, camera, resolution, subsample, sensitivity));
        if (rv != EAGAIN) {
            return rv;
        }
    }
}


int main(int argc, char** argv) {
    std::srand(static_cast<unsigned>(Timer::now()));
    PRNG::result_type seed = PRNG::hash(static_cast<PRNG::result_type>(std::rand()));

    bool benchmark = false;
    bool subsample = true; // half internal resolution
    int resolution_x = 800; // non-square aspect supported by padding
    int resolution_y = 800;

    int opt;
    while ((opt = getopt(argc, argv, "bfx:y:s:")) != -1) {
        switch (opt) {
            case 'b':
                benchmark = true;
                break;
            case 'f':
                subsample = false;
                break;
            case 'x':
                resolution_x = atoi(optarg);
                break;
            case 'y':
                resolution_y = atoi(optarg);
                break;
            case 's':
                seed = static_cast<PRNG::result_type>(strtoul(optarg, NULL, 10));
                break;
            default:
                return EXIT_FAILURE;
        }
    }
    if (optind != argc) {
        return EXIT_FAILURE;
    }

    while (true) {
        int rv = run(seed, {resolution_x, resolution_y}, subsample, benchmark);
        if (rv != 0 && rv != ERESTART) {
            return EXIT_SUCCESS;
        }
        seed = PRNG::hash(seed);
    }
}