/*
 *  Copyright (C) 2005 FUJITA Tomonori <tomof@acm.org>
 *  Copyright (C) 2007 - 2018 Vladislav Bolkhovitin
 *  Copyright (C) 2007 - 2018 Western Digital Corporation
 *
 *  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.
 *
 *  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 "iscsi_trace_flag.h"
#include "iscsi.h"
#include "digest.h"

#define	CHECK_PARAM(info, iparams, word, min, max)				\
do {										\
	if (!(info)->partial || ((info)->partial & 1 << key_##word)) {		\
		TRACE_DBG("%s: %u", #word, (iparams)[key_##word]);		\
		if ((iparams)[key_##word] < (min) ||				\
			(iparams)[key_##word] > (max)) {			\
			if ((iparams)[key_##word] < (min)) {			\
				(iparams)[key_##word] = (min);			\
				PRINT_WARNING("%s: %u is too small, resetting "	\
					"it to allowed min %u",			\
					#word, (iparams)[key_##word], (min));	\
			} else {						\
				PRINT_WARNING("%s: %u is too big, resetting "	\
					"it to allowed max %u",			\
					#word, (iparams)[key_##word], (max));	\
				(iparams)[key_##word] = (max);			\
			}							\
		}								\
	}									\
} while (0)

#define	SET_PARAM(params, info, iparams, word)					\
({										\
	int changed = 0;							\
	if (!(info)->partial || ((info)->partial & 1 << key_##word)) {		\
		if ((params)->word != (iparams)[key_##word])			\
			changed = 1;						\
		(params)->word = (iparams)[key_##word];				\
		TRACE_DBG("%s set to %u", #word, (params)->word);		\
	}									\
	changed;								\
})

#define	GET_PARAM(params, info, iparams, word)	\
	((iparams)[key_##word] = (params)->word)

const char *iscsi_get_bool_value(int val)
{
	if (val)
		return "Yes";
	else
		return "No";
}

const char *iscsi_get_digest_name(int val, char *res)
{
	int pos = 0;

	if (val & DIGEST_NONE)
		pos = sprintf(&res[pos], "None");

	if (val & DIGEST_CRC32C)
		pos += sprintf(&res[pos], "%s%s", (pos != 0) ? ", " : "",
			"CRC32C");

	if (pos == 0)
		sprintf(&res[pos], "Unknown");

	return res;
}

static void log_params(struct iscsi_sess_params *params)
{
	char hdigest_name[64], ddigest_name[64];

	PRINT_INFO("Negotiated parameters: InitialR2T %s, ImmediateData %s, MaxConnections %d, MaxRecvDataSegmentLength %d, MaxXmitDataSegmentLength %d, ",
		iscsi_get_bool_value(params->initial_r2t),
		iscsi_get_bool_value(params->immediate_data),
		params->max_connections,
		params->max_recv_data_length, params->max_xmit_data_length);
	PRINT_INFO("    MaxBurstLength %d, FirstBurstLength %d, DefaultTime2Wait %d, DefaultTime2Retain %d, ",
		params->max_burst_length, params->first_burst_length,
		params->default_wait_time, params->default_retain_time);
	PRINT_INFO("    MaxOutstandingR2T %d, DataPDUInOrder %s, DataSequenceInOrder %s, ErrorRecoveryLevel %d, ",
		params->max_outstanding_r2t,
		iscsi_get_bool_value(params->data_pdu_inorder),
		iscsi_get_bool_value(params->data_sequence_inorder),
		params->error_recovery_level);
	PRINT_INFO("    HeaderDigest %s, DataDigest %s, OFMarker %s, IFMarker %s, OFMarkInt %d, IFMarkInt %d, RDMAExtensions %s",
		iscsi_get_digest_name(params->header_digest, hdigest_name),
		iscsi_get_digest_name(params->data_digest, ddigest_name),
		iscsi_get_bool_value(params->ofmarker),
		iscsi_get_bool_value(params->ifmarker),
		params->ofmarkint, params->ifmarkint,
		iscsi_get_bool_value(params->rdma_extensions));
}

/* target_mutex supposed to be locked */
static void sess_params_check(struct iscsi_kern_params_info *info)
{
	int32_t *iparams = info->session_params;
	const int max_len = ISCSI_CONN_IOV_MAX * PAGE_SIZE;

	/*
	 * This is only kernel sanity check. Actual data validity checks
	 * performed in the user space.
	 */

	CHECK_PARAM(info, iparams, initial_r2t, 0, 1);
	CHECK_PARAM(info, iparams, immediate_data, 0, 1);
	CHECK_PARAM(info, iparams, max_connections, 1, 1);
	CHECK_PARAM(info, iparams, max_recv_data_length, 512, max_len);
	CHECK_PARAM(info, iparams, max_xmit_data_length, 512, max_len);
	CHECK_PARAM(info, iparams, max_burst_length, 512, max_len);
	CHECK_PARAM(info, iparams, first_burst_length, 512, max_len);
	CHECK_PARAM(info, iparams, max_outstanding_r2t, 1, 65535);
	CHECK_PARAM(info, iparams, error_recovery_level, 0, 0);
	CHECK_PARAM(info, iparams, data_pdu_inorder, 0, 1);
	CHECK_PARAM(info, iparams, data_sequence_inorder, 0, 1);

	digest_alg_available(&iparams[key_header_digest]);
	digest_alg_available(&iparams[key_data_digest]);

	CHECK_PARAM(info, iparams, ofmarker, 0, 0);
	CHECK_PARAM(info, iparams, ifmarker, 0, 0);

	/* iSER related parameters */
	CHECK_PARAM(info, iparams, rdma_extensions, 0, 1);
	CHECK_PARAM(info, iparams, target_recv_data_length, 512, max_len);
	CHECK_PARAM(info, iparams, initiator_recv_data_length, 512, max_len);

	return;
}

/* target_mutex supposed to be locked */
static void sess_params_set(struct iscsi_sess_params *params,
			   struct iscsi_kern_params_info *info)
{
	int32_t *iparams = info->session_params;

	SET_PARAM(params, info, iparams, initial_r2t);
	SET_PARAM(params, info, iparams, immediate_data);
	SET_PARAM(params, info, iparams, max_connections);
	SET_PARAM(params, info, iparams, max_recv_data_length);
	SET_PARAM(params, info, iparams, max_xmit_data_length);
	SET_PARAM(params, info, iparams, max_burst_length);
	SET_PARAM(params, info, iparams, first_burst_length);
	SET_PARAM(params, info, iparams, default_wait_time);
	SET_PARAM(params, info, iparams, default_retain_time);
	SET_PARAM(params, info, iparams, max_outstanding_r2t);
	SET_PARAM(params, info, iparams, data_pdu_inorder);
	SET_PARAM(params, info, iparams, data_sequence_inorder);
	SET_PARAM(params, info, iparams, error_recovery_level);
	SET_PARAM(params, info, iparams, header_digest);
	SET_PARAM(params, info, iparams, data_digest);
	SET_PARAM(params, info, iparams, ofmarker);
	SET_PARAM(params, info, iparams, ifmarker);
	SET_PARAM(params, info, iparams, ofmarkint);
	SET_PARAM(params, info, iparams, ifmarkint);

	/* iSER related parameters */
	SET_PARAM(params, info, iparams, rdma_extensions);
	SET_PARAM(params, info, iparams, target_recv_data_length);
	SET_PARAM(params, info, iparams, initiator_recv_data_length);
	return;
}

static void sess_params_get(struct iscsi_sess_params *params,
			   struct iscsi_kern_params_info *info)
{
	int32_t *iparams = info->session_params;

	GET_PARAM(params, info, iparams, initial_r2t);
	GET_PARAM(params, info, iparams, immediate_data);
	GET_PARAM(params, info, iparams, max_connections);
	GET_PARAM(params, info, iparams, max_recv_data_length);
	GET_PARAM(params, info, iparams, max_xmit_data_length);
	GET_PARAM(params, info, iparams, max_burst_length);
	GET_PARAM(params, info, iparams, first_burst_length);
	GET_PARAM(params, info, iparams, default_wait_time);
	GET_PARAM(params, info, iparams, default_retain_time);
	GET_PARAM(params, info, iparams, max_outstanding_r2t);
	GET_PARAM(params, info, iparams, data_pdu_inorder);
	GET_PARAM(params, info, iparams, data_sequence_inorder);
	GET_PARAM(params, info, iparams, error_recovery_level);
	GET_PARAM(params, info, iparams, header_digest);
	GET_PARAM(params, info, iparams, data_digest);
	GET_PARAM(params, info, iparams, ofmarker);
	GET_PARAM(params, info, iparams, ifmarker);
	GET_PARAM(params, info, iparams, ofmarkint);
	GET_PARAM(params, info, iparams, ifmarkint);

	/* iSER related parameters */
	GET_PARAM(params, info, iparams, rdma_extensions);
	GET_PARAM(params, info, iparams, target_recv_data_length);
	GET_PARAM(params, info, iparams, initiator_recv_data_length);
	return;
}

/* target_mutex supposed to be locked */
static void tgt_params_check(struct iscsi_session *session,
	struct iscsi_kern_params_info *info)
{
	int32_t *iparams = info->target_params;
	unsigned int rsp_timeout, nop_in_timeout;

	/*
	 * This is only kernel sanity check. Actual data validity checks
	 * performed in the user space.
	 */

	CHECK_PARAM(info, iparams, queued_cmnds, MIN_NR_QUEUED_CMNDS,
		MAX_NR_QUEUED_CMNDS);
	CHECK_PARAM(info, iparams, rsp_timeout, MIN_RSP_TIMEOUT,
		MAX_RSP_TIMEOUT);
	CHECK_PARAM(info, iparams, nop_in_interval, MIN_NOP_IN_INTERVAL,
		MAX_NOP_IN_INTERVAL);
	CHECK_PARAM(info, iparams, nop_in_timeout, MIN_NOP_IN_TIMEOUT,
		MAX_NOP_IN_TIMEOUT);

	/*
	 * We adjust too long timeout in req_add_to_write_timeout_list()
	 * only for NOPs, so check and warn if this assumption isn't honored.
	 */
	if (!info->partial || (info->partial & 1 << key_rsp_timeout))
		rsp_timeout = iparams[key_rsp_timeout];
	else
		rsp_timeout = session->tgt_params.rsp_timeout;
	if (!info->partial || (info->partial & 1 << key_nop_in_timeout))
		nop_in_timeout = iparams[key_nop_in_timeout];
	else
		nop_in_timeout = session->tgt_params.nop_in_timeout;
	if (nop_in_timeout > rsp_timeout)
		PRINT_WARNING("%s", "RspTimeout should be >= NopInTimeout, otherwise data transfer failure could take up to NopInTimeout long to detect");

	return;
}

/* target_mutex supposed to be locked */
static int iscsi_tgt_params_set(struct iscsi_session *session,
		      struct iscsi_kern_params_info *info, int set)
{
	struct iscsi_tgt_params *params = &session->tgt_params;
	int32_t *iparams = info->target_params;

	lockdep_assert_held(&session->target->target_mutex);

	if (set) {
		struct iscsi_conn *conn;

		tgt_params_check(session, info);

		SET_PARAM(params, info, iparams, queued_cmnds);
		SET_PARAM(params, info, iparams, rsp_timeout);
		SET_PARAM(params, info, iparams, nop_in_interval);
		SET_PARAM(params, info, iparams, nop_in_timeout);

		PRINT_INFO("Target parameters set for session %llx: QueuedCommands %d, Response timeout %d, Nop-In interval %d, Nop-In timeout %d", session->sid,
			params->queued_cmnds, params->rsp_timeout,
			params->nop_in_interval, params->nop_in_timeout);

		list_for_each_entry(conn, &session->conn_list,
					conn_list_entry) {
			conn->data_rsp_timeout =
				session->tgt_params.rsp_timeout * HZ;
			conn->nop_in_interval =
				session->tgt_params.nop_in_interval * HZ;
			conn->nop_in_timeout =
				session->tgt_params.nop_in_timeout * HZ;
			spin_lock_bh(&conn->conn_thr_pool->rd_lock);
			if (!conn->closing && (conn->nop_in_interval > 0)) {
				TRACE_DBG("Schedule Nop-In work for conn %p",
					  conn);
				schedule_delayed_work(&conn->nop_in_delayed_work,
					conn->nop_in_interval + ISCSI_ADD_SCHED_TIME);
			}
			spin_unlock_bh(&conn->conn_thr_pool->rd_lock);
		}
	} else {
		GET_PARAM(params, info, iparams, queued_cmnds);
		GET_PARAM(params, info, iparams, rsp_timeout);
		GET_PARAM(params, info, iparams, nop_in_interval);
		GET_PARAM(params, info, iparams, nop_in_timeout);
	}

	return 0;
}

/* target_mutex supposed to be locked */
static int iscsi_sess_params_set(struct iscsi_session *session,
	struct iscsi_kern_params_info *info, int set)
{
	struct iscsi_sess_params *params;

	if (set)
		sess_params_check(info);

	params = &session->sess_params;

	if (set) {
		sess_params_set(params, info);
		log_params(params);
	} else
		sess_params_get(params, info);

	return 0;
}

/* target_mutex supposed to be locked */
int iscsi_params_set(struct iscsi_target *target,
	struct iscsi_kern_params_info *info, int set)
{
	int err;
	struct iscsi_session *session;

	lockdep_assert_held(&target->target_mutex);

	if (info->sid == 0) {
		PRINT_ERROR("sid must not be %d", 0);
		err = -EINVAL;
		goto out;
	}

	session = session_lookup(target, info->sid);
	if (session == NULL) {
		PRINT_ERROR("Session for sid %llx not found", info->sid);
		err = -ENOENT;
		goto out;
	}

	if (set && !list_empty(&session->conn_list) &&
	    (info->params_type != key_target)) {
		err = -EBUSY;
		goto out;
	}

	if (info->params_type == key_session)
		err = iscsi_sess_params_set(session, info, set);
	else if (info->params_type == key_target)
		err = iscsi_tgt_params_set(session, info, set);
	else
		err = -EINVAL;

out:
	return err;
}
