#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <memory.h>
#include <libusb-1.0/libusb.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
#define ROUND       2

#define SCREEN_MODE_RGB565      0x00
#define SCREEN_MODE_NV12        0x01
#define SCREEN_MODE_MJPEG       0x02

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, MPRO");
        }
        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) {
	    if (code == 0x00000008)
		sprintf(buf, "4.3inch, 480x800x16, VOCORE-4IN3RGB");
	    else
                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 if (code == 0x00000007)
                sprintf(buf, "4inch, 480x800x16, VOCORE-4INRGB");
            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;

    case 0x00000403:
        if (buf)
            sprintf(buf, "3.4inch, 800x800x16/24, VOCORE-3R4RGB, MPRO");
        if (frame_size)
            *frame_size = 800 * 800 * 2;
        if (margin)
            *margin = MARGIN;
        if (pos)
            *pos = ROUND;
        break;
        
    case 0x0000000a:
        if (buf)
            sprintf(buf, "10inch, 1024x600x16, VOCORE-10IN-SCREEN, MPRO");
        if (frame_size)
            *frame_size = 1024 * 600 * 2;
        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;
}

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)] |  m+flip(0~3)\n");
        printf("show frame: screen_test frame.dat\n");
        printf("show frame but no touch test: screen_test frame.dat notouch\n");
        printf("adjust brightness: screen_test b50\n");
        printf("flip/mirror screen(not support all screens): screen_test m2\n");
        return 0;
    }
    
    printf("Version: %s,%s\n\n", __DATE__, __TIME__);
    
    // 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];
    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";
        if (pos == ROUND)
            argv[1] = "800x800d.bmp";
    }
    
    // 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]);
        }
    }

    if (argv[1][0] == 's') {
        // only show the screen type.
        return 0;
    }
    
    if (argv[1][0] == 'b') {
        int brightness = atoi(argv[1] + 1);
        uint8_t cmdb[] = {0x00, 0x51, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
        
        printf("set brightness to %d\n", brightness);
        cmdb[6] = brightness;
        res = libusb_control_transfer(handle, 0x40, 0xb0, 0, 0, cmdb, 8, 100);
    } else if (argv[1][0] == 'm') {
        uint8_t cmdr[] = {0x00, 0x36, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
        cmdr[6] = atoi(argv[1] + 1);
        cmdr[7] = atoi(argv[1] + 1);
        libusb_control_transfer(handle, 0x40, 0xb0, 0, 0, cmdr, 8, 100);
    } else {
        // 2. initialize screen and show frame.
        
        // 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.
        FILE *fp = fopen(argv[1], "rb");
        if (fp == NULL) {
            printf("Input file is not exists\n");
            return -1;
        }

        uint8_t mode;
        if (strstr(argv[1], ".bmp")) {
            fseek(fp, 0, SEEK_END);
            frame_size = ftell(fp) - 138;
            // skip bitmap first 138 bitmap header.
            fseek(fp, 138, SEEK_SET);
            mode = SCREEN_MODE_RGB565;
        }
        if (strstr(argv[1], ".yuv")) {
            fseek(fp, 0, SEEK_END);
            frame_size = ftell(fp);
            fseek(fp, 0, SEEK_SET);
            mode = SCREEN_MODE_NV12;
        }
        if (strstr(argv[1], ".jpg")) {
            fseek(fp, 0, SEEK_END);
            frame_size = ftell(fp);
            fseek(fp, 0, SEEK_SET);
            mode = SCREEN_MODE_MJPEG;
        }

        uint8_t *frame = malloc(frame_size + margin);
        if (fp == NULL || frame_size > fread(frame + margin, 1, frame_size, fp)) {
            printf("Input file do not have enough data, size = %d.\n"
                   "Press any key to exit.\n", frame_size);
            getchar();
            return -1;
        }
        fclose(fp);

        // write start frame command 0x2c.
        uint8_t cmd2[] = {mode, 0x2c, frame_size, frame_size >> 8, frame_size >> 16, 0x00};
        // example: send frame to screen.
        res = libusb_control_transfer(handle, 0x40, 0xb0, 0, 0, cmd2, 6, 100);
        res = libusb_bulk_transfer(handle, 0x02, frame, frame_size, NULL, 100);

        free(frame);
        
        // 3. now handle touch data.
        unsigned char cmd[128] = {0};
        struct touch *t = (struct touch *)cmd;
        int used = 0;
        
        // has the third parameter "notouch", we do not check touch.
        if (argc == 3)
            goto end;
        
        printf("\n\nPlease tap on touch screen to test.\n");
        while (1) {
            int x1, x2, y1, y2, t1, t2;
            char map[][6] = {"PRESS", "HOVER", "DRAG"};
            argc = libusb_interrupt_transfer(handle, 0x81, cmd, 128, &used, -1);
            if (argc < 0)
                break;
            
            x1 = (((int)t->p[0].xh.x.h) << 8) + t->p[0].xl;
            y1 = (((int)t->p[0].yh.y.h) << 8) + t->p[0].yl;
            t1 = t->p[0].xh.x.f;
            x2 = (((int)t->p[1].xh.x.h) << 8) + t->p[1].xl;
            y2 = (((int)t->p[1].yh.y.h) << 8) + t->p[1].yl;
            t2 = t->p[1].xh.x.f;
            
            if (t->p[0].yh.y.id < 2)
                printf("%d:%s (%d, %d) ", t->p[0].yh.y.id, map[t1], x1, y1);
            if (t->p[1].yh.y.id < 2)
                printf("%d:%s (%d, %d) ", t->p[1].yh.y.id, map[t2], x2, y2);
            if (t->p[0].yh.y.id < 2 || t->p[0].yh.y.id < 2)
                printf("\r\n");
        }
    }
    
end:
    // close usb port.
    libusb_release_interface(handle, 0);
    libusb_close(handle);
    libusb_exit(context);
    return 0;
}
