/*
 * 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 "diot_util_error.h"
#include "diot_util_i2c.h"

#define EEPROM_DRIVER_PATH "/sys/bus/i2c/drivers/at24"
#define FMC_EEPROM_PATH "/sys/bus/i2c/drivers/at24/" FMC_EEPROM_I2C "-00" FMC_EEPROM_I2C_ADDR "/eeprom"
#define PSU_DRIVER_PATH "/sys/bus/i2c/drivers/hartmann-psu"
#define XIIC_DRIVER_PATH "/sys/bus/platform/drivers/xiic-i2c"
#define XIIC_ADDRESS_2 "a0010000.i2c"

#define PB_EEPROM_PATH "/sys/bus/i2c/drivers/at24/" PB_EEPROM_I2C "-00" PB_EEPROM_I2C_ADDR "/eeprom"
#define PB_EEPROM_FULL_I2C_ADDR PB_EEPROM_I2C "-00" PB_EEPROM_I2C_ADDR
char * psu_addr[] = {
    [1] = PSU_I2C "-00" PSU1_I2C_ADDR,
    [2] = PSU_I2C "-00" PSU2_I2C_ADDR,
};


#define	CONSUMER	"diot_util"
const char *chipname = "gpiochip0";
const char *chiplabel = "zynqmp_gpio";
struct gpiod_chip *chip;

typedef enum {
    dir_in,
    dir_out
} dir_t;

struct gpio_entry {
    char *gpio_name;
    struct gpiod_line *gpio_line;
    dir_t dir;
    int def_out;
};

enum diot_gpio{
    USER_LED2,
    SYSEN_N,
#if SB_VER == 1
    FMC_PRSNT,
#elif SB_VER == 2
    FMC_PRSNT_N,
#endif
    PCB_VER0,
    PCB_VER1,
    PCB_VER2,
    PCB_VER3,
    PWR_Cycle_REQ,
    PWR_Cycle_STR,
#if SB_VER == 1
    P_PRES0,
    P_PRES1,
    SERVMOD_N1,
    SERVMOD_N2,
    SERVMOD_N3,
    SERVMOD_N4,
    SERVMOD_N5,
    SERVMOD_N6,
    SERVMOD_N7,
    SERVMOD_N8,
#elif SB_VER == 2
    P_PRES0_N,
    P_PRES1_N,
    SERVMOD_1,
    SERVMOD_2,
    SERVMOD_3,
    SERVMOD_4,
    SERVMOD_5,
    SERVMOD_6,
    SERVMOD_7,
    SERVMOD_8,
#endif
    RST_N,
    PSU_ALERT,
    F_RST,
    MAX_GPIO,
};

#define DECLARE_GPIO_IN(pin) [pin] = { .gpio_name = #pin, .dir = dir_in }
#define DECLARE_GPIO_OUT(pin, default_state) [pin] = \
        { .gpio_name = #pin, .dir = dir_out, .def_out = default_state }

struct gpio_entry gpio_entries[MAX_GPIO] = {
    DECLARE_GPIO_OUT(USER_LED2, 1),
    DECLARE_GPIO_IN(SYSEN_N),
#if SB_VER == 1
    DECLARE_GPIO_IN(FMC_PRSNT),
#elif SB_VER == 2
    DECLARE_GPIO_IN(FMC_PRSNT_N),
#endif
    DECLARE_GPIO_IN(PCB_VER0),
    DECLARE_GPIO_IN(PCB_VER1),
    DECLARE_GPIO_IN(PCB_VER2),
    DECLARE_GPIO_IN(PCB_VER3),
    DECLARE_GPIO_OUT(PWR_Cycle_REQ, 1),
    DECLARE_GPIO_OUT(PWR_Cycle_STR, 1),
#if SB_VER == 1
    DECLARE_GPIO_IN(P_PRES0),
    DECLARE_GPIO_IN(P_PRES1),
    DECLARE_GPIO_IN(SERVMOD_N1),
    DECLARE_GPIO_IN(SERVMOD_N2),
    DECLARE_GPIO_IN(SERVMOD_N3),
    DECLARE_GPIO_IN(SERVMOD_N4),
    DECLARE_GPIO_IN(SERVMOD_N5),
    DECLARE_GPIO_IN(SERVMOD_N6),
    DECLARE_GPIO_IN(SERVMOD_N7),
    DECLARE_GPIO_IN(SERVMOD_N8),
#elif SB_VER == 2
    DECLARE_GPIO_IN(P_PRES0_N),
    DECLARE_GPIO_IN(P_PRES1_N),
    DECLARE_GPIO_IN(SERVMOD_1),
    DECLARE_GPIO_IN(SERVMOD_2),
    DECLARE_GPIO_IN(SERVMOD_3),
    DECLARE_GPIO_IN(SERVMOD_4),
    DECLARE_GPIO_IN(SERVMOD_5),
    DECLARE_GPIO_IN(SERVMOD_6),
    DECLARE_GPIO_IN(SERVMOD_7),
    DECLARE_GPIO_IN(SERVMOD_8),
#endif
    DECLARE_GPIO_OUT(RST_N, 1),
    DECLARE_GPIO_IN(PSU_ALERT),
    DECLARE_GPIO_OUT(F_RST, 1),
};

/* commands */
int cmd_print_status(char *params);
int cmd_led2(char *params);
int cmd_peripheral_rst(char *params);
int cmd_power_cycle(char *params);

static int request_gpios(void);
static int release_gpios(void);

int cmd_peripheral_rst(char *params)
{
    int ret;

    printf("Reseting peripheral boards...\n");
    ret = gpiod_line_set_value(gpio_entries[RST_N].gpio_line, 0);
    if (ret < 0)
	return ret;
    usleep(1000);
    ret = gpiod_line_set_value(gpio_entries[RST_N].gpio_line, 1);
    if (ret < 0)
	return ret;

    return 0;
}

int cmd_power_cycle(char *params)
{
    int ret;

    printf("Power cycle...\n");
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_REQ].gpio_line, 1);
    if (ret < 0)
	return ret;
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_STR].gpio_line, 0);
    if (ret < 0)
	return ret;
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_STR].gpio_line, 1);
    if (ret < 0)
	return ret;
    usleep(1000);
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_STR].gpio_line, 0);
    if (ret < 0)
	return ret;
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_REQ].gpio_line, 0);
    if (ret < 0)
	return ret;
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_STR].gpio_line, 1);
    if (ret < 0)
	return ret;
    usleep(1000);
    ret = gpiod_line_set_value(gpio_entries[PWR_Cycle_STR].gpio_line, 0);
    if (ret < 0)
	return ret;

    return 0;

}

int cmd_led2(char *params)
{
    int val = 0, ret;

    if (!strncmp(params, "on", 2) || !strncmp(params, "1", 1)) {
	printf("LED2 on\n");
	val = 1;
    } else if (!strncmp(params, "off", 3) || !strncmp(params, "0", 1)) {
	printf("LED2 off\n");
	val = 0;
    } else {
	printf("Wrong parameter\n");
	errno = EINVAL;
	return -1;
    }

    ret = gpiod_line_set_value(gpio_entries[USER_LED2].gpio_line, val);

    return ret;

}

int open_gpios(void)
{
    int ret;

    chip = gpiod_chip_open_by_label(chiplabel);

    if (!chip) {
	    perror("Open gpio chip failed\n");
	    exit (1);
    }

    ret = request_gpios();
    if (ret < 0) {
	gpiod_chip_close(chip);

	printf("Unable to request GPIOs. Exiting...\n");
	printf("Is another instance running?\n");
	exit (1);
    }

    return ret;
}

int close_gpios(void)
{
    release_gpios();
    gpiod_chip_close(chip);

    return 0;
}

static int request_gpios(void)
{
    int i;
    struct gpiod_line *tmp_line;
    int ret;

    for (i = 0; i < MAX_GPIO; i++) {
	if (!gpio_entries[i].gpio_name)
	    continue;

	tmp_line = gpiod_chip_find_line(chip, gpio_entries[i].gpio_name);
	if (!tmp_line) {
	    diot_error_add("Error for line %s (%d)\n",
			   gpio_entries[i].gpio_name, i);
	    return -1;
	}
	gpio_entries[i].gpio_line = tmp_line;
	if (gpio_entries[i].dir == dir_in) {
	    ret = gpiod_line_request_input(tmp_line, CONSUMER);
	    if (ret < 0) {
		diot_error_add("Request line as %s (%d) input failed\n",
		gpio_entries[i].gpio_name, i);
		return ret;
	    }
	} else if (gpio_entries[i].dir == dir_out) {
	    ret = gpiod_line_request_output(tmp_line, CONSUMER,
					    gpio_entries[i].def_out);
	    if (ret < 0) {
		diot_error_add("Request line as %s (%d) input failed\n",
		gpio_entries[i].gpio_name, i);
		return ret;
	    }
	}
    }

    return 0;
}

static int release_gpios(void)
{
    int i;

    for (i = 0; i < MAX_GPIO; i++) {
	if (!gpio_entries[i].gpio_name)
	    continue;

	gpiod_line_release(gpio_entries[i].gpio_line);
	gpio_entries[i].gpio_line = NULL;
    }

    return 0;
}

int read_pcb_ver(void)
{
    int pcb_version = 0;

    pcb_version = gpiod_line_get_value(gpio_entries[PCB_VER3].gpio_line);
    pcb_version <<= 1;
    pcb_version += gpiod_line_get_value(gpio_entries[PCB_VER2].gpio_line);
    pcb_version <<= 1;
    pcb_version += gpiod_line_get_value(gpio_entries[PCB_VER1].gpio_line);
    pcb_version <<= 1;
    pcb_version += gpiod_line_get_value(gpio_entries[PCB_VER0].gpio_line);

    return pcb_version;
}

static int get_fmc_prsnt(void)
{
#if SB_VER == 1
    return FMC_PRSNT;
#elif SB_VER == 2
    return FMC_PRSNT_N;
#endif
}

int read_mezzanine_presence(void)
{
    /* pin is low when FMC card is present */
    return !gpiod_line_get_value(gpio_entries[get_fmc_prsnt()].gpio_line);
}

int get_p_pres(int i)
{
    if (i == 0) {
#if SB_VER == 1
	return P_PRES0;
#elif SB_VER == 2
	return P_PRES0_N;
#endif
    } else if (i == 1) {
#if SB_VER == 1
	return P_PRES1;
#elif SB_VER == 2
	return P_PRES1_N;
#endif
    }
    return -1;
}

int read_psu_presence(int psu)
{
    int psi_pin;

    if (psu == 1)
	psi_pin = get_p_pres(0);
    else if (psu == 2)
	psi_pin = get_p_pres(1);
    else
	return -1;

    if (psi_pin < 0)
	return psi_pin;

    return !gpiod_line_get_value(gpio_entries[psi_pin].gpio_line);
}

static int get_servmod_1(void)
{
#if SB_VER == 1
	return SERVMOD_N1;
#elif SB_VER == 2
	return SERVMOD_1;
#endif
}

enum diot_gpio conv_slot_to_servmod(int slot)
{
    if (slot < 1 || slot > 8)
	return -1;

    return (slot - 1) + get_servmod_1();
}

int read_slot_use(int slot)
{
    enum diot_gpio slot_pin;

    slot_pin = conv_slot_to_servmod(slot);
    if (slot_pin < 0)
	return -1;

    return !gpiod_line_get_value(gpio_entries[slot_pin].gpio_line);
}


int read_system_slot_presence(void)
{
    return !gpiod_line_get_value(gpio_entries[SYSEN_N].gpio_line);
}

int read_psu_alert(void)
{
    return gpiod_line_get_value(gpio_entries[PSU_ALERT].gpio_line);
}

/* Read Peripheral Board's (pb) EEPROM
 *
 * Set corresponding SERVMOD_N GPIO to output
 * Set corresponding SERVMOD_N GPIO to high
 * bind eeprom to at24 driver
 * read eeprom
 * unbind eeprom from at24 driver
 * Set corresponding SERVMOD_N GPIO to input
 */
int read_pb_eeprom(uint8_t *eeprom_buff, size_t eeprom_size, int slot)
{
    int ret;
    enum diot_gpio slot_pin;
    struct gpiod_line *tmp_line;
    char *msg;
    FILE *eeprom_file;
    FILE *bind_file;

    slot_pin = conv_slot_to_servmod(slot);

    if (slot_pin < 0)
	return -1;

    tmp_line = gpiod_chip_find_line(chip, gpio_entries[slot_pin].gpio_name);
    if (!tmp_line) {
	diot_error_add("Error for line %s (%d)\n",
		       gpio_entries[slot_pin].gpio_name,
	       slot_pin);
	return -1;
    }

    gpiod_line_release(tmp_line);
    if ((ret = gpiod_line_request_output(tmp_line, CONSUMER, 1)) < 0) {
	diot_error_add("Error %d setting SERVMOD_N GPIO as output for slot %d\n",
		       ret, slot);
	return -1;
    }

    /* Set corresponding SERVMOD_N to high */
    if (gpiod_line_set_value(gpio_entries[slot_pin].gpio_line, 1) < 0) {
	diot_error_add("Error setting SERVMOD_N high for slot %d\n", slot);
	ret = -1;
	goto out_set_input;
    }

    /* Bind PB's EEPROM. All EEPROM's are at the same address */
    /* equivalent of echo "3-0018" > /sys/bus/i2c/drivers/at24/bind */
    bind_file = fopen(EEPROM_DRIVER_PATH "/bind", "w");
    if (!bind_file) {
	diot_error_add("Unable to open bind file (%s) for slot %d\n",
		       EEPROM_DRIVER_PATH "/bind", slot);
	ret = -1;
	goto out_set_input;
    }
    msg = PB_EEPROM_FULL_I2C_ADDR;
    ret = fwrite(msg, 1, sizeof(msg), bind_file);

    fclose(bind_file);


    /* Read eeprom */
    eeprom_file = fopen(PB_EEPROM_PATH, "rb");
    if (!eeprom_file) {
	diot_error_add("Unable to open eeprom file (%s) for slot %d\n",
		       PB_EEPROM_PATH, slot);
	ret = -1;
	goto out_unbind;
    }
    ret = fread(eeprom_buff, 1, eeprom_size, eeprom_file);

    fclose(eeprom_file);


out_unbind:
    /* Unbind PB's EEPROM */
    /* equivalent of echo "3-0018" > /sys/bus/i2c/drivers/at24/unbind */
    bind_file = fopen(EEPROM_DRIVER_PATH "/unbind", "w");
    if (!bind_file) {
	diot_error_add("Unable to open bind file (%s) for slot %d\n",
		       EEPROM_DRIVER_PATH "/unbind", slot);
	ret = -1;
	goto out_set_input;
    }
    msg = PB_EEPROM_FULL_I2C_ADDR;
    ret = fwrite(msg, sizeof(msg), 1, bind_file);

    fclose(bind_file);

out_set_input:
    gpiod_line_release(tmp_line);
    /* Set corresponding SERVMOD_N as input */
    if (gpiod_line_request_input(tmp_line, CONSUMER) < 0) {
	diot_error_add("Error setting SERVMOD_N GPIO as output for slot %d\n",
		       slot);
	return -1;
    }

    return ret;
}

int read_fmc_eeprom(uint8_t *eeprom_buff, size_t eeprom_size)
{
    int ret;
    FILE *eeprom_file;

    eeprom_file = fopen(FMC_EEPROM_PATH, "rb");
    if (!eeprom_file) {
	diot_error_add("Unable to open FMC's eeprom file (%s)\n",
		       FMC_EEPROM_PATH);
	return -1;
    }
    ret = fread(eeprom_buff, 1, eeprom_size, eeprom_file);

    fclose(eeprom_file);

    return ret;
}

static int psu_check_sysfs_presence(int psu)
{
    char path[50];
    FILE *name_file;

    /* equivalent of checking the presence of a file
     * /sys/bus/i2c/drivers/hartmann-psu/2-0051/name */
    snprintf(path, sizeof(path), "%s/%s/%s", PSU_DRIVER_PATH, psu_addr[psu],
	     "name");

    name_file = fopen(path, "r");
    if (!name_file) {
	/* File not present */
	return 0;
    }

    fclose(name_file);

    /* File present */
    return 1;
}

int psu_added_or_removed(int psu)
{
    int gpio_presence;
    int sysfs_presence;
    char *msg;
    char *path;
    int ret;
    FILE *bind_file;

    gpio_presence = read_psu_presence(psu);
    sysfs_presence = psu_check_sysfs_presence(psu);

    if ((gpio_presence && sysfs_presence)
	|| (!gpio_presence && !sysfs_presence)) {
	/* Nothing to do. PSU was detected by GPIO and is present in sysfs
	 * or PSU was not detected nor is present in sysfs */
	return 0;
    }

    msg = psu_addr[psu];

    if (gpio_presence && !sysfs_presence) {
	/* Add (register) to sysfs */
	/* equivalent of echo "3-0018" > /sys/bus/i2c/drivers/hartmann-psu/unbind */
	path = PSU_DRIVER_PATH "/bind";
    }

    if (!gpio_presence && sysfs_presence) {
	/* Remove (unregister) from sysfs */
	/* equivalent of echo "3-0018" > /sys/bus/i2c/drivers/hartmann-psu/bind */
	path = PSU_DRIVER_PATH "/unbind";
    }

    /* Wait 200ms to give a i2c slave on the inserted PSU time to initialize
     * properly */
    usleep(200000);

    /* Add (register) to sysfs */
    /* equivalent of echo "3-0018" > /sys/bus/i2c/drivers/hartmann-psu/bind */
    bind_file = fopen(path, "w");
    if (!bind_file) {
	diot_error_add("Unable to open bind/unbind file (%s) for psu %d\n",
		    path, psu);
	return -1;
    }

    ret = fwrite(msg, sizeof(msg), 1, bind_file);

    fclose(bind_file);

    return ret;
}

/* Write test to a file
 * return 0 on error */
static int echo_to_file(char * text, char * target_file_name)
{
    FILE *target_file;
    int ret = 0;

    target_file = fopen(target_file_name, "w");
    if (!target_file) {
	diot_error_add("Unable to open file (%s)\n", target_file_name);
	return 0;
    }

    ret = fwrite(text, strlen(text), 1, target_file);

    if (!ret)
	diot_error_add("Unable to write \"%s\" to file (%s)\n",
		       text, target_file_name);

    fclose(target_file);

    return ret;
}

/* Try to restore i2c after PSU insertion/removal
 * return 0 on error */
int restore_i2c(void)
{

#if SB_VER == 2
    diot_error_add("%s not tested on HWv2!\n");
    return 0;
#endif

    if (!(
#if SB_VER == 1
	  echo_to_file(XIIC_ADDRESS_2, XIIC_DRIVER_PATH "/unbind") &&
	  echo_to_file(XIIC_ADDRESS_2, XIIC_DRIVER_PATH "/bind") &&
#endif
	  echo_to_file(psu_addr[1], PSU_DRIVER_PATH "/unbind") &&
	  echo_to_file(psu_addr[1], PSU_DRIVER_PATH "/bind") &&
	  echo_to_file(psu_addr[2], PSU_DRIVER_PATH "/unbind") &&
	  echo_to_file(psu_addr[2], PSU_DRIVER_PATH "/bind")
	  )
    ) {
	diot_error_add("ERROR: Unable to restore i2c-3\n");
	return 0;
    }

    return 1;
}

int gpio_fantray_reset(int val)
{
    return gpiod_line_set_value(gpio_entries[F_RST].gpio_line, val);
}
