/*
 * Copyright (c) 2014 Red Hat, Inc.
 *
 * This file is licensed to you under your choice of the GNU Lesser
 * General Public License, version 2.1 or any later version (LGPLv2.1 or
 * later), or the Apache License 2.0.
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <unistd.h>
#include <sys/uio.h>
#include <string.h>
#include <scsi/scsi.h>
#include <endian.h>
#include <errno.h>
#include <assert.h>

#include "libtcmu_log.h"
#include "libtcmu_common.h"
#include "libtcmu_priv.h"
#include "be_byteshift.h"

int tcmu_cdb_get_length(uint8_t *cdb)
{
	uint8_t group_code = cdb[0] >> 5;

	/* See spc-4 4.2.5.1 operation code */
	switch (group_code) {
	case 0: /*000b for 6 bytes commands */
		return 6;
	case 1: /*001b for 10 bytes commands */
	case 2: /*010b for 10 bytes commands */
		return 10;
	case 3: /*011b Reserved ? */
		if (cdb[0] == 0x7f)
			return 8 + cdb[7];
		goto cdb_not_supp;
	case 4: /*100b for 16 bytes commands */
		return 16;
	case 5: /*101b for 12 bytes commands */
		return 12;
	case 6: /*110b Vendor Specific */
	case 7: /*111b Vendor Specific */
	default:
		/* TODO: */
		goto cdb_not_supp;
	}

cdb_not_supp:
	tcmu_err("CDB %x0x not supported.\n", cdb[0]);
	return -EINVAL;
}

uint64_t tcmu_cdb_get_lba(uint8_t *cdb)
{
	uint16_t val;

	switch (tcmu_cdb_get_length(cdb)) {
	case 6:
		val = be16toh(*((uint16_t *)&cdb[2]));
		return ((cdb[1] & 0x1f) << 16) | val;
	case 10:
		return be32toh(*((u_int32_t *)&cdb[2]));
	case 12:
		return be32toh(*((u_int32_t *)&cdb[2]));
	case 16:
		return be64toh(*((u_int64_t *)&cdb[2]));
	default:
		assert_perror(EINVAL);
		return 0;	/* not reached */
	}
}

uint32_t tcmu_cdb_get_xfer_length(uint8_t *cdb)
{
	switch (tcmu_cdb_get_length(cdb)) {
	case 6:
		return cdb[4];
	case 10:
		return be16toh(*((uint16_t *)&cdb[7]));
	case 12:
		return be32toh(*((u_int32_t *)&cdb[6]));
	case 16:
		return be32toh(*((u_int32_t *)&cdb[10]));
	default:
		assert_perror(EINVAL);
		return 0;	/* not reached */
	}
}

/*
 * Returns location of first mismatch between bytes in mem and the iovec.
 * If they are the same, return -1.
 */
off_t tcmu_iovec_compare(void *mem, struct iovec *iovec, size_t size)
{
	off_t mem_off;
	int ret;

	mem_off = 0;
	while (size) {
		size_t part = min(size, iovec->iov_len);

		ret = memcmp(mem + mem_off, iovec->iov_base, part);
		if (ret) {
			size_t pos;
			char *spos = mem + mem_off;
			char *dpos = iovec->iov_base;

			/*
			 * Data differed, this is assumed to be 'rare'
			 * so use a much more expensive byte-by-byte
			 * comparison to find out at which offset the
			 * data differs.
			 */
			for (pos = 0; pos < part && *spos++ == *dpos++;
			     pos++)
				;

			return pos + mem_off;
		}

		size -= part;
		mem_off += part;
		iovec++;
	}

	return -1;
}

/*
 * Consume an iovec. Count must not exceed the total iovec[] size.
 */
size_t tcmu_iovec_seek(struct iovec *iovec, size_t count)
{
	size_t consumed = 0;

	while (count) {
		if (count >= iovec->iov_len) {
			count -= iovec->iov_len;
			iovec->iov_len = 0;
			iovec++;
			consumed++;
		} else {
			iovec->iov_base += count;
			iovec->iov_len -= count;
			count = 0;
		}
	}

	return consumed;
}

/*
 * Consume an iovec. Count must not exceed the total iovec[] size.
 * iove count should be updated.
 */
void tcmu_cmd_seek(struct tcmulib_cmd *cmd, size_t count)
{
	cmd->iov_cnt -= tcmu_iovec_seek(cmd->iovec, count);
}

size_t tcmu_iovec_length(struct iovec *iovec, size_t iov_cnt)
{
	size_t length = 0;

	while (iov_cnt) {
		length += iovec->iov_len;
		iovec++;
		iov_cnt--;
	}

	return length;
}

void __tcmu_sense_set_data(uint8_t *sense_buf, uint8_t key, uint16_t asc_ascq)
{
	sense_buf[0] |= 0x70;	/* fixed, current */
	sense_buf[2] = key;
	sense_buf[7] = 0xa;
	sense_buf[12] = (asc_ascq >> 8) & 0xff;
	sense_buf[13] = asc_ascq & 0xff;
}

int tcmu_sense_set_data(uint8_t *sense_buf, uint8_t key, uint16_t asc_ascq)
{
	memset(sense_buf, 0, SENSE_BUFFERSIZE);
	__tcmu_sense_set_data(sense_buf, key, asc_ascq);
	return TCMU_STS_PASSTHROUGH_ERR;
}

void tcmu_sense_set_key_specific_info(uint8_t *sense_buf, uint16_t info)
{
	memset(sense_buf, 0, 18);

	put_unaligned_be16(info, &sense_buf[16]);
	/* Set SKSV bit */
	sense_buf[15] |= 0x80;
}

void tcmu_sense_set_info(uint8_t *sense_buf, uint32_t info)
{
	memset(sense_buf, 0, 18);

	put_unaligned_be32(info, &sense_buf[3]);
	/* Set VALID bit */
	sense_buf[0] |= 0x80;
}

/*
 * Zero iovec.
 */
void tcmu_iovec_zero(struct iovec *iovec, size_t iov_cnt)
{
	while (iov_cnt) {
		bzero(iovec->iov_base, iovec->iov_len);

		iovec++;
		iov_cnt--;
	}
}

static inline bool tcmu_zeroed_mem(const char *buf, size_t size)
{
    int i;

    for (i = 0; i < size; i++) {
        if (buf[i])
		return false;
    }

    return true;
}

bool tcmu_iovec_zeroed(struct iovec *iovec, size_t iov_cnt)
{
    int i;

    for (i = 0; i < iov_cnt; i++) {
        if (!tcmu_zeroed_mem(iovec[i].iov_base, iovec[i].iov_len))
		return false;
    }

    return true;
}

/*
 * Copy data into an iovec, and consume the space in the iovec.
 *
 * Will truncate instead of overrunning the iovec.
 */
size_t tcmu_memcpy_into_iovec(
	struct iovec *iovec,
	size_t iov_cnt,
	void *src,
	size_t len)
{
	size_t copied = 0;

	while (len && iov_cnt) {
		size_t to_copy = min(iovec->iov_len, len);

		if (to_copy) {
			memcpy(iovec->iov_base, src + copied, to_copy);

			len -= to_copy;
			copied += to_copy;
			iovec->iov_base += to_copy;
			iovec->iov_len -= to_copy;
		}

		iovec++;
		iov_cnt--;
	}

	return copied;
}

/*
 * Copy data from an iovec, and consume the space in the iovec.
 */
size_t tcmu_memcpy_from_iovec(
	void *dest,
	size_t len,
	struct iovec *iovec,
	size_t iov_cnt)
{
	size_t copied = 0;

	while (len && iov_cnt) {
		size_t to_copy = min(iovec->iov_len, len);

		if (to_copy) {
			memcpy(dest + copied, iovec->iov_base, to_copy);

			len -= to_copy;
			copied += to_copy;
			iovec->iov_base += to_copy;
			iovec->iov_len -= to_copy;
		}

		iovec++;
		iov_cnt--;
	}

	return copied;
}

#define CDB_TO_BUF_SIZE(bytes) ((bytes) * 3 + 2)
#define CDB_FIX_BYTES 64 /* 64 bytes for default */
#define CDB_FIX_SIZE CDB_TO_BUF_SIZE(CDB_FIX_BYTES)
void tcmu_cdb_print_info(struct tcmu_device *dev,
			 const struct tcmulib_cmd *cmd,
			 const char *info)
{
	int i, n, bytes, info_len = 0;
	char fix[CDB_FIX_SIZE], *buf;

	buf = fix;

	bytes = tcmu_cdb_get_length(cmd->cdb);
	if (bytes < 0)
		return;

	if (info)
		info_len = strlen(info);

	if (CDB_TO_BUF_SIZE(bytes) + info_len > CDB_FIX_SIZE) {
		buf = malloc(CDB_TO_BUF_SIZE(bytes) + info_len);
		if (!buf) {
			tcmu_dev_err(dev, "out of memory\n");
			return;
		}
	}

	for (i = 0, n = 0; i < bytes; i++) {
		n += sprintf(buf + n, "%x ", cmd->cdb[i]);
	}

	if (info)
		n += sprintf(buf + n, "%s", info);

	sprintf(buf + n, "\n");

	if (info) {
		tcmu_dev_dbg(dev, "%s", buf);
	} else {
		tcmu_dev_dbg_scsi_cmd(dev, "%s", buf);
	}

	if (buf != fix)
		free(buf);
}

void tcmu_thread_cancel(pthread_t thread)
{
	void *join_retval;
	int ret;

	ret = pthread_cancel(thread);
	if (ret) {
		tcmu_err("pthread_cancel failed with value %d\n", ret);
		return;
	}

	ret = pthread_join(thread, &join_retval);
	if (ret) {
		tcmu_err("pthread_join failed with value %d\n", ret);
		return;
	}

	if (join_retval != PTHREAD_CANCELED)
		tcmu_err("unexpected join retval: %p\n", join_retval);
}
