#include <cmath>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <SDL.h>
#include <SDL_ttf.h>
#include "SDL2_gfxPrimitives.h"
#include <stdexcept>
#include <string>
#include <vector>

// SDL variables
SDL_Event ev;
SDL_Renderer *sdlRenderer;

// Button parameters
const unsigned B_P = 15; // padding, default 15
const unsigned B_W = 160; // width, default 160
const unsigned B_H = 40; // height, deault 40

// Screen parameters
const unsigned WIDTH = 560; // default 560
const unsigned HEIGHT = WIDTH+B_H*2; // default WIDTH+B_H*2

// Colours!
Uint32 c_bg = 0xEEEEEEFF;
Uint32 c_button= 0x03A9F4FF;
Uint32 c_textdisplay = 0x666666FF;
Uint32 c_zero = 0x03A9F4FF;
Uint32 c_one = 0xFF9800FF;
Uint32 c_solved = 0x8BC34AFF;
Uint32 c_unsolved = 0x757575FF;

void read_colours() {
    std::ifstream c("colours.ini");
    int i = 42;
    if(!c.is_open())
        return;
    c.ignore(1024, '\n');
    c >> std::hex >> c_bg;
    c.ignore(i, '\n');
    c >> std::hex >> c_button;
    c.ignore(i, '\n');
    c >> std::hex >> c_textdisplay;
    c.ignore(i, '\n');
    c >> std::hex >> c_zero;
    c.ignore(i, '\n');
    c >> std::hex >> c_one;
    c.ignore(i, '\n');
    c >> std::hex >> c_solved;
    c.ignore(i, '\n');
    c >> std::hex >> c_unsolved;
    c.ignore(i, '\n');
}

// Puts a string of text on the screen, centered both vertically
// and horizontally around its x and y coordinates
void textRGB(int x, int y, int fontsize, std::string text, Uint8 r, Uint8 g, Uint8 b) {
    TTF_Init();
    
    TTF_Font *font = TTF_OpenFont("SourceSansPro-Regular.ttf", fontsize);
    if(!font)
        throw std::runtime_error("Font file not found!");
    SDL_Color color = {r, g, b};
    SDL_Surface *text_surface = TTF_RenderUTF8_Blended(font, text.c_str(), color);
    SDL_Texture *text_texture = SDL_CreateTextureFromSurface(sdlRenderer, text_surface);
    SDL_Rect rect = {x-text_surface->w/2, y-text_surface->h/2, text_surface->w, text_surface->h};
    SDL_RenderCopy(sdlRenderer, text_texture, NULL, &rect);
    
    TTF_CloseFont(font);
    SDL_FreeSurface(text_surface);
    SDL_DestroyTexture(text_texture);
}

// Standard SDL timer
Uint32 timer(Uint32 ms, void *param) {
    SDL_Event ev;
    ev.type = SDL_USEREVENT;
    SDL_PushEvent(&ev);
    return ms;
}

// Converts time stored in an unsigned int in 0.1 second units
// to format "0.0 seconds"
std::string to_seconds(unsigned t) {
    if(t == 0)
        return "-";
    return std::to_string(t/10) + '.' + std::to_string(t%10) + " seconds";
}

// A class that can store and manage 3 high scores for 3 difficulty levels
class HighScores {
    unsigned scores[9];
    const char* fn; // Stored to use for saving to file
    
    void swap(unsigned& x, unsigned& y) {
        unsigned temp = x;
        x = y;
        y = temp;
    }
    
    void sort(unsigned arr[3]) {
        // This is really quite awful, but it works
        if(arr[0] == 0) swap(arr[0], arr[2]);
        if(arr[1] == 0) swap(arr[1], arr[2]);
        
        if(arr[0] > arr[1] && arr[1] != 0) swap(arr[0], arr[1]);
        if(arr[1] > arr[2] && arr[2] != 0) swap(arr[1], arr[2]);
        if(arr[0] > arr[1] && arr[1] != 0) swap(arr[0], arr[1]);
    }
    
public:
    HighScores(const char* filename) : fn(filename) {
        std::ifstream in(fn);
        if(!in.is_open()) // In case the file doesn't exist
            for(int i = 0; i < 9; i++)
                scores[i] = 0;
        else
            for(int i = 0; i < 9; i++)
                in >> scores[i];
        sort(scores); // Easy
        sort(scores+3); // Medium
        sort(scores+6); // Hard
    }
    
    ~HighScores() {
        std::ofstream out(fn);
        for(int i = 0; i < 9; i++)
            out << scores[i] << ' ';
        out << std::endl;
    }
    
    // returns true if score was a high score
    // difficulty param: 0 - easy, 1 - medium, 2 - hard
    bool add(int diff, unsigned time) {
        if(diff < 0 || diff > 2)
            throw std::out_of_range("Difficulty can be a number from range 0-2\n");
            
        unsigned *arr = scores + diff*3;
        if(arr[2] == 0 || time < arr[2]) {
            arr[2] = time;
            sort(arr);
            return true;
        }
        return false;
    }
    
    unsigned get(unsigned n) const {
        if(n < 0 || n >= 9)
            throw std::out_of_range("High score indexes are in range 0-8");
        return scores[n];
    }
};

// Just one more global variable here
HighScores *hs;

// An NxN comparable and copiable grid of bools
// ctor can be used to randomize or fill with "false"
template <int N>
struct SimpleGrid {
    bool values[N][N];
    
    SimpleGrid(bool randomize = true) {
        if(N < 1 || N > 32)
            throw std::out_of_range("Grid size must be between 1 and 32.\n");
            
        for(int i = 0; i < N; i++)
            for(int j = 0; j < N; j++)
                values[i][j] = randomize ? (rand()%3) : false;
    }
    
    bool operator==(SimpleGrid const& rhs) const {
        for(int i = 0; i < N; i++)
            for(int j = 0; j < N; j++)
                if(values[i][j] != rhs.values[i][j])
                    return false;
        return true;
    }
    
    bool operator!=(SimpleGrid const& rhs) const {
        return !(*this == rhs);
    }
    
    void operator=(SimpleGrid const& rhs) {
        for(int i = 0; i < N; i++)
            for(int j = 0; j < N; j++)
                values[i][j] = rhs.values[i][j];
    }
};

struct Button {
    std::string label;
    void (*func)();
    int x, y, w, h;
    
    // For buttons with fix positions
    void draw() {
        draw(y);
    }
    
    // For buttons with dynamic vertical positions
    void draw(int height) {
        y = height;
        boxColor(sdlRenderer, x, y, x+w, y+h, c_button);
        textRGB(x+w/2, y+h/2, h/2, label.c_str(), 255, 255, 255);
        SDL_RenderPresent(sdlRenderer);
    }
    
    // Checks if a click happened on the button
    bool click(int x_, int y_) {
        return (x <= x_ && x_ <= x+w && y <= y_ && y_ <= y+h);
    }
    
    Button(std::string label, void (*func)(), int x, int y, int w, int h)
        : label(label), func(func), x(x), y(y), w(w), h(h) {}
};

// A button sized text display
struct TextDisplay : public Button {
private:
    bool click(int, int);
    void (*func)();
public:
    TextDisplay(std::string label, int x, int y, int w, int h)
        : Button(label, NULL, x, y, w, h)
    {}
    
    void draw() {
        boxColor(sdlRenderer, x, y, x+w, y+h, c_textdisplay);
        textRGB(x+w/2, y+h/2, h/2, label.c_str(), 255, 255, 255);
        SDL_RenderPresent(sdlRenderer);
    }
};


template <int N>
class GameScreen  {
    SimpleGrid<N> hidden; // Randomized grid
    SimpleGrid<N> visible; // Player interactable grid
    
    TextDisplay time_display;
    Button surr;
    
    SDL_TimerID id;
    
    unsigned time; // in 0.1 second units
    
    // Decimal values of hidden rows/columns
    unsigned rows[N];
    unsigned columns[N];
    // True if completed
    bool row_statuses[N];
    bool column_statuses[N];
    // Grid parameters
    unsigned padding;
    unsigned margin;
    double size; // Double to make it play nicer
public:
    GameScreen()
        : hidden(true)
        , visible(false)
        , time_display("0.0 seconds", WIDTH/2-B_P/2-B_W, HEIGHT-B_P-B_H, B_W, B_H)
        , surr("Surrender!", NULL, WIDTH/2+B_P/2, HEIGHT-B_P-B_H, B_W, B_H)
        , time(0)
        , padding(15)
        , margin(10)
        , size((double)(WIDTH-2*padding-N*margin)/(N+1)) {
        boxColor(sdlRenderer, 0, 0, WIDTH, HEIGHT, c_bg);
        init_numbers();
        update_statuses();
        draw();
        surr.draw();
        time_display.draw();
    }
    
    // Calculates decimal numbers from the binaries
    // in the hidden grid
    void init_numbers() {
        for(int i = 0; i < N; i++) {
            unsigned result = 0;
            for(int j = 0; j < N; j++) {
                result *= 2;
                if(hidden.values[i][j])
                    result += 1;
            }
            rows[i] = result;
        }
        for(int i = 0; i < N; i++) {
            unsigned result = 0;
            for(int j = 0; j < N; j++) {
                result *= 2;
                if(hidden.values[j][i])
                    result += 1;
            }
            columns[i] = result;
        }
    }
    
    // Checks if any rows or columns are complete
    void update_statuses() {
        for(int i = 0; i < N; i++) {
            bool matching = true;
            for(int j = 0; j < N; j++)
                if(visible.values[i][j] != hidden.values[i][j])
                    matching = false;
            row_statuses[i] = matching;
        }
        for(int i = 0; i < N; i++) {
            bool matching = true;
            for(int j = 0; j < N; j++)
                if(visible.values[j][i] != hidden.values[j][i])
                    matching = false;
            column_statuses[i] = matching;
        }
    }
    
    void draw() {
        // Background (only top square of the screen!)
        boxColor(sdlRenderer, 0, 0, WIDTH, WIDTH, c_bg);
        
        // NxN grid
        for(int j = 0; j < N; j++) { // rows
            for(int i = 0; i < N; i++) { // columns
                int x = padding+i*margin+i*size;
                int y = padding+j*margin+j*size;
                roundedBoxColor(sdlRenderer, x, y, x+size, y+size, size/12,visible.values[j][i]?c_one:c_zero);
                textRGB(x+size/2, y+size/2, size/2, visible.values[j][i]?"1":"0", 255, 255, 255);
            }
        }
        
        // Row numbers
        for(int j = 0; j < N; j++) {
            int x = padding+N*margin+N*size;
            int y = padding+j*margin+j*size;
            roundedBoxColor(sdlRenderer, x, y, x+size, y+size, size/12, row_statuses[j]?c_solved:c_unsolved);
            textRGB(x+size/2, y+size/2, size/2, std::to_string(rows[j]).c_str(), 255, 255, 255);
        }
        
        // Column numbers
        for(int i = 0; i < N; i++) {
            int x = padding+i*margin+i*size;
            int y = padding+N*margin+N*size;
            roundedBoxColor(sdlRenderer, x, y, x+size, y+size, size/12, column_statuses[i]?c_solved:c_unsolved);
            textRGB(x+size/2, y+size/2, size/2, std::to_string(columns[i]).c_str(), 255, 255, 255);
        }
        
        SDL_RenderPresent(sdlRenderer);
    }
    
    void loop() {
        id = SDL_AddTimer(100, timer, NULL); // Counter producing events every .1 seconds
        
        bool surrendered = false;
        bool solved = false;
        while(SDL_WaitEvent(&ev) && !surrendered && !solved) {
            switch(ev.type) {
                case SDL_MOUSEBUTTONDOWN:
                    for(int j = 0; j < N; j++) { // rows
                        for(int i = 0; i < N; i++) { // columns
                            int x = padding+i*margin+i*size;
                            int y = padding+j*margin+j*size;
                            if(x <= ev.button.x && ev.button.x <= x+size && y <= ev.button.y && ev.button.y <= y+size) {
                                visible.values[j][i] = !visible.values[j][i];
                                update_statuses();
                                draw();
                                goto skip;
                            }
                        }
                    }
                    if(surr.click(ev.button.x, ev.button.y)) {
                        surrendered = true;
                    }
                    skip:
                    break;
                case SDL_USEREVENT:
                    if(hidden == visible) {
                        solved = true;
                        break;
                    }
                    time++;
                    time_display.label = to_seconds(time);
                    time_display.draw();
                    break;
                    
                case SDL_QUIT:
                    SDL_RemoveTimer(id);
                    return;
            }
        }
        
        SDL_RemoveTimer(id);
        
        if(surrendered) {
            visible = hidden; // Make the visible grid solved
            update_statuses();
            surr.label = "Back"; // Repurpose button
            draw(); // Redraw solved grid
            surr.draw(); // Redraw button with new label
            SDL_WaitEvent(&ev);
            while(SDL_WaitEvent(&ev)) {
                if(ev.type == SDL_MOUSEBUTTONDOWN && surr.click(ev.button.x, ev.button.y))
                    break;
            }
        }
        else {
            bool new_high_score;
            switch(N) {
                case 4:
                    new_high_score = hs->add(0, time);
                    break;
                case 6:
                    new_high_score = hs->add(1, time);
                    break;
                case 8:
                    new_high_score = hs->add(2, time);
                    break;
                    // case 4: case 6: case 8: new_high_score = hs->add(N/2-2, time); break;
            }
            
            boxColor(sdlRenderer, 0, 0, WIDTH, HEIGHT, c_bg); // Background
            
            int height = HEIGHT/2 - B_P*3;
            
            textRGB(WIDTH/2, height, B_P*2, "Congratulations!", 110, 110, 110);
            height += B_P*2;
            textRGB(WIDTH/2, height, B_P, std::string("Your total time for this ") + ((N==4)?"Easy":(N==6)?"Medium":"Hard") + " round is:", 110, 110, 110);
            height += B_P*1.5;
            textRGB(WIDTH/2, height, B_P*1.5, to_seconds(time), 110, 110, 110);
            
            if(new_high_score) {
                height += B_P*1.5;
                textRGB(WIDTH/2, height, B_P, "This has been recorded as one of your", 110, 110, 110);
                height += B_P;
                textRGB(WIDTH/2, height, B_P, "new best times!", 110, 110, 110);
            }
            
            Button back("BACK", nullptr, WIDTH/2-B_W/2, 0, B_W, B_H);
            back.draw(HEIGHT-B_P-B_H);
            while(SDL_WaitEvent(&ev) && ev.type != SDL_QUIT) {
                if(ev.type == SDL_MOUSEBUTTONUP && back.click(ev.button.x, ev.button.y))
                    break;
            }
        }
    }
};

template <int N>
void game_screen() {
    GameScreen<N> g;
    g.loop();
}

void best_times() {
    boxColor(sdlRenderer, 0, 0, WIDTH, HEIGHT, c_bg);
    
    int height = B_H;
    
    textRGB(WIDTH/2, height, B_P*2, "Hard", 110, 110, 110);
    height += B_P*2;
    for(int i = 6; i < 9; i++) {
        textRGB(WIDTH/2, height, B_P*1.5, to_seconds(hs->get(i)), 110, 110, 110);
        height += B_P*2;
    }
    height += B_P;
    textRGB(WIDTH/2, height, B_P*2, "Medium", 110, 110, 110);
    height += B_P*2;
    for(int i = 3; i < 6; i++) {
        textRGB(WIDTH/2, height, B_P*1.5, to_seconds(hs->get(i)), 110, 110, 110);
        height += B_P*2;
    }
    height += B_P;
    textRGB(WIDTH/2, height, B_P*2, "Easy", 110, 110, 110);
    height += B_P*2;
    for(int i = 0; i < 3; i++) {
        textRGB(WIDTH/2, height, B_P*1.5, to_seconds(hs->get(i)), 110, 110, 110);
        height += B_P*2;
    }
    
    SDL_RenderPresent(sdlRenderer);
    
    Button back("BACK", nullptr, WIDTH/2-B_W/2, 0, B_W, B_H);
    back.draw(HEIGHT-B_P-B_H);
    
    while(SDL_WaitEvent(&ev) && ev.type != SDL_QUIT) {
        if(ev.type == SDL_MOUSEBUTTONUP && back.click(ev.button.x, ev.button.y))
            break;
    }
}

void quit() {
    SDL_Event ev;
    ev.type = SDL_QUIT;
    SDL_PushEvent(&ev);
}

class Menu {
    std::vector<Button> buttons;
    HighScores scores;
    
    void draw() {
        boxColor(sdlRenderer, 0, 0, WIDTH, HEIGHT, c_bg);
        
        // This will break if there are no buttons in the menu
        // But really that should never be the case
        // So I'm not even gonna throw an exception for that here
        int height = HEIGHT/2 - (buttons.size()*B_H + (buttons.size()-1)*B_P)/2;
        for(Button &b : buttons) {
            b.draw(height);
            height = height + B_H + B_P;
        }
        SDL_RenderPresent(sdlRenderer);
    }
    
    void add_button(std::string label, void (*func)()) {
        buttons.push_back(Button(label, func, WIDTH/2-B_W/2, 0, B_W, B_H)); // y value does not matter here
    }
    
public:
    Menu() : scores("scores.dat") {
        read_colours();
        add_button("EASY (4X4)", game_screen<4>);
        add_button("MEDIUM (6X6)", game_screen<6>);
        add_button("HARD (8X8)", game_screen<8>);
        add_button("BEST TIMES", best_times);
        add_button("QUIT", quit);
        hs = &scores;
    }
    
    void run() {
        draw();
        while(SDL_WaitEvent(&ev)) {
            switch(ev.type) {
                case SDL_MOUSEBUTTONUP:
                    for(Button &b : buttons) {
                        if(b.click(ev.button.x, ev.button.y)) {
                            try {
                                b.func();
                            }
                            catch(std::exception& x) {
                                std::cerr << x.what() << std::endl;
                            }
                            break;
                        }
                    }
                    draw();
                    break;
                case SDL_QUIT:
                    return;
            }
        }
    }
};

int main(int argc, char *argv[]) {
    SDL_Window *sdlWindow;
    try {
        sdlWindow = SDL_CreateWindow("Bits",
                                     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 error) {
        SDL_Quit();
        return error;
    }
    
    srand(time(0));
    
    try {
        Menu m;
        m.run();
    }
    catch(std::runtime_error& x) {
        std::cerr << "Runtime error occured: " << x.what() << std::endl;
    }
    
    SDL_Quit();
    return 0;
}
