/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (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 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */


#include <sys/types.h>
#include <sys/cmn_err.h>
#include <sys/conf.h>
#include <sys/ddi_impldefs.h>
#include <sys/autoconf.h>
#include <sys/systm.h>
#include <sys/modctl.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/ddi_subrdefs.h>
#include <sys/promif.h>
#include <sys/machsystm.h>
#include <sys/ddi_intr_impl.h>
#include <sys/hypervisor_api.h>
#include <sys/intr.h>
#include <sys/hsvc.h>

#define	SUN4V_REG_SPEC2CFG_HDL(x)	((x >> 32) & ~(0xfull << 28))

static kmutex_t vnex_id_lock;
/*
 * Vnex name  to pil map
 */
typedef struct vnex_regspec {
	uint64_t physaddr;
	uint64_t size;
} vnex_regspec_t;

struct vnex_pil_map {
	caddr_t	name;
	uint32_t pil;
};

/* vnex interrupt descriptor */
typedef struct vnex_id {
	dev_info_t *vid_dip;
	uint32_t vid_ino;
	uint64_t vid_ihdl;
	uint_t	(*vid_handler)();
	caddr_t	vid_arg1;
	caddr_t	vid_arg2;
	ddi_intr_handle_impl_t *vid_ddi_hdlp;
	uint64_t vid_cfg_hdl;
	struct vnex_id *vid_next;
} vnex_id_t;

/* vnex interrupt descriptor list */
static vnex_id_t *vnex_id_list;

hrtime_t vnex_pending_timeout = 2ull * NANOSEC; /* 2 seconds in nanoseconds */

/*
 * vnex interrupt descriptor list manipulation functions
 */

static vnex_id_t *vnex_locate_id(dev_info_t *dip, uint32_t ino);
static vnex_id_t *vnex_alloc_id(dev_info_t *dip, uint32_t ino,
	uint64_t dhdl);
static void vnex_add_id(vnex_id_t *vid_p);
static void vnex_rem_id(vnex_id_t *vid_p);
static void vnex_free_id(vnex_id_t *vid_p);

uint_t vnex_intr_wrapper(caddr_t arg);

static struct vnex_pil_map vnex_name_to_pil[] = {
	{"console", 	PIL_12},
	{"fma",		PIL_5},
	{"echo", 	PIL_3},
	{"loop", 	PIL_3},
	{"sunmc", 	PIL_3},
	{"sunvts", 	PIL_3},
	{"explorer", 	PIL_3},
	{"ncp", 	PIL_8},
	{"crypto", 	PIL_8}
};

#define	VNEX_MAX_DEVS	(sizeof (vnex_name_to_pil) /	\
			    sizeof (struct vnex_pil_map))

/*
 * Config information
 */
static int vnex_intr_ops(dev_info_t *dip, dev_info_t *rdip,
    ddi_intr_op_t intr_op, ddi_intr_handle_impl_t *hdlp, void *result);

static int
vnex_ctl(dev_info_t *, dev_info_t *, ddi_ctl_enum_t, void *, void *);

static struct bus_ops vnex_bus_ops = {
	BUSO_REV,
	nullbusmap,
	NULL,	/* NO OP */
	NULL,	/* NO OP */
	NULL,	/* NO OP */
	i_ddi_map_fault,
	ddi_no_dma_map,
	ddi_no_dma_allochdl,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	vnex_ctl,
	ddi_bus_prop_op,
	NULL,	/* (*bus_get_eventcookie)();    */
	NULL,	/* (*bus_add_eventcall)();	*/
	NULL,	/* (*bus_remove_eventcall)();   */
	NULL,	/* (*bus_post_event)();		*/
	NULL,	/* (*bus_intr_ctl)();		*/
	NULL,	/* (*bus_config)();		*/
	NULL,	/* (*bus_unconfig)();		*/
	NULL,	/* (*bus_fm_init)();		*/
	NULL,	/* (*bus_fm_fini)();		*/
	NULL,	/* (*bus_fm_access_enter)();	*/
	NULL,	/* (*bus_fm_access_fini)();	*/
	NULL,	/* (*bus_power)();		*/
	vnex_intr_ops	/* (*bus_intr_op)();	*/
};

static int vnex_attach(dev_info_t *dip, ddi_attach_cmd_t cmd);
static int vnex_detach(dev_info_t *dip, ddi_detach_cmd_t cmd);

static struct dev_ops pseudo_ops = {
	DEVO_REV,		/* devo_rev, */
	0,			/* refcnt  */
	ddi_no_info,		/* info */
	nulldev,		/* identify */
	nulldev,		/* probe */
	vnex_attach,		/* attach */
	vnex_detach,		/* detach */
	nodev,			/* reset */
	(struct cb_ops *)0,	/* driver operations */
	&vnex_bus_ops,		/* bus operations */
	nulldev,		/* power */
	ddi_quiesce_not_needed,		/* quiesce */
};

/*
 * Module linkage information for the kernel.
 */

static struct modldrv modldrv = {
	&mod_driverops, /* Type of module.  This one is a pseudo driver */
	"sun4v virtual-devices nexus driver",
	&pseudo_ops,	/* driver ops */
};

static struct modlinkage modlinkage = {
	MODREV_1, (void *)&modldrv, NULL
};

int
_init(void)
{
	uint64_t mjrnum;
	uint64_t mnrnum;

	/*
	 * Check HV intr group api versioning.
	 * This driver uses the old interrupt routines which are supported
	 * in old firmware in the CORE API group and in newer firmware in
	 * the INTR API group.  Support for these calls will be dropped
	 * once the INTR API group major goes to 2.
	 */

	if ((hsvc_version(HSVC_GROUP_INTR, &mjrnum, &mnrnum) == 0) &&
	    (mjrnum > 1)) {
		cmn_err(CE_WARN, "niumx: unsupported intr api group: "
		    "maj:0x%lx, min:0x%lx", mjrnum, mnrnum);
		return (ENOTSUP);
	}

	return (mod_install(&modlinkage));
}

int
_fini(void)
{
	return (mod_remove(&modlinkage));
}

int
_info(struct modinfo *modinfop)
{
	return (mod_info(&modlinkage, modinfop));
}

/*ARGSUSED*/
void
vnex_intr_dist(void *arg)
{
	vnex_id_t *vid_p;
	uint32_t cpuid;
	int	intr_state;
	hrtime_t start;

	mutex_enter(&vnex_id_lock);

	for (vid_p = vnex_id_list; vid_p != NULL;
	    vid_p = vid_p->vid_next) {
		/*
		 * Don't do anything for disabled interrupts.
		 * vnex_enable_intr takes care of redistributing interrupts.
		 */
		if ((hvio_intr_getvalid(vid_p->vid_ihdl,
		    &intr_state) == H_EOK) && (intr_state == HV_INTR_NOTVALID))
				continue;

		cpuid = intr_dist_cpuid();

		(void) hvio_intr_setvalid(vid_p->vid_ihdl, HV_INTR_NOTVALID);
		/*
		 * Make a best effort to wait for pending interrupts to finish.
		 * There is not much we can do if we timeout.
		 */
		start = gethrtime();
		while (!panicstr &&
		    (hvio_intr_getstate(vid_p->vid_ihdl, &intr_state) ==
		    H_EOK) && (intr_state == HV_INTR_DELIVERED_STATE)) {
			if (gethrtime() - start > vnex_pending_timeout) {
				cmn_err(CE_WARN, "vnex_intr_dist: %s%d "
				    "ino 0x%x pending: timedout\n",
				    ddi_driver_name(vid_p->vid_dip),
				    ddi_get_instance(vid_p->vid_dip),
				    vid_p->vid_ino);
				break;
			}
		}
		(void) hvio_intr_settarget(vid_p->vid_ihdl, cpuid);
		(void) hvio_intr_setvalid(vid_p->vid_ihdl, HV_INTR_VALID);
	}
	mutex_exit(&vnex_id_lock);
}

static int
vnex_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
	switch (cmd) {
	case DDI_ATTACH:
		/*
		 * Intitialize interrupt descriptor list
		 * and mutex.
		 */
		vnex_id_list = NULL;
		mutex_init(&vnex_id_lock, NULL, MUTEX_DRIVER, NULL);
		/*
		 * Add interrupt redistribution callback.
		 */
		intr_dist_add(vnex_intr_dist, dip);
		return (DDI_SUCCESS);

	case DDI_RESUME:
		return (DDI_SUCCESS);

	default:
		return (DDI_FAILURE);
	}
}

/*ARGSUSED*/
static int
vnex_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
	switch (cmd) {
	case DDI_DETACH:
		return (DDI_FAILURE);

	case DDI_SUSPEND:
		return (DDI_SUCCESS);

	default:
		return (DDI_FAILURE);
	}
}

static int
vnex_ctl(dev_info_t *dip, dev_info_t *rdip,
	ddi_ctl_enum_t ctlop, void *arg, void *result)
{
	char	name[12];	/* enough for a decimal integer */
	int		reglen;
	uint32_t	*vnex_regspec;

	switch (ctlop) {
	case DDI_CTLOPS_REPORTDEV:
		if (rdip == NULL)
			return (DDI_FAILURE);
		cmn_err(CE_CONT, "?virtual-device: %s%d\n",
		    ddi_driver_name(rdip), ddi_get_instance(rdip));
		return (DDI_SUCCESS);

	case DDI_CTLOPS_INITCHILD:
	{
		dev_info_t *child = (dev_info_t *)arg;

		if (ddi_getlongprop(DDI_DEV_T_ANY, child, DDI_PROP_DONTPASS,
		    "reg", (caddr_t)&vnex_regspec, &reglen) != DDI_SUCCESS)
			return (DDI_FAILURE);

		(void) sprintf(name, "%x", *vnex_regspec);
		ddi_set_name_addr(child, name);
		ddi_set_parent_data(child, NULL);
		kmem_free((caddr_t)vnex_regspec, reglen);
		return (DDI_SUCCESS);

	}

	case DDI_CTLOPS_UNINITCHILD:
	{
		dev_info_t *child = (dev_info_t *)arg;

		ddi_set_name_addr(child, NULL);
		ddi_remove_minor_node(arg, NULL);
		return (DDI_SUCCESS);
	}

	/*
	 * These ops correspond to functions that "shouldn't" be called
	 * by a pseudo driver.  So we whinge when we're called.
	 */
	case DDI_CTLOPS_DMAPMAPC:
	case DDI_CTLOPS_REPORTINT:
	case DDI_CTLOPS_REGSIZE:
	{
		*((off_t *)result) = 0;
		return (DDI_SUCCESS);
	}
	case DDI_CTLOPS_NREGS:
	{
		dev_info_t *child = rdip;
		if (ddi_getlongprop(DDI_DEV_T_ANY, child, DDI_PROP_DONTPASS,
		    "reg", (caddr_t)&vnex_regspec, &reglen) != DDI_SUCCESS)
			return (DDI_FAILURE);
		*((uint_t *)result) = reglen / sizeof (uint32_t);
		kmem_free((caddr_t)vnex_regspec, reglen);
		return (DDI_SUCCESS);
	}
	case DDI_CTLOPS_SIDDEV:
	case DDI_CTLOPS_SLAVEONLY:
	case DDI_CTLOPS_AFFINITY:
	case DDI_CTLOPS_IOMIN:
	case DDI_CTLOPS_POKE:
	case DDI_CTLOPS_PEEK:
		cmn_err(CE_CONT, "%s%d: invalid op (%d) from %s%d\n",
		    ddi_get_name(dip), ddi_get_instance(dip),
		    ctlop, ddi_get_name(rdip), ddi_get_instance(rdip));
		return (DDI_FAILURE);

	/*
	 * Everything else (e.g. PTOB/BTOP/BTOPR requests) we pass up
	 */
	default:
		return (ddi_ctlops(dip, rdip, ctlop, arg, result));
	}
}

static int
vnex_get_pil(dev_info_t *rdip)
{
	int i;
	caddr_t	name;

	name = ddi_node_name(rdip);
	for (i = 0; i < VNEX_MAX_DEVS; i++) {
		if (strcmp(vnex_name_to_pil[i].name,
		    name) == 0) {
			return (vnex_name_to_pil[i].pil);
		}
	}
	/*
	 * if not found pil is 0
	 */
	return (0);
}

static int
vnex_enable_intr(dev_info_t *rdip, ddi_intr_handle_impl_t *hdlp)
{
	vnex_id_t *vid_p;
	uint32_t cpuid;

	vid_p = vnex_locate_id(rdip, hdlp->ih_vector);

	ASSERT(vid_p != NULL);

	cpuid = intr_dist_cpuid();

	if ((hvio_intr_settarget(vid_p->vid_ihdl, cpuid)) != H_EOK) {
		return (DDI_FAILURE);
	}

	if (hvio_intr_setstate(vid_p->vid_ihdl, HV_INTR_IDLE_STATE) != H_EOK) {
		return (DDI_FAILURE);
	}

	if ((hvio_intr_setvalid(vid_p->vid_ihdl, HV_INTR_VALID)) != H_EOK) {
		return (DDI_FAILURE);
	}

	return (DDI_SUCCESS);
}

static int
vnex_disable_intr(dev_info_t *rdip, ddi_intr_handle_impl_t *hdlp)
{
	vnex_id_t *vid_p;

	vid_p = vnex_locate_id(rdip, hdlp->ih_vector);

	ASSERT(vid_p != NULL);

	if (hvio_intr_setvalid(vid_p->vid_ihdl, HV_INTR_NOTVALID) != H_EOK) {
		return (DDI_FAILURE);
	}

	return (DDI_SUCCESS);
}

int
vnex_ino_to_inum(dev_info_t *dip, uint32_t ino)
{
	vnex_id_t		*vid_p;
	ddi_intr_handle_impl_t	*hdlp;

	if ((vid_p = vnex_locate_id(dip, ino)) == NULL)
		return (-1);
	else if ((hdlp = vid_p->vid_ddi_hdlp) == NULL)
		return (-1);
	else
		return (hdlp->ih_inum);
}

static int
vnex_add_intr(dev_info_t *dip, dev_info_t *rdip,
    ddi_intr_handle_impl_t *hdlp)
{
	int reglen, ret = DDI_SUCCESS;
	vnex_id_t	*vid_p;
	uint64_t cfg;
	uint32_t ino;
	uint64_t ihdl;
	vnex_regspec_t *reg_p;

	if (ddi_getlongprop(DDI_DEV_T_ANY, dip,
	    DDI_PROP_DONTPASS, "reg", (caddr_t)&reg_p,
	    &reglen) != DDI_SUCCESS) {
		return (DDI_FAILURE);
	}

	/*
	 * get the sun4v config handle for this device
	 */

	cfg = SUN4V_REG_SPEC2CFG_HDL(reg_p->physaddr);
	kmem_free(reg_p, reglen);
	ino = hdlp->ih_vector;

	/*
	 * call hv to get vihdl
	 */
	if (hvio_intr_devino_to_sysino(cfg, ino, &ihdl) != H_EOK)
		return (DDI_FAILURE);

	hdlp->ih_vector = ihdl;
	/*
	 * Allocate a interrupt descriptor (id) with the
	 * the interrupt handler and append it to
	 * the id list.
	 */

	vid_p = vnex_alloc_id(rdip, ino, cfg);
	vid_p->vid_ihdl = ihdl;
	vid_p->vid_handler =  hdlp->ih_cb_func;
	vid_p->vid_arg1 =  hdlp->ih_cb_arg1;
	vid_p->vid_arg2 =  hdlp->ih_cb_arg2;
	vid_p->vid_ddi_hdlp =  hdlp;

	DDI_INTR_ASSIGN_HDLR_N_ARGS(hdlp,
	    (ddi_intr_handler_t *)vnex_intr_wrapper, (caddr_t)vid_p, NULL);

	if (hdlp->ih_pri == 0) {
		hdlp->ih_pri = vnex_get_pil(rdip);
	}

	ret = i_ddi_add_ivintr(hdlp);
	if (ret != DDI_SUCCESS) {
		return (ret);
	}

	DDI_INTR_ASSIGN_HDLR_N_ARGS(hdlp, vid_p->vid_handler,
	    vid_p->vid_arg1, vid_p->vid_arg2);

	return (ret);
}

static int
vnex_remove_intr(dev_info_t *rdip,
	ddi_intr_handle_impl_t *hdlp)
{
	vnex_id_t *vid_p;
	uint32_t ino;
	int ret = DDI_SUCCESS;

	ino = hdlp->ih_vector;
	vid_p = vnex_locate_id(rdip, ino);

	hdlp->ih_vector = vid_p->vid_ihdl;
	i_ddi_rem_ivintr(hdlp);

	vnex_free_id(vid_p);

	return (ret);
}

static int
vnex_intr_ops(dev_info_t *dip, dev_info_t *rdip,
    ddi_intr_op_t intr_op, ddi_intr_handle_impl_t *hdlp, void *result)
{
	int	ret = DDI_SUCCESS;

	switch (intr_op) {
		case DDI_INTROP_GETCAP:
			*(int *)result = DDI_INTR_FLAG_LEVEL;
			break;
		case DDI_INTROP_ALLOC:
			*(int *)result = hdlp->ih_scratch1;
			break;
		case DDI_INTROP_GETPRI:
			*(int *)result = hdlp->ih_pri ?
			    hdlp->ih_pri : vnex_get_pil(rdip);
			break;
		case DDI_INTROP_FREE:
			break;
		case DDI_INTROP_SETPRI:
			break;
		case DDI_INTROP_ADDISR:
			ret = vnex_add_intr(dip, rdip, hdlp);
			break;
		case DDI_INTROP_REMISR:
			ret = vnex_remove_intr(rdip, hdlp);
			break;
		case DDI_INTROP_ENABLE:
			ret = vnex_enable_intr(rdip, hdlp);
			break;
		case DDI_INTROP_DISABLE:
			ret = vnex_disable_intr(rdip, hdlp);
			break;
		case DDI_INTROP_NINTRS:
		case DDI_INTROP_NAVAIL:
			*(int *)result = i_ddi_get_intx_nintrs(rdip);
			break;
		case DDI_INTROP_SUPPORTED_TYPES:
			*(int *)result = i_ddi_get_intx_nintrs(rdip) ?
			    DDI_INTR_TYPE_FIXED : 0;
			break;
		default:
			ret = DDI_ENOTSUP;
			break;
	}

	return (ret);
}

vnex_id_t *
vnex_alloc_id(dev_info_t *dip, uint32_t ino, uint64_t dhdl)
{
	vnex_id_t *vid_p = kmem_alloc(sizeof (vnex_id_t), KM_SLEEP);

	vid_p->vid_dip = dip;
	vid_p->vid_ino = ino;
	vid_p->vid_cfg_hdl = dhdl;

	mutex_enter(&vnex_id_lock);
	vnex_add_id(vid_p);
	mutex_exit(&vnex_id_lock);

	return (vid_p);
}

vnex_id_t *
vnex_locate_id(dev_info_t *dip, uint32_t ino)
{
	vnex_id_t *vid_p;

	mutex_enter(&vnex_id_lock);
	vid_p = vnex_id_list;

	while (vid_p != NULL) {
		if (vid_p->vid_dip == dip && vid_p->vid_ino == ino) {
			mutex_exit(&vnex_id_lock);
			return (vid_p);
		}
		vid_p = vid_p->vid_next;
	}
	mutex_exit(&vnex_id_lock);
	return (NULL);
}

static void
vnex_free_id(vnex_id_t *vid_p)
{
	mutex_enter(&vnex_id_lock);
	vnex_rem_id(vid_p);
	mutex_exit(&vnex_id_lock);

	kmem_free(vid_p, sizeof (*vid_p));
}

static void
vnex_rem_id(vnex_id_t *vid_p)
{
	vnex_id_t *prev_p = vnex_id_list;

	if (vnex_id_list == NULL)
		cmn_err(CE_PANIC, "vnex: interrupt list empty");

	if (vid_p == NULL)
		cmn_err(CE_PANIC, "vnex: no element to remove");

	if (vnex_id_list == vid_p) {
		vnex_id_list = vid_p->vid_next;
	} else {
		while (prev_p != NULL && prev_p->vid_next != vid_p)
			prev_p = prev_p->vid_next;

		if (prev_p == NULL)
			cmn_err(CE_PANIC, "vnex: element %p not in list",
			    (void *) vid_p);

		prev_p->vid_next = vid_p->vid_next;
	}
}

static void
vnex_add_id(vnex_id_t *vid_p)
{
	vid_p->vid_next = vnex_id_list;
	vnex_id_list = vid_p;
}

uint_t
vnex_intr_wrapper(caddr_t arg)
{
	vnex_id_t *vid_p = (vnex_id_t *)arg;
	int res;
	uint_t (*handler)();
	caddr_t handler_arg1;
	caddr_t handler_arg2;

	handler = vid_p->vid_handler;
	handler_arg1 = vid_p->vid_arg1;
	handler_arg2 = vid_p->vid_arg2;

	res = (*handler)(handler_arg1, handler_arg2);

	(void) hvio_intr_setstate(vid_p->vid_ihdl, HV_INTR_IDLE_STATE);

	return (res);
}