#include <SDL.h>
#include <SDL_gfxPrimitives.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>

/** Way too many globals here */
int WIDTH = 24*20;
int HEIGHT = 24*20;
int padding_top = 24;
int padding_lft = 24;
int padding_bot = 48;
int padding_rgt = 24;
int pixelsize = 24;
int speed = 200;

typedef struct {
    int r, g, b;
} Colour;

typedef enum Direction {
    left,
    right,
    up,
    down,
    stop
} Direction;

typedef enum Gamestate {
    single,
    multi
} Gamestate;

typedef struct snakeblock{
    int x, y;
    int r, g, b;
    struct snakeblock* next;
} Snakeblock;

typedef struct food{
    int x, y;
} Food;

void Snakeblock_debugprintf(Snakeblock *snake) {
    while (snake != NULL) {
        printf("%d:%d, %p\n", snake->x, snake->y, snake->next);
        snake = snake->next;
    }
    printf("\n");
}

int Snakeblock_length(Snakeblock *snake) {
    int length = 0;
    while (snake != NULL) {
        snake = snake->next;
        length++;
    }
    return length;
}

int score(Snakeblock *snake) {
    int length = Snakeblock_length(snake);
    int i;
    int score = 0;
    for (i = 3; i < length; i++)
        score += i-3;
    return score * (1 + ((speed/50)-4)*0.5);
}

Snakeblock *Snakeblock_free(Snakeblock *snake) {
    while (snake != NULL) {
        Snakeblock* del = snake;
        snake = snake->next;
        free(del);
    }
    return NULL;
}

Snakeblock *Snakeblock_add(Snakeblock *snake, int x, int y, Colour snakecolour) {
    Snakeblock *mov = snake;
    Snakeblock *lag = NULL;
    while (mov != NULL) {
        lag = mov;
        mov = mov->next;
    }
    Snakeblock *created = malloc(sizeof(Snakeblock));
    created->x = x;
    created->y = y;
    created->r = snakecolour.r;
    created->g = snakecolour.g;
    created->b = snakecolour.b;
    created->next = NULL;

    if (lag != NULL)
        lag->next = created;
    else
        snake = created;

    return snake;
}

Snakeblock *Snakeblock_init(int id, Snakeblock *snake, Colour snakecolour, Direction dir, Direction *state) {
    if (id == 2) {
        switch(dir) {
            case up: dir = down; break;
            case down: dir = up; break;
            case left: dir = right; break;
            case right: dir = left; break;
            default:break;
        }
    }
    *state = dir;
    int i;
    int snakelength = 4;
    int x, y;
    switch(dir) {
        case up:
            x = padding_lft/pixelsize - 1;
            y = (HEIGHT-padding_bot)/pixelsize - 5;
            for (i = 1; i <= snakelength; i++)
                snake = Snakeblock_add(snake, x, y++, snakecolour);
        break;
        case down:
            x = (WIDTH-padding_rgt)/pixelsize - 2;
            y = padding_top/pixelsize + 2;
            for (i = 1; i <= snakelength; i++)
                snake = Snakeblock_add(snake, x, y--, snakecolour);
        break;
        case left:
            x = (WIDTH-padding_rgt)/pixelsize - 5;
            y = (HEIGHT-padding_bot)/pixelsize - 2;
            for (i = 1; i <= snakelength; i++)
                snake = Snakeblock_add(snake, x++, y, snakecolour);
        break;
        case right:
            x = padding_lft/pixelsize + 2;
            y = padding_top/pixelsize - 1;
            for (i = 1; i <= snakelength; i++)
                snake = Snakeblock_add(snake, x--, y, snakecolour);
        break;
        default: break;
    }
    return snake;
}

Snakeblock *Snakeblock_add_front(Snakeblock *snake, int x, int y, Colour snakecolour) {
    Snakeblock *created = malloc(sizeof(Snakeblock));
    created->x = x;
    created->y = y;
    created->r = snakecolour.r;
    created->g = snakecolour.g;
    created->b = snakecolour.b;
    if (snake != NULL)
        created->next = snake;
    else
        created->next = NULL;
    snake = created;
    //Snakeblock_debugprintf(snake);
    return snake;
}

void Snakeblock_cut_last(Snakeblock *snake) {
    if (snake == NULL) {
        return; /** Write stuff here, although it's never gonna be NULL when function is called */
    }
    if (snake->next == NULL) {
        return; /** Stuff here too */
    }
    Snakeblock *mov = snake->next;
    Snakeblock *lag = snake;
    while (mov->next != NULL) {
        lag = mov;
        mov = mov->next;
    }
    free(mov);
    lag->next = NULL;
}

void Food_draw(SDL_Surface *screen, Food food, Colour foodcolour) {
    boxRGBA(screen, padding_lft+food.x*pixelsize, padding_top+food.y*pixelsize, padding_lft+(food.x+1)*pixelsize, padding_top+(food.y+1)*pixelsize, foodcolour.r, foodcolour.g, foodcolour.b, 75);
    boxRGBA(screen, padding_lft+food.x*pixelsize+pixelsize/3, padding_top+food.y*pixelsize+pixelsize/3, padding_lft+(food.x+1)*pixelsize-pixelsize/3, padding_top+(food.y+1)*pixelsize-pixelsize/3, foodcolour.r, foodcolour.g, foodcolour.b, 255);
}

void Snakeblock_draw(SDL_Surface *screen, Snakeblock block) {
    boxRGBA(screen, padding_lft+block.x*pixelsize+1, padding_top+block.y*pixelsize+1, padding_lft+(block.x+1)*pixelsize-1, padding_top+(block.y+1)*pixelsize-1, block.r, block.g, block.b, 255);
}

void Snake_draw(SDL_Surface *screen, Snakeblock *snake) {
    while(snake != NULL) {
        Snakeblock_draw(screen, *snake);
        snake = snake->next;
    }
}

Uint32 idozit(Uint32 ms, void *param) {
    SDL_Event ev;
    ev.type = SDL_USEREVENT;
    SDL_PushEvent(&ev);
    return ms;
}

void crash(Snakeblock *snake, Snakeblock *tail, Direction *state) {
    *state = stop;
    snake->r = 255 - snake->r;
    snake->g = 255 - snake->g;
    snake->b = 255 - snake->b;
    if (tail != NULL) {
        tail->r = 255 - tail->r;
        tail->g = 255 - tail->g;
        tail->b = 255 - tail->b;
    }
}

int snakebite(Snakeblock *head, Snakeblock *tail, Direction *state) {
    switch(*state) {
        case left:
            if(head->x - 1 == tail->x && head->y == tail->y) {
                printf("biteleft\n");
                crash(head, tail, state);
                return 1;
            }
        break;
        case right:
            if(head->x + 1 == tail->x && head->y == tail->y) {
                printf("biteright\n");
                crash(head, tail, state);
                return 1;
            }
        break;
        case up:
            if(head->x == tail->x && head->y - 1 == tail->y) {
                printf("biteup\n");
                crash(head, tail, state);
                return 1;
            }
        break;
        case down:
            if(head->x == tail->x && head->y + 1 == tail->y) {
                printf("bitedown\n");
                crash(head, tail, state);
                return 1;
            }
        break;
        default: break;
    }
    return 0;
}

Snakeblock *Snake_progress(Snakeblock *snake, Snakeblock *othersnake, Gamestate gamemode, Direction *state, Food *food, Colour snakecolour) {
    if (snake == NULL)
        return snake; /** Not that that can ever happen... */
    Snakeblock *mov = snake->next;
    while (mov != NULL) {
        if (snakebite(snake, mov, state)) {
            printf("snakebite\n");
            return snake;
        }
        mov = mov->next;
    }
    if (gamemode == multi) {
        mov = othersnake->next;
        while (mov != NULL) {
            if (snakebite(snake, mov, state)) { /** Should be state2 here, doesn't really matter though because symmetry */
                printf("snakebite2\n");
                return snake;
            }
            mov = mov->next;
        }
        Snakeblock *i = snake, *j = NULL;
        while (i != NULL) {
            j = othersnake;
            while (j != NULL) {
                if (i->x == j->x && i->y == j->y) {
                    printf("twosnakecrash\n");
                    crash(i, j, state);
                    return snake;
                }
                j = j->next;
            }
            i = i->next;
        }
    }

    switch(*state) {
        case left:
            if (snake->x > 0)
                snake = Snakeblock_add_front(snake, snake->x-1, snake->y, snakecolour);
            else {
                printf("headcrashleft\n");
                crash(snake, NULL, state);
                return snake;
            }
        break;
        case right:
            if (snake->x < (WIDTH-(padding_lft+padding_rgt))/pixelsize - 1)
                snake = Snakeblock_add_front(snake, snake->x+1, snake->y, snakecolour);
            else {
                crash(snake, NULL, state);
                printf("headcrashright\n");
                return snake;
            }
        break;
        case up:
            if (snake->y > 0)
                snake = Snakeblock_add_front(snake, snake->x, snake->y-1, snakecolour);
            else {
                crash(snake, NULL, state);
                printf("headcrashup\n");
                return snake;
            }
        break;
        case down:
            if (snake->y < (HEIGHT-(padding_top+padding_bot))/pixelsize - 1)
                snake = Snakeblock_add_front(snake, snake->x, snake->y+1, snakecolour);
            else {
                crash(snake, NULL, state);
                printf("headcrashdown\n");
                return snake;
            }
        break;
        default:
        break;
    }
    if (food->x == snake->x && food->y == snake->y) {
        short foodisinsideofsnake;
        do {
            foodisinsideofsnake = 0;
            food->x = rand()%(WIDTH-(padding_lft+padding_rgt))/pixelsize;
            food->y = rand()%(HEIGHT-(padding_top+padding_bot))/pixelsize;
            Snakeblock *mov = snake;
            while(mov != NULL) {
                    if (mov->x == food->x && mov->y == food->y)
                        foodisinsideofsnake = 1;
                    mov = mov->next;
            }
        } while(foodisinsideofsnake);
    }
    else
        Snakeblock_cut_last(snake);
    return snake;
}

int highscore_read(void){
    int highscore = 0;
    FILE *fp = fopen("hscore.txt", "rt");
    if (fp != NULL)
        fscanf(fp, "%d", &highscore);
    fclose(fp);
    return highscore;
}

void highscore_write(int highscore){
    FILE *fp = fopen("hscore.txt", "wt");
    fprintf(fp, "%d\n", highscore);
    fclose(fp);
}

void menu(SDL_Surface *screen, SDL_TimerID id, SDL_Event ev, Gamestate *gamemode, short *quit, Colour snakecolour, Colour snakecolour2, int hscore) {
    id = SDL_AddTimer(100, idozit, NULL);
    int selection = 0;
    while (SDL_WaitEvent(&ev) && !selection) {
        switch(ev.type) {
            case SDL_KEYDOWN:
                switch(ev.key.keysym.sym) {
                    case SDLK_1:
                    case SDLK_KP1:
                        *gamemode = single;
                        selection = 1;
                    break;
                    case SDLK_2:
                    case SDLK_KP2:
                        *gamemode = multi;
                    selection = 1;
                    break;
                    case SDLK_q:
                        *quit = 1;
                        selection = 1;
                    break;
                    case SDLK_UP:
                        switch(pixelsize) {
                            case 16: pixelsize = 20; break;
                            case 20: pixelsize = 24; break;
                            case 24: pixelsize = 30; break;
                            case 30: pixelsize = 32; break;
                            case 32: pixelsize = 40; break;
                            default: break;
                        }
                        padding_lft = padding_rgt = padding_top = pixelsize;
                        padding_bot = 2*pixelsize;
                    break;
                    case SDLK_DOWN:
                        switch(pixelsize) {
                            case 40: pixelsize = 32; break;
                            case 32: pixelsize = 30; break;
                            case 30: pixelsize = 24; break;
                            case 24: pixelsize = 20; break;
                            case 20: pixelsize = 16; break;
                            default: break;
                        }
                        padding_lft = padding_rgt = padding_top = pixelsize;
                        padding_bot = 2*pixelsize;
                    break;
                    case SDLK_LEFT:
                        if(speed > 100)
                            speed -= 50;
                    break;
                    case SDLK_RIGHT:
                        if(speed < 350)
                            speed +=50;
                    break;
                    default:
                    break;
                }
            break;
            case SDL_QUIT:
                *quit = 1;
            break;
            default:
            break;
        }
        boxRGBA(screen, 0, 0, WIDTH, HEIGHT, 240, 240, 240, 255);
        boxRGBA(screen, padding_lft, padding_top, WIDTH-padding_rgt, HEIGHT-padding_bot, 255, 255, 255, 255);
        char menutitles[3][20] = {"(1) SINGLE PLAYER", "(2) MULTIPLAYER", "(Q) QUIT GAME"};
        int i;
        for (i = 0; i < 3; i++)
            stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+i*13, menutitles[i], 0, 0, 0, 255);

        char pixelstring[30];
        sprintf(pixelstring, "PIXEL SIZE (BETA): %d", pixelsize);
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6)*13, pixelstring, 0, 0, 0, 255);
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+1)*13, "(UP/DOWN arrows)", 0, 0, 0, 255);
        boxRGBA(screen, WIDTH/2+100, HEIGHT/3+(6)*13-pixelsize/2+4, WIDTH/2+100+pixelsize, HEIGHT/3+(6)*13+pixelsize/2+4, snakecolour.r, snakecolour.g, snakecolour.b, 255);

        char speedstring[50];
        sprintf(speedstring, "SPEED: ");
        for (i = 1; i < speed/50; i++)
            strcat(speedstring, "+");

        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+3)*13, speedstring, 0, 0, 0, 255);
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+4)*13, "(LEFT/RIGHT arrows)", 0, 0, 0, 255);

        char menuscore[30];
        sprintf(menuscore, "HIGH SCORE: %d", hscore);
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+2+0+4)*13, menuscore, 0, 0, 0, 255);

        char controls[3][30] = {"CONTROLS", "PLAYER 1: ARROW KEYS", "PLAYER 2: WASD KEYS"};
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+4+0+4)*13, controls[0], 0, 0, 0, 255);
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+4+1+4)*13, controls[1], snakecolour.r, snakecolour.g, snakecolour.b, 255);
        stringRGBA(screen, WIDTH/2-10*8, HEIGHT/3+(6+4+2+4)*13, controls[2], snakecolour2.r, snakecolour2.g, snakecolour2.b, 255);

        SDL_Flip(screen);
    }
    SDL_RemoveTimer(id);
}

void gameinit(Direction *state, Direction *state2, Direction *next_state, Direction *next_state2, Snakeblock **snake, Snakeblock **snake2, Food *food, Food *food2, Colour snakecolour, Colour snakecolour2, Direction dir) {
    *state = stop;
    *state2 = stop;
    *next_state = stop;
    *next_state2 = stop;
    *snake = Snakeblock_init(1, *snake, snakecolour, dir, state);
    *snake2 = Snakeblock_init(2, *snake2, snakecolour2, dir, state2);
    food->x = rand()%((WIDTH-(padding_lft+padding_rgt))/pixelsize);
    food2->x = rand()%((WIDTH-(padding_lft+padding_rgt))/pixelsize);
    food->y = rand()%((HEIGHT-(padding_top+padding_bot))/pixelsize);
    food2->y = rand()%((HEIGHT-(padding_top+padding_bot))/pixelsize);
}

int main(int argc, char *argv[]) {
    SDL_Event ev;
    SDL_Surface *screen;
    SDL_TimerID id;
    /** fun */
    id = SDL_AddTimer(10, idozit, NULL);
    SDL_RemoveTimer(id);
    /** /fun */
    srand(time(0));
    int hscore = highscore_read();

    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
    screen=SDL_SetVideoMode(WIDTH, HEIGHT, 0, SDL_ANYFORMAT);
    if (!screen) {
        fprintf(stderr, "Failed to open SDL window!\n");
        exit(1);
    }
    SDL_WM_SetCaption("Snake", "Snake");

    Gamestate gamemode = multi;
    short quit = 0;
    short pause = 0;

    Colour snakecolour, snakecolour2;
    Direction state = 0, state2 = 0;
    Direction next_state = stop, next_state2 = stop;
    Direction dir = rand()%4;
    Snakeblock *snake = NULL, *snake2 = NULL; ///ssssssssss
    Food food, food2;
    snakecolour.r = 0;
    snakecolour.g = 200;
    snakecolour.b = 255;
    snakecolour2.r = 0;
    snakecolour2.g = 200;
    snakecolour2.b = 100;

    menu(screen, id, ev, &gamemode, &quit, snakecolour, snakecolour2, hscore); /** way bigger than you'd think */
    gameinit(&state, &state2, &next_state, &next_state2, &snake, &snake2, &food, &food2, snakecolour, snakecolour2, dir);

    id = SDL_AddTimer(400-speed, idozit, NULL);

    while (SDL_WaitEvent(&ev) && !quit) {
        switch(ev.type) {
            case SDL_KEYDOWN:
                switch(ev.key.keysym.sym) {
                    case SDLK_UP:   next_state = up;    break;
                    case SDLK_LEFT: next_state = left;  break;
                    case SDLK_DOWN: next_state = down;  break;
                    case SDLK_RIGHT:next_state = right; break;
                    case SDLK_w:    next_state2 = up;   break;
                    case SDLK_a:    next_state2 = left; break;
                    case SDLK_d:    next_state2 = right;break;
                    case SDLK_s:    next_state2 = down; break;
                    case SDLK_SPACE:
                        if (state != stop && !pause)
                            pause = 1;
                        else if (state != stop && pause) {
                            id = SDL_AddTimer(400-speed, idozit, NULL);
                            pause = 0;
                        }                               break;
                    case SDLK_r:
                        if(state == stop || state2 == stop) {
                            snake = Snakeblock_free(snake);
                            snake2 = Snakeblock_free(snake2);
                            state = stop;
                            state2 = stop;
                            next_state = stop;
                            next_state2 = stop;
                            snake = Snakeblock_init(1, snake, snakecolour, dir, &state);
                            snake2 = Snakeblock_init(2, snake2, snakecolour2, dir, &state2);
                            food.x = rand()%((WIDTH-(padding_lft+padding_rgt))/pixelsize);
                            food2.x = rand()%((WIDTH-(padding_lft+padding_rgt))/pixelsize);
                            food.y = rand()%((HEIGHT-(padding_top+padding_bot))/pixelsize);
                            food2.y = rand()%((HEIGHT-(padding_top+padding_bot))/pixelsize);
                            id = SDL_AddTimer(400-speed, idozit, NULL);
                        }
                    break;
                    case SDLK_m:
                        snake = Snakeblock_free(snake);
                        snake2 = Snakeblock_free(snake2);
                        dir = rand()%4;
                        menu(screen, id, ev, &gamemode, &quit, snakecolour, snakecolour2, hscore);
                        gameinit(&state, &state2, &next_state, &next_state2, &snake, &snake2, &food, &food2, snakecolour, snakecolour2, dir);
                        SDL_RemoveTimer(id);
                        id = SDL_AddTimer(400-speed, idozit, NULL);
                    break;
                    case SDLK_q:    quit = 1;           break;
                    default:                            break;
                }
            break;
            case SDL_USEREVENT:
                switch(next_state) {
                    case left:  if (state == up || state == down)       state = left;   break;
                    case right: if (state == up || state == down)       state = right;  break;
                    case up:    if (state == left || state == right)    state = up;     break;
                    case down:  if (state == left || state == right)    state = down;   break;
                    default: break;
                }
                switch(next_state2) {
                    case left:  if (state2 == up || state2 == down)     state2 = left;  break;
                    case right: if (state2 == up || state2 == down)     state2 = right; break;
                    case up:    if (state2 == left || state2 == right)  state2 = up;    break;
                    case down:  if (state2 == left || state2 == right)  state2 = down;  break;
                    default: break;
                }

                boxRGBA(screen, 0, 0, WIDTH, HEIGHT, 240, 240, 240, 255); /** Grey box */
                boxRGBA(screen, padding_lft, padding_top, WIDTH-padding_rgt, HEIGHT-padding_bot, 255, 255, 255, 255); /** White box */
                int i;
                for (i = padding_lft; i <= WIDTH - padding_rgt; i += pixelsize)
                    lineRGBA(screen, i, padding_top, i, HEIGHT-padding_bot, 240, 240, 240, 75);
                for (i = padding_top; i <= HEIGHT - padding_bot; i += pixelsize)
                    lineRGBA(screen, padding_lft, i, WIDTH-padding_rgt, i, 240, 240, 240, 75);

                snake = Snake_progress(snake, snake2, gamemode, &state, &food, snakecolour);
                if (gamemode == multi)
                    snake2 = Snake_progress(snake2, snake, gamemode, &state2, &food2, snakecolour2);
                Snake_draw(screen, snake);
                if (gamemode == multi)
                    Snake_draw(screen, snake2);
                Food_draw(screen, food, snakecolour);
                if (gamemode == multi)
                    Food_draw(screen, food2, snakecolour2);

                if (state == stop || state2 == stop) {
                    SDL_RemoveTimer(id);
                    stringRGBA(screen, WIDTH/2-8*4.5, HEIGHT/2-4, "GAME OVER", 0, 0, 0, 255);
                    char wintext[20];
                    if (gamemode == single) {
                        sprintf(wintext, "SCORE: %d", score(snake));
                        stringRGBA(screen, WIDTH/2-8*4.5, HEIGHT/2-4+13, wintext, 0, 0, 0, 255);
                    }

                    if(score(snake)>hscore || score(snake2)>hscore){
                        hscore = (int)fmax(score(snake), score(snake2));
                        char hscoretext[30];
                        sprintf(hscoretext, "NEW HIGH SCORE: %d", hscore);
                        Colour hscorecolour = {0, 0, 0};
                        if (hscore == score(snake)) hscorecolour = snakecolour;
                        if (hscore == score(snake2)) hscorecolour = snakecolour2;
                        stringRGBA(screen, WIDTH/2-8*8, HEIGHT/2-4+26, hscoretext, hscorecolour.r, hscorecolour.g, hscorecolour.b, 255);
                    }

                }
                if (pause && state != stop && state2 != stop) {
                    SDL_RemoveTimer(id);
                    stringRGBA(screen, WIDTH/2-8*5.5, HEIGHT/2-4, "GAME PAUSED", 0, 0, 0, 255);
                }
                char scoretext[50];
                sprintf(scoretext, "SCORE: %d", score(snake));
                stringRGBA(screen, padding_lft+4, HEIGHT-10-10, scoretext, snakecolour.r, snakecolour.g, snakecolour.b, 255);
                if (gamemode == multi) {
                    char scoretext2[50];
                    sprintf(scoretext2, "SCORE: %d", score(snake2));
                    stringRGBA(screen, padding_lft+4, HEIGHT-10, scoretext2, snakecolour2.r, snakecolour2.g, snakecolour2.b, 255);
                }

                stringRGBA(screen, WIDTH-padding_rgt-18*8-4-140, HEIGHT-10-10, "(M) Return to menu", 0, 0, 0, 255);
                stringRGBA(screen, WIDTH-padding_rgt-18*8-4-140, HEIGHT-10, "(SPACE) Pause Game", 0, 0, 0, 255);
                stringRGBA(screen, WIDTH-padding_rgt-14*8-4, HEIGHT-10-10, "(R) Reset Game", 0, 0, 0, 255);
                stringRGBA(screen, WIDTH-padding_rgt-14*8-4, HEIGHT-10, "(Q) Quit Game", 0, 0, 0, 255);

                SDL_Flip(screen);
            break;
            case SDL_QUIT:
                quit = 1;
            default:
            break;
        }
    }

    highscore_write(hscore);
    Snakeblock_free(snake);
    Snakeblock_free(snake2);
    SDL_RemoveTimer(id);
    SDL_Quit();

    return 0;
}

/** (c) zsmb 2014 */
