/*
 * Intel(R) Enclosure LED Utilities
 * Copyright (C) 2009-2019 Intel Corporation.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#if _HAVE_DMALLOC_H
#include <dmalloc.h>
#endif

#include <scsi/sg_lib.h>
#include <scsi/sg_cmds_extra.h>

#include "cntrl.h"
#include "config.h"
#include "enclosure.h"
#include "list.h"
#include "scsi.h"
#include "ses.h"
#include "status.h"
#include "sysfs.h"
#include "utils.h"

static int debug = 0;

static int get_ses_page(int fd, struct ses_page *p, int pg_code)
{
	int ret;
	int retry_count = 3;

	do {
		ret = sg_ll_receive_diag(fd, 1, pg_code, p->buf, sizeof(p->buf),
					 0, debug);
	} while (ret && retry_count--);

	if (!ret)
		p->len = (p->buf[2] << 8) + p->buf[3] + 4;
	return ret;
}

static int process_page1(struct ses_pages *sp)
{
	int num_encl;		/* number of subenclosures */
	unsigned char *ed;	/* Enclosure Descriptor */
	int len = 0;
	int sum_headers = 0;	/* Number of Type descriptor headers */
	int i = 0;

	/* How many enclosures is in the main enclosure? */
	num_encl = sp->page1->buf[1] + 1;
	/* Go to Enclosure Descriptor */
	ed = sp->page1->buf + 8;
	for (i = 0; i < num_encl; i++, ed += len) {
		if (ed + 3 > sp->page1->buf + sp->page1->len) {
			log_debug
			    ("SES: Error, response pare 1 truncated at %d\n",
			     i);
			return 1;
		}
		sum_headers += ed[2];
		len = ed[3] + 4;
		if (len < 40) {
			log_debug("SES: Response too short for page 1\n");
			continue;
		}
	}

	sp->page1_types = (struct type_descriptor_header *)ed;
	sp->page1_types_len = sum_headers;

	/* ed is on type descr header */
	for (i = 0; i < sum_headers; i++, ed += 4) {
		if (ed > sp->page1->buf + sp->page1->len) {
			log_debug("SES: Response page 1 truncated at %d\n", i);
			return 1;
		}
	}
	return 0;
}

static struct ses_pages *ses_init(void)
{
	struct ses_pages *sp;
	sp = calloc(1, sizeof(*sp));
	if (!sp)
		return NULL;
	sp->page1 = calloc(1, sizeof(struct ses_page));
	if (!sp->page1)
		goto sp1;
	sp->page2 = calloc(1, sizeof(struct ses_page));
	if (!sp->page2)
		goto sp2;
	return sp;
 sp2:
	free(sp->page1);
 sp1:
	free(sp);
	return NULL;
}

static void ses_free(struct ses_pages *sp)
{
	if (!sp)
		return;
	free(sp->page1);
	free(sp->page2);
	free(sp->page10);
	free(sp);
}

static void dump_p10(unsigned char *p)
{
	int i;
	printf("----------------------------------------------\n");
	for (i = 0; i < 8; i++, p += 16) {
		printf("%p: %02x %02x %02x %02x %02x %02x %02x " \
		       "%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", (void *)p,
		       p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7],
		       p[8], p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
	}
}

static int enclosure_open(const struct enclosure_device *enclosure)
{
	int fd = -1;

	if (enclosure->dev_path)
		fd = open(enclosure->dev_path, O_RDWR);

	return fd;
}

static int enclosure_load_pages(struct enclosure_device *enclosure)
{
	int ret;
	int fd;
	struct ses_pages *sp;

	if (enclosure->ses_pages)
		return 0;

	fd = enclosure_open(enclosure);
	if (fd == -1)
		return 1;

	sp = ses_init();
	if (!sp) {
		ret = 1;
		goto end;
	}

	/* Read configuration. */
	ret = get_ses_page(fd, sp->page1, ENCL_CFG_DIAG_STATUS);
	if (ret)
		goto end;

	ret = process_page1(sp);
	if (ret)
		goto end;

	/* Get Enclosure Status */
	ret = get_ses_page(fd, sp->page2, ENCL_CTRL_DIAG_STATUS);
end:
	close(fd);
	if (ret)
		ses_free(sp);
	else
		enclosure->ses_pages = sp;
	return ret;
}

static int enclosure_load_page10(struct enclosure_device *enclosure)
{
	int ret;
	int fd;
	struct ses_page *p;

	if (enclosure->ses_pages && enclosure->ses_pages->page10)
		return 0;

	ret = enclosure_load_pages(enclosure);
	if (ret)
		return ret;

	fd = enclosure_open(enclosure);
	if (fd == -1)
		return 1;

	p = calloc(1, sizeof(struct ses_page));
	if (!p) {
		ret = 1;
		goto end;
	}

	/* Additional Element Status */
	ret = get_ses_page(fd, p, ENCL_ADDITIONAL_EL_STATUS);
end:
	close(fd);
	if (ret)
		free(p);
	else
		enclosure->ses_pages->page10 = p;

	return ret;
}

static void enclosure_free_pages(struct enclosure_device *enclosure)
{
	ses_free(enclosure->ses_pages);
	enclosure->ses_pages = NULL;
}

static void print_page10(struct ses_pages *sp)
{
	unsigned char *ai = sp->page10->buf + 8;
	int i = 0, len = 0, eip = 0;
	unsigned char *sas = NULL;

	while (ai < sp->page10->buf + sp->page10->len) {
		printf("%s()[%d]: Inv: %d, EIP: %d, Proto: 0x%04x\n", __func__,
		       i++, ((ai[0] & 0x80) >> 7), ((ai[0] & 0x10) >> 4),
		       (unsigned int) (ai[0] & 0xf));
		printf("\tDescriptor len (x-1): %d\n", ai[1] + 1);
		eip = ai[0] & 0x10;
		if (eip)
			printf("\tElement Index: %d\n", ai[3]);
		len = ai[1] + 2;
		if ((ai[0] & 0xf) == SCSI_PROTOCOL_SAS) {
			if (eip)
				sas = ai + 4;
			else
				sas = ai + 2;
			printf("\tProtocol SAS:\n");
			printf("\tNumber of phy descriptors: %d\n", sas[0]);
			printf("\tNot all phys: %d, descriptor type: 0x%1x\n",
			       (sas[1] & 1), ((sas[1] & 0xc0) >> 6));
			if (eip) {
				printf("\tDevice slot number: %d\n", sas[3]);
				sas += 2;
			}
			sas += 2;
			printf("\tDevice type: 0x%01x\n",
				(unsigned int)((sas[0] & 0x70) >> 4));
			printf("\tSMP Initiator Port: 0x%01x\n",
				(unsigned int)((sas[2] & 2) >> 1));
			printf("\tSTP Initiator Port: 0x%01x\n",
				(unsigned int)((sas[2] & 4) >> 2));
			printf("\tSSP Initiator Port: 0x%01x\n",
				(unsigned int)((sas[2] & 8) >> 3));
			printf("\tSATA DEVICE: 0x%01x\n",
				(unsigned int)(sas[3] & 1));
			printf("\tSMP Target Port: 0x%01x\n",
				(unsigned int)((sas[3] & 2) >> 1));
			printf("\tSTP Target Port: 0x%01x\n",
				(unsigned int)((sas[3] & 4) >> 2));
			printf("\tSSP Target Port: 0x%01x\n",
				(unsigned int)((sas[3] & 8) >> 3));
			printf("\tSATA Port Selector: 0x%01x\n",
				(unsigned int)((sas[3] & 0X80) >> 7));
			printf
			    ("\tAttached SAS Address: 0x%02x%02x%02x%02x%02x%02x%02x%02x\n",
			     sas[4], sas[5], sas[6], sas[7], sas[8], sas[9],
			     sas[10], sas[11]);
			printf
			    ("\tSAS Address: 0x%02x%02x%02x%02x%02x%02x%02x%02x\n",
			     sas[12], sas[13], sas[14], sas[15], sas[16],
			     sas[17], sas[18], sas[19]);
			printf("\tPHY Identified: 0x%01x\n", sas[20]);
		} else
			printf("\tProtocol not SAS: 0x%02x, skipping\n",
				(unsigned int)(ai[0] & 0xf));
		/* */
		ai += len;
	}
	return;
}

static enum ibpi_pattern ibpi_to_ses(enum ibpi_pattern ibpi)
{
	switch (ibpi) {
	case IBPI_PATTERN_UNKNOWN:
	case IBPI_PATTERN_ONESHOT_NORMAL:
	case IBPI_PATTERN_NORMAL:
		return SES_REQ_OK;
	case IBPI_PATTERN_FAILED_ARRAY:
		return SES_REQ_IFA;
	case IBPI_PATTERN_DEGRADED:
		return SES_REQ_ICA;
	case IBPI_PATTERN_REBUILD:
		return SES_REQ_REBUILD;
	case IBPI_PATTERN_FAILED_DRIVE:
		return SES_REQ_FAULT;
	case IBPI_PATTERN_LOCATE:
		return SES_REQ_IDENT;
	case IBPI_PATTERN_HOTSPARE:
		return SES_REQ_HOSTSPARE;
	case IBPI_PATTERN_PFA:
		return SES_REQ_PRDFAIL;
	default:
		return ibpi;
	}
}

static int ses_set_message(enum ibpi_pattern ibpi, struct ses_slot_ctrl_elem *el)
{
	struct ses_slot_ctrl_elem msg;

	memset(&msg, 0, sizeof(msg));
	if (ibpi == IBPI_PATTERN_LOCATE_OFF) {
		/*
		 * For locate_off we don't set a new state, just clear the
		 * IDENT bit and the bits that are reserved or have different
		 * meanings in Status and Control pages (RQST ACTIVE and
		 * RQST MISSING).
		 */
		_clr_ident(el->b);
		el->b2 &= 0x4e;
		el->b3 &= 0x3c;
		return 0;
	}

	switch (ibpi_to_ses(ibpi)) {
	case SES_REQ_ABORT:
		_set_abrt(msg.b);
		break;
	case SES_REQ_REBUILD:
		_set_rebuild(msg.b);
		break;
	case SES_REQ_IFA:
		_set_ifa(msg.b);
		break;
	case SES_REQ_ICA:
		_set_ica(msg.b);
		break;
	case SES_REQ_CONS_CHECK:
		_set_cons_check(msg.b);
		break;
	case SES_REQ_HOSTSPARE:
		_set_hspare(msg.b);
		break;
	case SES_REQ_RSVD_DEV:
		_set_rsvd_dev(msg.b);
		break;
	case SES_REQ_OK:
		_set_ok(msg.b);
		break;
	case SES_REQ_IDENT:
		_set_ident(msg.b);
		break;
	case SES_REQ_RM:
		_set_rm(msg.b);
		break;
	case SES_REQ_INS:
		_set_ins(msg.b);
		break;
	case SES_REQ_MISSING:
		_set_miss(msg.b);
		break;
	case SES_REQ_DNR:
		_set_dnr(msg.b);
		break;
	case SES_REQ_ACTIVE:
		_set_actv(msg.b);
		break;
	case SES_REQ_EN_BB:
		_set_enbb(msg.b);
		break;
	case SES_REQ_EN_BA:
		_set_enba(msg.b);
		break;
	case SES_REQ_DEV_OFF:
		_set_off(msg.b);
		break;
	case SES_REQ_FAULT:
		_set_fault(msg.b);
		break;
	case SES_REQ_PRDFAIL:
		_set_prdfail(msg.b);
		break;
	default:
		return 1;
	}

	*el = msg;

	return 0;
}

static int ses_write_msg(enum ibpi_pattern ibpi, struct block_device *device)
{
	struct ses_pages *sp = device->enclosure->ses_pages;
	int idx = device->encl_index;
	/* Move do descriptors */
	struct ses_slot_ctrl_elem *descriptors = (void *)(sp->page2->buf + 8);
	int i;
	struct ses_slot_ctrl_elem *desc_element = NULL;
	element_type local_element_type = SES_UNSPECIFIED;

	for (i = 0; i < sp->page1_types_len; i++) {
		struct type_descriptor_header *t = &sp->page1_types[i];

		descriptors++; /* At first, skip overall header. */

		if (t->element_type == SES_DEVICE_SLOT ||
		    t->element_type == SES_ARRAY_DEVICE_SLOT) {
			if (local_element_type < t->element_type &&
			    t->num_of_elements > idx) {
				local_element_type = t->element_type;
				desc_element = &descriptors[idx];
			}
		} else {
			/*
			 * Device Slot and Array Device Slot elements are
			 * always first on the type descriptor header list
			 */
			break;
		}

		descriptors += t->num_of_elements;
	}

	if (desc_element) {
		int ret = ses_set_message(ibpi, desc_element);
		if (ret)
			return ret;
		/* keep PRDFAIL, clear rest */
		desc_element->common_control &= 0x40;
		/* set select */
		desc_element->common_control |= 0x80;

		/* second byte is valid only for Array Device Slot */
		if (local_element_type != SES_ARRAY_DEVICE_SLOT)
			desc_element->array_slot_control = 0;

		return 0;
	}

	return 1;
}

static int ses_send_diag(struct enclosure_device *enclosure)
{
	int ret;
	int fd;

	fd = enclosure_open(enclosure);
	if (fd == -1)
		return 1;

	ret = sg_ll_send_diag(fd, 0, 1, 0, 0, 0, 0,
			      enclosure->ses_pages->page2->buf,
			      enclosure->ses_pages->page2->len,
			      0, debug);
	close(fd);
	return ret;
}

static char *get_drive_end_dev(const char *path)
{
	char *s, *c, *p;

	c = strstr(path, "end_device");
	if (!c)
		return NULL;
	s = strchr(c, '/');
	if (!s)
		return NULL;
	p = calloc(s - c + 1, sizeof(*p));
	if (!p)
		return NULL;

	strncpy(p, c, s - c);
	return p;
}

static uint64_t get_drive_sas_addr(const char *path)
{
	uint64_t ret = 0;
	size_t size = strlen(path) * 2;
	char *buff, *end_dev;

	/* Make big buffer. */
	buff = malloc(size + 1);
	if (!buff)
		return ret;

	end_dev = get_drive_end_dev(path);
	if (!end_dev) {
		free(buff);
		return ret;
	}

	snprintf(buff, size, "/sys/class/sas_end_device/%s/device/sas_device/%s",
		 end_dev, end_dev);

	ret = get_uint64(buff, ret, "sas_address");

	free(end_dev);
	free(buff);

	return ret;
}

static int get_encl_slot(struct block_device *device)
{
	struct ses_pages *sp;
	unsigned char *add_desc = NULL;
	unsigned char *ap = NULL, *addr_p = NULL;
	int i, j, len = 0;
	uint64_t addr, addr_cmp;
	int idx;

	/* try to get slot from sysfs */
	idx = get_int(device->cntrl_path, -1, "slot");
	if (idx != -1)
		return idx;

	/*
	 * Older kernels may not have the "slot" sysfs attribute,
	 * fallback to Page10 method.
	 */
	if (enclosure_load_page10(device->enclosure))
		return -1;
	sp = device->enclosure->ses_pages;

	addr = get_drive_sas_addr(device->sysfs_path);
	if (!addr)
		return -1;

	if (debug)
		print_page10(sp);

	/* Check Page10 for address. Extract index. */
	ap = add_desc = sp->page10->buf + 8;
	for (i = 0; i < sp->page1_types_len; i++) {
		struct type_descriptor_header *t = &sp->page1_types[i];

		if (t->element_type == SES_DEVICE_SLOT ||
		    t->element_type == SES_ARRAY_DEVICE_SLOT) {
			for (j = 0; j < t->num_of_elements; j++, ap += len) {
				if (debug)
					dump_p10(ap);
				/* Get Additional Element Status Descriptor */
				/* length (x-1) */
				len = ap[1] + 2;
				if ((ap[0] & 0xf) != SCSI_PROTOCOL_SAS)
					continue;	/* need SAS PROTO */
				/* It is a SAS protocol, go on */
				if ((ap[0] & 0x10))	/* Check EIP */
					addr_p = ap + 8;
				else
					addr_p = ap + 4;
				/* Process only PHY 0 descriptor. */

				/* Convert be64 to le64 */
				addr_cmp = ((uint64_t)addr_p[12] << 8*7) |
					   ((uint64_t)addr_p[13] << 8*6) |
					   ((uint64_t)addr_p[14] << 8*5) |
					   ((uint64_t)addr_p[15] << 8*4) |
					   ((uint64_t)addr_p[16] << 8*3) |
					   ((uint64_t)addr_p[17] << 8*2) |
					   ((uint64_t)addr_p[18] << 8*1) |
					   ((uint64_t)addr_p[19]);

				if (addr == addr_cmp) {
					idx = ap[0] & 0x10 ? ap[3] : j;
					return idx;
				}
			}
		} else {
			/*
			 * Device Slot and Array Device Slot elements are
			 * always first on the type descriptor header list
			 */
			break;
		}
	}
	return -1;
}

/**
 */
static int _slot_match(const char *slot_path, const char *device_path)
{
	char temp[PATH_MAX], link[PATH_MAX];

	snprintf(temp, sizeof(temp), "%s/device", slot_path);

	if (realpath(temp, link) == NULL)
		return 0;

	return strncmp(link, device_path, strlen(link)) == 0;
}

/**
 */
static char *_slot_find(const char *enclo_path, const char *device_path)
{
	struct list dir;
	char *temp, *result = NULL;

	if (scan_dir(enclo_path, &dir) == 0) {
		list_for_each(&dir, temp) {
			if (_slot_match(temp, device_path)) {
				result = str_dup(temp);
				break;
			}
		}
		list_erase(&dir);
	}
	return result;
}

int scsi_get_enclosure(struct block_device *device)
{
	struct enclosure_device *encl;

	if (!device || !device->sysfs_path)
		return 0;

	list_for_each(sysfs_get_enclosure_devices(), encl) {
		if (_slot_match(encl->sysfs_path, device->cntrl_path)) {
			device->enclosure = encl;
			device->encl_index = get_encl_slot(device);
			break;
		}
	}

	return (device->enclosure != NULL && device->encl_index != -1);
}

/**
 */
int scsi_ses_write(struct block_device *device, enum ibpi_pattern ibpi)
{
	int ret;

	if (!device || !device->sysfs_path || !device->enclosure ||
	    device->encl_index == -1)
		__set_errno_and_return(EINVAL);

	/* write only if state has changed */
	if (ibpi == device->ibpi_prev)
		return 1;

	if ((ibpi < IBPI_PATTERN_NORMAL) || (ibpi > SES_REQ_FAULT))
		__set_errno_and_return(ERANGE);

	ret = enclosure_load_pages(device->enclosure);
	if (ret) {
		log_warning
		    ("Unable to send %s message to %s. Device is missing?",
		     ibpi2str(ibpi), strstr(device->sysfs_path, "host"));
		return ret;
	}

	return ses_write_msg(ibpi, device);
}

int scsi_ses_flush(struct block_device *device)
{
	int ret;

	if (!device || !device->enclosure)
		return 1;

	if (!device->enclosure->ses_pages)
		return 0;

	ret = ses_send_diag(device->enclosure);

	enclosure_free_pages(device->enclosure);
	return ret;
}

/**
 * @brief Gets a path to slot of sas controller.
 *
 * This function returns a sysfs path to component of enclosure the device
 * belongs to.
 *
 * @param[in]      path           Canonical sysfs path to block device.
 *
 * @return A sysfs path to controller device associated with the given
 *         block device if successful, otherwise NULL pointer.
 */
static char *sas_get_slot_path(const char *path, const char *ctrl_path)
{
	char *host;
	char host_path[PATH_MAX] = { 0 };
	size_t ctrl_path_len = strlen(ctrl_path);

	if (strncmp(path, ctrl_path, ctrl_path_len) != 0)
		return NULL;
	host = get_path_hostN(path);
	if (host) {
		snprintf(host_path, sizeof(host_path), "%s/%s/bsg/sas_%s",
			 ctrl_path, host, host);
		free(host);
	}
	return str_dup(host_path);
}

/**
 */
static char *_get_enc_slot_path(const char *path)
{
	struct enclosure_device *device;
	char *result = NULL;

	list_for_each(sysfs_get_enclosure_devices(), device) {
		result = _slot_find(device->sysfs_path, path);
		if (result != NULL)
			break;
	}
	return result;
}

/**
 */
char *scsi_get_slot_path(const char *path, const char *ctrl_path)
{
	char *result = NULL;

	result = _get_enc_slot_path(path);
	if (!result)
		result = sas_get_slot_path(path, ctrl_path);
	return result;
}
