/*
  XTulator: A portable, open-source 80186 PC emulator.
  Copyright (C)2020 Mike Chambers

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#define TIMING_CYCLE 20000      // loop calls time in microseconds

#include <SDL2/SDL.h>

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <sys/time.h>

#include "sdlconsole.h"
#include "libbmp.h"

#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitToggle(value, bit) ((value) ^= (1UL << (bit)))
#define bitWrite(value, bit, bitvalue) ((bitvalue) ? bitSet(value, bit) : bitClear(value, bit))

bmp_img font;

uint32_t *imgArea;
uint8_t *terminalBuffer;

uint16_t screenWidth , screenHeight;
uint16_t terminalWidth, terminalHeight;


// define needed box chars
#define lu      0xE2    // left-up
#define ru      0xE1    // right-up
#define ld      0xE3    // left-down
#define rd      0xE0    // right-down
#define vl      0xB3    // vetrical
#define hl      0xC4    // horizotal
#define hld     0xED    // hor-line-down
#define hlu     0xEC    // hor-line-up
#define vll     0xEA    // vert-line-left,
#define vlr     0XEB    // vert-line-right
#define crs     0xC5    // cross
#define oth     0x00    // other


void sdlconsole_blit();
int sdlconsole_setWindow(int w, int h);
void sdlconsole_setTitle(char* title);
//void sdlconsole_renderChar(uint8_t charNum, uint16_t xChar, uint16_t yChar);

uint64_t timing_cur;
uint64_t timing_freq = 1000000;

uint64_t timing_getCur() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    timing_cur = (uint64_t)tv.tv_sec * 1000000 + (uint64_t)tv.tv_usec;

    return timing_cur;
}

SDL_Window *sdlconsole_window = NULL;
SDL_Renderer *sdlconsole_renderer = NULL;
SDL_Texture *sdlconsole_texture = NULL;

int sdlconsole_curw, sdlconsole_curh;

char* sdlconsole_title;

// TODO:
void sdlconsole_printf(char* format, char* text, uint16_t x, uint16_t y);
void sdlconsole_printfsquare(char* format, char* text, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool autoscroll);

void sdlconsole_scroll(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t scrollAmount, uint8_t direction, char* newData) {
    // check for boundaries
    if ( x > terminalWidth) return;
    if ( y > terminalHeight) return;
    if ( w == 0 || w > terminalWidth) return;
    if ( h == 0 || h > terminalHeight) return;
    if (scrollAmount == 0) return;
    //todo: more tests with scollAmount and other variable safety tests

    while (scrollAmount > 0 ) {
        scrollAmount--;
        switch(direction) {
            case SCROLL_UP:
                for (uint16_t i = y; i < (y + h); i++) {
                    uint16_t dstLine, srcLine;
                    dstLine = i;
                    srcLine = i + 1;
                    for (uint16_t j = 0; j < w; j++) {      // copy from src line to dst line
                        if (dstLine >= (y + h - 1) ) {
                            if ( strlen(newData) > j ) {
                                sdlconsole_putChar(newData[j], x + j, dstLine); // newData
                            } else {
                                sdlconsole_putChar(0xFF, x + j, dstLine);   // empty line
                            }
                        } else {
                            sdlconsole_putChar(sdlconsole_getChar(x + j, srcLine), x + j, dstLine);
                        }
                    }
                }
                break;
            case SCROLL_DOWN:
                for (uint16_t i = y + h - 1; i >= y; i--) {
                    uint16_t dstLine, srcLine;
                    dstLine = i;
                    srcLine = i - 1;
                    for (uint16_t j = 0; j < w; j++) {      // copy from src line to dst line
                        if (dstLine <= y ) {
                            if ( strlen(newData) > j ) {
                                sdlconsole_putChar(newData[j], x + j, dstLine); // newData
                            } else {
                                sdlconsole_putChar(0xFF, x + j, dstLine);   // empty line
                            }
                        } else {
                            sdlconsole_putChar(sdlconsole_getChar(x + j, srcLine), x + j, dstLine);
                        }
                    }
                }
                break;
            case SCROLL_LEFT:
                for (uint16_t i = x; i < (x + w); i++) {
                    uint16_t dstLine, srcLine;
                    dstLine = i;
                    srcLine = i + 1;
                    for (uint16_t j = 0; j < h; j++) {      // copy from src line to dst line
                        if (dstLine >= (x + w - 1 ) ) {
                            if ( strlen(newData) > j ) {
                                sdlconsole_putChar(newData[j], y + j, dstLine); // newData
                            } else {
                                sdlconsole_putChar(0xFF, y + j, dstLine);   // empty line
                            }
                        } else {
                            sdlconsole_putChar(sdlconsole_getChar(srcLine, y + j), dstLine, y + j);
                        }
                    }
                }
                break;
            case SCROLL_RIGHT:
                for (uint16_t i = (x + w - 1); i >= x; i--) {
                    uint16_t dstLine, srcLine;
                    dstLine = i;
                    srcLine = i - 1;
                    for (uint16_t j = 0; j < h; j++) {      // copy from src line to dst line
                        if (dstLine <= x ) {
                            if ( strlen(newData) > j ) {
                                sdlconsole_putChar(newData[j], y + j, dstLine); // newData
                            } else {
                                sdlconsole_putChar(0xFF, y + j, dstLine);   // empty line
                            }
                        } else {
                            sdlconsole_putChar(sdlconsole_getChar(srcLine, y + j), dstLine, y + j);
                        }
                    }
                }
                break;
            default:
                return;
        }
    }

}

enum LINE_MODE {
    LINE_UPPPER = 0,
    LINE_LOWER = 1,
    LINE_LEFT = 2,
    LINE_RIGHT = 3
};

uint8_t sdlconsole_overlapSolver(uint8_t newChar, uint16_t x, uint16_t y, uint8_t whatLine) {

    // array of all box chars
    // must be in the same order as translation matrix
    // used to find existing box char index
    // used to find new box char index
    uint8_t isBox[] = { lu,  ru,  ld,  rd,  vl,  hl, hld, hlu, vll, vlr, crs};

    // boxCharMatrix[w][h] - translation matrix
    // w - index of what we want
    // h - index of what we have
    // w dimenstion data must be the same order as in isBox[] array
    // h dimension must follow the the same order as in isBox array

/*
    uint8_t boxCharMatrix[6][12]= {
    // have: oth,  lu,  ru,  ld,  rd,  vl,  rl, hld, hlu, vll, vlr, crs
// want
           {  lu,  lu, hld, vll, crs, vll, hld, hld, crs, vll, crs, crs}, //left-up
           {  ru, hld,  ru, crs, vlr, vlr, hld, hld, crs, crs, vlr, crs}, //right-up
           {  ld, vll, crs,  ld, hlu, vll, hlu, crs, hlu, vll, crs, crs}, //left-down
           {  rd, crs, vlr, hlu,  rd, vlr, hlu, crs, hlu, crs, vlr, crs}, //right-down
           {  vl, vll, vlr, vll, vlr,  vl, crs, crs, crs, vll, vlr, crs}, //vetical
           {  hl, hld, hld, hlu, hlu, crs,  hl, hld, hlu, crs, crs, crs}  //horizotal
    };
*/
    uint8_t boxCharMatrix[4][6][12]= {
        {   // upper horizontal line
        // have: oth,  lu,  ru,  ld,  rd,  vl,  rl, hld, hlu, vll, vlr, crs
               {  lu,  lu, hld, vll, crs, vll, hld, hld, crs, vll, crs, crs}, //left-up
               {  ru, hld,  ru, crs, vlr, vlr, hld, hld, crs, crs, vlr, crs}, //right-up
               {  ld, vll, crs,  ld, hlu, vll, hlu, crs, hlu, vll, crs, crs}, //left-down
               {  rd, crs, vlr, hlu,  rd, vlr, hlu, crs, hlu, crs, vlr, crs}, //right-down
               {  vl, vll, vlr, vll, vlr,  vl, crs, crs, crs, vll, vlr, crs}, //vetical
               {  hl, hld, hld, hlu, hlu, hlu,  hl, hlu, hlu, hlu, hlu, hlu}  //horizotal
       },
        {   // horizontal lower line
        // have: oth,  lu,  ru,  ld,  rd,  vl,  rl, hld, hlu, vll, vlr, crs
               {  lu,  lu, hld, vll, crs, vll, hld, hld, crs, vll, crs, crs}, //left-up
               {  ru, hld,  ru, crs, vlr, vlr, hld, hld, crs, crs, vlr, crs}, //right-up
               {  ld, vll, crs,  ld, hlu, vll, hlu, crs, hlu, vll, crs, crs}, //left-down
               {  rd, crs, vlr, hlu,  rd, vlr, hlu, crs, hlu, crs, vlr, crs}, //right-down
               {  vl, vll, vlr, vll, vlr,  vl, crs, crs, crs, vll, vlr, crs}, //vetical
               {  hl, hld, hld, hld, hld, hld,  hl, hld, hld, hld, hld, hld}  //horizotal
       },
        { // left vertical line
        // have: oth,  lu,  ru,  ld,  rd,  vl,  rl, hld, hlu, vll, vlr, crs
               {  lu,  lu, hld, vll, crs, vll, hld, hld, crs, vll, crs, crs}, //left-up
               {  ru, hld,  ru, crs, vlr, vlr, hld, hld, crs, crs, vlr, crs}, //right-up
               {  ld, vll, crs,  ld, hlu, vll, hlu, crs, hlu, vll, crs, crs}, //left-down
               {  rd, crs, vlr, hlu,  rd, vlr, hlu, crs, hlu, crs, vlr, crs}, //right-down
               {  vl,  vl, vll,  vl, vlr,  vl, vlr, vlr, vlr,  vl, vlr, vll}, //vetical
               {  hl, hld, hld, hlu, hlu, crs,  hl, hld, hlu, crs, crs, crs}  //horizotal
       },
        { // right vertical line
        // have: oth,  lu,  ru,  ld,  rd,  vl,  rl, hld, hlu, vll, vlr, crs
               {  lu,  lu, hld, vll, crs, vll, hld, hld, crs, vll, crs, crs}, //left-up
               {  ru, hld,  ru, crs, vlr, vlr, hld, hld, crs, crs, vlr, crs}, //right-up
               {  ld, vll, crs,  ld, hlu, vll, hlu, crs, hlu, vll, crs, crs}, //left-down
               {  rd, crs, vlr, hlu,  rd, vlr, hlu, crs, hlu, crs, vlr, crs}, //right-down
               {  vl, vll,  vl, vll,  vl,  vl, vll, vll, vll, vll,  vl, vll}, //vetical
               {  hl, hld, hld, hlu, hlu, crs,  hl, hld, hlu, crs, crs, crs}  //horizotal
       }
    };

    // get index of old char in terminal buffer
    uint8_t foundBoxChar = 0;   // FIXME: by default return 0th element
    for (uint8_t i = 0; i < sizeof(isBox); i++) {
        if (sdlconsole_getChar(x, y) == isBox[i]) {
            foundBoxChar = i + 1;
            break;
        }
    }
    // foundBoxChar contains array index of old boxchar for decision
    // FIXME: see note above

    for (uint8_t i = 0; i < 6; i++) {
        if (newChar == isBox[i]) {
            return boxCharMatrix[whatLine][i][foundBoxChar];
        }
    }

    return sdlconsole_getChar(x, y);
}

void sdlconsole_drawSquare(uint16_t x, uint16_t y, uint16_t w, uint16_t h, char* title, bool overlap) {
//void sdlconsole_drawSquare(uint16_t x, uint16_t y, uint16_t w, uint16_t h, char* title) {
    uint8_t tmp;
    // TODO: add overlapping
    w--; h--;       // size adjustment

    // check for start point boundaries
    if ( x >= terminalWidth || y >= terminalHeight) return;
    // check for width height boundaries
    if ( w == 0 || h == 0 || w >= terminalWidth || h >= terminalHeight) return;


    tmp = sdlconsole_overlapSolver(lu, x,y, LINE_UPPPER);
    sdlconsole_putChar(tmp, x, y);

    tmp = sdlconsole_overlapSolver(ru, x + w,y, LINE_UPPPER);
    sdlconsole_putChar(tmp, x + w, y);

    tmp = sdlconsole_overlapSolver(ld, x, y + h, LINE_LOWER);
    sdlconsole_putChar(tmp, x, y + h);

    tmp = sdlconsole_overlapSolver(rd, x + w, y + h, LINE_LOWER);
    sdlconsole_putChar(tmp, x + w, y + h);

    // horizontal lines
    for (uint16_t i = 1; i < w; i++) {

        // upper line and/or title text
        // FIXME: need more test with zero len text.
        //if ( ((i - 1) <= strlen(title)) && ((i -  w - 1) >= strlen(title)) ) {    // maybe this???
        if ( ((i - 1) < strlen(title)) && ((i -  w - 1) >= strlen(title)) ) {
            sdlconsole_putChar(title[i - 1], i + x, y);
        } else {
            tmp = sdlconsole_overlapSolver(hl, i + x, y, LINE_UPPPER);
            sdlconsole_putChar(tmp, i + x, y);
        }
        // lower line
        tmp = sdlconsole_overlapSolver(hl, i + x, y + h, LINE_LOWER);
        sdlconsole_putChar(tmp, i + x, y + h);

    }

    // vertical lines
    for (uint16_t i = y+1; i < y+h; i++) {
        tmp = sdlconsole_overlapSolver(vl, x, i, LINE_LEFT);
        sdlconsole_putChar(tmp, x, i);     // upper
        tmp = sdlconsole_overlapSolver(vl, x + w, i, LINE_RIGHT);
        sdlconsole_putChar(tmp, x+w, i);   // lower
    }

    // clear inside square
    for (uint16_t i = y + 1; i < y + h; i++) {
        for (uint16_t j = x + 1; j < x + w; j++) {
            sdlconsole_putChar(0xFF, j, i);
        }
    }
}

void sdlconsole_putChar(uint8_t charNum, uint16_t x, uint16_t y) {
    if ( x > terminalWidth || y > terminalHeight) return;
    terminalBuffer[ y * terminalWidth + x ] = charNum;
}

uint8_t sdlconsole_getChar(uint16_t x, uint16_t y) {
    return terminalBuffer[ y * terminalWidth + x ];
}

void sdlconsole_renderChar(uint8_t charNum, uint16_t xChar, uint16_t yChar) {
    uint32_t r, g, b, tmp;

    uint16_t charWidth, charHeight;
    uint16_t xx, yy;

    charWidth = font.img_header.biWidth / 16;
    charHeight = font.img_header.biHeight / 16;

    for (uint16_t y = 0; y < charHeight; y++) {
        for (uint16_t x = 0; x < charWidth; x++) {
            xx = x + charWidth * ((uint16_t)charNum % 16);
            yy = y + charHeight * ((uint16_t)charNum / 16);
            r = (uint32_t)font.img_pixels[yy][xx].red;
            g = (uint32_t)font.img_pixels[yy][xx].green;
            b = (uint32_t)font.img_pixels[yy][xx].blue;
            tmp = (r << 16) + (g << 8) + b;
            imgArea[ ((yChar * charHeight + y) * (charWidth * terminalWidth)) + (xChar * charWidth + x) ] = tmp;
        }
    }
}

/*
 * render all characters from terminal buffer to SDL
 * by replacing sdlconsole_renderChar(char, x, y)
 * to alternative function you can do the same in 
 * any output device inculding VT100 terminal (+ ANSI ESC sequences)
 * or retro IBM PC video card in text mode using BIOS calls
 * or mainpulating video buffer directly
 */
void sdlconsole_renderTerminal() {
    for (uint16_t y = 0; y < terminalHeight; y++) {
        for (uint16_t x = 0; x < terminalWidth; x++) {
            sdlconsole_renderChar(terminalBuffer[ y * terminalWidth + x ], x, y);
        }
    }
    //sdlconsole_blit();
}

int sdlconsole_init(uint16_t termW, uint16_t termH, char *title, char *fontFile) {

    if ( bmp_img_read(&font, fontFile) != BMP_OK) return -1;

    terminalWidth = termW;
    terminalHeight = termH;

    terminalBuffer = malloc(terminalHeight * terminalWidth);
    for (uint16_t y = 0; y < terminalHeight; y++ ) {
        for (uint16_t x = 0; x < terminalWidth; x++ ) {
            terminalBuffer[x + terminalWidth * y] = 0xFF;
        }
    }

    screenWidth = font.img_header.biWidth / 16 * terminalWidth;
    screenHeight = font.img_header.biHeight / 16 * terminalHeight;

    imgArea = malloc(screenHeight * screenWidth * sizeof(uint32_t) );
    for (uint16_t y = 0; y < screenHeight; y++ ) {
        for (uint16_t x = 0; x < screenWidth; x++ ) {
            imgArea[x + screenWidth * y] = 0;
        }
    }

    if (SDL_Init(SDL_INIT_VIDEO)) return -1;
    sdlconsole_title = title;
    sdlconsole_window = SDL_CreateWindow(sdlconsole_title,
        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
        screenWidth, screenHeight,
        SDL_WINDOW_OPENGL);
    if (sdlconsole_window == NULL) return -1;
    if (sdlconsole_setWindow(screenWidth, screenHeight)) return -1;

    return 0;
}

void sdlconsole_end() {
    // do SDL deinit things
    SDL_Quit();
    free(imgArea);
    free(terminalBuffer);
}
int sdlconsole_setWindow(int w, int h) {
    if (sdlconsole_renderer != NULL) SDL_DestroyRenderer(sdlconsole_renderer);
    if (sdlconsole_texture != NULL) SDL_DestroyTexture(sdlconsole_texture);
    sdlconsole_renderer = NULL;
    sdlconsole_texture = NULL;

    SDL_SetWindowSize(sdlconsole_window, w, h);

    sdlconsole_renderer = SDL_CreateRenderer(sdlconsole_window, -1, 0);
    if (sdlconsole_renderer == NULL) return -1;

    sdlconsole_texture = SDL_CreateTexture(sdlconsole_renderer,
        SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING,
        w, h);
    if (sdlconsole_texture == NULL) return -1;

    sdlconsole_curw = w;
    sdlconsole_curh = h;

    return 0;
}

void sdlconsole_setTitle(char* title) { //appends something to the main title, doesn't replace it all
    char tmp[1024];
    sprintf(tmp, "%s - %s", sdlconsole_title, title);
    SDL_SetWindowTitle(sdlconsole_window, tmp);
}

void sdlconsole_blit() {
    static uint64_t lasttime = 0;

    uint64_t curtime;
    curtime = timing_getCur();

    if ((screenWidth != sdlconsole_curw) || (screenHeight != sdlconsole_curh)) {
        sdlconsole_setWindow(screenWidth, screenHeight);
    }
    SDL_UpdateTexture(sdlconsole_texture, NULL, imgArea, screenWidth * sizeof(uint32_t));
    SDL_RenderClear(sdlconsole_renderer);
    SDL_RenderCopy(sdlconsole_renderer, sdlconsole_texture, NULL, NULL);
    SDL_RenderPresent(sdlconsole_renderer);

    if (lasttime != 0) {
        static uint64_t sdlconsole_frameTime[30];
        static uint8_t sdlconsole_frameIdx;

        sdlconsole_frameTime[sdlconsole_frameIdx++] = curtime - lasttime;
        if (sdlconsole_frameIdx == 30) {
            int i, avgcount;
            uint64_t curavg;

            sdlconsole_frameIdx = 0;
            avgcount = 0;
            curavg = 0;
            for (i = 0; i < 30; i++) {
                if (sdlconsole_frameTime[i] != 0) {
                    curavg += sdlconsole_frameTime[i];
                    avgcount++;
                }
            }
            curavg /= avgcount;
            char tmp[64];
            sprintf(tmp, "%.2f FPS", (double)((timing_freq * 10) / curavg) / 10);
            sdlconsole_setTitle(tmp);
        }
    }
    lasttime = curtime;
}

int sdlconsole_loop() {
    static uint64_t lasttime = 0;

    if (timing_getCur() - lasttime < TIMING_CYCLE) return SDLCONSOLE_EVENT_NONE;
    lasttime = timing_cur;

    SDL_Event event;

    //uint64_t oldTime;
    //oldTime = timing_getCur();
    sdlconsole_renderTerminal();
    //printf("delta %lu\n", timing_getCur() - oldTime);
    sdlconsole_blit();
    

    if (!SDL_PollEvent(&event)) return SDLCONSOLE_EVENT_NONE;
    switch (event.type) {
        case SDL_KEYDOWN:
            if (event.key.repeat) return SDLCONSOLE_EVENT_NONE;
            switch (event.key.keysym.sym) {
                case SDLK_ESCAPE:
                    return SDLCONSOLE_EVENT_QUIT;
                case SDLK_F11:
                    return SDLCONSOLE_EVENT_DEBUG_1;
                case SDLK_F12:
                    return SDLCONSOLE_EVENT_DEBUG_2;
                case SDLK_UP:
                    return SDLCONSOLE_EVENT_UP;
                case SDLK_DOWN:
                    return SDLCONSOLE_EVENT_DOWN;
                case SDLK_LEFT:
                    return SDLCONSOLE_EVENT_LEFT;
                case SDLK_RIGHT:
                    return SDLCONSOLE_EVENT_RIGHT;
                case SDLK_RETURN:
                case SDLK_RETURN2:
                case SDLK_KP_ENTER:
                    return SDLCONSOLE_EVENT_RETURN;
                case SDLK_SPACE:
                    return SDLCONSOLE_EVENT_SPACE;
                default:
                    if (event.key.keysym.sym == SDLK_LCTRL) {
                    }
                    if (event.key.keysym.sym == SDLK_LALT) {
                    }
                    if (event.key.keysym.sym == SDLK_LSHIFT || event.key.keysym.sym == SDLK_RSHIFT) {
                    }
                    return SDLCONSOLE_EVENT_KEY;

            }
        case SDL_KEYUP:
            if (event.key.repeat) return SDLCONSOLE_EVENT_NONE;
            if (event.key.keysym.sym == SDLK_LCTRL) {
            }
            if (event.key.keysym.sym == SDLK_LALT) {
            }
            if (event.key.keysym.sym == SDLK_LSHIFT || event.key.keysym.sym == SDLK_RSHIFT) {
            }
            //return SDLCONSOLE_EVENT_KEY;      // key up event happened
            return SDLCONSOLE_EVENT_NONE;       // no key up event
        case SDL_QUIT:
            return SDLCONSOLE_EVENT_QUIT;
    }
    return SDLCONSOLE_EVENT_NONE;
}
