#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#ifdef _WIN32
#include <locale.h>
#endif

#include "burn_intf.h"
#include "firm_check.h"

#define L502BURN_OPT_HELP           'h'
#define L502BURN_OPT_INTF           'i'
#define L502BURN_OPT_READ           'r'
#define L502BURN_OPT_IOADDR         'a'
#define L502BURN_OPT_READ_START     'f'
#define L502BURN_OPT_READ_LAST      'l'
#define L502BURN_OPT_SERIAL         's'
#define L502BURN_OPT_DEVNAME        'd'
#define L502BURN_OPT_INVERSE        0x10000
#define L502BURN_OPT_MAIN_ONLY      0x10001
#define L502BURN_OPT_ALL            0x10002
#define L502BURN_OPT_DONT_CHECK_VER 0x10003
#define L502BURN_OPT_RESERV_ONLY    0x10004
#define L502BURN_OPT_CON_TIME       0x10005
#define L502BURN_OPT_VERSION        0x10006

static const struct option f_long_opt[] = {
    {"help",        no_argument,        0, L502BURN_OPT_HELP},
    {"intf",        required_argument,  0, L502BURN_OPT_INTF},
    {"read",        no_argument,        0, L502BURN_OPT_READ},
    {"first",       required_argument,  0, L502BURN_OPT_READ_START},
    {"last",        required_argument,  0, L502BURN_OPT_READ_LAST},
    {"io-addr",     required_argument,  0, L502BURN_OPT_IOADDR},
    {"serial",      required_argument,  0, L502BURN_OPT_SERIAL},
    {"devname",     required_argument,  0, L502BURN_OPT_DEVNAME},
    {"bit-inverse", no_argument,        0, L502BURN_OPT_INVERSE},
    {"main-only",   no_argument,        0, L502BURN_OPT_MAIN_ONLY},
    {"all",         no_argument,        0, L502BURN_OPT_ALL},
    {"dont-check-ver", no_argument,     0, L502BURN_OPT_DONT_CHECK_VER},
    {"reserv-only", no_argument,        0, L502BURN_OPT_RESERV_ONLY},
    {"con-time",    required_argument,  0, L502BURN_OPT_CON_TIME},
    {"version",     no_argument,        0, L502BURN_OPT_VERSION},
    {0,0,0,0}
};



static const char* f_opt_str = "hri:a:f:l:s:d:";


static const char* f_usage_descr = \
"\nУтилита lxfw_update предназначена для обновление прошивок некоторых устройств\n"
"    фирмы \"Л Кард\". В настоящее время поддерживается обновление прошивок ПЛИС\n"
"    для модулей E502 и L502. Прошивки должны состоять из двух файлов, одного с\n"
"    расширением .lxfw, содержащего сами данные прошивки с метоинформацией, и \n"
"    файла с расширением .sig, имя которого повторяет имя первого, включая\n"
"    расширение, с подписью данной прошивки.\n"
"\nИспользование:\n"\
" lxfw_update --serial=<sn> <file>\n\n"\
" Обновить прошивку для устройства с указанным серийным номером из файла\n\n"\
" lxfw_update --all <file>\n\n"\
" Обновить прошивку всех найденных устройтсв, которым подходит указанная прошивка\n\n";

extern t_burn_intf g_pcie_intf;

static const t_burn_intf *f_intfs[] = {
    &g_pcie_intf
};


typedef enum {
    X502_EEPROM_PROT_ALL          = 0, /**< Защищена вся область памяти */
    X502_EEPROM_PROT_WR_USER      = 1, /**< Разрешено изменение только пользовательской части */
    X502_EEPROM_PROT_WR_SETTINGS  = 2, /**< Кроме пользовательской части, разрешено изменение
                                           области настроек, флага загрузки и второй копии прошивки ПЛИС */
    X502_EEPROM_PROT_WR_FPGA_FIRM = 3, /**< Разрешено изменение основной прошивки ПЛИС */
    X502_EEPROM_PROT_WR_ALL       = 4, /**< Разрешена запись в любую область памяти */
} t_x502_eeprom_prot_state;


typedef struct {
    uint32_t size;
    uint32_t sign;
    uint32_t crc32;
    uint32_t flags;
} t_firm_prefix;

//#define FPGA_FIRM_SIGN   0xE502A5A5

//#define L502_FLASH_FIRM_PREFIX  0x100000
//#define L502_FLASH_FIRM_START   0x100010
//#define L502_FLASH_FIRM_LAST    0x16FFFF
#define L502_FLASH_BOOT_FLAG    0x170000
#define L502_FLASH_SIZE         0x200000



#define L502BURN_BLOCK_SIZE 4096
static uint8_t f_block[L502BURN_BLOCK_SIZE], f_block2[L502BURN_BLOCK_SIZE];


typedef struct
{
    uint32_t prefix;
    uint32_t firm;
    uint32_t last;
} t_firm_addrs;

static t_firm_addrs f_firm_addrs[2] =
{
    {0x180000, 0x180010, 0x1EFFFF},
    {0x100000, 0x100010, 0x16FFFF}
};




static void f_print_usage(void) {
    printf("%s", f_usage_descr);
}

static void f_print_version(void) {
    fprintf(stdout, "lxfw-update version: %s\n", PROJECT_VERSION);
}
static void f_print_usage_err(const char *msg) {
    fprintf(stderr, "Ошибка входных параметров: %s.\nДля правильного использования вызовете lxfw_update --help\n", msg);
}


static void f_print_status(const t_lxfw_burn_opt* st, int err) {
    if (err != 0) {
        fprintf(stderr, "Ошибка %d: %s!\n",  err,
                err < LXFW_ERR_INVALID_PARAM ? lxfw_get_err_str(err) : st->intf->get_err_str(err));
    } else {
        printf("Ок.\n"); fflush(stdout);
        fflush(stdout);
    }
}



static int f_parse_options(t_lxfw_burn_opt* st, int argc, char** argv, int *out) {
    int err = 0;
    int opt = 0;

    *out = 0;
    st->port = 0x378;
    st->op = L502BURN_OP_WRITE;
    st->read_start = 0;
    st->read_last = L502_FLASH_SIZE-1;
    st->invers  = 0;
    st->serial = 0;
    st->intf = f_intfs[0];
    st->main_only = 0;
    st->all = 0;
    st->filenames_cnt = 0;
    st->devname = NULL;
    st->ver_dont_check = 0;
    st->reserv_only = 0;
    st->suc_updated_devcnt = 0;

    /************************** разбор опций *****************************/
    while ((opt!=-1) && !err && !*out) {
        int ind;
        opt = getopt_long(argc, argv, f_opt_str,
                          f_long_opt, &ind);

        switch (opt) {
            case -1:
                break;
            case L502BURN_OPT_HELP:
                f_print_usage();
                *out = 1;
                break;
            case L502BURN_OPT_VERSION:
                f_print_version();
                *out = 1;
                break;
            case L502BURN_OPT_INVERSE:
                st->invers = 1;
                break;
            case L502BURN_OPT_READ:
                st->op = L502BURN_OP_READ;
                break;
            case L502BURN_OPT_READ_START:
                sscanf(optarg, "%i", &st->read_start);
                if ((st->read_start < 0) || (st->read_start>=L502_FLASH_SIZE)) {
                    err = LXFW_ERR_INVALID_PARAM;
                    f_print_usage_err("Неверный адрес начала чтения");
                }
                break;
            case L502BURN_OPT_READ_LAST:
                sscanf(optarg, "%i", &st->read_last);
                if ((st->read_last < 0) || (st->read_last>=L502_FLASH_SIZE)) {
                    err = LXFW_ERR_INVALID_PARAM;
                    f_print_usage_err("Неверный адрес завершения чтения");
                }
                break;
            case L502BURN_OPT_SERIAL:
                st->serial = optarg;
                break;
            case L502BURN_OPT_DEVNAME:
                st->devname = optarg;
                break;
            case L502BURN_OPT_ALL:
                st->all = 1;
                break;
            case L502BURN_OPT_DONT_CHECK_VER:
                st->ver_dont_check = 1;
                break;
            case L502BURN_OPT_MAIN_ONLY:
                st->main_only = 1;
                break;
            case L502BURN_OPT_RESERV_ONLY:
                st->reserv_only = 1;
                break;
            case L502BURN_OPT_CON_TIME:
                st->con_tout = atoi(optarg);
                break;
            default:
                err = LXFW_ERR_INVALID_PARAM;
                f_print_usage_err("Неизвестная опция командной строки!");
                break;
        }
    }

    if (!*out && !err) {
        if (st->read_last < st->read_start) {
             err = LXFW_ERR_INVALID_PARAM;
             f_print_usage_err("Последний адрес чтения должен быть больше или равен первого");
        } else {
            st->filenames_cnt = argc-optind;
            if (st->filenames_cnt < 1) {
                err = LXFW_ERR_INVALID_PARAM;
                f_print_usage_err("Не указан файл прошивки");
            } else if (st->filenames_cnt > LXFW_MAX_FILENAMES) {
                err = LXFW_ERR_INVALID_PARAM;
                f_print_usage_err("Указано слишком много файлов прошивки");
            } else {
                int i;

                for (i = 0; i < st->filenames_cnt; i++)
                    st->filenames[i] = argv[optind + i];
            }
        }
    }
    return err;
}


static unsigned char f_inverse_byte(unsigned char byte) {
    unsigned char res = 0;
    int j;
    for (j=0; j < 8; j++) {
        if (byte & (1 << j))
            res |= (1 << (7-j));
    }
    return res;
}

static int f_erase(const t_lxfw_burn_opt* opts, uint32_t first, uint32_t last) {
    int err = 0;
    uint32_t addr;
    printf("Стирание памяти (0x%x-0x%x)... ", first, last); fflush(stdout);
    for (addr=first; (addr < last) && !err; addr+=64*1024) {
        err = opts->intf->flash_erase_64k(addr);
    }
    f_print_status(opts, err);
    return err;
}

static int f_write(const t_lxfw_burn_opt* opts,  const t_firm_prefix* prefix,
                   uint32_t addr, uint32_t size) {
    unsigned char last[2], wr_last = 0;
    /* запись... */
    int err = 0;
    size_t offs = 0;

    printf("Запись прошивки   "); fflush(stdout);

    err = opts->intf->flash_write_start(addr, (const uint8_t*)prefix,
                                            sizeof(t_firm_prefix));

    while (!err && size) {
        uint32_t wr_size = (uint32_t)lxfw_get_data(opts->cur_fw_info, f_block, offs,
                                                   L502BURN_BLOCK_SIZE);
        if (wr_size) {
            uint32_t i;
            offs+=wr_size;
            if (opts->invers) {
                for (i=0; i < wr_size; i++) {
                    f_block[i] = f_inverse_byte(f_block[i]);
                }
            }

            if (wr_size & 1) {
                wr_last = 1;
                last[0] = f_block[wr_size-1];
                last[1] = 0xFF;
                wr_size-=1;
            }

            err = opts->intf->flash_write_next(f_block, wr_size);
            if (!err) {
                printf(".");
            } else {
                printf("!");
            }
            fflush(stdout);

        }
        size-=wr_size;

        if (!err && wr_last)
        {
            err = opts->intf->flash_write_next(last, 2);
            size--;
        }
    }


    opts->intf->flash_write_stop();


    printf(err != 0 ? "\n" : " ");
    f_print_status(opts, err);

    return err;
}

static int f_firm_check(const t_lxfw_burn_opt* opts, const t_firm_prefix* prefix,
                   uint32_t addr, uint32_t size) {
    int err = 0;
    uint32_t i;
    t_firm_prefix check_pref;

    printf("Проверка прошивки ");

    /* проверяем префикс */
    err = opts->intf->flash_read(addr, (uint8_t*)&check_pref, sizeof(t_firm_prefix));

    for (i=0; (i < sizeof(check_pref))&&!err; i++) {
        if (((char*)&check_pref)[i]!=((char*)prefix)[i]) {
            err = LXFW_ERR_DEVICE_CHECK;
            fprintf(stderr, "\nОшибка проверки! Записано 0x%02x, прочитано 0x%02x. Адрес = 0x%06x\n",
                ((uint8_t*)prefix)[i], ((uint8_t*)&check_pref)[i], addr + i);
        }
    }

    if (!err) {
        size_t offs = 0;
        addr+=sizeof(t_firm_prefix);

        while (!err && size) {
            uint32_t wr_size = (uint32_t)lxfw_get_data(opts->cur_fw_info, f_block, offs, L502BURN_BLOCK_SIZE);
            if (wr_size) {
                offs+=wr_size;

                err =  opts->intf->flash_read(addr, f_block2, wr_size);
                if (!err) {
                    for (i=0; (i < wr_size)&&!err; i++) {
                        if (opts->invers)
                            f_block2[i] = f_inverse_byte(f_block2[i]);

                        if (f_block[i]!=f_block2[i]) {
                            err = LXFW_ERR_DEVICE_CHECK;
                            fprintf(stderr, "\nОшибка проверки! Записано 0x%02x, прочитано 0x%02x. Адрес = 0x%06x\n",
                                f_block[i], f_block2[i], addr + i);
                        }
                    }

                    if (!err) {
                        printf("."); fflush(stdout);
                    }
                }
            }

            size-=wr_size;
            addr+=wr_size;
        }
    }

    printf(err != 0 ? "\n" : " ");
    f_print_status(opts, err);
    return err;
}


static int f_set_boot_flag(const t_lxfw_burn_opt* opts, uint8_t flag) {
    int err;
    printf("Установка флага загрузки... ");
    err = opts->intf->flash_erase_64k(L502_FLASH_BOOT_FLAG);
    if (!err) {
        uint8_t pref[2] = {flag,0};
        err = opts->intf->flash_write_start(L502_FLASH_BOOT_FLAG, pref, 2);
        if (!err)
            err = opts->intf->flash_write_stop();
    }

    if (!err) {
        uint8_t pref;
        err = opts->intf->flash_read(L502_FLASH_BOOT_FLAG, &pref,1);
        if (pref!=flag)
            err = LXFW_ERR_INVALID_BOOT_FLAG;
    }
    f_print_status(opts, err);
    return err;
}

static int f_get_boot_flag(const t_lxfw_burn_opt* opts, uint8_t* flag) {
    int err = opts->intf->flash_read(L502_FLASH_BOOT_FLAG, flag, 1);
    if (!err)
        *flag&=1;
    return err;
}





static int f_write_firm_par(const t_lxfw_burn_opt* opts, t_firm_prefix pref) {
    int size = pref.size;
    t_firm_addrs* addrs;
    int err=0;
    uint8_t flag;

    err = f_get_boot_flag(opts, &flag);
    if (!err) {
        flag^=1;
        addrs =  &f_firm_addrs[flag];
        printf("Запись прошивки для значения флага = %d\n", flag);
    }

    if (!err) {
        printf("Снятие защиты памяти... ");
        /* защищаем область 0x180000 или 0x1F0000 в зависимости от флага */
        err = opts->intf->flash_set_prot(flag ? X502_EEPROM_PROT_WR_SETTINGS :
                                             X502_EEPROM_PROT_WR_FPGA_FIRM);
        f_print_status(opts, err);
    }



    /* стираем область памяти под прошивку */
    if (!err) {        
        err = f_erase(opts, addrs->prefix, addrs->last);
    }

    if (!err) {         
         err = f_write(opts, &pref, addrs->prefix, size);
    }

    if (!err) {        
        err = f_firm_check(opts, &pref, addrs->prefix, size);
    }

    if (!err && !opts->reserv_only) {
        /* устанавливаем загрузочный флаг в 1, чтобы указать, что
           грузимся из нужной области */

        err = f_set_boot_flag(opts, flag);        
    }

    return err;
}






static int f_write_firm(t_lxfw_burn_opt* opts) {
    int err = 0;
    int stop_err;
    int i;

    t_firm_prefix pref;
    opts->cur_fw_info = 0;

    printf("Проверка устройства... "); fflush(stdout);
    /* Ищем прошивку, которая бы подходила к устройству */
    for (i = 0; (i < opts->filenames_cnt) && (opts->cur_fw_info == 0); i++)  {
        if (opts->intf->check_dev)
            err = opts->intf->check_dev(opts->firm_infos[i]);
        if (!err && opts->intf->check_ver && !opts->ver_dont_check) {
            const char* ver = lxfw_get_version(opts->firm_infos[i]);
            err = opts->intf->check_ver(opts, ver);
        }
        if (!err) {
            opts->cur_fw_info = opts->firm_infos[i];
        }
    }

    f_print_status(opts, err);
    if (!err) {
        const char *inv_val;
        memset(&pref, 0, sizeof(pref));
        lxfw_get_size(opts->cur_fw_info, &pref.size);
        lxfw_get_wr_sign(opts->cur_fw_info, &pref.sign);
        lxfw_get_crc32(opts->cur_fw_info, &pref.crc32);

        inv_val = lxfw_get_property(opts->cur_fw_info, "bit_inverse");
        if ((inv_val != NULL) && !strcmp(inv_val, "true")) {
            opts->invers = 1;
        } else {
            opts->invers = 0;
        }

        if (!err) {
            err = f_write_firm_par(opts, pref);
            if (!err && !opts->main_only && !opts->reserv_only)
                err = f_write_firm_par(opts, pref);
            if (!err && !opts->main_only && !opts->reserv_only){
                uint8_t flag = 0;
                err = f_get_boot_flag(opts, &flag);
                if (!err && (flag != 0)) {
                    err = f_set_boot_flag(opts, 0);
                }
            }
        }

        /* защищаем всю область, включая переписанную прошивку */
        printf("Установка защиты памяти... "); fflush(stdout);
        stop_err = opts->intf->flash_set_prot(X502_EEPROM_PROT_ALL);
        f_print_status(opts, stop_err);
    }

    if (!err) {
        printf("Проверка работы... "); fflush(stdout);
        err = opts->intf->reload_info();
        f_print_status(opts, err);
    }

    return err;
}


static int f_proc_op(t_lxfw_burn_opt* opts) {
    int32_t err = 0;
    t_ltimer con_tmr;

    printf("Открытие устройства... "); fflush(stdout);
    ltimer_set(&con_tmr, LTIMER_CLOCK_TICKS_TO_MS(opts->con_tout));
    do {
        err = opts->intf->open(opts);
    } while (err && !ltimer_expired(&con_tmr));

    if (err)
        f_print_status(opts, err);

    if (!err) {
        switch (opts->op) {
            case L502BURN_OP_WRITE:
                err = f_write_firm(opts);
                break;
            default:
                err = LXFW_ERR_INVALID_PARAM;
                fprintf(stderr, "Неизвестаная команда!\n");
                break;
            //case L502BURN_OP_READ:
              //  err = f_read_flash(&par, &hnd);
               // break;
        }
        opts->intf->close();
    }

    if (!err) {
        opts->suc_updated_devcnt++;
    }
    return err;
}

int main(int argc, char **argv) {
    t_lxfw_burn_opt opts;
    int err, out, i;
#ifdef _WIN32
    /* для вывода русских букв в консоль для ОС Windows*/
    setlocale(LC_ALL, "");
#endif

    err = f_parse_options(&opts, argc, argv, &out);
    if (!err && !out) {
        printf("Проверка файла прошивки... "); fflush(stdout);
        for (i = 0; (i < opts.filenames_cnt) && !err; i++) {
            err = lxfw_check_sign(opts.filenames[i]);
            if (!err)
                err = lxfw_init(opts.filenames[i], &opts.firm_infos[i]);
        }

        if (!err && (opts.devname==NULL))
            opts.devname = lxfw_get_devname(opts.firm_infos[0]);
        f_print_status(&opts, err);

        if (!err) {
            if (opts.all) {
                opts.intf->exec_for_all(&opts, f_proc_op);
                printf("\nУспешно обновлено устройств: %d\n", opts.suc_updated_devcnt);
            } else {
                f_proc_op(&opts);
            }

            for (i = 0; i < opts.filenames_cnt; i++)
                lxfw_clean(opts.firm_infos[i]);
        }
        lxfw_cleanup();
    }
    return err;
}




