/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2002 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * s1394_cmp.c
 *    1394 Services Layer Connection Management Procedures Support Routines
 */

#include <sys/conf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/cmn_err.h>
#include <sys/types.h>
#include <sys/kmem.h>
#include <sys/1394/t1394.h>
#include <sys/1394/s1394.h>
#include <sys/1394/h1394.h>

static void s1394_cmp_init(s1394_hal_t *hal);
static void s1394_cmp_fini(s1394_hal_t *hal);
static void s1394_cmp_ompr_recv_read_request(cmd1394_cmd_t *req);
static void s1394_cmp_impr_recv_read_request(cmd1394_cmd_t *req);
static void s1394_cmp_ompr_recv_lock_request(cmd1394_cmd_t *req);
static void s1394_cmp_impr_recv_lock_request(cmd1394_cmd_t *req);
static void s1394_cmp_notify_reg_change(s1394_hal_t *hal, t1394_cmp_reg_t reg,
    s1394_target_t *self);


/*
 * number of retries to notify registered targets in case target list
 * changes while the list rwlock is dropped for the time of callback
 */
uint_t s1394_cmp_notify_retry_cnt = 3;

s1394_fa_descr_t s1394_cmp_ompr_descr = {
	IEC61883_CMP_OMPR_ADDR,
	4,
	T1394_ADDR_RDENBL | T1394_ADDR_LKENBL,
	{
		s1394_cmp_ompr_recv_read_request,
		NULL,
		s1394_cmp_ompr_recv_lock_request
	},
	0
};

s1394_fa_descr_t s1394_cmp_impr_descr = {
	IEC61883_CMP_IMPR_ADDR,
	4,
	T1394_ADDR_RDENBL | T1394_ADDR_LKENBL,
	{
		s1394_cmp_impr_recv_read_request,
		NULL,
		s1394_cmp_impr_recv_lock_request
	},
	0
};


int
s1394_cmp_register(s1394_target_t *target, t1394_cmp_evts_t *evts)
{
	s1394_hal_t	*hal = target->on_hal;
	static t1394_cmp_evts_t default_evts = { NULL, NULL };

	rw_enter(&hal->target_list_rwlock, RW_WRITER);
	/*
	 * if registering the first target, claim and initialize addresses
	 */
	if (s1394_fa_list_is_empty(hal, S1394_FA_TYPE_CMP)) {
		if (s1394_fa_claim_addr(hal, S1394_FA_TYPE_CMP_OMPR,
		    &s1394_cmp_ompr_descr) != DDI_SUCCESS) {
			rw_exit(&hal->target_list_rwlock);
			return (DDI_FAILURE);
		}

		if (s1394_fa_claim_addr(hal, S1394_FA_TYPE_CMP_IMPR,
		    &s1394_cmp_impr_descr) != DDI_SUCCESS) {
			s1394_fa_free_addr(hal, S1394_FA_TYPE_CMP_OMPR);
			rw_exit(&hal->target_list_rwlock);
			return (DDI_FAILURE);
		}

		s1394_cmp_init(hal);
	}

	/* Add on the target list (we only use one list) */
	s1394_fa_list_add(hal, target, S1394_FA_TYPE_CMP);

	if (evts == NULL) {
		evts = &default_evts;
	}
	target->target_fa[S1394_FA_TYPE_CMP].fat_u.cmp.cm_evts = *evts;

	rw_exit(&hal->target_list_rwlock);

	return (DDI_SUCCESS);
}

int
s1394_cmp_unregister(s1394_target_t *target)
{
	s1394_hal_t	*hal = target->on_hal;

	rw_enter(&hal->target_list_rwlock, RW_WRITER);

	if (s1394_fa_list_remove(hal, target,
	    S1394_FA_TYPE_CMP) == DDI_SUCCESS) {
		if (s1394_fa_list_is_empty(hal, S1394_FA_TYPE_CMP)) {
			s1394_fa_free_addr(hal, S1394_FA_TYPE_CMP_OMPR);
			s1394_fa_free_addr(hal, S1394_FA_TYPE_CMP_IMPR);
			s1394_cmp_fini(hal);
		}
	}

	rw_exit(&hal->target_list_rwlock);

	return (DDI_SUCCESS);
}

int
s1394_cmp_read(s1394_target_t *target, t1394_cmp_reg_t reg, uint32_t *valp)
{
	s1394_hal_t	*hal = target->on_hal;
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;
	int		ret = DDI_FAILURE;

	if (reg == T1394_CMP_OMPR) {
		rw_enter(&cmp->cmp_ompr_rwlock, RW_READER);
		*valp = cmp->cmp_ompr_val;
		rw_exit(&cmp->cmp_ompr_rwlock);
		ret = DDI_SUCCESS;
	} else if (reg == T1394_CMP_IMPR) {
		rw_enter(&cmp->cmp_impr_rwlock, RW_READER);
		*valp = cmp->cmp_impr_val;
		rw_exit(&cmp->cmp_impr_rwlock);
		ret = DDI_SUCCESS;
	}

	return (ret);
}

int
s1394_cmp_cas(s1394_target_t *target, t1394_cmp_reg_t reg, uint32_t arg_val,
		uint32_t new_val, uint32_t *old_valp)
{
	s1394_hal_t	*hal = target->on_hal;
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;
	int		ret = DDI_SUCCESS;

	if (reg == T1394_CMP_OMPR) {
		rw_enter(&cmp->cmp_ompr_rwlock, RW_WRITER);
		*old_valp = cmp->cmp_ompr_val;
		if (cmp->cmp_ompr_val == arg_val) {
			cmp->cmp_ompr_val = new_val;
		}
		rw_exit(&cmp->cmp_ompr_rwlock);
	} else if (reg == T1394_CMP_IMPR) {
		rw_enter(&cmp->cmp_impr_rwlock, RW_WRITER);
		*old_valp = cmp->cmp_impr_val;
		if (cmp->cmp_impr_val == arg_val) {
			cmp->cmp_impr_val = new_val;
		}
		rw_exit(&cmp->cmp_impr_rwlock);
	} else {
		ret = DDI_FAILURE;
	}

	/* notify other targets */
	if (ret == DDI_SUCCESS) {
		s1394_cmp_notify_reg_change(hal, reg, target);
	}

	return (ret);
}

static void
s1394_cmp_init(s1394_hal_t *hal)
{
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;

	rw_init(&cmp->cmp_ompr_rwlock, NULL, RW_DRIVER, NULL);
	rw_init(&cmp->cmp_impr_rwlock, NULL, RW_DRIVER, NULL);

	cmp->cmp_ompr_val = IEC61883_CMP_OMPR_INIT_VAL;
	cmp->cmp_impr_val = IEC61883_CMP_IMPR_INIT_VAL;
}

static void
s1394_cmp_fini(s1394_hal_t *hal)
{
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;

	rw_destroy(&cmp->cmp_ompr_rwlock);
	rw_destroy(&cmp->cmp_impr_rwlock);
}

/*
 * iMPR/oMPR read/lock requests
 */
static void
s1394_cmp_ompr_recv_read_request(cmd1394_cmd_t *req)
{
	s1394_hal_t	*hal = req->cmd_callback_arg;
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;

	if (req->cmd_type != CMD1394_ASYNCH_RD_QUAD) {
		req->cmd_result = IEEE1394_RESP_TYPE_ERROR;
	} else {
		rw_enter(&cmp->cmp_ompr_rwlock, RW_READER);
		req->cmd_u.q.quadlet_data = cmp->cmp_ompr_val;
		rw_exit(&cmp->cmp_ompr_rwlock);
		req->cmd_result = IEEE1394_RESP_COMPLETE;
	}

	(void) s1394_send_response(hal, req);
}

static void
s1394_cmp_impr_recv_read_request(cmd1394_cmd_t *req)
{
	s1394_hal_t	*hal = req->cmd_callback_arg;
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;

	if (req->cmd_type != CMD1394_ASYNCH_RD_QUAD) {
		req->cmd_result = IEEE1394_RESP_TYPE_ERROR;
	} else {
		rw_enter(&cmp->cmp_impr_rwlock, RW_READER);
		req->cmd_u.q.quadlet_data = cmp->cmp_impr_val;
		rw_exit(&cmp->cmp_impr_rwlock);
		req->cmd_result = IEEE1394_RESP_COMPLETE;
	}

	(void) s1394_send_response(hal, req);
}

static void
s1394_cmp_ompr_recv_lock_request(cmd1394_cmd_t *req)
{
	s1394_hal_t	*hal = req->cmd_callback_arg;
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;
	boolean_t	notify = B_TRUE;

	if ((req->cmd_type != CMD1394_ASYNCH_LOCK_32) ||
	    (req->cmd_u.l32.lock_type != CMD1394_LOCK_COMPARE_SWAP)) {
		req->cmd_result = IEEE1394_RESP_TYPE_ERROR;
		notify = B_FALSE;
	} else {
		rw_enter(&cmp->cmp_ompr_rwlock, RW_WRITER);
		req->cmd_u.l32.old_value = cmp->cmp_ompr_val;
		if (cmp->cmp_ompr_val == req->cmd_u.l32.arg_value) {
			/* write only allowed bits */
			cmp->cmp_ompr_val = (req->cmd_u.l32.data_value &
			    IEC61883_CMP_OMPR_LOCK_MASK) |
			    (cmp->cmp_ompr_val & ~IEC61883_CMP_OMPR_LOCK_MASK);
		}
		rw_exit(&cmp->cmp_ompr_rwlock);
		req->cmd_result = IEEE1394_RESP_COMPLETE;
	}

	(void) s1394_send_response(hal, req);

	/* notify all targets */
	if (notify) {
		s1394_cmp_notify_reg_change(hal, T1394_CMP_OMPR, NULL);
	}
}

static void
s1394_cmp_impr_recv_lock_request(cmd1394_cmd_t *req)
{
	s1394_hal_t	*hal = req->cmd_callback_arg;
	s1394_cmp_hal_t *cmp = &hal->hal_cmp;
	boolean_t	notify = B_TRUE;

	if ((req->cmd_type != CMD1394_ASYNCH_LOCK_32) ||
	    (req->cmd_u.l32.lock_type != CMD1394_LOCK_COMPARE_SWAP)) {
		req->cmd_result = IEEE1394_RESP_TYPE_ERROR;
		notify = B_FALSE;
	} else {
		rw_enter(&cmp->cmp_impr_rwlock, RW_WRITER);
		req->cmd_u.l32.old_value = cmp->cmp_impr_val;
		if (cmp->cmp_impr_val == req->cmd_u.l32.arg_value) {
			/* write only allowed bits */
			cmp->cmp_impr_val = (req->cmd_u.l32.data_value &
			    IEC61883_CMP_IMPR_LOCK_MASK) |
			    (cmp->cmp_impr_val & ~IEC61883_CMP_IMPR_LOCK_MASK);
		}
		rw_exit(&cmp->cmp_impr_rwlock);
		req->cmd_result = IEEE1394_RESP_COMPLETE;
	}

	(void) s1394_send_response(hal, req);

	/* notify all targets */
	if (notify) {
		s1394_cmp_notify_reg_change(hal, T1394_CMP_IMPR, NULL);
	}
}

/*
 * Notify registered targets except 'self' about register value change
 */
static void
s1394_cmp_notify_reg_change(s1394_hal_t *hal, t1394_cmp_reg_t reg,
    s1394_target_t *self)
{
	s1394_target_t	*target;
	s1394_fa_target_t *fat;
	uint_t		saved_gen;
	int		num_retries = 0;
	void		(*cb)(opaque_t, t1394_cmp_reg_t);
	opaque_t	arg;

	rw_enter(&hal->target_list_rwlock, RW_READER);

start:
	target = hal->hal_fa[S1394_FA_TYPE_CMP].fal_head;

	for (; target; target = fat->fat_next) {
		fat = &target->target_fa[S1394_FA_TYPE_CMP];

		/*
		 * even if the target list changes when the lock is dropped,
		 * comparing with self is safe because the target should
		 * not unregister until all CMP operations are completed
		 */
		if (target == self) {
			continue;
		}

		cb = fat->fat_u.cmp.cm_evts.cmp_reg_change;
		if (cb == NULL) {
			continue;
		}
		arg = fat->fat_u.cmp.cm_evts.cmp_arg;

		saved_gen = s1394_fa_list_gen(hal, S1394_FA_TYPE_CMP);

		rw_exit(&hal->target_list_rwlock);
		cb(arg, reg);
		rw_enter(&hal->target_list_rwlock, RW_READER);

		/*
		 * List could change while we dropped the lock. In such
		 * case, start all over again, because missing a register
		 * change can have more serious consequences for a
		 * target than receiving same notification more than once
		 */
		if (saved_gen != s1394_fa_list_gen(hal, S1394_FA_TYPE_CMP)) {
			if (++num_retries <= s1394_cmp_notify_retry_cnt) {
				goto start;
			} else {
				break;
			}
		}
	}

	rw_exit(&hal->target_list_rwlock);
}