// Published by devloop.org under MIT License, see: https://www.devloop.org/MIT-license.txt

#include <random>
#include <SDL/SDL.h>
#include <emscripten.h>
#include <emscripten/html5.h>

constexpr int screen_width_{ 800 };
constexpr int screen_height_{ 600 };

constexpr int world_width_{ 256 };
constexpr int world_height_{ 256 };

SDL_Surface* surface_{ nullptr };
Uint32 pixels_[world_width_ * world_height_]{};

struct Colors {
    Uint32 cave{ 0 };
    Uint32 bg{ 0 };
    Uint32 player{ 0 };
    Uint32 obstacle_inner{ 0 };
} colors_;

constexpr int initial_gap_height_{ 130 };
struct CaveState {
    int gap_y{ (world_height_ - initial_gap_height_) / 2 };
    int gap_height{ initial_gap_height_ };
    int gap_delta{ 0 };
} cave_;

struct Player {
    int x{ 13 };
    float y{ world_height_ / 2.0f };
    float dy{ 0.0f };
    bool lost{ false };
} player_;

void InitColors() {
    colors_.cave = SDL_MapRGB(surface_->format, 0, 70, 0);
    colors_.bg = SDL_MapRGB(surface_->format, 0, 0, 0);
    colors_.player = SDL_MapRGB(surface_->format, 255, 0, 0);
    colors_.obstacle_inner = SDL_MapRGB(surface_->format, 0, 30, 0);
}

void DrawVerticalLine(int x, int y_start, int y_end, Uint32 color) {
    for (int y{ y_start }; y < y_end; ++y) {
        pixels_[y * world_width_ + x] = color;
    }
}

void GenerateCaveLine(int gap_x = world_width_ - 1) {
    // clear line
    DrawVerticalLine(gap_x, 0, world_height_, colors_.bg);
    // faded upper line
    for (int y{ cave_.gap_y }, i{ 0 }; y >= 0 && i < 20; --y, ++i) {
        pixels_[y * world_width_ + gap_x] = SDL_MapRGB(surface_->format, 0, 100 - i * 5, 0);
    }
    // faded lower line
    for (int y{ cave_.gap_y + cave_.gap_height }, i{ 0 }; y < world_height_ && i < 20; ++y, ++i) {
        pixels_[y * world_width_ + gap_x] = SDL_MapRGB(surface_->format, 0, 100 - i * 5, 0);
    }
}

void GenerateObstacle() {
    int obstacle_height{ cave_.gap_height / 2 };
    int obstacle_start{ cave_.gap_y + (rand() % (cave_.gap_height / 2)) };
    int obstacle_end{ obstacle_start + obstacle_height };
    DrawVerticalLine(world_width_ - 1, obstacle_start, obstacle_end, colors_.cave);
    DrawVerticalLine(world_width_ - 2, obstacle_start, obstacle_end, colors_.obstacle_inner);
    DrawVerticalLine(world_width_ - 3, obstacle_start, obstacle_end, colors_.obstacle_inner);
    DrawVerticalLine(world_width_ - 4, obstacle_start, obstacle_end, colors_.cave);
}

void UpdateState() {
    static int frame{ 0 };
    ++frame;

    { // scroll cave by moving pixels to the left
        for (int y{ 0 }; y < world_height_; ++y) {
            for (int x{ 0 }; x < world_width_ - 1; ++x) {
                int i{ y * world_width_ + x };
                pixels_[i] = pixels_[i + 1];
            }
        }
        GenerateCaveLine();
        if ((frame % world_width_) == 0) {
            GenerateObstacle();
        }
    }

    // shrink gap
    if (frame % world_width_ == 0) {
        cave_.gap_height = std::max(cave_.gap_height - 1, 40);
    }
    
    { // move gap
        if ((frame % 7) == 0) {
            // change gap direction on every 7th frame
            constexpr int max_delta{ 3 };
            cave_.gap_delta = -max_delta + (rand() % (max_delta * 2 + 1));
        }
        cave_.gap_y = std::min(117, std::max(0, cave_.gap_y + cave_.gap_delta));
    }

    { // update player
        int i{ static_cast<int>(player_.y) * world_width_ + player_.x };
        // check collision
        if (pixels_[i] != colors_.bg) {
            player_.lost = true;
        }
        // grow player
        if (frame % (world_width_ / 3) == 0) {
            player_.x += 1;
        }
        // render player
        pixels_[i] = colors_.player;
        pixels_[i - world_width_] = colors_.player;
        pixels_[i + world_width_] = colors_.player;
        // move player
        player_.y += player_.dy;
        player_.dy = std::min(1.3f, player_.dy + 0.05f);
    }
}

void Step(void* arg) {
    if (player_.lost) {
        for (int n{ 0 }; n < 131; ++n) {
            int i{ rand() % (world_width_ * world_height_) };
            pixels_[i] = colors_.bg;
        }
    } else {
        UpdateState();
    }
    
    // render
    if (SDL_MUSTLOCK(surface_)) SDL_LockSurface(surface_);
    memcpy(surface_->pixels, pixels_, sizeof(Uint32) * world_width_ * world_height_);
    if (SDL_MUSTLOCK(surface_)) SDL_UnlockSurface(surface_);

    SDL_Surface* screen{ static_cast<SDL_Surface*>(arg) };
    SDL_Rect dst{ 0, 0, screen_width_, screen_height_ };
    SDL_BlitScaled(surface_, nullptr, screen, &dst);
    SDL_Flip(screen);
}

void ResetGame() {
    cave_ = CaveState();
    for (int x{ 0 }; x < world_width_; ++x) {
        GenerateCaveLine(x);
    }
    player_ = Player();
}

void OnInput() {
    if (player_.lost) {
        ResetGame();
    }
    player_.dy = -1.3f;
}

EM_BOOL OnTouchStart(int event_type, const EmscriptenTouchEvent *touch_event, void *user_data) {
    OnInput();
    return true;
}

EM_BOOL OnMouseDown(int event_type, const EmscriptenMouseEvent *mouse_event, void *user_data) {
    OnInput();
    return true;
}

extern "C" int main(int argc, char** argv) {
    srand(time(nullptr));
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Surface* screen{ SDL_SetVideoMode(screen_width_, screen_height_, 32, SDL_SWSURFACE) };
    auto fmt{ screen->format };
    surface_ = SDL_CreateRGBSurface(SDL_SWSURFACE, world_width_, world_height_, 
        fmt->BitsPerPixel, fmt->Rmask, fmt->Gmask, fmt->Bmask, fmt->Amask);
    emscripten_set_touchstart_callback("canvas", nullptr, true, OnTouchStart);
    emscripten_set_mousedown_callback("canvas", nullptr, true, OnMouseDown);
    InitColors();
    ResetGame();
    emscripten_set_main_loop_arg(Step, screen, 0, 1);
    SDL_Quit();
    return 0;
}
