/*
 ============================================================================
 Name        : 2048.c
 Author      : Maurits van der Schee
 Description : Console version of the game "2048" for GNU/Linux
 ============================================================================
 */

#define _XOPEN_SOURCE 500

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
#include <c8080/console.h>
#include <c8080/keys.h>
#ifdef ARCH_MICRO80_COLOR
#include <c8080/colors.h>
#endif

#define SIZE 4
uint32_t score = 0;
uint8_t board[SIZE][SIZE];

void drawBoard() {
    uint8_t x, y;
    uint8_t t;
    MoveCursorHome();

#ifdef ARCH_MICRO80_COLOR
    SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_YELLOW);
#endif

    printf("Игра 2048 %17d очков", score);

#ifdef ARCH_MICRO80_COLOR
    SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_WHITE);
#endif

    for (y = 0; y < SIZE; y++) {
        printf("\n\n\n\n");
        for (x = 0; x < SIZE; x++) {
            if (board[x][y] != 0) {
                char s[8];
                snprintf(s, 8, "%u", 1 << board[x][y]);
                t = 7 - strlen(s);
                printf("│%*s%s%*s", t - t / 2, "", s, t / 2, "");
            } else {
                printf("│       ");
            }
        }
    }

    printf("\n\n\n\n");
}

void prepareScreen() {
    uint8_t x, y, i;
#ifdef ARCH_MICRO80_COLOR
    SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_WHITE);
#endif
    HideCursor();
    ClearConsole();

    printf("\n\n┌───────┬───────┬───────┬───────┐\n");
    for (y = 0;; y++) {
        for (i = 0; i < 3; i++)
            printf("│       │       │       │       │\n");
        if (y == SIZE - 1)
            break;
        printf("├───────┼───────┼───────┼───────┤\n");
    }
    printf("└───────┴───────┴───────┴───────┘\n");

#ifdef ARCH_MICRO80_COLOR
    SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_YELLOW);
#endif

    printf("\n          W,A,S,D или R,Q\n");

#ifdef ARCH_MICRO80_COLOR
    SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_WHITE);
#endif

    drawBoard();
}

uint8_t findTarget(uint8_t array[SIZE], uint8_t x, uint8_t stop) {
    uint8_t t;
    // if the position is already on the first, don't evaluate
    if (x == 0) {
        return x;
    }
    for (t = x - 1;; t--) {
        if (array[t] != 0) {
            if (array[t] != array[x]) {
                // merge is not possible, take next position
                return t + 1;
            }
            return t;
        } else {
            // we should not slide further, return this one
            if (t == stop) {
                return t;
            }
        }
    }
    // we did not find a
    return x;
}

bool slideArray(uint8_t array[SIZE]) {
    bool success = false;
    uint8_t x, t, stop = 0;

    for (x = 0; x < SIZE; x++) {
        if (array[x] != 0) {
            t = findTarget(array, x, stop);
            // if target is not original position, then move or merge
            if (t != x) {
                // if target is zero, this is a move
                if (array[t] == 0) {
                    array[t] = array[x];
                } else if (array[t] == array[x]) {
                    // merge (increase power of two)
                    array[t]++;
                    // increase score
                    score += (uint32_t)1 << array[t];
                    // set stop to avoid double merge
                    stop = t + 1;
                }
                array[x] = 0;
                success = true;
            }
        }
    }
    return success;
}

void rotateBoard() {
    uint8_t i, j, n = SIZE;
    uint8_t tmp;
    for (i = 0; i < n / 2; i++) {
        for (j = i; j < n - i - 1; j++) {
            tmp = board[i][j];
            board[i][j] = board[j][n - i - 1];
            board[j][n - i - 1] = board[n - i - 1][n - j - 1];
            board[n - i - 1][n - j - 1] = board[n - j - 1][i];
            board[n - j - 1][i] = tmp;
        }
    }
}

bool moveUp() {
    bool success = false;
    uint8_t x;
    for (x = 0; x < SIZE; x++) {
        success |= slideArray(board[x]);
    }
    return success;
}

bool moveLeft() {
    bool success;
    rotateBoard();
    success = moveUp();
    rotateBoard();
    rotateBoard();
    rotateBoard();
    return success;
}

bool moveDown() {
    bool success;
    rotateBoard();
    rotateBoard();
    success = moveUp();
    rotateBoard();
    rotateBoard();
    return success;
}

bool moveRight() {
    bool success;
    rotateBoard();
    rotateBoard();
    rotateBoard();
    success = moveUp();
    rotateBoard();
    return success;
}

bool findPairDown() {
    bool success = false;
    uint8_t x, y;
    for (x = 0; x < SIZE; x++) {
        for (y = 0; y < SIZE - 1; y++) {
            if (board[x][y] == board[x][y + 1])
                return true;
        }
    }
    return success;
}

uint8_t countEmpty() {
    uint8_t x, y;
    uint8_t count = 0;
    for (x = 0; x < SIZE; x++) {
        for (y = 0; y < SIZE; y++) {
            if (board[x][y] == 0) {
                count++;
            }
        }
    }
    return count;
}

bool gameEnded() {
    bool ended = true;
    if (countEmpty() > 0)
        return false;
    if (findPairDown())
        return false;
    rotateBoard();
    if (findPairDown())
        ended = false;
    rotateBoard();
    rotateBoard();
    rotateBoard();
    return ended;
}

void addRandom() {
    static bool initialized = false;
    uint8_t x, y;
    uint8_t r, len = 0;
    uint8_t n, list[SIZE * SIZE][2];

    if (!initialized) {
        // srand(time(NULL));
        initialized = true;
    }

    for (x = 0; x < SIZE; x++) {
        for (y = 0; y < SIZE; y++) {
            if (board[x][y] == 0) {
                list[len][0] = x;
                list[len][1] = y;
                len++;
            }
        }
    }

    if (len > 0) {
        r = (uint8_t)rand() % len;
        x = list[r][0];
        y = list[r][1];
        n = ((uint8_t)rand() % 10) / 9 + 1;
        board[x][y] = n;
    }
}

void initBoard() {
    uint8_t x, y;
    for (x = 0; x < SIZE; x++) {
        for (y = 0; y < SIZE; y++) {
            board[x][y] = 0;
        }
    }
    addRandom();
    addRandom();
    prepareScreen();
    score = 0;
    drawBoard();
}

int test() {
    uint8_t array[SIZE];
    // these are exponents with base 2 (1=2 2=4 3=8)
    static const uint8_t data[] = {0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 2, 0, 0, 0, 0, 1, 0, 1, 2, 0, 0, 0, 1, 0,
                                   0, 1, 2, 0, 0, 0, 1, 0, 1, 0, 2, 0, 0, 0, 1, 1, 1, 0, 2, 1, 0, 0, 1, 0, 1, 1,
                                   2, 1, 0, 0, 1, 1, 0, 1, 2, 1, 0, 0, 1, 1, 1, 1, 2, 2, 0, 0, 2, 2, 1, 1, 3, 2,
                                   0, 0, 1, 1, 2, 2, 2, 3, 0, 0, 3, 0, 1, 1, 3, 2, 0, 0, 2, 0, 1, 1, 2, 2, 0, 0};
    const uint8_t *in, *out;
    uint8_t t, tests;
    uint8_t i;
    bool success = true;

    tests = (sizeof(data) / sizeof(data[0])) / (2 * SIZE);
    for (t = 0; t < tests; t++) {
        in = data + t * 2 * SIZE;
        out = in + SIZE;
        for (i = 0; i < SIZE; i++) {
            array[i] = in[i];
        }
        slideArray(array);
        for (i = 0; i < SIZE; i++) {
            if (array[i] != out[i]) {
                success = false;
            }
        }
        if (success == false) {
            for (i = 0; i < SIZE; i++) {
                printf("%d ", in[i]);
            }
            printf("=> ");
            for (i = 0; i < SIZE; i++) {
                printf("%d ", array[i]);
            }
            printf("expected ");
            for (i = 0; i < SIZE; i++) {
                printf("%d ", in[i]);
            }
            printf("=> ");
            for (i = 0; i < SIZE; i++) {
                printf("%d ", out[i]);
            }
            printf("\n");
            break;
        }
    }
    if (success) {
        printf("All %u tests executed successfully\n", tests);
    }
    return !success;
}

static bool getchar_y_n(void) {
    for (;;) {
        switch(getchar()) {
            case 'y':
            case 'Y':
                return true;
            case 'n':
            case 'N':
                return false;
        }
    }
}

int main(int argc, char *argv[]) {
    char c;
    bool success;

    // TODO: if (argc == 2 && strcmp(argv[1],"test")==0) {
    // TODO: 	return test();
    // TODO: }

    initBoard();

    while (true) {
        c = getchar();
        if (c == 27) {
            c = getchar();
            if (c == '[') {
                c = getchar();
                switch (c) {
                    case 'A':
                        c = 'W';
                        break;
                    case 'B':
                        c = 'S';
                        break;
                    case 'D':
                        c = 'A';
                        break;
                    case 'C':
                        c = 'D';
                        break;
                    default:
                        continue;
                }
            }
        }
        switch (c) {
            case 'a':
            case 'A':
            case KEY_LEFT:
                success = moveLeft();
                break;
            case 'd':
            case 'D':
            case KEY_RIGHT:
                success = moveRight();
                break;
            case 'w':
            case 'W':
            case KEY_UP:
                success = moveUp();
                break;
            case 's':
            case 'S':
            case KEY_DOWN:
                success = moveDown();
                break;
            default:
                success = false;
        }
        if (success) {
            drawBoard();
            addRandom();
            drawBoard();
            if (gameEnded()) {
#ifdef ARCH_MICRO80_COLOR
                SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_RED);
#endif
                printf("            GAME OVER          \n");
                c = 'r';
            }
        }
        if (c == 'q' || c == 'Q') {
#ifdef ARCH_MICRO80_COLOR
            SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_RED);
#endif
            printf("            QUIT? (Y/N)         \n");
            if (getchar_y_n())
                break;
            prepareScreen();
        }
        if (c == 'r' || c == 'R') {
#ifdef ARCH_MICRO80_COLOR
            SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_RED);
#endif
            printf("          RESTART? (Y/N)       \n");
            if (getchar_y_n())
                initBoard();
            else
                prepareScreen();
        }
    }

#ifdef ARCH_MICRO80_COLOR
    SetConsoleColor(COLOR_PAPER_BLUE | COLOR_INK_LIGHT_WHITE);
#endif
    ShowCursor();
    ClearConsole();

    return EXIT_SUCCESS;
}
