/*
 * Copyright (c) 2015 American Megatrends, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#define _BSD_SOURCE

#include <ipmitool/helper.h>
#include <ipmitool/log.h>
#include <ipmitool/bswap.h>
#include <ipmitool/ipmi.h>
#include <ipmitool/ipmi_intf.h>
#include <ipmitool/ipmi_oem.h>
#include <ipmitool/ipmi_strings.h>
#include <ipmitool/ipmi_constants.h>
#include <scsi/sg.h>
#include <sys/ioctl.h>
#include <scsi/scsi_ioctl.h>
#include <scsi/scsi.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#define PACKED __attribute__ ((packed))
#define BEGIN_SIG                   "$G2-CONFIG-HOST$"
#define BEGIN_SIG_LEN               16
#define MAX_REQUEST_SIZE            64 * 1024
#define CMD_RESERVED                0x0000
#define SCSI_AMICMD_CURI_WRITE      0xE2
#define SCSI_AMICMD_CURI_READ       0xE3
#define SCSI_AMIDEF_CMD_SECTOR      0x01
#define SCSI_AMIDEF_DATA_SECTOR     0x02
#define ERR_SUCCESS                 0       /* Success */
#define ERR_BIG_DATA                1       /* Too Much Data */
#define ERR_NO_DATA                 2       /* No/Less Data Available */
#define ERR_UNSUPPORTED             3       /* Unsupported Command */
#define IN_PROCESS                  0x8000  /* Bit 15 of Status */
#define SCSI_AMICMD_ID              0xEE

/* SCSI Command Packets */
typedef struct {
	unsigned char   OpCode;
	unsigned char   Lun;
	unsigned int    Lba;
	union {
		struct {
			unsigned char   Reserved6;
			unsigned short  Length;
			unsigned char   Reserved9[3];
		} PACKED Cmd10;
		struct Len32 {
			unsigned int    Length32;
			unsigned char   Reserved10[2];
		} PACKED Cmd12;
	} PACKED CmdLen;
} PACKED SCSI_COMMAND_PACKET;

typedef struct {
	uint8_t byNetFnLUN;
	uint8_t byCmd;
	uint8_t byData[MAX_REQUEST_SIZE];
} PACKED IPMIUSBRequest_T;

typedef struct {
	uint8_t   BeginSig[BEGIN_SIG_LEN];
	uint16_t  Command;
	uint16_t  Status;
	uint32_t  DataInLen;
	uint32_t  DataOutLen;
	uint32_t  InternalUseDataIn;
	uint32_t  InternalUseDataOut;
} CONFIG_CMD;

static int ipmi_usb_setup(struct ipmi_intf *intf);
static struct ipmi_rs *ipmi_usb_send_cmd(struct ipmi_intf *intf,
		struct ipmi_rq *req);

struct ipmi_intf ipmi_usb_intf = {
	.name = "usb",
	.desc = "IPMI USB Interface(OEM Interface for AMI Devices)",
	.setup = ipmi_usb_setup,
	.sendrecv = ipmi_usb_send_cmd,
};

int
scsiProbeNew(int *num_ami_devices, int *sg_nos)
{
	int inplen = *num_ami_devices;
	int numdevfound = 0;
	char linebuf[81];
	char vendor[81];
	int lineno = 0;
	FILE *fp;

	fp = fopen("/proc/scsi/sg/device_strs", "r");
	if (fp == NULL) {
		/* Return 1 on error */
		return 1;
	}

	while (1) {
		/* Read line by line and search for "AMI" */
		if (fgets(linebuf, 80, fp) == NULL) {
			break;
		}

		if (sscanf(linebuf, "%s", vendor) == 1) {
			if (strncmp(vendor, "AMI", strlen("AMI")) == 0) {
				numdevfound++;
				sg_nos[numdevfound - 1] = lineno;
				if (numdevfound == inplen) {
					break;
				}
			}
			lineno++;
		}
	}

	*num_ami_devices = numdevfound;
	if (fp != NULL) {
		fclose(fp);
		fp = NULL;
	}

	return 0;
}

int
OpenCD(struct ipmi_intf *intf, char *CDName)
{
	intf->fd = open(CDName, O_RDWR);
	if (intf->fd == (-1)) {
		lprintf(LOG_ERR, "OpenCD:Unable to open device, %s",
				strerror(errno));
		return 1;
	}
	return 0;
}

int
sendscsicmd_SGIO(int cd_desc, unsigned char *cdb_buf, unsigned char cdb_len,
		void *data_buf, unsigned int *data_len, int direction,
		void *sense_buf, unsigned char slen, unsigned int timeout)
{
	sg_io_hdr_t io_hdr;

	/* Prepare command */
	memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
	io_hdr.interface_id = 'S';
	io_hdr.cmd_len = cdb_len;

	/* Transfer direction and length */
	io_hdr.dxfer_direction = direction;
	io_hdr.dxfer_len = *data_len;

	io_hdr.dxferp = data_buf;

	io_hdr.cmdp = cdb_buf;

	io_hdr.sbp = (unsigned char *)sense_buf;
	io_hdr.mx_sb_len = slen;

	io_hdr.timeout = timeout;

	if (!timeout) {
		io_hdr.timeout = 20000;
	}

	if (ioctl(cd_desc, SG_IO, &io_hdr) < 0) {
		lprintf(LOG_ERR, "sendscsicmd_SGIO: SG_IO ioctl error");
		return 1;
	} else {
		if (io_hdr.status != 0) {
			return 1;
		}
	}

	if (!timeout) {
		return 0;
	}

	if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
		lprintf(LOG_DEBUG, "sendscsicmd_SGIO: SG_INFO_OK - Not OK");
	} else {
		lprintf(LOG_DEBUG, "sendscsicmd_SGIO: SG_INFO_OK - OK");
		return 0;
	}

	return 1;
}

int
AMI_SPT_CMD_Identify(int cd_desc, char *szSignature)
{
	SCSI_COMMAND_PACKET IdPkt = {0};
	int ret;
	unsigned int siglen = 10;

	IdPkt.OpCode = SCSI_AMICMD_ID;
	ret = sendscsicmd_SGIO(cd_desc, (unsigned char *)&IdPkt,
				10, szSignature, &siglen, SG_DXFER_FROM_DEV,
				NULL, 0, 5000);

	return ret;
}

int
IsG2Drive(int cd_desc)
{
	char szSignature[15];
	int ret;

	memset(szSignature, 0, 15);

	flock(cd_desc, LOCK_EX);
	ret = AMI_SPT_CMD_Identify(cd_desc, szSignature);
	flock(cd_desc, LOCK_UN);
	if (ret != 0) {
		lprintf(LOG_DEBUG,
				"IsG2Drive:Unable to send ID command to the device");
		return 1;
	}

	if (strncmp(szSignature, "$$$AMI$$$", strlen("$$$AMI$$$")) != 0) {
		lprintf(LOG_ERR,
				"IsG2Drive:Signature mismatch when ID command sent");
		return 1;
	}

	return 0;
}

int
FindG2CDROM(struct ipmi_intf *intf)
{
	int err = 0;
	char device[256];
	int devarray[16];
	int numdev = 16;
	int iter;
	err = scsiProbeNew(&numdev, devarray);

	if (err == 0 && numdev > 0) {
		for (iter = 0; iter < numdev; iter++) {
			sprintf(device, "/dev/sg%d", devarray[iter]);

			if (!OpenCD(intf, device)) {
				if (!IsG2Drive(intf->fd)) {
					lprintf(LOG_DEBUG, "USB Device found");
					return 1;
				}
				close(intf->fd);
			}
		}
	} else {
		lprintf(LOG_DEBUG, "Unable to find Virtual CDROM Device");
	}

	return 0;
}

static int
ipmi_usb_setup(struct ipmi_intf *intf)
{
	if (FindG2CDROM(intf) == 0) {
		lprintf(LOG_ERR, "Error in USB session setup \n");
		return (-1);
	}
	intf->opened = 1;
	return 0;
}

void
InitCmdHeader(CONFIG_CMD *pG2CDCmdHeader)
{
	memset(pG2CDCmdHeader, 0, sizeof(CONFIG_CMD));
	memcpy((char *)pG2CDCmdHeader->BeginSig, BEGIN_SIG, BEGIN_SIG_LEN);
}

int
AMI_SPT_CMD_SendCmd(int cd_desc, char *Buffer, char type, uint16_t buflen,
		unsigned int timeout)
{
	SCSI_COMMAND_PACKET Cmdpkt;
	char sensebuff[32];
	int ret;
	unsigned int pktLen;
	int count = 3;

	memset(&Cmdpkt, 0, sizeof(SCSI_COMMAND_PACKET));

	Cmdpkt.OpCode = SCSI_AMICMD_CURI_WRITE;
	Cmdpkt.Lba = htonl(type);
	Cmdpkt.CmdLen.Cmd10.Length = htons(1);

	pktLen = buflen;
	while (count > 0) {
		ret = sendscsicmd_SGIO(cd_desc, (unsigned char *)&Cmdpkt,
				10, Buffer, &pktLen, SG_DXFER_TO_DEV,
				sensebuff, 32, timeout);
		count--;
		if (ret == 0) {
			break;
		} else {
			ret = (-1);
		}
	}

	return ret;
}

int
AMI_SPT_CMD_RecvCmd(int cd_desc, char *Buffer, char type, uint16_t buflen)
{
	SCSI_COMMAND_PACKET Cmdpkt;
	char sensebuff[32];
	int ret;
	unsigned int pktLen;
	int count = 3;

	memset(&Cmdpkt, 0, sizeof(SCSI_COMMAND_PACKET));

	Cmdpkt.OpCode = SCSI_AMICMD_CURI_READ;
	Cmdpkt.Lba = htonl(type);
	Cmdpkt.CmdLen.Cmd10.Length = htons(1);

	pktLen = buflen;
	while (count > 0) {
		ret = sendscsicmd_SGIO(cd_desc, (unsigned char *)&Cmdpkt,
				10, Buffer, &pktLen, SG_DXFER_FROM_DEV,
				sensebuff, 32, 5000);
		count--;
		if (0 == ret) {
			break;
		} else {
			ret = (-1);
		}
	}

	return ret;
}

int
ReadCD(int cd_desc, char CmdData, char *Buffer, uint32_t DataLen)
{
	int ret;

	ret = AMI_SPT_CMD_RecvCmd(cd_desc, Buffer, CmdData, DataLen);
	if (ret != 0) {
		lprintf(LOG_ERR, "Error while reading CD-Drive");
		return (-1);
	}
	return 0;
}

int
WriteCD(int cd_desc, char CmdData, char *Buffer, unsigned int timeout,
		uint32_t DataLen)
{
	int ret;

	ret = AMI_SPT_CMD_SendCmd(cd_desc, Buffer, CmdData, DataLen, timeout);
	if (ret != 0) {
		lprintf(LOG_ERR, "Error while writing to CD-Drive");
		return (-1);
	}
	return 0;
}

int
WriteSplitData(struct ipmi_intf *intf, char *Buffer, char Sector,
			uint32_t NumBytes, uint32_t timeout)
{
	uint32_t BytesWritten = 0;
	int retVal;

	if (NumBytes == 0) {
		return 0;
	}

	while (BytesWritten < NumBytes) {
		if ((retVal = WriteCD(intf->fd, Sector,
						(Buffer + BytesWritten),
						timeout, NumBytes)) != 0) {
			return retVal;
		}

		BytesWritten += NumBytes;
	}

	return 0;
}

int
ReadSplitData(struct ipmi_intf *intf, char *Buffer, char Sector,
				uint32_t NumBytes)
{
	uint32_t BytesRead = 0;

	if (NumBytes == 0) {
		return 0;
	}

	while (BytesRead < NumBytes) {
		if (ReadCD(intf->fd, Sector, (Buffer + BytesRead),
					NumBytes) == (-1)) {
			return 1;
		}
		BytesRead += NumBytes;
	}

	return 0;
}

int
WaitForCommandCompletion(struct ipmi_intf *intf, CONFIG_CMD *pG2CDCmdHeader,
		uint32_t timeout, uint32_t DataLen)
{
	uint32_t TimeCounter = 0;

	do {
		if (ReadCD(intf->fd, SCSI_AMIDEF_CMD_SECTOR,
					(char *)(pG2CDCmdHeader), DataLen) == (-1)) {
			lprintf(LOG_ERR, "ReadCD returned ERROR");
			return 1;
		}

		if (pG2CDCmdHeader->Status & IN_PROCESS) {
			usleep(1000);
			if (timeout > 0) {
				TimeCounter++;
				if (TimeCounter == (timeout + 1)) {
					return 2;
				}
			}
		} else {
			lprintf(LOG_DEBUG, "Command completed");
			break;
		}
	} while (1);

	return 0;
}

int
SendDataToUSBDriver(struct ipmi_intf *intf, char *ReqBuffer,
			unsigned int ReqBuffLen, unsigned char *ResBuffer,
			int *ResBuffLen, unsigned int timeout)
{
	char CmdHeaderBuffer[sizeof(CONFIG_CMD)];
	int retVal;
	int waitretval = 0;
	unsigned int to = 0;
	uint32_t DataLen = 0;

	CONFIG_CMD *pG2CDCmdHeader = (CONFIG_CMD *)CmdHeaderBuffer;

	/* FillHeader */
	InitCmdHeader(pG2CDCmdHeader);

	/* Set command number */
	pG2CDCmdHeader->Command = CMD_RESERVED;

	/* Fill Lengths */
	pG2CDCmdHeader->DataOutLen = *ResBuffLen;
	pG2CDCmdHeader->DataInLen = ReqBuffLen;

	if (!timeout) {
		to = 3000;
	}

	DataLen = sizeof(CONFIG_CMD);

	if (WriteCD(intf->fd, SCSI_AMIDEF_CMD_SECTOR,
				(char *)(pG2CDCmdHeader), to, DataLen) == (-1)) {
		lprintf(LOG_ERR,
				"Error in Write CD of SCSI_AMIDEF_CMD_SECTOR");
		return (-1);
	}

	/* Write the data to hard disk */
	if ((retVal = WriteSplitData(intf, ReqBuffer,
					SCSI_AMIDEF_DATA_SECTOR,
					ReqBuffLen, timeout)) != 0) {
		lprintf(LOG_ERR,
				"Error in WriteSplitData of SCSI_AMIDEF_DATA_SECTOR");
		return (-1);
	}

	if (!timeout) {
		return 0;
	}

	/* Read Status now */
	waitretval = WaitForCommandCompletion(intf, pG2CDCmdHeader, timeout,
			DataLen);
	if (waitretval != 0) {
		lprintf(LOG_ERR, "WaitForCommandComplete failed");
		return (0 - waitretval);
	} else {
		lprintf(LOG_DEBUG, "WaitForCommandCompletion SUCCESS");
	}

	switch (pG2CDCmdHeader->Status) {
		case ERR_SUCCESS:
			*ResBuffLen = pG2CDCmdHeader->DataOutLen;
			lprintf(LOG_DEBUG, "Before ReadSplitData %x", *ResBuffLen);
			if (ReadSplitData(intf, (char *)ResBuffer,
						SCSI_AMIDEF_DATA_SECTOR,
						pG2CDCmdHeader->DataOutLen) != 0) {
				lprintf(LOG_ERR,
						"Err ReadSplitData SCSI_AMIDEF_DATA_SCTR");
				return (-1);
			}
			/* Additional read to see verify there was not problem
			 * with the previous read
			 */
			DataLen = sizeof(CONFIG_CMD);
			ReadCD(intf->fd, SCSI_AMIDEF_CMD_SECTOR,
					(char *)(pG2CDCmdHeader), DataLen);
			break;
		case ERR_BIG_DATA:
			lprintf(LOG_ERR, "Too much data");
			break;
		case ERR_NO_DATA:
			lprintf(LOG_ERR, "Too little data");
			break;
		case ERR_UNSUPPORTED:
			lprintf(LOG_ERR, "Unsupported command");
			break;
		default:
			lprintf(LOG_ERR, "Unknown status");
	}

	return pG2CDCmdHeader->Status;
}

static struct ipmi_rs *
ipmi_usb_send_cmd(struct ipmi_intf *intf, struct ipmi_rq *req)
{
	static struct ipmi_rs rsp;
	long timeout = 20000;
	uint8_t byRet = 0;
	char ReqBuff[MAX_REQUEST_SIZE] = {0};
	IPMIUSBRequest_T *pReqPkt = (IPMIUSBRequest_T *)ReqBuff;
	int retries = 0;
	/********** FORM IPMI PACKET *****************/
	pReqPkt->byNetFnLUN = req->msg.netfn << 2;
	pReqPkt->byNetFnLUN += req->msg.lun;
	pReqPkt->byCmd = req->msg.cmd;
	if (req->msg.data_len) {
		memcpy(pReqPkt->byData, req->msg.data, req->msg.data_len);
	}

	/********** SEND DATA TO USB ******************/
	while (retries < 3) {
		retries++;
		byRet = SendDataToUSBDriver(intf, ReqBuff,
				2 + req->msg.data_len, rsp.data,
				&rsp.data_len,timeout);

		if (byRet == 0) {
			break;
		}
	}

	if (retries == 3) {
		lprintf(LOG_ERR,
				"Error while sending command using",
				"SendDataToUSBDriver");
		rsp.ccode = byRet;
		return &rsp;
	}

	rsp.ccode = rsp.data[0];

	/* Save response data for caller */
	if ((rsp.ccode == 0) && (rsp.data_len > 0)) {
		memmove(rsp.data, rsp.data + 1, rsp.data_len - 1);
		rsp.data[rsp.data_len] = 0;
		rsp.data_len -= 1;
	}
	return &rsp;
}
