#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <memory.h>

// gcc test.c -g -o test -llz4 -lusb-1.0
#include <libusb-1.0/libusb.h>
#include <lz4.h>

static libusb_context *context = NULL;
static libusb_device_handle *handle = NULL;

#define FRAMESIZE   768000   // 800x480x16bit
#define FRAMESIZE5  819840   // 854x480x16bit
#define MARGIN      0
#define MARGIN5     320
#define VERTICAL    0
#define HORIZONTAL  1

union axis{
    struct hx{
        unsigned char h:4;
        unsigned char u:2;
        unsigned char f:2;
    } x;
    
    struct hy{
        unsigned char h:4;
        unsigned char id:4;
    } y;
    
    char c;
};

struct point {
    union axis xh;
    unsigned char xl;
    union axis yh;
    unsigned char yl;
    
    unsigned char weight;
    unsigned char misc;
};

struct touch {
    unsigned char unused[2];
    unsigned char count;
    struct point p[2];
};

int get_screen(libusb_device_handle *h)
{
    unsigned char buf[5] = {0x51, 0x02, 0x04, 0x1f, 0xfc};
    libusb_control_transfer(h, 0x40, 0xb5, 0, 0, buf, 5, 100);
    libusb_control_transfer(h, 0xc0, 0xb6, 0, 0, buf, 1, 100);
    libusb_control_transfer(h, 0xc0, 0xb7, 0, 0, buf, 5, 100);
    return ((int *)(buf + 1))[0];
}

int get_version(libusb_device_handle *h)
{
    unsigned char buf[5] = {0x51, 0x02, 0x04, 0x1f, 0xf8};
    libusb_control_transfer(h, 0x40, 0xb5, 0, 0, buf, 5, 100);
    libusb_control_transfer(h, 0xc0, 0xb6, 0, 0, buf, 1, 100);
    libusb_control_transfer(h, 0xc0, 0xb7, 0, 0, buf, 5, 100);
    return ((int *)(buf + 1))[0];
}

void get_screen_id(libusb_device_handle *h, unsigned char *id)
{
    unsigned char cmd[] = {0x51, 0x02, 0x08, 0x1f, 0xf0};
    unsigned char buf[9] = {0};
    
    libusb_control_transfer(h, 0x40, 0xb5, 0, 0, cmd, 5, 100);
    libusb_control_transfer(h, 0xc0, 0xb6, 0, 0, cmd, 1, 100);
    libusb_control_transfer(h, 0xc0, 0xb7, 0, 0, buf, 9, 100);
    memcpy(id, buf + 1, 8);
}

void print_screen_id(libusb_device_handle *h)
{
    unsigned char id[8] = {0};
    get_screen_id(h, id);
    printf("%02x%02x-%02x%02x-%02x%02x%02x%02x\n",
           id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7]);
}

void get_screen_version(libusb_device_handle *h, int *frame_size, int *margin, 
                        int *pos, char *buf)
{
    uint32_t ver = get_screen(h);
    uint32_t code = get_version(h);

    printf("ver = %08x, code = %08x\r\n", ver, code);    
    switch (ver) {
    case 0x00000005:
        if (buf) {
            if (code == 0xffffffff)
                sprintf(buf, "5inch, 480x854x16/24, SLM5.0-81FPC-A");
            if (code == 0x00000000)
                sprintf(buf, "5inch, 480x854x16/24, D500FPC9373-C");
            if (code == 0x00000003)
                sprintf(buf, "5inch, 480x854x16/24, D500FPC931A-A");
        }
        if (frame_size)
            *frame_size = FRAMESIZE5;
        if (margin) {
            *margin = MARGIN5;
            if (code == 0x00000003)
                *margin = 0;
        }
        if (pos)
            *pos = VERTICAL;
        break;

    case 0x00001005:
        if (buf) {
            if (code == 0x00000004)
                sprintf(buf, "5inch, 720x1280x16, OLED");
        }
        if (frame_size)
            *frame_size = 720 * 1280 * 2;
        if (margin)
            *margin = 0;
        if (pos)
            *pos = VERTICAL;
        break;
        
    case 0x00000304:
        if (buf)
            sprintf(buf, "4.3inch, 480x800x16/24, D430FPC9316-A");
        if (frame_size)
            *frame_size = FRAMESIZE;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = VERTICAL;
        break;
        
    case 0x00000004:
        if (buf) {
            if (code == 0x00000002)
                sprintf(buf, "4inch, 480x800x16/24, VOCORE-4NDNV7B");
            else
                sprintf(buf, "4inch, 480x800x16, TOSHIBA-2122");
        }
        if (frame_size)
            *frame_size = FRAMESIZE;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = VERTICAL;
        break;
        
    case 0x00000b04:
        if (buf && code == 0xffffffff)
            sprintf(buf, "4inch, 480x800x16/24, D397FPC9367-B");
        if (frame_size)
            *frame_size = FRAMESIZE;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = VERTICAL;
        break;
        
    case 0x00000104:
        if (buf) {
            if (code == 0xffffffff)
                sprintf(buf, "4inch, 480x800x16/24, DJN-1922");
            else if (code == 0x00000001)
                sprintf(buf, "4inch, 480x800x16/24, SLM4.0-33FPC-A");
            else if (code == 0x00000002)
                sprintf(buf, "4inch, 480x800x16/24, VOCORE-4NDNV10B");
        }
        if (frame_size)
            *frame_size = FRAMESIZE;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = VERTICAL;
        break;
        
    case 0x00000007:
        if (buf)
            sprintf(buf, "6.8inch, 800x480x16/24, D680FPC930G-A");
        if (frame_size)
            *frame_size = FRAMESIZE;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = HORIZONTAL;
        break;
        
    default:
        if (buf)
            sprintf(buf, "4inch/5inch, 480x800x16, code = %08x", ver);
        if (frame_size)
            *frame_size = FRAMESIZE;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = VERTICAL;
        break;
    }
}

int get_port_handle(uint16_t vid, uint16_t pid, libusb_device_handle *handles[], int size)
{
    struct libusb_device **list = NULL;
    int used = 0;
    
    int count = libusb_get_device_list(context, &list);
    for (int i = 0; i < count; i++) {
        struct libusb_device_descriptor desc;
        
        memset(&desc, 0, sizeof(struct libusb_device_descriptor));
        if (libusb_get_device_descriptor(list[i], &desc))
            continue;
        
        int curport = libusb_get_port_number(list[i]) - 1;
        if (desc.idVendor == vid && desc.idProduct == pid) {
            char screen_type[0x100] = {0};
            
            libusb_open(list[i], &handles[used]);
            
            libusb_claim_interface(handles[used], 0);
            //libusb_set_interface_alt_setting(handles[used], 0, 0);
            
            get_screen_version(handles[used], NULL, NULL, NULL, screen_type);
            
            printf("Find screen at USB physics port %d\n", curport);
            printf("Type: %s\n", screen_type);
            printf("Screen UID: "); print_screen_id(handles[used]);
            
            used++;
            if (used >= size) {
                printf("reach max allowed devices number for this test.\n");
                break;
            }
        }
    }
    
    libusb_free_device_list(list, 0);
    return used;
}

uint8_t *alloc_from_file(const char *name, int *size)
{
    FILE *fp = fopen(name, "rb");
    if (fp == NULL)
        return NULL;

    fseek(fp, 0, SEEK_END);

    *size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    uint8_t *buf = malloc(*size);
    if (buf)
        fread(buf, 1, *size, fp);
    fclose(fp);

    return buf;
}

int main(int argc, char *argv[])
{
    int frame_size = 0, margin = 0, pos = VERTICAL, res;
    libusb_device_handle *handles[0x10] = {NULL};    
    
    if (argc > 3) {
        printf("screen_test [bmp16 data | b+brightness(0~255)]\n");
        printf("show frame: screen_test frame.dat\n");
        printf("adjust brightness: screen_test b50\n");
        return 0;
    }
    
    printf("Version: %s,%s\n\n", __DATE__, __TIME__);
    srand(time(NULL));
    
    // 1. find and open usb device.
    printf("Scaning for VoCore screen...\n\n");
    
    libusb_init(&context);
    int device_count = get_port_handle(0xc872, 0x1004, handles, 0x10);
    if (device_count == 0) {
        printf("No screen find, please check driver or USB cable.\n"
               "Press any key to exit.\n");
        getchar();      // avoid suddenly exit for windows.
        return -1;
    }
    
    handle = handles[0];
    if (device_count != 1)
        printf("Detected %d screen(s), use first one for test.\n", device_count);
    
    // we need this data to send correct frame.
    get_screen_version(handle, &frame_size, &margin, &pos, NULL);
    printf("screen direction: %s\n", pos ? "HORIZONTAL" : "VERTICAL");
    
    // default show frame on screen.
    if (argc == 1) {
        if (pos == VERTICAL)
            argv[1] = "framev.dat";
        if (pos == HORIZONTAL)
            argv[1] = "frameh.dat";
    }
    
    // close rest screen handles.
    for (int i = 1; i < device_count; i++) {
        if (handles[i]) {
            libusb_release_interface(handles[i], 0);
            libusb_close(handles[i]);
        }
    }

    // quit sleep mode, or it will show empty screen as default.
    uint8_t cmd1[] = {0x00, 0x29, 0x00, 0x00, 0x00, 0x00};
    libusb_control_transfer(handle, 0x40, 0xb0, 0, 0, cmd1, 6, 100);

    // read data to buffer and write to screen.
    int size1, size2;
    uint8_t *frame1 = alloc_from_file("480x800x1.bmp", &size1);
    uint8_t *frame2 = alloc_from_file("480x800x2.bmp", &size2);
    uint8_t *data1 = frame1 + 138;
    uint8_t *data2 = frame2 + 138;
    uint8_t *data = data1;

    // write start frame command 0x2c, compress with lz4.
    uint8_t *buf = (uint8_t *)malloc(0x200000);
    uint8_t *cdat = (uint8_t *)malloc(0x200000);

    struct screen_command {
        uint8_t mode;
        uint8_t cmd;
        uint8_t size[4];
        uint16_t x;
        uint16_t y;
        uint16_t w;
        uint16_t h;
    } cmd_p;

    cmd_p.cmd = 0x2c;
    for (int loop = 1; ; loop++) {
        int w, h, x, y;
        cmd_p.w = 50 + rand() % 150; w = cmd_p.w;
        cmd_p.h = 50 + rand() % 150; h = cmd_p.h;
        cmd_p.x = rand() % (480 - cmd_p.w); x = cmd_p.x;
        cmd_p.y = rand() % (800 - cmd_p.h); y = cmd_p.y;
        int size = w * h * 2;

        for (int i = 0; i < h; i++)
            memcpy(buf + i * w * 2, data + (480 * (y + i) + x) * 2, w * 2);

// partically draw: for version >= 0.15
// USE_LZ4: for version >= 0.22
#ifdef USE_LZ4
        int psize = 14, csize;
        cmd_p.mode = 0x04;
        csize = LZ4_compress_default(buf, cdat, size, 0x200000);
#else
        int psize = 12, csize = size;
        cmd_p.mode = 0x00;
        memcpy(cdat, buf, csize);
#endif

        memcpy(cmd_p.size, &csize, 4);
        res = libusb_control_transfer(handle, 0x40, 0xb0, 0, 0, (void *)&cmd_p, psize, 100);
        res = libusb_bulk_transfer(handle, 0x02, cdat, csize, NULL, 100);

        if (!(loop & 0xff)) {
            if ((loop >> 8) & 1) {
                data = data2;
            } else {
                data = data1;
            }

            printf("LOOP: %d\r\n", loop);
        }
    }

    free(buf);
    free(cdat);
    free(frame1);
    free(frame2);
    
end:
    // close usb port.
    libusb_release_interface(handle, 0);
    libusb_close(handle);
    libusb_exit(context);
    return 0;
}
