/*
 * diot_util_fantray - functions specific to Fantray (monimod) used in DIOT
 *                     crate
 *
 * Author: Adam Wujek for CERN
 * Copyright CERN 2022
 *
 */

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#include "diot_util.h"
#include "diot_util_i2c.h"
#include "diot_util_error.h"
#include "diot_util_fantray.h"
#include "diot_util_gpio.h"
#include "util.h"

#define FANTRAY_FW_MAX_SIZE 0x3E000

static int cmd_fantray_status(char *params);
static int cmd_fantray_reset(char *params);
static int cmd_fantray_reset_hard(char *params);
static int cmd_fantray_set_fan(char *params);
static int cmd_fantray_load_fw(char *params);


struct command_entry commands_list_fantray[] = {
    { .cmd_name = "status",     .cmd_func = cmd_fantray_status,     .cmd_help_string = "Get the status of the Fantray module\n",   .cmd_params = NULL},
    { .cmd_name = "reset",      .cmd_func = cmd_fantray_reset,      .cmd_help_string = "Reset Fantray module\n",                   .cmd_params = NULL},
    { .cmd_name = "reset_hard", .cmd_func = cmd_fantray_reset_hard, .cmd_help_string = "Hard Reset Fantray module (using GPIO)\n", .cmd_params = NULL},
    { .cmd_name = "fan",        .cmd_func = cmd_fantray_set_fan,    .cmd_help_string = "Set fan(s) speed for Fantray module\n",    .cmd_params = "<1|2|3|all>"},
    { .cmd_name = "load_fw",    .cmd_func = cmd_fantray_load_fw,    .cmd_help_string = "Update Fantray's firmware\n",              .cmd_params = "<filename>"},
    { .cmd_name = NULL }
};


static int fantray_bind_driver(char *bind_path)
{
    FILE *bind_file;
    char *msg;
    int ret = 0;

    /* Unbind monimod */
    /* Equivalent of echo "9-0012" > /sys/bus/i2c/drivers/monimod/(un)bind */
    bind_file = fopen(bind_path, "w");
    if (!bind_file) {
	diot_error_add("Unable to open bind file (%s) for monimod\n",
		bind_path);
	return -1;
    }

    msg = FANTRAY_FULL_I2C_ADDR;

    (void)! fwrite(msg, sizeof(msg), 1, bind_file);
    if (ferror(bind_file)) {
	diot_error_add("Error while writing file (%s)\n", bind_path);
	ret = -1;
    }

    fclose(bind_file);

    return ret;
}

static int open_i2c_dev(int i2cbus)
{
    int i2c_file;
    char filename[20];

    sprintf(filename, "/dev/i2c-%d", i2cbus);
    i2c_file = open(filename, O_RDWR);

    if (i2c_file < 0) {
	diot_error_add("Error: Could not open file \"/dev/i2c-%d\": %s\n",
		       i2cbus, i2cbus, strerror(errno));
    }

    return i2c_file;
}


/* From i2c-tools project */
static int set_slave_addr(int i2c_file, int address, int force)
{
    int ret = 0;

    /* With force, let the user read from/write to the registers
       even when a driver is also running */
    if ((ret = ioctl(i2c_file, force ? I2C_SLAVE_FORCE : I2C_SLAVE, address)
	< 0)) {
	diot_error_add("Error: Could not set address to 0x%02x: %s\n",
		       address, strerror(errno));
	return ret;
    }

    return ret;
}


static int fantray_i2c_reset(void)
{
    int i2cbus, i2c_file;
    int ret = 0;

    i2cbus = atoi(FANTRAY_I2C);
    i2c_file = open_i2c_dev(i2cbus);
    if (i2c_file < 0)
	return i2c_file;

    if ((ret = set_slave_addr(i2c_file, strtol(FANTRAY_I2C_ADDR, NULL, 16), 0))
	< 0) {
	goto out;
    }

    ret = i2c_smbus_write_byte_data(i2c_file, FANTRAY_I2C_REG_RESET, 1);

out:
    close(i2c_file);
    return ret;
}

static int cmd_fantray_reset(char *params)
{
    int rc;
    int ret;

    diot_error_clear();
    /* Unbind the Fantray from the monimod driver */
    /* Ignore errors of unbind */
    fantray_bind_driver(FANTRAY_DRIVER_PATH "/unbind");

    /* Reset Fantray */
    ret = fantray_i2c_reset();

    /* Give the Fantray some time to start */
    sleep(2);

    /* Bind the Fantray to the monimod driver */
    rc = fantray_bind_driver(FANTRAY_DRIVER_PATH "/bind");

    diot_error_print();
    if (ret) {
	return -1;
    }

    return rc;
}

static int cmd_fantray_reset_hard(char *params)
{
    int rc;
    int ret;

    diot_error_clear();
    /* Unbind the Fantray from the monimod driver */
    /* Ignore errors of unbind */
    fantray_bind_driver(FANTRAY_DRIVER_PATH "/unbind");

    /* Reset Fantray */
    ret = gpio_fantray_reset(0);
    if (ret < 0)
	printf("Unable to reset Fantray (ret = %d)\n", ret);

    sleep(1);

    ret = gpio_fantray_reset(1);
    if (ret < 0)
	printf("Unable to reset Fantray (ret = %d)\n", ret);

    /* Give the Fantray some time to start */
    sleep(2);

    /* Bind the Fantray to the monimod driver */
    rc = fantray_bind_driver(FANTRAY_DRIVER_PATH "/bind");

    diot_error_print();
    if (ret) {
	return -1;
    }

    return rc;
}


static int fantray_status(void)
{
    char buff_hwmon[20];
    char buff[20];
    char buff_file[100];
    int tmp;
    char *use_pec = "Error";

    if (!get_hwmon_number(FANTRAY_HWMON_SYS_PATH, buff_hwmon,
			  sizeof(buff_hwmon))) {
	diot_error_add("Unable to get hwmon number for Fantray. "
		       "Fantray probably not insterted\n");
	return -1;
    }

    printf("Fantray status\n");
    printf("----------------+------------------+\n");
    sprintf(buff_file, DEBUGFS_PMBUS "/%s/monimod/fw_type", buff_hwmon);
    printf("Firmware type   |%17s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/monimod/uptime_secs",
	    buff_hwmon);
    get_file_content_int(buff_file, &tmp);
    printf("Uptime          |%4dd %02dh %02dm %02ds |\n",
	   tmp / (60 * 60 * 24),
	   (tmp % (60 * 60 * 24)) / 3600,
	   (tmp % (60 * 60)) / 60,
	   tmp % 60);

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/monimod/tmr_error_cnt",
	    buff_hwmon);
    printf("TMR error count |%17s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/monimod/use_pec",
	    buff_hwmon);
    if (get_file_content_int(buff_file, &tmp) >= 0) {
	if (tmp == 0)
	    use_pec = "False";
	else if (tmp == 1)
	    use_pec = "True";
    }
    printf("PEC enabled     |%17s |\n", use_pec);
    printf("----------------+------------------+\n");

    return 0;
}

static int fantray_hwmon_status(void)
{
    char buff_hwmon[20];
    char buff[30];
    char buff_file[100];

    if (!get_hwmon_number(FANTRAY_HWMON_SYS_PATH, buff_hwmon,
			  sizeof(buff_hwmon))) {
	diot_error_add("Unable to get hwmon number for Fantray. "
		       "Fantray probably not insterted\n");
	return -EINVAL;
    }

    printf("Fantray information\n");
    printf("----------------------+---------------------+\n");
    sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_id", buff_hwmon);
    if (strcmp("Error",
	       end_str_at_newline(get_file_content(buff_file, buff,
						    sizeof(buff))))) {
	printf("Manufacturer ID       |%20s |\n", buff);
    }

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_model", buff_hwmon);
    printf("Manufacturer Model    |%20s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_revision", buff_hwmon);
    printf("Manufacturer revision |%20s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_location", buff_hwmon);
    printf("Manufacturer location |%20s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_date", buff_hwmon);
    printf("Manufacture date      |%20s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/mfr_serial", buff_hwmon);
    printf("Manufacturer serial   |%20s |\n",
	   end_str_at_newline(get_file_content(buff_file, buff, sizeof(buff))));

    sprintf(buff_file, DEBUGFS_PMBUS "/%s/ic_device_rev", buff_hwmon);
    if (strcmp("Error",
		end_str_at_newline(get_file_content(buff_file, buff,
						    sizeof(buff))))) {
	printf("IC device revision    |%20s |\n", buff);
    }

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

    return 0;
}

static int cmd_fantray_status(char *params)
{
    int ret1;
    int ret2;

    diot_error_clear();

    ret1 = fantray_hwmon_status();
    printf("\n");
    ret2 = fantray_status();
    printf("\n");

    diot_error_print();

    if (ret1 != 0) {
	return ret1;
    }

    return ret2;
}


static int fantray_set_fan_speed(int fan, char *hwmon, int fan_speed)
{
    char buff[sizeof(FANTRAY_HWMON_SYS_PATH) + 40];
    int fan_speed_new;
    int tmp;
    int ret;

    /* Get hwmon+number */
    snprintf(buff, sizeof(buff), "%s/%s/fan%d_target", FANTRAY_HWMON_SYS_PATH, hwmon, fan);
    /* read fan speed */
    if ((ret = get_file_content_int(buff, &tmp)) < 0) {
	printf("Unable to get fan speed for fan %d %s %d\n", fan, buff, ret);
	return -1;
    }
    /* Write fan speed */
    if ((ret = set_file_content_int(buff, fan_speed)) < 0) {
	printf("Unable to set fan speed for fan %d %s %d\n", fan, buff, ret);
	return -1;
    }
    /* Read fan speed again */
    if ((ret = get_file_content_int(buff, &fan_speed_new)) < 0) {
	printf("Unable to get fan speed for fan %d %s %d\n", fan, buff, ret);
	return -1;
    }

    /* Compare the old and the new speeds */
    if (fan_speed_new != fan_speed) {
	printf("Validation error of Fan %d speed was %d, set to %d, "
	       "but read %d\n", fan, tmp, fan_speed, fan_speed_new);
	return -1;
    } else {
	printf("Fan %d speed was %d, set to %d\n", fan, tmp, fan_speed_new);
    }

    return 0;
}

static int cmd_fantray_set_fan(char *params)
{
    int fan_selected = 0;
    char *fan_speed = NULL;
    int fan_speed_int = -1;
    int ret = 0;
    int i;
    char buff_hwmon[20];

    if (!params || !(*params)) {
	printf("Not enough parameters for fan subcommand, please define "
	       "the fan number and the new speed\n");
	return -EINVAL;
    }

    fan_speed = strchr(params, ' ');

    while (fan_speed && *fan_speed && (*fan_speed == ' '))
	fan_speed++;
    if (!fan_speed || !(*fan_speed)) {
	printf("Not enough parameters for fan subcommand, please define "
	       "the new speed\n");
	return -EINVAL;
    }

    fan_selected = atoi(params);
    if (!strncmp(params, "all", strlen("all")))
	fan_selected = -1;

    if ((fan_selected < 1 || fan_selected > 3) && fan_selected != -1) {
	printf("Wrong fan number. Use 1, 2, 3 or all\n");
	return -EINVAL;
    }

    fan_speed_int = atoi(fan_speed);

    if (!get_hwmon_number(FANTRAY_HWMON_SYS_PATH, buff_hwmon,
			  sizeof(buff_hwmon))) {
	printf("Unable to get hwmon number for Fantray. "
	       "Fantray probably not insterted\n");
	return -EINVAL;
    }
    for (i = 1; i <= 3; i++) {
	if ((fan_selected == i) || (fan_selected == -1))
	    if (fantray_set_fan_speed(i, buff_hwmon, fan_speed_int) < 0) {
		printf("Unable to set speed %d for fan %d\n", fan_speed_int, i);
		ret = -EINVAL;
		/* Don't brake, try to set speed for other */
	    }
    }
    return ret;
}

static int fantray_load_fw(int i2c_file, FILE *fw_file, long fw_file_blocks)
{
    uint8_t data_block[8];
    int i, j, ret = 0;
    uint32_t chksum = 0;
    uint16_t chksum16 = 0;
    uint32_t read_chksum = 0;
    int read_bytes;



    /* write size of the new firmware */
    /* size is the number of 8 byte blocks */
    ret = i2c_smbus_write_word_data(i2c_file, FANTRAY_I2C_REG_WRITTEN_FW_SIZE, fw_file_blocks);
    if (ret < 0) {
        printf("Unable to write size of the new firmware (ret=%d)\n", ret);
	return -1;
    }

    printf("Transferring the new firmware     ");

    for (i = 0; i < fw_file_blocks; i++) {
        read_bytes = fread(data_block, 1, 8, fw_file);
	if (ferror(fw_file)) {
	    printf("\nError while reading the new firmware from file (%s)\n", strerror(errno));
	    return -1;
	}
	if ((i % (fw_file_blocks/100)) == 0) {
	    printf("\b\b\b\b %02ld%%", i * 100 / fw_file_blocks);
	    fflush(stdout);
	}
	for (j = 0; j < read_bytes; j++) {
	    chksum += data_block[j];
	    }
	/* clear the remaining part of 8 bytes block */
	for (j = read_bytes; j < 8; j++) {
	    data_block[j] = 0;
	    }
	ret = i2c_smbus_write_i2c_block_data(i2c_file, FANTRAY_I2C_REG_WRITTEN_FW_BLOCK, 8, data_block);
	if (ret < 0) {
	    printf("\nError while writing the new firmware (%s) block %d\n", strerror(errno), i);
	    return -1;
	}
    }

    printf("\b\b\b\b100%% \n");
    chksum16 = (chksum & 0xffff) + (chksum >> 16);

    printf("Writing checksum of the new firmware\n");

    /* write checksum of the new firmware */
    ret = i2c_smbus_write_word_data(i2c_file, FANTRAY_I2C_REG_WRITTEN_FW_CHKSUM, chksum16);
    if (ret < 0) {
        printf("Unable to write checksum of the new firmware (ret=%d)\n", ret);
	return -1;
    }
    read_chksum = i2c_smbus_read_word_data(i2c_file, FANTRAY_I2C_REG_LOCAL_FW_CHKSUM);
    if (read_chksum < 0) {
        printf("Unable to read checksum of the new firmware (ret=%d)\n", read_chksum);
	return -1;
    }

    if (chksum16 != read_chksum) {
	printf("Wrong checksum! calculated 0x%04x, read 0x%04x\n", chksum16, read_chksum);
	return -EPROTO;
    }

    return 0;
}

static long get_size_of_file(FILE *fw_file, char *filename)
{
    long fw_file_size;

    /* get the size of the new firmware */
    if (fseek(fw_file, 0, SEEK_END) == -1) {
        printf("Unable to get the size of %s\n", filename);
	return -1;
    }

    fw_file_size = ftell(fw_file);
    if (fw_file_size == -1) {
        printf("Unable to get the size of %s\n", filename);
	return -1;
    }

    if (fseek(fw_file, 0, SEEK_SET) == -1) {
        printf("Unable to rewind the file %s\n", filename);
	return -1;
    }

    return fw_file_size;
}

static int cmd_fantray_load_fw(char *params)
{
    int ret, ret_end;
    int i2cbus, i2c_file;
    long fw_file_size;
    long fw_file_blocks;
    char *p;
    FILE *fw_file = NULL;
    int errno_local;

    diot_error_clear();

    /* remove trailing spaces from params (filename) */
    if (params) {
	p = params + strlen(params);
	while (*(--p) == ' ')
	    *p = '\0';
    }
    if (!params || !(*params)) {
	printf("Not enough parameters. "
	       "Please provide a file with firmware to program\n");
	return -EINVAL;
    }

    /* open file with firmware */
    fw_file = fopen(params, "r");
    if (fw_file == NULL) {
	errno_local = errno;
        printf("Unable to open file \"%s\"\n", params);
	return -errno_local;
    }

    fw_file_size = get_size_of_file(fw_file, params);
    if (fw_file_size < 0) {
	ret = fw_file_size;
	goto close_fw_file;
    }

    fw_file_blocks = (fw_file_size - 1) / 8 + 1;

    if (fw_file_blocks > FANTRAY_FW_MAX_SIZE / 8) {
	printf("File \"%s\" too big (%ld). Maximum accepted %d\n",
	       params, fw_file_size, FANTRAY_FW_MAX_SIZE);
	ret = -EFBIG;
	goto close_fw_file;
    }

    /* Unbind the Fantray from the monimod driver */
    printf("Unbinding Fantray from the driver\n");
    /* Ignore problems during unbind */
    fantray_bind_driver(FANTRAY_DRIVER_PATH "/unbind");

    printf("Opening I2C\n");
    /* Open i2c channel */
    i2cbus = atoi(FANTRAY_I2C);
    i2c_file = open_i2c_dev(i2cbus);
    if (i2c_file < 0) {
	ret = i2c_file;
	goto close_fw_file;
    }

    /* Use fantray's i2c address */
    if ((ret = set_slave_addr(i2c_file,  strtol(FANTRAY_I2C_ADDR, NULL, 16), 0))
	< 0) {
	goto close_i2c;
    }

    printf("Checking Fantray bootmode\n");
    /* Check firmware mode */
    ret = i2c_smbus_read_byte_data(i2c_file, FANTRAY_I2C_REG_BOOT_NEW_FW);
    if (ret < 0) {
	printf("Error: Unable to get boot mode\n");
	goto close_i2c;
    }

    if (ret == FANTRAY_FW_TYPE_MAIN) {
	printf("Fantray in main mode. Switching to bootloader mode\n");

	/* Switch to boot loader */
	ret = i2c_smbus_write_byte_data(i2c_file, FANTRAY_I2C_REG_BOOT_NEW_FW,
					1);
	if (ret < 0) {
	    printf("Error: unable to set boot mode of the Fantray!\n");
	    goto close_i2c;
	}

	/* Let the bootloader to start */
	sleep(2);
	/* Confirm that bootloader is started */
	ret = i2c_smbus_read_byte_data(i2c_file, FANTRAY_I2C_REG_BOOT_NEW_FW);
	if (ret < 0) {
	    printf("Error: unable to get the boot mode of the Fantray!\n");
	    goto close_i2c;
	}

    }

    if (ret == FANTRAY_FW_TYPE_BOOTLOADER) {
	printf("Fantray in bootloader mode\n");
    } else if (ret == FANTRAY_FW_TYPE_MAIN) {
	printf("Error: Fantray still in main mode!\n");
	goto close_i2c;
    } else {
	printf("Error: Unknown (%d) boot mode!\n", ret);
	goto close_i2c;
    }


    printf("Loading firmware\n");
    fantray_load_fw(i2c_file, fw_file, fw_file_blocks);

    printf("Switching Fantray to main firmware mode\n");
    /* Switch to main firmware */
    ret = i2c_smbus_write_byte_data(i2c_file, FANTRAY_I2C_REG_BOOT_NEW_FW, 1);
    if (ret < 0) {
	printf("Error: unable to set the boot mode of the Fantray!\n");
	goto close_i2c;
    }

    /* Let the main firmware to start */
    sleep(1);

    /* Confirm that main firmware is started */
    ret = i2c_smbus_read_byte_data(i2c_file, FANTRAY_I2C_REG_BOOT_NEW_FW);
    if (ret < 0) {
	printf("Error: unable to get boot mode of the Fantray!\n");
	goto close_i2c;
    } else if (ret == FANTRAY_FW_TYPE_MAIN) {
	printf("Fantray in main mode\n");
    } else if (ret == FANTRAY_FW_TYPE_BOOTLOADER) {
	printf("Error: Fantray still in bootloader mode\n");
	goto close_i2c;
    } else {
	printf("Error: Unknown (%d) boot mode!\n", ret);
	goto close_i2c;
    }

close_i2c:
    printf("Closing I2C\n");
    close(i2c_file);

    printf("Binding Fantray to the driver\n");
    /* bind the Fantray to the monimod driver */
    ret_end = fantray_bind_driver(FANTRAY_DRIVER_PATH "/bind");
    if (ret_end < 0)
	printf("Unable to bind Fantray to the driver\n");

close_fw_file:
    fclose(fw_file);

    if (ret < 0)
	printf("!!!Flashing Fantray failed!!!\n");
    else
	printf("!!!Flashing Fantray successful!!!\n");

    printf("\n");
    diot_error_print();

    return ret;
}

int cmd_fantray(char *params)
{
    struct command_entry *entry;
    char *cmd_start_p = NULL;
    int ret;
    int cmd_len;

    cmd_start_p = params;

    if (!params || !(*params)) {
	printf("Please provide subcommand for the Fantray command\n");
	cmd_help("fantray");
	return -ENOENT;
    }

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

    entry = commands_list_fantray;
    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);
	    break;
	}
	entry++;
    }
    if (!(entry && entry->cmd_name)) {
	printf("Commad \"%s\" not found\n", cmd_start_p);
	ret = -ENOENT;
    }

    return ret;
}
