/*
 * diot_util - diagnostic tool for DIOT system board
 *
 * Author: Adam Wujek for CERN
 * Copyright CERN 2021
 *
 */


#include <stdio.h>
#include <string.h>
#include <gpiod.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>

#include <readline/readline.h>
#include <readline/history.h>
#include "diot_util.h"
#include "term.h"
#include "diot_util_clk.h"
#include "diot_util_gpio.h"
#include "diot_util_sensors.h"
#include "diot_util_error.h"
#include "diot_util_pts.h"
#include "diot_util_fantray.h"
#include "diot_util_i2c.h"
#include "fru.h"
#include "util.h"

#include "fru_utils.h"
#include "fpga_device.h"

#define DTB_HW_VER_FILE		"/sys/firmware/devicetree/base/chosen/hw_ver"
#define DTB_MODEBOOT_FILE	"/sys/firmware/devicetree/base/chosen/modeboot"
#define DTB_UIMAGE_SOURCE_FILE	"/sys/firmware/devicetree/base/chosen/uimage_source"
#define DTB_RESET_REASON_FILE	"/sys/firmware/devicetree/base/chosen/reset_reason"

#define READLINE_PROMPT "diot_util> "

#define FPGA_DEVICE_ADDR 0x80000000

#define RED_CHECK(x) x ? COLOR_RED: ""
#define RED_OFF(x) x ? COLOR_OFF: ""

/* readline related */
char **command_completion(const char *, int, int);
char *command_name_generator(const char *, int);

/* commands */
int cmd_print_status(char *params);
int cmd_exit(char *params);
int cmd_help(char *params);
int cmd_version(char *params);

struct string_map reset_reason_db[] = {
    { "DEBUG",    "debuger"      },
    { "SOFT",     "soft"         },
    { "SRST",     "external"     },
    { "PS-ONLY",  "ps-only"      },
    { "PMU",      "pmu"          },
    { "INTERNAL", "internal POR" },
    { "EXTERNAL", "external POR" },
};

struct string_map boot_mode_db[] = {
    { "usb_dfu_spl", "usb"     },
    { "jtagboot",    "jtag"    },
    { "qspiboot",    "qspi"    },
    { "emmcboot",    "emmc"    },
    { "sdboot",      "microSD" },
    { "nandboot",    "nand"    },
};

struct command_entry commands_list[] = {
    { .cmd_name = "status",            .cmd_func = cmd_print_status,    .cmd_help_string = "Print status\n",                               .cmd_params = NULL},
    { .cmd_name = "exit",              .cmd_func = cmd_exit,            .cmd_help_string = "quit program\n",                               .cmd_params = NULL},
    { .cmd_name = "quit",              .cmd_func = cmd_exit,            .cmd_help_string = "quit program\n",                               .cmd_params = NULL},
    { .cmd_name = "help",              .cmd_func = cmd_help,            .cmd_help_string = "Print help\n",                                 .cmd_params = NULL},
    { .cmd_name = "version",           .cmd_func = cmd_version,         .cmd_help_string = "Print version information\n",                  .cmd_params = NULL},
    { .cmd_name = "led2",              .cmd_func = cmd_led2,            .cmd_help_string = "Enable/disable LED2\n",                        .cmd_params = "on|off"},
    { .cmd_name = "peripheral_reset",  .cmd_func = cmd_peripheral_rst,  .cmd_help_string = "Reset peripheral boards\n",                    .cmd_params = NULL},
    { .cmd_name = "power_cycle_crate", .cmd_func = cmd_power_cycle,     .cmd_help_string = "Power cycle DI/OT create\n",                   .cmd_params = NULL},
    { .cmd_name = "sensors_expert",    .cmd_func = cmd_sensors_expert,  .cmd_help_string = "Display sensors in expert mode\n",             .cmd_params = NULL},
    { .cmd_name = "sensors",           .cmd_func = cmd_sensors,         .cmd_help_string = "Display sensors short version\n",              .cmd_params = NULL},
    { .cmd_name = "sensors_refresh",   .cmd_func = cmd_sensors_refresh, .cmd_help_string = "Display sensors short version with refresh\n", .cmd_params = NULL},
    { .cmd_name = "clocks",            .cmd_func = cmd_clocks,          .cmd_help_string = "Display info about clocks\n",                  .cmd_params = NULL},
    { .cmd_name = "pts",               .cmd_func = cmd_pts,             .cmd_help_string = "Run PTS basic checks\n",                       .cmd_params = NULL},
    { .cmd_name = "fantray",           .cmd_func = cmd_fantray,         .cmd_help_string = "Commands for fantray module. Type \"help "
                                                                                           "fantray\" for more information about "
                                                                                           "subcommands\n",                                .cmd_params = NULL},
    { .cmd_name = "gateware",          .cmd_func = cmd_fpga,            .cmd_help_string = "Print information related to the gateware. Type help for more information\n",  .cmd_params = NULL},
    { .cmd_name = NULL }
};

int cmd_help(char *params)
{
    struct command_entry *entry = commands_list;
    char buf[50];

    /* Print fantray's help if requested */
    if (params && !strncmp(params, "fantray", strlen("fantray")))
        entry = commands_list_fantray;
        printf("diot_util for HW version %d\n", SB_VER);
        printf("Available commands:\n");
        while (entry->cmd_name) {
        sprintf(buf, "%s %s", entry->cmd_name,
        entry->cmd_params ? entry->cmd_params : "");
        printf("%-18s - %s", buf, entry->cmd_help_string);
        entry++;
    }

    return 0;
}

int cmd_version(char *params)
{
    printf("diot_util version information:\n");
    printf("HW version:    %d\n", SB_VER);
    printf("GIT commit id: %s\n", __GIT_VER__);
    printf("Build date:    %s\n", __DATE__);
    printf("Build time:    %s\n", __TIME__);
    printf("Built by:      %s\n", __GIT_USR__);

    return 0;
}


int cmd_exit(char *params)
{
    printf("Exiting...\n");

    close_gpios();
    exit(0);
}

static void print_slot_status(void)
{
    int i;
    int ret;
    int slot_use;
    uint8_t eeprom_buf[1024*10];
    struct FRU_DATA *fru = NULL;

    printf("Peripheral Boards Info\n");
    printf("-----+---------+----------------------+----------------------+----------------------+----------------------+\n");
    printf("Slot | Present |     Manufacturer     |     Product_name     |     Serial number    |      Part Number     |\n");
    printf("-----+---------+----------------------+----------------------+----------------------+----------------------+\n");

    for (i = 1; i <= 8; i++) {
	slot_use = read_slot_use(i);
	printf("%4d |%8s |", i, slot_use ? "Y": "N");

	if (!slot_use) {
	    printf(" %20s | %20s | %20s | %20s |\n", "", "", "", "");
	    continue;
	}

	ret = read_pb_eeprom(eeprom_buf, sizeof(eeprom_buf), i);
	if (!ret) {
	    printf(" %20s | %20s | %20s | %20s |\n", "read error", "read error", "read error", "read error");
	    continue;
	}

	fru = parse_FRU(eeprom_buf);
	if (!fru || !fru->Board_Area) {
	    printf(" %20s | %20s | %20s | %20s |\n", "parse error", "parse error", "parse error", "parse error");
	    continue;
	}

	printf(" %20s | %20s | %20s | %20s |\n",
	       dump_fru_field_string(fru->Board_Area->manufacturer),
	       dump_fru_field_string(fru->Board_Area->product_name),
	       dump_fru_field_string(fru->Board_Area->serial_number),
	       dump_fru_field_string(fru->Board_Area->part_number)
	       );

	free(fru);
	fru = NULL;
    }

    printf("-----+---------+----------------------+----------------------+----------------------+----------------------+\n");
}

static void print_fmc_status(void)
{
    int ret;
    uint8_t eeprom_buf[1024*10];
    struct FRU_DATA *fru = NULL;

    if (!read_mezzanine_presence())
	return;

    ret = read_fmc_eeprom(eeprom_buf, sizeof(eeprom_buf));
    if (!ret)
	return;

    fru = parse_FRU(eeprom_buf);

    if (!fru || !fru->Board_Area) {
	diot_error_add("Unable to parse FMC's eeprom\n");
	return;
    }

    printf("----------------------+----------------------+----------------------+----------------------+\n");
    printf("     Manufacturer     |     Product_name     |     Serial number    |      Part Number     |\n");
    printf("----------------------+----------------------+----------------------+----------------------+\n");
    printf(" %20s | %20s | %20s | %20s |\n",
	    dump_fru_field_string(fru->Board_Area->manufacturer),
	    dump_fru_field_string(fru->Board_Area->product_name),
	    dump_fru_field_string(fru->Board_Area->serial_number),
	    dump_fru_field_string(fru->Board_Area->part_number)
	    );
    printf("----------------------+----------------------+----------------------+----------------------+\n");

    free(fru);
}

static void print_uboot_info(void)
{
    char buff[30];

    printf("Boot information\n");
    printf("-------------------+----------------+\n");

    printf("DTB for HW version |%15s |\n",
	    get_file_content(DTB_HW_VER_FILE, buff, sizeof(buff)));

    printf("uboot boot source  |%15s |\n",
	    string_map_get(get_file_content(DTB_MODEBOOT_FILE, buff,
					   sizeof(buff)),
			  boot_mode_db,
			  sizeof(boot_mode_db)/sizeof(boot_mode_db[0]))
	  );

    printf("uimage boot source |%15s |\n",
	    get_file_content(DTB_UIMAGE_SOURCE_FILE, buff, sizeof(buff)));

    printf("reset reason       |%15s |\n",
	   string_map_get(get_file_content(DTB_RESET_REASON_FILE, buff,
					   sizeof(buff)),
			  reset_reason_db,
			  sizeof(reset_reason_db)/sizeof(reset_reason_db[0]))
	  );

    printf("-------------------+----------------+\n");
}

static int print_psu_hwmon_status(void)
{
    char buff_hwmon_psu1[20];
    char buff_hwmon_psu2[20];
    char buff_psu1[30];
    char buff_psu2[30];
    char *buff_psu1_p;
    char *buff_psu2_p;
    char buff_file[100];
    int psu1_present = read_psu_presence(1);
    int psu2_present = read_psu_presence(2);

    if (!psu1_present) {
	strcpy(buff_psu1, "N/A");
	buff_psu1_p = buff_psu1;
    } else if(!get_hwmon_number(PSU1_HWMON_SYS_PATH, buff_hwmon_psu1,
			     sizeof(buff_hwmon_psu1))) {
	diot_error_add("Unable to get hwmon number for PSU1.\n");
	psu1_present = 0;
	strcpy(buff_psu1, "Error");
	buff_psu1_p = buff_psu1;
    }

    if (!psu2_present) {
	strcpy(buff_psu2, "N/A");
	buff_psu2_p = buff_psu2;
    } else if(!get_hwmon_number(PSU2_HWMON_SYS_PATH, buff_hwmon_psu2,
			     sizeof(buff_hwmon_psu2))) {
	diot_error_add("Unable to get hwmon number for PSU2.\n");
	psu2_present = 0;
	strcpy(buff_psu2, "Error");
	buff_psu2_p = buff_psu2;
    }

    printf("PSU information\n");
    printf("---------+---------------------+---------------------+\n");
    printf("         |         PSU1        |         PSU2        |\n");
    printf("---------+---------------------+---------------------+\n");

    if (psu1_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_model", buff_hwmon_psu1);
	buff_psu1_p = end_str_at_newline(get_file_content(buff_file, buff_psu1,
							  sizeof(buff_psu1)));
    }
    if (psu2_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_model", buff_hwmon_psu2);
	buff_psu2_p = end_str_at_newline(get_file_content(buff_file, buff_psu2,
							  sizeof(buff_psu2)));
    }
    printf("Model    |%20s |%20s |\n", buff_psu1_p, buff_psu2_p);

    if (psu1_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_revision", buff_hwmon_psu1);
	buff_psu1_p = end_str_at_newline(get_file_content(buff_file, buff_psu1,
							  sizeof(buff_psu1)));
    }
    if (psu2_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_revision", buff_hwmon_psu2);
	buff_psu2_p = end_str_at_newline(get_file_content(buff_file, buff_psu2,
							  sizeof(buff_psu2)));
    }
    printf("Revision |%20s |%20s |\n", buff_psu1_p, buff_psu2_p);

    if (psu1_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_location", buff_hwmon_psu1);
	buff_psu1_p = end_str_at_newline(get_file_content(buff_file, buff_psu1,
							  sizeof(buff_psu1)));
    }
    if (psu2_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_location", buff_hwmon_psu2);
	buff_psu2_p = end_str_at_newline(get_file_content(buff_file, buff_psu2,
							  sizeof(buff_psu2)));
    }
    printf("Location |%20s |%20s |\n", buff_psu1_p, buff_psu2_p);

    if (psu1_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_date", buff_hwmon_psu1);
	buff_psu1_p = end_str_at_newline(get_file_content(buff_file, buff_psu1,
							  sizeof(buff_psu1)));
    }
    if (psu2_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_date", buff_hwmon_psu2);
	buff_psu2_p = end_str_at_newline(get_file_content(buff_file, buff_psu2,
							  sizeof(buff_psu2)));
    }
    printf("Date     |%20s |%20s |\n", buff_psu1_p, buff_psu2_p);

    if (psu1_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_serial", buff_hwmon_psu1);
	buff_psu1_p = end_str_at_newline(get_file_content(buff_file, buff_psu1,
							  sizeof(buff_psu1)));
    }
    if (psu2_present) {
	sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_serial", buff_hwmon_psu2);
	buff_psu2_p = end_str_at_newline(get_file_content(buff_file, buff_psu2,
							  sizeof(buff_psu2)));
    }
    printf("Serial   |%20s |%20s |\n", buff_psu1_p, buff_psu2_p);


    printf("---------+---------------------+---------------------+\n");


    return 0;
}

int cmd_print_status(char *params)
{
    int hw_version;

    diot_error_clear();

    printf("DI/OT Crate Info\n");
    printf("----------------------------+--------------+\n");
    hw_version = read_pcb_ver();
    printf("System Board hw revision    | %s%12d%s |\n",
	   RED_CHECK(hw_version != SB_VER),
	   hw_version,
	   RED_OFF(hw_version != SB_VER));
    if (hw_version != SB_VER)
	diot_error_add("ERROR: diot_util compiled for HW version %d, "
		       "but running on HW version %d!\n",
		       SB_VER, hw_version);
    printf("System Board in System Slot | %12s |\n", read_system_slot_presence() ? "yes" : "no");
    printf("FMC mezzanine               | %12s |\n", read_mezzanine_presence() ? "present" : "not present");
    printf("PSU 1                       | %12s |\n", read_psu_presence(1) ? "present" : "not present");
    printf("PSU 2                       | %12s |\n", read_psu_presence(2) ? "present" : "not present");
    printf("PSU Alert                   | %12s |\n", read_psu_alert() ? "yes" : "no");
    printf("----------------------------+--------------+\n");
    printf("\n");

    print_slot_status();
    printf("\n");
    print_fmc_status();
    printf("\n");

    print_uboot_info();
    printf("\n");

    print_psu_hwmon_status();
    printf("\n");

    diot_error_print();

    return 1;
}

void main_help(char *prog_name)
{
    printf("Usage:\n");
    printf("%s [-h] [-e <\"cmd1; cmd2; ...\">\n", prog_name);
    printf("   -h - print this help\n");
    printf("   -e <\"cmd1; cmd2; ...\"> - run in script mode, execute all commands passed as argument\n");
}

int main(int argc, char **argv)
{
    struct command_entry *entry = commands_list;

    int ret;
    char *line_read = NULL;
    char *cmd_start_p = NULL;
    int cmd_len;
    char *params;
    char *script_commands = NULL;
    char *next_script_cmd = NULL;
    char *curr_script_cmd = NULL;
    int opt;

    printf("DIOT diagnostic tool\n\n");

    while ((opt = getopt(argc, argv, "he:v")) != -1) {
	switch(opt)
	{
	    case 'h':
		main_help(argv[0]);
		cmd_help(NULL);
		exit(0);
		break;
	    case 'v':
		cmd_version(NULL);
		exit(0);
		break;
	    case 'e':
		next_script_cmd = script_commands = strdup(optarg);
		break;
	    default:
		break;
	}
    }

    ret = open_gpios();

    if (!script_commands)
	printf("Type \"help\" to list available commands\n");

    rl_attempted_completion_function = command_completion;
    rl_completer_word_break_characters = " ";

    while (1) {
	/* If the buffer has already been allocated, return the memory
	to the free pool. */
	if (line_read) {
	    free (line_read);
	    line_read = (char *)NULL;
	}

	if (script_commands) {
	    /* We're in the script mode, parse the input command */
	    curr_script_cmd = next_script_cmd;

	    /* Skip spaces and semicolons */
	    while (curr_script_cmd && *curr_script_cmd && ((*curr_script_cmd == ' ') || (*curr_script_cmd == ';')))
		curr_script_cmd++;

	    if (curr_script_cmd && *curr_script_cmd) {
		/* If there is anything left search for next semicolon */
		next_script_cmd = strchr(curr_script_cmd, ';');

		/* Find the length of the current command */
		if (next_script_cmd) {
		    cmd_len = next_script_cmd - curr_script_cmd;
		} else
		    cmd_len = strlen(curr_script_cmd);

		/* Copy command like readline would do */
		line_read = strndup(curr_script_cmd, cmd_len);
	    }
	} else {
	    /* Get a line from the user. */
	    line_read = readline (READLINE_PROMPT);
	}

	/* Ctrl+D or end of script */
	if (!line_read)
	    cmd_exit(NULL);

	/* If the line has any text in it, save it on the history. */
	if (line_read && *line_read)
	    add_history (line_read);

	cmd_start_p = line_read;

	/* Skip spaces at the beginning of the line */
	while (*cmd_start_p == ' ')
		cmd_start_p++;

	/* Prompt for another command if the line is empty */
	if (*cmd_start_p == '\0')
		continue;

	params = strchr(cmd_start_p, ' ');
	if (params) {
	    cmd_len = params - cmd_start_p;
	    /* skip spaces for params */
	    while (*params && (*params == ' '))
		params++;
	} else
	    cmd_len = strlen(cmd_start_p);

	ret = 0;
	entry = commands_list;
	while (entry && entry->cmd_name) {
	    if (cmd_len == strlen(entry->cmd_name)
		&& !strncmp(entry->cmd_name, cmd_start_p, cmd_len)
	    ) {
		ret = entry->cmd_func(params);
		if (ret < 0)
		    printf("Command %s failed with error code (%d)\n",
			   entry->cmd_name, ret);
		break;
	    }
	    entry++;
	}
	if (!(entry && entry->cmd_name)) {
	    printf("Commad \"%s\" not found\n", cmd_start_p);
	    ret = 1;
	}
    }

    if (script_commands)
	free(script_commands);
    return ret;
}

char *
command_name_generator_fantray(const char *text, int state);

char *
command_name_generator_gateware(const char *text, int state);

char **
command_completion(const char *text, int start, int end)
{
    /* Don't add entries from the filesystem to completion list */
    rl_attempted_completion_over = 1;

    /* Call completion function if the first word */
    if (start == 0){
	   return rl_completion_matches(text, command_name_generator);
    }
    if (!strncmp(rl_line_buffer, "fantray load_fw", strlen("fantray load_fw"))) {
	   return rl_completion_matches(text, rl_filename_completion_function);
    }
    if (!strncmp(rl_line_buffer, "fantray", strlen("fantray"))) {
	   return rl_completion_matches(text, command_name_generator_fantray);
    }
    if (!strncmp(rl_line_buffer, "gateware", strlen("gateware"))) {
       return rl_completion_matches(text, command_name_generator_gateware);
    }
    return NULL;
}

char *
command_name_generator(const char *text, int state)
{
    static int list_index, len;
    char *name;

    if (!state) {
        list_index = 0;
        len = strlen(text);
    }

    while ((commands_list[list_index].cmd_name) && (name = commands_list[list_index++].cmd_name)) {
        if (strncmp(name, text, len) == 0) {
            return strdup(name);
        }
    }

    return NULL;
}

/* command name generator for fantray */
char *command_name_generator_fantray(const char *text, int state)
{
    static int list_index, len;
    char *name;

    if (!state) {
        list_index = 0;
        len = strlen(text);
    }

    while ((commands_list_fantray[list_index].cmd_name) && (name = commands_list_fantray[list_index++].cmd_name)) {
        if (strncmp(name, text, len) == 0) {
            return strdup(name);
        }
    }

    return NULL;
}

/* command name generator for fantray */
char *command_name_generator_gateware(const char *text, int state)
{
    static int list_index, len;
    char *name;

    if (!state) {
        list_index = 0;
        len = strlen(text);
    }

    while ((commands_list_fpga_device[list_index].cmd_name) && (name = commands_list_fpga_device[list_index++].cmd_name)) {
        if (strncmp(name, text, len) == 0) {
            return strdup(name);
        }
    }

    return NULL;
}