/*
    Copyright 2023 Quectel Wireless Solutions Co.,Ltd

    Quectel hereby grants customers of Quectel a license to use, modify,
    distribute and publish the Software in binary form provided that
    customers shall have no right to reverse engineer, reverse assemble,
    decompile or reduce to source code form any portion of the Software.
    Under no circumstances may customers modify, demonstrate, use, deliver
    or disclose any portion of the Software in source code form.
*/

#include "usb_linux.h"
#include "hostdl_packet.h"

#define true (1 == 1)
#define false (1 != 1)

#define MAX_SEND_BUFFER_SIZE 1280
#define MAX_RECEIVE_BUFFER_SIZE 1280
unsigned char g_Transmit_Buffer[MAX_SEND_BUFFER_SIZE];
int g_Transmit_Length;

unsigned char g_Receive_Buffer[MAX_RECEIVE_BUFFER_SIZE];
int g_Receive_Bytes;

static void *stream_usb_handle;

static void dump_buffer(unsigned char *buff, int len)
{
    int i = 0;

    dbg_time("dump buffer: %d bytes\n", len);
    for (i = 0; i < len; i++)
    {
        dbg_time("%02x ", buff[i]);
    }
    dbg_time("\nend\n");
}

#define CRC_16_L_SEED 0xFFFF
#define CRC_TAB_SIZE 256 /* 2^CRC_TAB_BITS      */
#define CRC_16_L_POLYNOMIAL 0x8408

static const uint16_t crc_16_l_table[CRC_TAB_SIZE] = {
    0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3,
    0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
    0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399,
    0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
    0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50,
    0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
    0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e,
    0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
    0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5,
    0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
    0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693,
    0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
    0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a,
    0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
    0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710,
    0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
    0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df,
    0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
    0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595,
    0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
    0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c,
    0x3de3, 0x2c6a, 0x1ef1, 0x0f78};

unsigned short crc_16_l_calc(unsigned char *buf_ptr, int len)
{
    int data, crc_16;
    for (crc_16 = CRC_16_L_SEED; len >= 8; len -= 8, buf_ptr++)
    {
        crc_16 = crc_16_l_table[(crc_16 ^ *buf_ptr) & 0x00ff] ^ (crc_16 >> 8);
    }
    if (len != 0)
    {
        data = ((int)(*buf_ptr)) << (16 - 8);

        while (len-- != 0)
        {
            if (((crc_16 ^ data) & 0x01) != 0)
            {
                crc_16 >>= 1;
                crc_16 ^= CRC_16_L_POLYNOMIAL;
            }
            else
            {
                crc_16 >>= 1;
            }

            data >>= 1;
        }
    }
    return (~crc_16);
}

void compute_reply_crc()
{
    unsigned short crc = crc_16_l_calc(g_Transmit_Buffer, g_Transmit_Length * 8);
    g_Transmit_Buffer[g_Transmit_Length] = crc & 0xFF;
    g_Transmit_Buffer[g_Transmit_Length + 1] = crc >> 8;
    g_Transmit_Length += 2;
}

static void compose_packet(unsigned char cmd, unsigned char *parameter, uint32_t parameter_len,
                           unsigned char *data, uint32_t data_len)
{
    uint32_t i;

    g_Transmit_Buffer[0] = cmd;
    if (parameter == NULL) parameter_len = 0;
    if (data == NULL) data_len = 0;
    for (i = 0; i < parameter_len; i++)
    {
        g_Transmit_Buffer[1 + i] = parameter[i];
    }
    for (i = 0; i < data_len; i++)
    {
        g_Transmit_Buffer[1 + parameter_len + i] = data[i];
    }
    g_Transmit_Length = 1 + parameter_len + data_len;
    g_Transmit_Buffer[g_Transmit_Length] = 0;
}

static unsigned char stream_tx_buf[1280];
#define CHECK_FOR_DATA() \
    do                   \
    {                    \
    } while (0)
#define TRANSMIT_BYTE(_byte)        \
    do                              \
    {                               \
        stream_tx_buf[j++] = _byte; \
    } while (0)

static int send_packet(int flag)
{
    int i;
    int ch;
    int j;

    j = 0;

    CHECK_FOR_DATA();

    /* Since we don't know how long it's been. */
    if (!!flag)
    {
        TRANSMIT_BYTE(0x7E);
    }

    for (i = 0; i < g_Transmit_Length; i++)
    {
        /* we only need to check once every 31 characters, since RX and TX
         * run at about the same speed, and our RX FIFO is 64 characters
         */
        if ((i & 31) == 31) CHECK_FOR_DATA();

        ch = g_Transmit_Buffer[i];

        if (ch == 0x7E || ch == 0x7D)
        {
            TRANSMIT_BYTE(0x7D);
            TRANSMIT_BYTE(0x20 ^ ch); /*lint !e734 */
        }
        else
        {
            TRANSMIT_BYTE(ch); /*lint !e734 */
        }
    }

    CHECK_FOR_DATA();
    TRANSMIT_BYTE(0x7E);

#if 0
  /* Hack for USB protocol.  If we have an exact multiple of the USB frame
   * size, then the last frame will not be sent out.  The USB standard says
   * that a "short packet" needs to be sent to flush the data.  Two flag
   * characters can serve as the short packet.  Doing it this way, we only
   * perform this test once on every entire packet from the target, so the
   * over head is not too much.
   */
    if ((j%512) == 0)
    {
        TRANSMIT_BYTE (0x7E);
        TRANSMIT_BYTE (0x7E);
    }
#endif

    return (qusb_noblock_write(stream_usb_handle, stream_tx_buf, j, j, 3000, 1) == j) ? 0 : -1;
}

static int remove_escape_hdlc_flag(unsigned char *buffer, int len)
{
    int i = 0;
    int index = 0;
    int escape = 0;
    // dump_buffer(buffer, len);
    if (len == 0) return 0;
    // ignore the first HDLC FLAG bytes
    while (buffer[i] == 0x7e)
    {
        i++;
    }
    // all bytes is HDLC FLAG
    if (i == len) return 0;
    for (; i < len; i++)
    {
        if (buffer[i] == 0x7D)
        {
            escape = 1;
            continue;
        }
        if (escape == 1)
        {
            escape = 0;
            buffer[i] ^= 0x20;
        }
        buffer[index++] = buffer[i];
    }
    buffer[index] = 0;
    // dump_buffer(buffer, index);
    return index;
}

static int receive_packet(void)
{
    int bytesread = 0;
    unsigned char *buff = g_Receive_Buffer;
    if (buff == NULL)
    {
        return -1;
    }

    int idx = 0;
    do
    {
        bytesread =
            qusb_noblock_read(stream_usb_handle, &buff[idx], MAX_RECEIVE_BUFFER_SIZE, 0, 3000);
        if (bytesread == 0)
        {
            // timeout may be error
            dbg_time("%s timeout\n", __FUNCTION__);
            break;
        }
        // dump_buffer(&buff[idx], bytesread);
        idx += bytesread;
        if (buff[idx - 1] == 0x7e)
        {
            // check the packet whether valid.
            g_Receive_Bytes = remove_escape_hdlc_flag(buff, idx);
            if (g_Receive_Bytes == 0)
            {
                continue;
            }
            else
            {
                return 1;
            }
        }
    } while (1);

    return 0;
}

static int handle_hello(void)
{
    static const char host_header[] = "QCOM fast download protocol host";
    // static const char target_header[] = "QCOM fast download protocol targ";
    // char string1[64];
    // int size;
    int err;
    dbg_time("%s\n", __func__);

    memset(&g_Transmit_Buffer[0], 0, sizeof(g_Transmit_Buffer));
    g_Transmit_Buffer[HELLO_CMD_OFFSET] = HELLO_REQ;
    memcpy(&g_Transmit_Buffer[HELLO_MAGIC_NUM_OFFSET], host_header, 32);
    g_Transmit_Buffer[HELLO_MAX_VER_OFFSET] = STREAM_DLOAD_MAX_VER;
    g_Transmit_Buffer[HELLO_MIN_VER_OFFSET] = STREAM_DLOAD_MIN_VER;
    g_Transmit_Buffer[HELLO_MAX_DATA_SZ_1_OFFSET] = 0;
    g_Transmit_Length = 36;

    compute_reply_crc();
    send_packet(1);

    int timeout = 5;
    do
    {
        err = receive_packet();
        if (err == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x02: return 1;
                case 0x0d: continue;
                default:
                    // dump_buffer(g_Receive_Buffer, 64);
                    return 0;
            }
        }
        else if (err == -1)
        {
            dbg_time("error = %d, strerr = %s\n", errno, strerror(errno));
            return 0;
        }
        timeout--;
    } while (timeout);

    return 0;
}

static int handle_security_mode(unsigned char trusted)
{
    dbg_time("%s trusted = %d\n", __func__, trusted);
    compose_packet(0x17, &trusted, 1, NULL, 0);
    compute_reply_crc();
    send_packet(1);
    int timeout = 5;
    do
    {
        if (receive_packet() == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x18: return 1;
                default: return 0;
            }
        }
        else
        {
            timeout--;
            if (timeout == 0)
            {
                dbg_time("%s timeout\n", __FUNCTION__);
                break; // return 0; -Werror,-Wunreachable-code-return
            }
        }
    } while (1);
    return 0;
}
/*
set download flag in module, quectel custom command,
if flag : reboot, module will enter DM
if not flag: reboot normal
*/
static int handle_quectel_download_flag(unsigned char mode)
{
    // byte mode = 1;
    compose_packet(0x60, &mode, 1, NULL, 0);
    compute_reply_crc();
    send_packet(1);
    int timeout = 5;
    do
    {
        if (receive_packet() == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x61:
                    switch (g_Receive_Buffer[1])
                    {
                        case 0x00: return 1;
                        default: return 0;
                    }
                    break;
                case 0x0E: dbg_time("Invalid command"); return 2;
                default: dump_buffer(g_Receive_Buffer, 64); return 0;
            }
        }
        else
        {
            timeout--;
            if (timeout == 0)
            {
                dbg_time("%s timeout\n", __FUNCTION__);
                return 0;
            }
        }
    } while (1);
}

static const char *stream_firehose_dir;
static int stread_fread(const char *filename, void **pp_filebuf)
{
    int filesize = 0;
    FILE *fp;
    char fullpath[MAX_PATH * 2];

    snprintf(fullpath, sizeof(fullpath), "%.240s/../%.240s", stream_firehose_dir, filename);
    fp = fopen(fullpath, "rb");
    if (fp == NULL)
    {
        dbg_time("fail to fope %s, errno: %d (%s)\n", fullpath, errno, strerror(errno));
        return 0;
    }

    fseek(fp, 0, SEEK_END);
    filesize = ftell(fp);

    *pp_filebuf = malloc(filesize);
    if (pp_filebuf == NULL)
    {
        dbg_time("fail to malloc %d, errno: %d (%s)\n", filesize, errno, strerror(errno));
        if (fp)
        {
            fclose(fp);
            fp = NULL;
        }
        return 0;
    }

    fseek(fp, 0, SEEK_SET);
    filesize = fread(*pp_filebuf, 1, filesize, fp);
    fclose(fp);

    dbg_time("%s filename=%s, filesize=%d\n", __func__, filename, filesize);
    return filesize;
}

static int handle_parti_tbl(unsigned char override)
{
    int timeout = 5;
    int filesize;
    void *filebuf;
    const char *partition_path = "partition.mbn";
    dbg_time("%s override = %d\n", __func__, override);

    filesize = stread_fread(partition_path, &filebuf);
    if (filesize <= 0)
    {
        if (filebuf)
        {
            free(filebuf);
            filebuf = NULL;
        }
        return 0;
    }

    compose_packet(0x19, &override, 1, filebuf, filesize);
    compute_reply_crc();
    send_packet(1);
    free(filebuf);

    do
    {
        if (receive_packet() == 1)
        {
            dbg_time("handle_parti_tbl command = %02x, status = %02x\n", g_Receive_Buffer[0],
                     g_Receive_Buffer[1]);
            switch (g_Receive_Buffer[0])
            {
                case 0x1a:
                    switch (g_Receive_Buffer[1])
                    {
                        case 0x00: return 1;
                        case 0x01: // 0x1 this means that the original partition is different from
                                   // the current partition,try to send partition
                            return 0;
                        case 0x02: // 0x2 Partition table format not recognized, does not accept
                                   // override
                            return 0;
                        case 0x03: // 0x3  Erase operation failed
                            return 0;
                            break;
                        default: return 0;
                    }
                default: return 0;
            }
        }
        else
        {
            timeout--;
            if (timeout == 0)
            {
                dbg_time("%s timeout\n", __FUNCTION__);
                return 0;
            }
        }
    } while (1);
}

static int handle_reset(void)
{
    dbg_time("%s\n", __func__);
    compose_packet(0x0b, NULL, 0, NULL, 0);
    compute_reply_crc();
    send_packet(1);
#if 1
    return 1;
#else
    int timeout = 5;
    do
    {
        if (receive_packet() == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x0c: return 1;
                case 0x0d: continue;
                default: dump_buffer(g_Receive_Buffer, 64); return 0;
            }
        }
        else
        {
            timeout--;
            if (timeout == 0)
            {
                dbg_time("%s timeout\n", __FUNCTION__);
                return 0;
            }
        }
    } while (1);
#endif
}

/******pkt_open_multi_image*******/

static void pkt_open_multi_image(unsigned char mode, unsigned char *data, uint32_t size)
{
    compose_packet(0x1b, &mode, 1, data, size);
    compute_reply_crc();
}

static int handle_openmulti(uint32_t size, unsigned char *data)
{
    int timeout = 5;
    unsigned char mode = 0x0e;

    pkt_open_multi_image(mode, data, size);
    send_packet(1);
    do
    {
        if (receive_packet() == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x1c: return 1;
                case 0x0d: continue;
                default: return 0;
            }
        }
        else
        {
            timeout--;
            if (timeout == 0)
            {
                dbg_time("%s timeout\n", __FUNCTION__);
                break;
            }
        }
    } while (1);
    return 0;
}

/******pkt_write_multi_image*******/
static void pkt_write_multi_image(uint32_t addr, unsigned char *data, uint16_t size)
{
    unsigned char parameter[4] = {(unsigned char)(addr)&0xff, (unsigned char)(addr >> 8) & 0xff,
                                  (unsigned char)(addr >> 16) & 0xff,
                                  (unsigned char)(addr >> 24) & 0xff};
    compose_packet(0x07, parameter, 4, data, size);
    compute_reply_crc();
}

static int handle_write(unsigned char *data, uint32_t size)
{
    // uint32_t total_size;
    uint32_t addr = 0;
    uint32_t writesize;
    uint32_t buffer_size = 1024;
    // int loop = 1;
    int retry_cnt = 3; // if send failed,send again
    int ret;

    // total_size = size;
    while (size)
    {
        writesize = (size < buffer_size) ? size : buffer_size;

        pkt_write_multi_image(addr, data, writesize);
    start_send_packet:
        ret = send_packet(1);
        if (0 != ret)
        {
            dbg_time("io read/write failed\n");
            return 0;
        }
        if (receive_packet() == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x08:
                    size -= writesize;
                    addr += writesize;
                    // retry_cnt=5;
                    break;
                default:
                    goto retry_send_packet;
                    // return 0;
            }
        }
        else
        {
        retry_send_packet:
            retry_cnt--;
            if (retry_cnt > 0)
            {
                goto start_send_packet;
            }
            else
            {
                dbg_time("value is [0x%02x]", g_Receive_Buffer[0]);
                return 0;
            }
        }
    }

    return 1;
}
/******PARTITION*******/
static int handle_close(void)
{
    int timeout = 5;
    compose_packet(0x15, NULL, 0, NULL, 0);
    compute_reply_crc();
    send_packet(1);

    do
    {
        if (receive_packet() == 1)
        {
            switch (g_Receive_Buffer[0])
            {
                case 0x16: return 1;
                default: return 0;
            }
        }
        else
        {
            timeout--;
            if (timeout == 0)
            {
                dbg_time("%s timeout\n", __FUNCTION__);
                break;
            }
        }
    } while (1);
    return 0;
}

static int do_flash_mbn(const char *partion, const char *filepath)
{
    int result = false;
    void *filebuf = NULL;
    int filesize = 0;

    dbg_time("%s %s\n", __func__, partion);

    if (filepath)
    {
        filesize = stread_fread(filepath, &filebuf);
        if (filesize <= 0)
        {
            if (filebuf)
            {
                free(filebuf);
                filebuf = NULL;
            }
            return 0;
        }
    }
    else
    {
        filesize = 4 * 1024;
        filebuf = (unsigned char *)malloc(filesize);
        if (filebuf == NULL)
        {
            return 0;
        }

        memset(filebuf, 0x00, filesize);
    }

    result = handle_openmulti(strlen(partion) + 1, (unsigned char *)partion);
    if (result == false)
    {
        dbg_time("%s open failed\n", partion);
        goto __fail;
    }

    dbg_time("sending '%s' (%dKB)\n", partion, (int)(filesize / 1024));

    result = handle_write(filebuf, filesize);
    if (result == false)
    {
        dbg_time("%s download failed\n", partion);
        goto __fail;
    }

    result = handle_close();
    if (result == false)
    {
        dbg_time("%s close failed.\n", partion);
        goto __fail;
    }

    dbg_time("OKAY\n");

__fail:
    free(filebuf);

    return result;
}

int stream_download(const char *firehose_dir, void *usb_handle, unsigned qusb_zlp_mode)
{
    (void)qusb_zlp_mode;
    stream_usb_handle = usb_handle;
    stream_firehose_dir = firehose_dir;

    if (handle_hello() == false)
    {
        dbg_time("Send hello command fail\n");
        return false;
    }

    /*
    hello packet will set dload flag in module, when upgrade interrup, restart module,module will
    enter dm(quectel sbl)
    */
    if (handle_security_mode(1) == false)
    {
        dbg_time("Send trust command fail\n");
        return false;
    }

    if (handle_parti_tbl(0) == false)
    {
        dbg_time("----------------------------------\n");
        dbg_time("Detect partition mismatch.\n");
        dbg_time("Download parition with override.\n");
        dbg_time("----------------------------------\n");

        if (handle_parti_tbl(1) == false)
        {
            dbg_time("override failed. \n");
            return false;
        }

        /*
        partition is not match, the download flag will be clear, so set it again, reset will clear
        it
        */
        if (handle_quectel_download_flag(1) == false)
        {
            dbg_time("Set Quectel download flag failed\n");
        }
        else
        {
            dbg_time("Set Quectel download flag successfully\n");
        }
    }

#if 1
    if (do_flash_mbn("0:SBL", "sbl1.mbn") == false)
    {
        return false;
    }
#endif

    if (handle_reset() == false)
    {
        dbg_time("Send reset command failed\n");
        return false;
    }

    dbg_time("%s successful\n", __func__);

    return true;
}

// retrieve module soft revision

typedef struct
{
    unsigned char cmd_code;
    unsigned char version;
    unsigned char reserved[2];
    unsigned char msm[4];
    unsigned char mobile_modle_number[4];
    unsigned char mobile_software_revision[1];
} __attribute__((packed)) extended_build_id_response_t;

int retrieve_soft_revision(void *usb_handle, uint8_t *mobile_software_revision, unsigned length)
{
    /*
    80-v1294-1_yyd_serial_interface_control_document_(icd)_for_cdma_dual-mode_subscriber_station_data
    3.4.122 Extended Build ID
    */
    uint8_t req1[] = {0x7E, 0x7C, 0x93, 0x49, 0x7E};
    int ret;
    uint8_t *rx_buff = malloc(2048);

    memset(mobile_software_revision, 0x00, length);

    if (rx_buff == NULL) return 0;

    ret = qusb_noblock_write(usb_handle, req1, sizeof(req1), sizeof(req1), 1000, 0);
    if (ret > 0)
    {
        ret = qusb_noblock_read(usb_handle, rx_buff, 2048, 1, 3000);
        if (ret > 0)
        {
            if (rx_buff[0] == 0x7C && rx_buff[ret - 1] == 0x7E)
            {
                extended_build_id_response_t *rsp = (extended_build_id_response_t *)rx_buff;
                (void)length;
                memcpy(mobile_software_revision, rsp->mobile_software_revision,
                       strlen((const char *)rsp->mobile_software_revision));
            }
        }
    }

    free(rx_buff);
    return (mobile_software_revision[0] != '\0');
}
