/*******************************************************************************
 * This file contains main functions related to iSCSI DataSequenceInOrder=No
 * and DataPDUInOrder=No.
 *
 \u00a9 Copyright 2007-2011 RisingTide Systems LLC.
 *
 * Licensed to the Linux Foundation under the General Public License (GPL) version 2.
 *
 * Author: Nicholas A. Bellinger <nab@linux-iscsi.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that 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.
 ******************************************************************************/

#include <linux/slab.h>
#include <linux/random.h>

#include "iscsi_target_core.h"
#include "iscsi_target_util.h"
#include "iscsi_target_seq_pdu_list.h"

#define OFFLOAD_BUF_SIZE	32768

void iscsit_dump_seq_list(struct iscsi_cmd *cmd)
{
	int i;
	struct iscsi_seq *seq;

	pr_debug("Dumping Sequence List for ITT: 0x%08x:\n",
			cmd->init_task_tag);

	for (i = 0; i < cmd->seq_count; i++) {
		seq = &cmd->seq_list[i];
		pr_debug("i: %d, pdu_start: %d, pdu_count: %d,"
			" offset: %d, xfer_len: %d, seq_send_order: %d,"
			" seq_no: %d\n", i, seq->pdu_start, seq->pdu_count,
			seq->offset, seq->xfer_len, seq->seq_send_order,
			seq->seq_no);
	}
}

void iscsit_dump_pdu_list(struct iscsi_cmd *cmd)
{
	int i;
	struct iscsi_pdu *pdu;

	pr_debug("Dumping PDU List for ITT: 0x%08x:\n",
			cmd->init_task_tag);

	for (i = 0; i < cmd->pdu_count; i++) {
		pdu = &cmd->pdu_list[i];
		pr_debug("i: %d, offset: %d, length: %d,"
			" pdu_send_order: %d, seq_no: %d\n", i, pdu->offset,
			pdu->length, pdu->pdu_send_order, pdu->seq_no);
	}
}

static void iscsit_ordered_seq_lists(
	struct iscsi_cmd *cmd,
	u8 type)
{
	u32 i, seq_count = 0;

	for (i = 0; i < cmd->seq_count; i++) {
		if (cmd->seq_list[i].type != SEQTYPE_NORMAL)
			continue;
		cmd->seq_list[i].seq_send_order = seq_count++;
	}
}

static void iscsit_ordered_pdu_lists(
	struct iscsi_cmd *cmd,
	u8 type)
{
	u32 i, pdu_send_order = 0, seq_no = 0;

	for (i = 0; i < cmd->pdu_count; i++) {
redo:
		if (cmd->pdu_list[i].seq_no == seq_no) {
			cmd->pdu_list[i].pdu_send_order = pdu_send_order++;
			continue;
		}
		seq_no++;
		pdu_send_order = 0;
		goto redo;
	}
}

/*
 *	Generate count random values into array.
 *	Use 0x80000000 to mark generates valued in array[].
 */
static void iscsit_create_random_array(u32 *array, u32 count)
{
	int i, j, k;

	if (count == 1) {
		array[0] = 0;
		return;
	}

	for (i = 0; i < count; i++) {
redo:
		get_random_bytes(&j, sizeof(u32));
		j = (1 + (int) (9999 + 1) - j) % count;
		for (k = 0; k < i + 1; k++) {
			j |= 0x80000000;
			if ((array[k] & 0x80000000) && (array[k] == j))
				goto redo;
		}
		array[i] = j;
	}

	for (i = 0; i < count; i++)
		array[i] &= ~0x80000000;
}

static int iscsit_randomize_pdu_lists(
	struct iscsi_cmd *cmd,
	u8 type)
{
	int i = 0;
	u32 *array, pdu_count, seq_count = 0, seq_no = 0, seq_offset = 0;

	for (pdu_count = 0; pdu_count < cmd->pdu_count; pdu_count++) {
redo:
		if (cmd->pdu_list[pdu_count].seq_no == seq_no) {
			seq_count++;
			continue;
		}
		array = kzalloc(seq_count * sizeof(u32), GFP_KERNEL);
		if (!array) {
			pr_err("Unable to allocate memory"
				" for random array.\n");
			return -1;
		}
		iscsit_create_random_array(array, seq_count);

		for (i = 0; i < seq_count; i++)
			cmd->pdu_list[seq_offset+i].pdu_send_order = array[i];

		kfree(array);

		seq_offset += seq_count;
		seq_count = 0;
		seq_no++;
		goto redo;
	}

	if (seq_count) {
		array = kzalloc(seq_count * sizeof(u32), GFP_KERNEL);
		if (!array) {
			pr_err("Unable to allocate memory for"
				" random array.\n");
			return -1;
		}
		iscsit_create_random_array(array, seq_count);

		for (i = 0; i < seq_count; i++)
			cmd->pdu_list[seq_offset+i].pdu_send_order = array[i];

		kfree(array);
	}

	return 0;
}

static int iscsit_randomize_seq_lists(
	struct iscsi_cmd *cmd,
	u8 type)
{
	int i, j = 0;
	u32 *array, seq_count = cmd->seq_count;

	if ((type == PDULIST_IMMEDIATE) || (type == PDULIST_UNSOLICITED))
		seq_count--;
	else if (type == PDULIST_IMMEDIATE_AND_UNSOLICITED)
		seq_count -= 2;

	if (!seq_count)
		return 0;

	array = kzalloc(seq_count * sizeof(u32), GFP_KERNEL);
	if (!array) {
		pr_err("Unable to allocate memory for random array.\n");
		return -1;
	}
	iscsit_create_random_array(array, seq_count);

	for (i = 0; i < cmd->seq_count; i++) {
		if (cmd->seq_list[i].type != SEQTYPE_NORMAL)
			continue;
		cmd->seq_list[i].seq_send_order = array[j++];
	}

	kfree(array);
	return 0;
}

static void iscsit_determine_counts_for_list(
	struct iscsi_cmd *cmd,
	struct iscsi_build_list *bl,
	u32 *seq_count,
	u32 *pdu_count)
{
	int check_immediate = 0;
	u32 burstlength = 0, offset = 0;
	u32 unsolicited_data_length = 0;
	struct iscsi_conn *conn = cmd->conn;

	if ((bl->type == PDULIST_IMMEDIATE) ||
	    (bl->type == PDULIST_IMMEDIATE_AND_UNSOLICITED))
		check_immediate = 1;

	if ((bl->type == PDULIST_UNSOLICITED) ||
	    (bl->type == PDULIST_IMMEDIATE_AND_UNSOLICITED))
		unsolicited_data_length = (cmd->data_length >
			conn->sess->sess_ops->FirstBurstLength) ?
			conn->sess->sess_ops->FirstBurstLength : cmd->data_length;

	while (offset < cmd->data_length) {
		*pdu_count += 1;

		if (check_immediate) {
			check_immediate = 0;
			offset += bl->immediate_data_length;
			*seq_count += 1;
			if (unsolicited_data_length)
				unsolicited_data_length -=
					bl->immediate_data_length;
			continue;
		}
		if (unsolicited_data_length > 0) {
			if ((offset + conn->conn_ops->MaxRecvDataSegmentLength)
					>= cmd->data_length) {
				unsolicited_data_length -=
					(cmd->data_length - offset);
				offset += (cmd->data_length - offset);
				continue;
			}
			if ((offset + conn->conn_ops->MaxRecvDataSegmentLength)
					>= conn->sess->sess_ops->FirstBurstLength) {
				unsolicited_data_length -=
					(conn->sess->sess_ops->FirstBurstLength -
					offset);
				offset += (conn->sess->sess_ops->FirstBurstLength -
					offset);
				burstlength = 0;
				*seq_count += 1;
				continue;
			}

			offset += conn->conn_ops->MaxRecvDataSegmentLength;
			unsolicited_data_length -=
				conn->conn_ops->MaxRecvDataSegmentLength;
			continue;
		}
		if ((offset + conn->conn_ops->MaxRecvDataSegmentLength) >=
		     cmd->data_length) {
			offset += (cmd->data_length - offset);
			continue;
		}
		if ((burstlength + conn->conn_ops->MaxRecvDataSegmentLength) >=
		     conn->sess->sess_ops->MaxBurstLength) {
			offset += (conn->sess->sess_ops->MaxBurstLength -
					burstlength);
			burstlength = 0;
			*seq_count += 1;
			continue;
		}

		burstlength += conn->conn_ops->MaxRecvDataSegmentLength;
		offset += conn->conn_ops->MaxRecvDataSegmentLength;
	}
}


/*
 *	Builds PDU and/or Sequence list,  called while DataSequenceInOrder=No
 *	and DataPDUInOrder=No.
 */
static int iscsit_build_pdu_and_seq_list(
	struct iscsi_cmd *cmd,
	struct iscsi_build_list *bl)
{
	int check_immediate = 0, datapduinorder, datasequenceinorder;
	u32 burstlength = 0, offset = 0, i = 0;
	u32 pdu_count = 0, seq_no = 0, unsolicited_data_length = 0;
	struct iscsi_conn *conn = cmd->conn;
	struct iscsi_pdu *pdu = cmd->pdu_list;
	struct iscsi_seq *seq = cmd->seq_list;

	datapduinorder = conn->sess->sess_ops->DataPDUInOrder;
	datasequenceinorder = conn->sess->sess_ops->DataSequenceInOrder;

	if ((bl->type == PDULIST_IMMEDIATE) ||
	    (bl->type == PDULIST_IMMEDIATE_AND_UNSOLICITED))
		check_immediate = 1;

	if ((bl->type == PDULIST_UNSOLICITED) ||
	    (bl->type == PDULIST_IMMEDIATE_AND_UNSOLICITED))
		unsolicited_data_length = (cmd->data_length >
			conn->sess->sess_ops->FirstBurstLength) ?
			conn->sess->sess_ops->FirstBurstLength : cmd->data_length;

	while (offset < cmd->data_length) {
		pdu_count++;
		if (!datapduinorder) {
			pdu[i].offset = offset;
			pdu[i].seq_no = seq_no;
		}
		if (!datasequenceinorder && (pdu_count == 1)) {
			seq[seq_no].pdu_start = i;
			seq[seq_no].seq_no = seq_no;
			seq[seq_no].offset = offset;
			seq[seq_no].orig_offset = offset;
		}

		if (check_immediate) {
			check_immediate = 0;
			if (!datapduinorder) {
				pdu[i].type = PDUTYPE_IMMEDIATE;
				pdu[i++].length = bl->immediate_data_length;
			}
			if (!datasequenceinorder) {
				seq[seq_no].type = SEQTYPE_IMMEDIATE;
				seq[seq_no].pdu_count = 1;
				seq[seq_no].xfer_len =
					bl->immediate_data_length;
			}
			offset += bl->immediate_data_length;
			pdu_count = 0;
			seq_no++;
			if (unsolicited_data_length)
				unsolicited_data_length -=
					bl->immediate_data_length;
			continue;
		}
		if (unsolicited_data_length > 0) {
			if ((offset +
			     conn->conn_ops->MaxRecvDataSegmentLength) >=
			     cmd->data_length) {
				if (!datapduinorder) {
					pdu[i].type = PDUTYPE_UNSOLICITED;
					pdu[i].length =
						(cmd->data_length - offset);
				}
				if (!datasequenceinorder) {
					seq[seq_no].type = SEQTYPE_UNSOLICITED;
					seq[seq_no].pdu_count = pdu_count;
					seq[seq_no].xfer_len = (burstlength +
						(cmd->data_length - offset));
				}
				unsolicited_data_length -=
						(cmd->data_length - offset);
				offset += (cmd->data_length - offset);
				continue;
			}
			if ((offset +
			     conn->conn_ops->MaxRecvDataSegmentLength) >=
					conn->sess->sess_ops->FirstBurstLength) {
				if (!datapduinorder) {
					pdu[i].type = PDUTYPE_UNSOLICITED;
					pdu[i++].length =
					   (conn->sess->sess_ops->FirstBurstLength -
						offset);
				}
				if (!datasequenceinorder) {
					seq[seq_no].type = SEQTYPE_UNSOLICITED;
					seq[seq_no].pdu_count = pdu_count;
					seq[seq_no].xfer_len = (burstlength +
					   (conn->sess->sess_ops->FirstBurstLength -
						offset));
				}
				unsolicited_data_length -=
					(conn->sess->sess_ops->FirstBurstLength -
						offset);
				offset += (conn->sess->sess_ops->FirstBurstLength -
						offset);
				burstlength = 0;
				pdu_count = 0;
				seq_no++;
				continue;
			}

			if (!datapduinorder) {
				pdu[i].type = PDUTYPE_UNSOLICITED;
				pdu[i++].length =
				     conn->conn_ops->MaxRecvDataSegmentLength;
			}
			burstlength += conn->conn_ops->MaxRecvDataSegmentLength;
			offset += conn->conn_ops->MaxRecvDataSegmentLength;
			unsolicited_data_length -=
				conn->conn_ops->MaxRecvDataSegmentLength;
			continue;
		}
		if ((offset + conn->conn_ops->MaxRecvDataSegmentLength) >=
		     cmd->data_length) {
			if (!datapduinorder) {
				pdu[i].type = PDUTYPE_NORMAL;
				pdu[i].length = (cmd->data_length - offset);
			}
			if (!datasequenceinorder) {
				seq[seq_no].type = SEQTYPE_NORMAL;
				seq[seq_no].pdu_count = pdu_count;
				seq[seq_no].xfer_len = (burstlength +
					(cmd->data_length - offset));
			}
			offset += (cmd->data_length - offset);
			continue;
		}
		if ((burstlength + conn->conn_ops->MaxRecvDataSegmentLength) >=
		     conn->sess->sess_ops->MaxBurstLength) {
			if (!datapduinorder) {
				pdu[i].type = PDUTYPE_NORMAL;
				pdu[i++].length =
					(conn->sess->sess_ops->MaxBurstLength -
						burstlength);
			}
			if (!datasequenceinorder) {
				seq[seq_no].type = SEQTYPE_NORMAL;
				seq[seq_no].pdu_count = pdu_count;
				seq[seq_no].xfer_len = (burstlength +
					(conn->sess->sess_ops->MaxBurstLength -
					burstlength));
			}
			offset += (conn->sess->sess_ops->MaxBurstLength -
					burstlength);
			burstlength = 0;
			pdu_count = 0;
			seq_no++;
			continue;
		}

		if (!datapduinorder) {
			pdu[i].type = PDUTYPE_NORMAL;
			pdu[i++].length =
				conn->conn_ops->MaxRecvDataSegmentLength;
		}
		burstlength += conn->conn_ops->MaxRecvDataSegmentLength;
		offset += conn->conn_ops->MaxRecvDataSegmentLength;
	}

	if (!datasequenceinorder) {
		if (bl->data_direction & ISCSI_PDU_WRITE) {
			if (bl->randomize & RANDOM_R2T_OFFSETS) {
				if (iscsit_randomize_seq_lists(cmd, bl->type)
						< 0)
					return -1;
			} else
				iscsit_ordered_seq_lists(cmd, bl->type);
		} else if (bl->data_direction & ISCSI_PDU_READ) {
			if (bl->randomize & RANDOM_DATAIN_SEQ_OFFSETS) {
				if (iscsit_randomize_seq_lists(cmd, bl->type)
						< 0)
					return -1;
			} else
				iscsit_ordered_seq_lists(cmd, bl->type);
		}
#if 0
		iscsit_dump_seq_list(cmd);
#endif
	}
	if (!datapduinorder) {
		if (bl->data_direction & ISCSI_PDU_WRITE) {
			if (bl->randomize & RANDOM_DATAOUT_PDU_OFFSETS) {
				if (iscsit_randomize_pdu_lists(cmd, bl->type)
						< 0)
					return -1;
			} else
				iscsit_ordered_pdu_lists(cmd, bl->type);
		} else if (bl->data_direction & ISCSI_PDU_READ) {
			if (bl->randomize & RANDOM_DATAIN_PDU_OFFSETS) {
				if (iscsit_randomize_pdu_lists(cmd, bl->type)
						< 0)
					return -1;
			} else
				iscsit_ordered_pdu_lists(cmd, bl->type);
		}
#if 0
		iscsit_dump_pdu_list(cmd);
#endif
	}

	return 0;
}

/*
 *	Only called while DataSequenceInOrder=No or DataPDUInOrder=No.
 */
int iscsit_do_build_list(
	struct iscsi_cmd *cmd,
	struct iscsi_build_list *bl)
{
	u32 pdu_count = 0, seq_count = 1;
	struct iscsi_conn *conn = cmd->conn;
	struct iscsi_pdu *pdu = NULL;
	struct iscsi_seq *seq = NULL;

	iscsit_determine_counts_for_list(cmd, bl, &seq_count, &pdu_count);

	if (!conn->sess->sess_ops->DataSequenceInOrder) {
		seq = kzalloc(seq_count * sizeof(struct iscsi_seq), GFP_ATOMIC);
		if (!seq) {
			pr_err("Unable to allocate struct iscsi_seq list\n");
			return -1;
		}
		cmd->seq_list = seq;
		cmd->seq_count = seq_count;
	}

	if (!conn->sess->sess_ops->DataPDUInOrder) {
		pdu = kzalloc(pdu_count * sizeof(struct iscsi_pdu), GFP_ATOMIC);
		if (!pdu) {
			pr_err("Unable to allocate struct iscsi_pdu list.\n");
			kfree(seq);
			return -1;
		}
		cmd->pdu_list = pdu;
		cmd->pdu_count = pdu_count;
	}

	return iscsit_build_pdu_and_seq_list(cmd, bl);
}

struct iscsi_pdu *iscsit_get_pdu_holder(
	struct iscsi_cmd *cmd,
	u32 offset,
	u32 length)
{
	u32 i;
	struct iscsi_pdu *pdu = NULL;

	if (!cmd->pdu_list) {
		pr_err("struct iscsi_cmd->pdu_list is NULL!\n");
		return NULL;
	}

	pdu = &cmd->pdu_list[0];

	for (i = 0; i < cmd->pdu_count; i++)
		if ((pdu[i].offset == offset) && (pdu[i].length == length))
			return &pdu[i];

	pr_err("Unable to locate PDU holder for ITT: 0x%08x, Offset:"
		" %u, Length: %u\n", cmd->init_task_tag, offset, length);
	return NULL;
}

struct iscsi_pdu *iscsit_get_pdu_holder_for_seq(
	struct iscsi_cmd *cmd,
	struct iscsi_seq *seq)
{
	u32 i;
	struct iscsi_conn *conn = cmd->conn;
	struct iscsi_pdu *pdu = NULL;

	if (!cmd->pdu_list) {
		pr_err("struct iscsi_cmd->pdu_list is NULL!\n");
		return NULL;
	}

	if (conn->sess->sess_ops->DataSequenceInOrder) {
redo:
		pdu = &cmd->pdu_list[cmd->pdu_start];

		for (i = 0; pdu[i].seq_no != cmd->seq_no; i++) {
#if 0
			pr_debug("pdu[i].seq_no: %d, pdu[i].pdu"
				"_send_order: %d, pdu[i].offset: %d,"
				" pdu[i].length: %d\n", pdu[i].seq_no,
				pdu[i].pdu_send_order, pdu[i].offset,
				pdu[i].length);
#endif
			if (pdu[i].pdu_send_order == cmd->pdu_send_order) {
				cmd->pdu_send_order++;
				return &pdu[i];
			}
		}

		cmd->pdu_start += cmd->pdu_send_order;
		cmd->pdu_send_order = 0;
		cmd->seq_no++;

		if (cmd->pdu_start < cmd->pdu_count)
			goto redo;

		pr_err("Command ITT: 0x%08x unable to locate"
			" struct iscsi_pdu for cmd->pdu_send_order: %u.\n",
			cmd->init_task_tag, cmd->pdu_send_order);
		return NULL;
	} else {
		if (!seq) {
			pr_err("struct iscsi_seq is NULL!\n");
			return NULL;
		}
#if 0
		pr_debug("seq->pdu_start: %d, seq->pdu_count: %d,"
			" seq->seq_no: %d\n", seq->pdu_start, seq->pdu_count,
			seq->seq_no);
#endif
		pdu = &cmd->pdu_list[seq->pdu_start];

		if (seq->pdu_send_order == seq->pdu_count) {
			pr_err("Command ITT: 0x%08x seq->pdu_send"
				"_order: %u equals seq->pdu_count: %u\n",
				cmd->init_task_tag, seq->pdu_send_order,
				seq->pdu_count);
			return NULL;
		}

		for (i = 0; i < seq->pdu_count; i++) {
			if (pdu[i].pdu_send_order == seq->pdu_send_order) {
				seq->pdu_send_order++;
				return &pdu[i];
			}
		}

		pr_err("Command ITT: 0x%08x unable to locate iscsi"
			"_pdu_t for seq->pdu_send_order: %u.\n",
			cmd->init_task_tag, seq->pdu_send_order);
		return NULL;
	}

	return NULL;
}

struct iscsi_seq *iscsit_get_seq_holder(
	struct iscsi_cmd *cmd,
	u32 offset,
	u32 length)
{
	u32 i;

	if (!cmd->seq_list) {
		pr_err("struct iscsi_cmd->seq_list is NULL!\n");
		return NULL;
	}

	for (i = 0; i < cmd->seq_count; i++) {
#if 0
		pr_debug("seq_list[i].orig_offset: %d, seq_list[i]."
			"xfer_len: %d, seq_list[i].seq_no %u\n",
			cmd->seq_list[i].orig_offset, cmd->seq_list[i].xfer_len,
			cmd->seq_list[i].seq_no);
#endif
		if ((cmd->seq_list[i].orig_offset +
				cmd->seq_list[i].xfer_len) >=
				(offset + length))
			return &cmd->seq_list[i];
	}

	pr_err("Unable to locate Sequence holder for ITT: 0x%08x,"
		" Offset: %u, Length: %u\n", cmd->init_task_tag, offset,
		length);
	return NULL;
}
