/*
Copyright 2020 KoroLion (github.com/KoroLion)
*/
#include <iostream>
#include <sstream>
#include <cmath>
#include <algorithm>

#include <iomanip>
#include <vector>
#include <string>
#include <sstream>

#include "SDL.h"
#include "SDL_TTF.h"

const int WIN_WIDTH = 1280;
const int WIN_HEIGHT = 800;

const char* WIN_TITLE = "Fall Simulator";
const int FPS = 60;

#define PI 3.1415

// configurate here
#define HEIGHT 200
#define SPEED 100
#define ANGLE 60
// endconf

template <typename T>
float sqr(T a) {
    return a * a;
}

std::string floatToStr(float v, int p) {
    std::stringstream stream;
    stream << std::fixed << std::setprecision(p) << v;
    std::string s = stream.str();
    return s;
}

void renderText(SDL_Renderer *renderer, const char *s,
        float x, float y, float size) {
    SDL_Texture *texture;
    TTF_Font *font = TTF_OpenFont("FreeSans.ttf", size);
    SDL_Surface *surface;
    SDL_Color textColor = {128, 128, 128, 0};

    surface = TTF_RenderText_Solid(font, s, textColor);
    texture = SDL_CreateTextureFromSurface(renderer, surface);
    SDL_Rect rect{(int)round(x), (int)round(y), surface->w, surface->h};
    SDL_RenderCopy(renderer, texture, NULL, &rect);
    SDL_FreeSurface(surface);
}


class Object {
 protected:
    float x, y;
    int width, height;
 public:
    Object(float x, float y, int width, int height): x(x), y(y), width(width), height(height) {};

    float getX() const { return x; }
    float getY() const { return y; }
    void setX(float x) { this->x = x; }
    void setY(float y) { this->y = y; }

    virtual void update(int fps) {};
    virtual void render(SDL_Renderer *renderer) {
        SDL_SetRenderDrawColor(renderer, 64, 80, 64, 0);

        SDL_Rect r{(int)x, (int)y, width, height};
        SDL_RenderFillRect(renderer, &r);
    };
};


class PhysObject: public Object {
 protected:
    float speedX, speedY, aX, aY;
    float m = 1;
 public:
    PhysObject(): Object(0, 0, 1, 1), speedX(0), speedY(0), aX(0), aY(0) {}
    PhysObject(float x, float y, float speed, float angle): Object(x, y, 15, 15) {
        speedX = speed * cos(angle * PI / 180);
        speedY = speed * sin(angle * PI / 180);
        aX = 0;
        aY = 9.8;
    }

    float getSpeedX() const { return speedX; }
    float getSpeedY() const { return speedY; }
    float getWidth() const { return width; }
    float getHeight() const { return height; }

    float getSpeed() {
        return sqrt(sqr(speedX) + sqr(speedY));
    }
    float getEKin() {
        return sqr(getSpeed()) * m / 2.0;
    }
    float getEPot(float height) {
        return m * aY * height;
    }

    void setSpeedX(float speedX) { this->speedX = speedX; }
    void setSpeedY(float speedY) { this->speedY = speedY; }

    void update(int fps) {
        x += speedX / fps;
        y -= speedY / fps;
        speedX += aX / fps;
        speedY -= aY / fps; // y axes is reversed
    }

    void render(SDL_Renderer *renderer) {
        SDL_SetRenderDrawColor(renderer, 32, 200, 32, 0);

        SDL_Rect r{(int)x, (int)y, width, height};
        SDL_RenderFillRect(renderer, &r);
    }
};

class Simulation {
 private:
    int width, height, floorHeight = 10;
    float t = 0;
    PhysObject *physObject;
    std::vector<Object*> objects;

    void renderFloor(SDL_Renderer *renderer) {
        SDL_SetRenderDrawColor(renderer, 128, 128, 128, 0);

        SDL_Rect r{0, height - floorHeight, width, floorHeight};
        SDL_RenderFillRect(renderer, &r);
    }

    float yToHeight(float y, int objHeight) {
        return height - y - floorHeight - objHeight;
    }

    void renderInfo(SDL_Renderer *renderer) {
        float h = yToHeight(physObject->getY(), physObject->getHeight());

        std::string st = floatToStr(t, 2);
        std::string sh = floatToStr(h, 2);
        std::string sv = floatToStr(physObject->getSpeed(), 2);

        renderText(renderer, ("t = " + st + " s").c_str(),   10, 10, 16);
        renderText(renderer, ("h = " + sh + " m").c_str(),   10, 30, 16);
        renderText(renderer, ("V = " + sv + " m/s").c_str(), 10, 50, 16);
    }

    void renderEInfoAndGraphs(SDL_Renderer *renderer, int x, int y) {
        float h = yToHeight(physObject->getY(), physObject->getHeight());
        float ep = physObject->getEPot(h);
        float ek = physObject->getEKin();
        float e = ep + ek;

        std::string sek = floatToStr(ek, 2);
        std::string sep = floatToStr(ep, 2);
        std::string se = floatToStr(e, 2);

        const int maxWidth = 300;
        SDL_Rect r{x, 200, maxWidth, 30};

        renderText(renderer, ("Ep = " + sek + " J").c_str(), x, y, 16);
        SDL_SetRenderDrawColor(renderer, 64, 64, 64, 0);
        r.y = y + 20;
        r.w = maxWidth;
        SDL_RenderFillRect(renderer, &r);
        r.w = maxWidth * (ep / e);
        SDL_SetRenderDrawColor(renderer, 32, 128, 32, 0);
        SDL_RenderFillRect(renderer, &r);

        renderText(renderer, ("Ek = " + sek + " J").c_str(), x, y + 70, 16);
        SDL_SetRenderDrawColor(renderer, 64, 64, 64, 0);
        r.y = y + 90;
        r.w = maxWidth;
        SDL_RenderFillRect(renderer, &r);
        r.w = maxWidth * (ek / e);
        SDL_SetRenderDrawColor(renderer, 32, 128, 128, 0);
        SDL_RenderFillRect(renderer, &r);

        renderText(renderer, ("E = " + se + " J").c_str(), x, y + 140, 16);
    }
 public:
    Simulation(int width, int height): width(width), height(height) {
        float h = HEIGHT;
        physObject = new PhysObject(50, 0, SPEED, ANGLE);
        physObject->setY(height - h - floorHeight - physObject->getWidth());
        objects.push_back(physObject);
    }

    ~Simulation() {
        std::for_each(
            objects.begin(),
            objects.end(),
            [](Object *obj) {
                delete obj;
            }
        );
    }

    void update(int fps) {
        t += 1.0 / fps;

        std::for_each(
            objects.begin(),
            objects.end(),
            [fps](Object* obj) {
                obj->update(60);
            }
        );

        if (physObject->getSpeed() > 0.1) {
            float x = physObject->getX() + physObject->getWidth() / 2;
            float y = physObject->getY() + physObject->getHeight() / 2;
            objects.push_back(new Object(x, y, 5, 5));
        }

        float collisionY = height - floorHeight - physObject->getHeight();
        if (physObject->getY() > collisionY) {
            physObject->setY(collisionY);
            physObject->setSpeedY(floor(abs(physObject->getSpeedY() / 2)));
        }
    }

    void render(SDL_Renderer *renderer) {
        std::for_each(
            objects.rbegin(),
            objects.rend(),
            [renderer](Object* obj) {
                obj->render(renderer);
            }
        );

        renderFloor(renderer);
        renderEInfoAndGraphs(renderer, 150, 10);
        renderInfo(renderer);
    }
};

class App {
 private:
    int width, height, fps;
    bool isRunning;

    SDL_Window *window;
    SDL_Renderer *renderer;

    Simulation *matPlot;

    void handleEvents() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            bool esc_or_quit = event.type == SDL_QUIT ||
                event.key.keysym.sym == SDLK_ESCAPE;
            bool win_close = event.type == SDL_WINDOWEVENT &&
                event.window.event == SDL_WINDOWEVENT_CLOSE;
            if (esc_or_quit || win_close) {
                isRunning = false;
            }
        }
    }

    void handleKeyboard() {
        const Uint8 *keys = SDL_GetKeyboardState(0);
    }

 public:
    App(const char* title, int width, int height, int initX, int initY,
        int fps):
            width(width), height(height), fps(fps), isRunning(true) {
        SDL_Init(SDL_INIT_VIDEO);
        TTF_Init();

        window = SDL_CreateWindow(title, initX, initY, width, height,
            SDL_WINDOW_OPENGL);
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED |
            SDL_RENDERER_PRESENTVSYNC);
        matPlot = new Simulation(width, height);
    }
    ~App() {
        delete matPlot;
        TTF_Quit();
        SDL_Quit();
    }

    void start() {
        while (isRunning) {
            handleEvents();
            handleKeyboard();

            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
            SDL_RenderClear(renderer);

            matPlot->update(fps);
            matPlot->render(renderer);

            SDL_RenderPresent(renderer);

            SDL_Delay(1000 / fps);
        }
    }
};

int main(int argc, char **argv) {
    std::stringstream ss;
    ss << WIN_TITLE << " (H = " << HEIGHT << " m, V = " << SPEED << " m/s, angle = " << ANGLE << " deg)";
    App app(ss.str().c_str(), WIN_WIDTH, WIN_HEIGHT, 100, 100, FPS);
    app.start();

    return 0;
}
