#include <SDL.h>
#include <SDL_image.h>
#include "SDL2_gfxPrimitives.h"
#include <cmath>
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <list>
#include <sstream>
#include <stdexcept>

/* Note
 * I should've probably used the word
 * 'velocity' instead of 'speed' in this code,
 * but at this point it's not worth refactoring
 * and to be honest, it's just shorter
 */

/* Screen size parameters */
const int WIDTH = 1024; /* default : 1024 */
const int HEIGHT = 768; /* default : 768 */

/* Speed of the ship's movement, everything else
 * has speed relative to this value - the game isn't
 * very playable outside the default setting */
const double SPEED = 3; /* default : 3 */

/* Global SDL Renderer */
SDL_Renderer* sdlRenderer;

/* Global texture pointers */
SDL_Texture* asteroid_textures[18];
SDL_Texture* ship_textures[360];

/**
 * @brief Calculates the distance between two points
 * @param x1 - x coordinate of the first point
 * @param y1 - y coordinate of the first point
 * @param x2 - x coordinate of the second point
 * @param y2 - y coordinate of the second point
 * @return The distance of the two points
 */
double dist(int x1, int y1, int x2, int y2) {
    return sqrt(pow(x1-x2, 2) + pow(y1-y2, 2));
}

class Projectile {
    double x, y;
    double speedx, speedy;
    unsigned age;
public:
    /**
     * @brief Projectile ctor
     * @param x - x coordinate for spawning the projectile
     * @param y - y coordinate for spawning the projectile
     * @param dirx - x coordinate to shoot the projectile at
     * @param diry - y coordinate to shoot the projectile at
     */
    Projectile(int x, int y, int dirx, int diry)
        : x(x), y(y), age(0) {
        
        double degree = atan2(diry-y, dirx-x);
        speedx = 2 * SPEED * cos(degree);
        speedy = 2 * SPEED * sin(degree);
    }
    
    /**
     * @brief Getter function
     * @return the projectile's x position
     */
    double getX() const {
        return x;
    }
    
    /**
     * @brief Getter function
     * @return the projectile's y position
     */
    double getY() const {
        return y;
    }
    
    /**
     * @brief Getter function
     * @return the projectile's x axis speed
     */
    double getSpeedX() const {
        return speedx;
    }
    
    /**
     * @brief Getter function
     * @return the projectile's y axis speed
     */
    double getSpeedY() const {
        return speedy;
    }
    
    /**
     * @brief Moves the projectile ahead by one step
     */
    void move() {
        x += speedx;
        y += speedy;
        
        /* Check if projectile reached the border of the screen */
        if(x > WIDTH) x = 0;
        else if(x < 0) x = WIDTH;
        if(y > HEIGHT) y = 0;
        else if(y < 0) y = HEIGHT;
        
        /* Increase age of the projectile */
        age++;
    }
    
    /**
     * @brief Draws the projectile using the global renderer
     */
    void draw() {
        /* A box is simple enough to draw, no need to complicate it */
        boxColor(sdlRenderer, x-1, y-1, x+1, y+1, 0xFFFFFFFF);
    }
    
    /**
     * @brief Checks if the projectile has passed its maximum age
     * @retval true - The projectile is too old and is to be destroyed
     * @retval false - The projectile isn't old enough yet
     */
    bool old() {
        /* Maximum projectile age can be adjusted here */
        return age > 200;
    }
};

class Ship {
private:
    int x, y; /* Coordinates of the ship */
    int r; /* Ship radius */
    bool left, right, up, down; /* These represent which buttons are currently pressed */
protected:
public:
    Ship(int x = WIDTH/2, int y = HEIGHT/2)
        : x(x)
        , y(y)
        , r(0)
        , left(false)
        , right(false)
        , up(false)
        , down(false)
    {}
    
    /**
     * @brief Getter function
     * @return the ship's x coordinate
     */
    int getX() const {
        return x;
    }
    
    /**
     * @brief Getter function
     * @return the ship's y coordinate
     */
    int getY() const {
        return y;
    }
    
    /**
     * @brief Uses 360 individual pre-rendered images to display the ship
     */
    void draw() {
        /* for debug purposes */
        //std::cerr << r << std::endl;
        
        SDL_Rect dest = {x-15, y-15, 30, 30};
        SDL_RenderCopy(sdlRenderer, ship_textures[r], NULL, &dest);
    }
    
    /**
     * @brief Points the ship towards the cursor
     * @param eventx - x position of the cursor
     * @param eventy - y position of the cursor
     */
    void setrotation(int eventx, int eventy) {
        /* This is magic, do not touch it */
        r = int(atan2(eventx-x, eventy-y) / 3.141592653589 * 180) + 180;
    }
    
    /**
     * @brief Moves the ship based on pressed keys
     */
    void move() {
        /* Move according to key states */
        if(left && !right) x -= SPEED;
        else if(right && !left) x += SPEED;
        if(up && !down) y -= SPEED;
        else if(down && !up) y += SPEED;
        
        /* Check if ship left screen, loop back around the other side */
        if(x > WIDTH+10) x = -10;
        if(y > HEIGHT+10) y = -10;
        if(x < -10) x = WIDTH+10;
        if(y < -10) y = HEIGHT+10;
    }
    
    /**
     * @brief Handles keyboard presses
     * @param key - the key pressed
     */
    void press(int key) {
        switch(key) {
            case SDLK_w:
            case SDLK_UP:
                up = true;
                break;
            case SDLK_a:
            case SDLK_LEFT:
                left = true;
                break;
            case SDLK_s:
            case SDLK_DOWN:
                down = true;
                break;
            case SDLK_d:
            case SDLK_RIGHT:
                right = true;
                break;
        }
    }
    
    /**
     * @brief Handles keyboard releases
     * @param key - the key released
     */
    void release(int key) {
        switch(key) {
            case SDLK_w:
            case SDLK_UP:
                up = false;
                break;
            case SDLK_a:
            case SDLK_LEFT:
                left = false;
                break;
            case SDLK_s:
            case SDLK_DOWN:
                down = false;
                break;
            case SDLK_d:
            case SDLK_RIGHT:
                right = false;
                break;
        }
    }
};

class Asteroid {
    int size;
    double x, y;
    double speedx, speedy;
    
    SDL_Texture* texture;
    SDL_Rect dest;
public:
    /**
     * @brief Creates a new Asteroid object
     * @detail uses the Ship's coordinates so that it doesn't spawn inside it
     *         if the asteroid is a remnant of an exploded one, takes the position
     *         of the old one as last two parameters
     * @param s - the size of the new asteroid
     * @param shipx - current x position of the Ship (so that we don't spawn into it)
     * @param shipy - current y position of the Ship (so that we don't spawn into it)
     * @param prevx - x position of the previous Asteroid
     * @param prevy - y position of the previous Asteroid
     */
    Asteroid(int s, int shipx, int shipy, int prevx = -1, int prevy = -1) : size(s) {
        if(size < 1 || 3 < size)
            throw 3; /* excellent exception handling method, temp TODO */
            
        /* Assigns a random colour */
        /* white, red, green, blue, yellow, pink */
        int colour = rand()%6;
        int texture_index = colour + (size-1)*6;
        texture = asteroid_textures[texture_index];
        
        /* Generate position */
        if(prevx == -1 && prevy == -1) { /* Didn't have a parent */
            do {
                x = rand() % WIDTH;
                y = rand() % HEIGHT;
            }
            while(dist(x, y, shipx, shipy) < 80);
        }
        else { /* Did have a parent */
            x = prevx + (rand()%20) - 10;
            y = prevy + (rand()%20) - 10;
        }
        
        /* Randomize speed in x and y directions (-50,50) */
        speedx = (rand()%100) - 50;
        speedy = (rand()%100) - 50;
        /* Calculate length of the speedx+speedy vector */
        double length = sqrt(pow(speedx,2)+pow(speedy,2));
        /* Normalize speeds so that every asteroid moves at the same pace */
        speedx = speedx / length * (SPEED);
        speedy = speedy / length * (SPEED);
    }
    
    /**
     * @brief Getter function
     * @return the asteroid's x position
     */
    double getX() const {
        return x;
    }
    
    /**
     * @brief Getter function
     * @return the asteroid's y position
     */
    double getY() const {
        return y;
    }
    
    /**
     * @brief Getter function
     * @return the asteroid's size
     */
    int getSize() const {
        return size;
    }
    
    /**
     * @brief Check if a projectile hit the asteroid
     * @param prx - the x coordinate of a projectile
     * @param pry - the y coordinate of a projectile
     * @retval true - the asteroid was hit
     * @retval false - the asteroid wasn't hit
     */
    bool hit(int prx, int pry) {
        return dist(x, y, prx, pry) <= size*10;
    }
    
    /**
     * @brief Draws the asteroid using the global renderer
     */
    void draw() {
        dest = {int(x)-size*10, (int)y-size*10, size*20, size*20};
        SDL_RenderCopy(sdlRenderer, texture, NULL, &dest);
    }
    
    /**
     * @brief Moves the asteroid ahead
     */
    void move() {
        x += speedx;
        y += speedy;
        
        /* Handles going past the borders of the screen */
        double halfsize = size*10/2; /* Half the size of the asteroid, in pixels */
        
        if(x > WIDTH+halfsize) x = -halfsize;
        else if(x < -halfsize) x = WIDTH+halfsize;
        
        if(y > HEIGHT+halfsize) y = -halfsize;
        else if(y < -halfsize) y = HEIGHT+halfsize;
    }
};

class Level {
    std::list<Asteroid> asteroids;
    std::list<Projectile> projectiles;
    Ship ship;
public:
    Level(int n) {
        for(int i = 1; i <= 3*n; i++) {
            asteroids.push_back(Asteroid((rand()%3)+1, ship.getX(), ship.getY()));
        }
    }
    
    /**
     * @brief Checks if the level is over
     * @retval true - the level is over
     * @retval false - the level still has asteroids left
     */
    bool complete() {
        return asteroids.empty();
    }
    
    /**
     * @brief Creates new asteroids when one is destroyed
     * @param a - the destroyed asteroid
     */
    void spawn_new(Asteroid const& a) {
        if(a.getSize() <= 1) {
            return;
        }
        
        /* Number of asteroids to spawn when one is destroyed can be adjusted here */
        int new_asteroids = 2;
        
        for(int i = 1; i <= new_asteroids; i++) {
            asteroids.push_back(Asteroid(a.getSize()-1, ship.getX(), ship.getY(), a.getX(), a.getY()));
        }
    }
    
    /**
     * @brief Moves every game element ahead, checks for collisions
     */
    void progress() {
        for(std::list<Projectile>::iterator p = projectiles.begin(); p != projectiles.end(); p++) {
            p->move();
            if(p->old())
                p = projectiles.erase(p);
        }
        
        for(std::list<Asteroid>::iterator a = asteroids.begin(); a != asteroids.end(); a++) {
            a->move();
            if(dist(ship.getX(), ship.getY(), a->getX(), a->getY()) < 10 + a->getSize()*10) {
                throw(-1);  /* Game over signal */
            }
            for(std::list<Projectile>::iterator p = projectiles.begin(); p != projectiles.end(); p++) {
                if(a->hit(p->getX(), p->getY())) { /* Did the projectile hit the asteroid? */
                    spawn_new(*a);
                    a = asteroids.erase(a);
                    a--;
                    projectiles.erase(p); /* Doesn't need to be assigned or decremented,
                                           * since we're breaking the loop */
                    break;
                }
            }
        }
        
        ship.move();
    }
    
    /**
     * @brief Redraws the whole screen
     */
    void draw() {
        // Background
        boxColor(sdlRenderer, 0, 0, WIDTH, HEIGHT, 0x080808FF);
        // Ship
        ship.draw();
        // Asteroids
        for(auto a : asteroids) {
            a.draw();
        }
        // Projectiles
        for(auto p : projectiles) {
            p.draw();
        }
    }
    
    /**
     * @brief Forwards key presses to the Ship object
     * @param key - the key pressed
     */
    void press(int key) {
        ship.press(key);
    }
    
    /**
     * @brief Forwards key releases to the Ship object
     * @param key - the key released
     */
    void release(int key) {
        ship.release(key);
    }
    
    /**
     * @brief Fires a new projectile in the direction of the cursor
     * @param dirx - the cursor's x position
     * @param diry - the cursor's y position
     */
    void fire(int dirx, int diry) {
        /* Maximum number of concurrent projectiles can be adjusted here */
        if(projectiles.size() <= 6) {
            projectiles.push_back(Projectile(ship.getX(), ship.getY(), dirx, diry));
        }
    }
    
    /**
     * @brief Rotates the ship towards the cursor
     * @param x - the cursor's x position
     * @param y - the cursor's y position
     */
    void updateposition(int x, int y) {
        ship.setrotation(x, y);
    }
};

/**
 * @brief Standard SDL timer function
 * @param ms - delay between the generated USEREVENTs
 */
Uint32 timer(Uint32 ms, void *param) {
    SDL_Event ev;
    ev.type = SDL_USEREVENT;
    SDL_PushEvent(&ev);
    return ms;
}

/**
 * @brief Loads texture files
 */
void texture_init() {
    /* Asteroid textures */
    const char* colours[] = {"blue", "green", "pink", "red", "white", "yellow"};
    
    for(int i = 0; i < 18; i++) {
        std::string filename("assets/asteroid");
        filename += std::to_string(i/6+1) + "_" + colours[i%6] + ".png";
        asteroid_textures[i] = IMG_LoadTexture(sdlRenderer, filename.c_str());
        if(asteroid_textures[i] == nullptr) {
            throw std::runtime_error("Texture loading failed");
        }
        SDL_SetTextureBlendMode(asteroid_textures[i], SDL_BLENDMODE_BLEND);
    }
    
    /* Ship textures */
    for(int i = 0; i < 360; i++) {
        std::string filename("assets/ship");
        filename += std::to_string(i) + ".png";
        ship_textures[i] = IMG_LoadTexture(sdlRenderer, filename.c_str());
        if(ship_textures[i] == nullptr) {
            throw std::runtime_error("Texture loading failed");
        }
        SDL_SetTextureBlendMode(ship_textures[i], SDL_BLENDMODE_BLEND);
    }
}

/**
 * @brief Frees texture files
 */
void texture_free() {
    for(int i = 0; i < 18; i++) {
        SDL_DestroyTexture(asteroid_textures[i]);
    }
    for(int i = 0; i < 360; i++) {
        SDL_DestroyTexture(ship_textures[i]);
    }
}

/**
 * @brief Creates a level and runs event loop until it's over
 * @param difficulty - the difficulty setting for the level
 * @retval true - Level successfully completed
 * @retval false - Level failed
 */
bool main_loop(int& difficulty) {
    SDL_Event ev;
    SDL_TimerID id;
    
    Level l(difficulty);
    int mousex = 0, mousey = 0;
    
    /* Level considered failed until completed */
    bool return_value = false;
    
    /* Predraw level */
    l.draw();
    
    /* Display level info */
    stringColor(sdlRenderer, WIDTH/2 - 11*4, HEIGHT/2 + 20, "LEVEL READY", 0xFFFFFFFF);
    stringColor(sdlRenderer, WIDTH/2 - 14*4, HEIGHT/2 + 40, (std::string("DIFFICULTY: ") + std::to_string(difficulty)).c_str(), 0xFFFFFFFF);
    stringColor(sdlRenderer, WIDTH/2 - 20*4, HEIGHT/2 + 60, "PRESS SPACE TO START", 0xFFFFFFFF);
    SDL_RenderPresent(sdlRenderer);
    
    try {
        /* Pause until SPACE or quit event */
        while(SDL_WaitEvent(&ev) && ev.key.keysym.sym != SDLK_SPACE) {
            if(ev.type == SDL_QUIT) {
                throw 0;
            }
        }
        
        /* Start timer */
        id = SDL_AddTimer(20, timer, NULL);
        while(SDL_WaitEvent(&ev)) {
            switch(ev.type) {
                case SDL_QUIT:
                    throw 0;
                    break;
                case SDL_MOUSEMOTION:
                    mousex = ev.button.x;
                    mousey = ev.button.y;
                    break;
                case SDL_MOUSEBUTTONDOWN:
                    l.fire(ev.button.x, ev.button.y);
                    break;
                case SDL_USEREVENT:
                    l.updateposition(mousex, mousey);
                    l.progress();
                    l.draw();
                    SDL_RenderPresent(sdlRenderer);
                    break;
                case SDL_KEYDOWN:
                    l.press(ev.key.keysym.sym);
                    break;
                case SDL_KEYUP:
                    l.release(ev.key.keysym.sym);
                    break;
            }
            
            if(l.complete()) {
                return_value = true;
                break;
            }
        }
    }
    catch(int n) {
        if(n == -1) { /* Game over */
            stringColor(sdlRenderer, WIDTH/2 - 9*4, HEIGHT/2, "GAME OVER", 0xFFFFFFFF);
            stringColor(sdlRenderer, WIDTH/2 - 17*4, HEIGHT/2 + 20, "PRESS ESC TO EXIT", 0xFFFFFFFF);
            SDL_RenderPresent(sdlRenderer);
            while(SDL_WaitEvent(&ev) && ev.key.keysym.sym != SDLK_ESCAPE && ev.type != SDL_QUIT);
        }
    }
    catch(std::exception& x) {
        std::cerr << x.what() << std::endl;
    }
    
    SDL_RemoveTimer(id);
    
    return return_value;
}

int main(int argc, char *argv[]) {
    srand(time(0));
    
    /* Start SDL */
    SDL_Window* sdlWindow;
    try {
        sdlWindow = SDL_CreateWindow("Spaaaaace",
                                     SDL_WINDOWPOS_UNDEFINED,	/* Alternatively, use SDL_WINDOWPOS_CENTERED here */
                                     SDL_WINDOWPOS_UNDEFINED,
                                     WIDTH, HEIGHT,
                                     0);
        if(sdlWindow == NULL)
            throw 1;
            
        sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, 0);
        if(sdlRenderer == NULL)
            throw 1;
    }
    catch(int ex) {
        std::cout << "ERROR IN MAIN(): " << ex << "\n" << std::endl;
        SDL_Quit();
        return ex;
    }
    
    /* Load textures */
    try {
        texture_init();
    }
    catch(std::exception& x) {
        std::cerr << x.what() << std::endl;
        return 1;
    }
    
    /* Run game */
    int diff = 1;
    while(main_loop(diff)) {
        diff++;
    };
    
    /* Free all textures */
    texture_free();
    
    return 0;
}
