/*
 * 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 (c) 2010, Oracle and/or its affiliates. All rights reserved.
 */

#include <sys/conf.h>
#include <sys/stat.h>
#include <sys/modctl.h>
#include <sys/taskq.h>
#include <sys/mdi_impldefs.h>
#include <sys/sunmdi.h>
#include <sys/sunpm.h>
#include <sys/ib/mgt/ibdm/ibdm_impl.h>
#include <sys/ib/ibnex/ibnex.h>
#include <sys/ib/ibnex/ibnex_devctl.h>
#include <sys/ib/ibtl/ibti.h>
#include <sys/ib/ibtl/impl/ibtl_ibnex.h>
#include <sys/file.h>
#include <sys/hwconf.h>
#include <sys/fs/dv_node.h>

void ibnex_handle_hca_attach(void *);
static int ibnex_hca_bus_config_one(dev_info_t *, void *,
		ddi_bus_config_op_t, uint_t *, dev_info_t **);

static ibnex_node_data_t *ibnex_get_cdip_info(dev_info_t *, char *,
		dev_info_t **, ibnex_node_type_t *);
static int ibnex_prom_devname_to_pkey_n_portnum(
		char *, ib_pkey_t *, uint8_t *);
static dev_info_t *ibnex_config_obp_args(dev_info_t *, char *);

extern int	ibnex_busctl(dev_info_t *,
		    dev_info_t *, ddi_ctl_enum_t, void *, void *);
extern int	ibnex_map_fault(dev_info_t *,
		    dev_info_t *, struct hat *, struct seg *,
			caddr_t, struct devpage *, pfn_t, uint_t, uint_t);
static int	ibnex_hca_bus_config(dev_info_t *, uint_t,
		    ddi_bus_config_op_t, void *, dev_info_t **);
static int	ibnex_hca_bus_unconfig(dev_info_t *,
		    uint_t, ddi_bus_config_op_t, void *);
extern dev_info_t	*ibnex_config_port_node(dev_info_t *, char *);
extern dev_info_t	*ibnex_config_obp_args(dev_info_t *, char *);
extern int		ibnex_ioc_bus_config_one(dev_info_t **, uint_t,
			    ddi_bus_config_op_t, void *, dev_info_t **, int *);
extern int		ibnex_pseudo_config_one(
		    ibnex_node_data_t *, char *, dev_info_t *);
extern void		ibnex_config_all_children(dev_info_t *);
extern void			ibnex_pseudo_initnodes(void);

extern int		ibnex_pseudo_mdi_config_one(int, void *, dev_info_t **,
			    char *, char *);
extern int			ibnex_get_dip_from_guid(ib_guid_t, int,
			    ib_pkey_t, dev_info_t **);
extern dev_info_t	*ibnex_commsvc_initnode(dev_info_t *,
			    ibdm_port_attr_t *, int, int, ib_pkey_t, int *,
			    int);
extern uint64_t		ibnex_str2hex(char *, int, int *);
extern int		ibnex_str2int(char *, int, int *);
extern void		ibnex_create_hcasvc_nodes(
			    dev_info_t *, ibdm_port_attr_t *);
extern void		ibnex_create_port_nodes(
			    dev_info_t *, ibdm_port_attr_t *);
extern void		ibnex_create_vppa_nodes(
			    dev_info_t *, ibdm_port_attr_t *);
extern int		ibnex_get_pkey_commsvc_index_portnum(
			    char *, int *, ib_pkey_t *, uint8_t *);

extern ibnex_t	ibnex;
extern int	ibnex_port_settling_time;

/*
 * The bus_ops structure defines the capabilities of HCA nexus driver.
 */
struct bus_ops ibnex_ci_busops = {
	BUSO_REV,
	nullbusmap,		/* bus_map */
	NULL,			/* bus_get_intrspec */
	NULL,			/* bus_add_intrspec */
	NULL,			/* bus_remove_intrspec */
	ibnex_map_fault,	/* Map Fault */
	ddi_no_dma_map,		/* DMA related entry points */
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	NULL,
	ibnex_busctl,		/* bus_ctl */
	ddi_bus_prop_op,	/* bus_prop_op */
	NULL,			/* bus_get_eventcookie	*/
	NULL,			/* bus_add_eventcall	*/
	NULL,			/* bus_remove_eventcall	*/
	NULL,			/* bus_post_event	*/
	NULL,
	ibnex_hca_bus_config,	/* bus config */
	ibnex_hca_bus_unconfig	/* bus unconfig */
};

/*
 * ibnex_hca_bus_config()
 *
 * BUS_CONFIG_ONE:
 *	Enumerate the exact instance of the driver. Use the device node name
 *	to locate the exact instance.
 *	Query IBDM to find whether the hardware exits for the instance of the
 *	driver. If exists, create a device node and return NDI_SUCCESS.
 *
 * BUS_CONFIG_ALL:
 *	Enumerate all the instances of all the possible children (seen before
 *	and never seen before).
 *
 * BUS_CONFIG_DRIVER:
 *	Enumerate all the instances of a particular driver.
 */
static int
ibnex_hca_bus_config(dev_info_t *parent, uint_t flag,
    ddi_bus_config_op_t op, void *devname, dev_info_t **child)
{
	int			ret = IBNEX_SUCCESS, circ;
	char			*srvname, nameaddr[MAXNAMELEN];
	dev_info_t		*cdip;
	ibnex_node_data_t	*node_data;
	ibnex_port_node_t	*port_node;

	/*
	 * In a normal case HCA is setup as a phci.
	 * If an HCA is in maintenance mode, its phci is not set up
	 * but the driver is attached to update the firmware. In this
	 * case, do not configure the MPxIO clients.
	 */
	if (mdi_component_is_phci(parent, NULL) == MDI_FAILURE) {
		if (op == BUS_CONFIG_ALL || op == BUS_CONFIG_DRIVER)
			return (NDI_SUCCESS);
		else
			return (NDI_FAILURE);
	}

	switch (op) {
	case BUS_CONFIG_ONE:
		IBTF_DPRINTF_L4("ibnex", "\thca_bus_config: CONFIG_ONE, "
		    "parent %p", parent);
		ret = ibnex_hca_bus_config_one(
		    parent, devname, op, &flag, child);
		break;

	case BUS_CONFIG_OBP_ARGS:
		mdi_devi_enter(parent, &circ);
		cdip = ibnex_config_obp_args(parent, devname);
		if (cdip) {
			/*
			 * Boot case.
			 * Special handling because the "devname"
			 * format for the enumerated device is
			 * different.
			 */
			node_data = ddi_get_parent_data(cdip);
			port_node = &node_data->node_data.port_node;
			if (node_data->node_type ==
			    IBNEX_VPPA_COMMSVC_NODE) {
				srvname =
				    ibnex.ibnex_vppa_comm_svc_names[
				    port_node->port_commsvc_idx];
				(void) snprintf(nameaddr, MAXNAMELEN,
				    "ibport@%x,%x,%s",
				    port_node->port_num,
				    port_node->port_pkey, srvname);
			}
			devname = (void *)nameaddr;
		} else {
			IBTF_DPRINTF_L2("ibnex", "\thca_bus_config: "
			    "CONFIG_OBP_ARGS : invalid state!!");

			ret = IBNEX_FAILURE;
		}
		mdi_devi_exit(parent, circ);
		break;

	case BUS_CONFIG_ALL:
		IBTF_DPRINTF_L4("ibnex",
		    "\thca_bus_config: CONFIG_ALL parent %p", parent);
		ibnex_config_all_children(parent);
		break;

	case BUS_CONFIG_DRIVER:
		IBTF_DPRINTF_L4("ibnex", "\thca_bus_config: "
		    "CONFIG_DRIVER parent %p", parent);
		ibnex_config_all_children(parent);
		break;

	default:
		IBTF_DPRINTF_L4("ibnex", "\thca_bus_config: error");
		ret = IBNEX_FAILURE;
		break;
	}


	if (ret == IBNEX_SUCCESS) {
		if (op == BUS_CONFIG_OBP_ARGS)
			op = BUS_CONFIG_ONE;

		ret = ndi_busop_bus_config(
		    parent, flag, op, devname, child, 0);
		IBTF_DPRINTF_L4("ibnex", "\thca_bus_config:"
		    "ndi_busop_bus_config : retval %d", ret);
		return (ret);
	}

	return (NDI_FAILURE);
}

/*
 * ibnex_hca_bus_unconfig()
 *
 *	Unconfigure a particular device node or all instance of a device
 *	driver device or all children of IBnex
 */
static int
ibnex_hca_bus_unconfig(dev_info_t *parent,
    uint_t flag, ddi_bus_config_op_t op, void *device_name)
{

	if (ndi_busop_bus_unconfig(parent, flag, op, device_name) !=
	    DDI_SUCCESS)
		return (DDI_FAILURE);

	if ((op == BUS_UNCONFIG_ALL || op == BUS_UNCONFIG_DRIVER) &&
	    (flag & NDI_UNCONFIG)) {
		ibnex_node_data_t	*ndp;
		dev_info_t		*dip = NULL;
		major_t			major = (major_t)(uintptr_t)device_name;

		mutex_enter(&ibnex.ibnex_mutex);

		if (major == -1) {
			/*
			 * HCA dip. When major number is -1 HCA is
			 * going away cleanup all the port nodes.
			 */
			for (ndp = ibnex.ibnex_port_node_head;
			    ndp; ndp = ndp->node_next) {
				ibnex_port_node_t	*port_node;

				port_node = &ndp->node_data.port_node;
				if (port_node->port_pdip == parent) {
					port_node->port_pdip = NULL;
					ndp->node_dip = NULL;
					ndp->node_state =
					    IBNEX_CFGADM_UNCONFIGURED;
				}
			}
		} else {
			/*
			 * HCA dip. Cleanup only the port nodes that
			 * match the major number.
			 */
			for (ndp = ibnex.ibnex_port_node_head;
			    ndp; ndp = ndp->node_next) {
				ibnex_port_node_t	*port_node;

				port_node = &ndp->node_data.port_node;
				dip = ndp->node_dip;
				if (dip && (ddi_driver_major(dip) ==
				    major) && port_node->port_pdip ==
				    parent) {
					port_node->port_pdip = NULL;
					ndp->node_dip = NULL;
					ndp->node_state =
					    IBNEX_CFGADM_UNCONFIGURED;
				}
			}
		}
		mutex_exit(&ibnex.ibnex_mutex);
	}
	return (DDI_SUCCESS);
}

/*
 * ibnex_config_obp_args()
 *	Configures a particular port node for a IP over IB communication
 *	service.
 *	The format of the input string "devname" is
 *		port=x,pkey=y,protocol=ip,<wanboot options>
 *	Thr format of the node name created here is
 *		ibport@<Port#>,<pkey>,<service name>
 *	where pkey = 0 for port communication service nodes
 *	Returns "dev_info_t" of the "child" node just created
 *	NULL when failed to enumerate the child node
 *
 */
static dev_info_t *
ibnex_config_obp_args(dev_info_t *parent, char *devname)
{
	int			ii, index;
	int			rval, iter = 0;
	char			*temp;
	uint8_t			port_num;
	ib_guid_t		hca_guid, port_guid;
	ib_pkey_t		pkey;
	dev_info_t		*cdip;
	boolean_t		displayed = B_FALSE;
	ibdm_port_attr_t	*port_attr;

	IBTF_DPRINTF_L4("ibnex", "\tconfig_obp_args: %s", devname);

	/* Is this OBP node for IPoIB ? */
	temp = devname;
	do {
		temp = strstr(temp, ",protocol=ip");
		if (temp == NULL)
			break;

		if (strlen(devname) > (int)((temp - devname) + 12)) {
			if (temp[12] == ',')
				break;
		} else {
			break;
		}
		temp++;
	} while (temp);

	if (temp == NULL)
		return (NULL);
	if (ibnex_prom_devname_to_pkey_n_portnum(
	    devname, &pkey, &port_num) != IBNEX_SUCCESS) {
		return (NULL);
	}
	for (index = 0; index < ibnex.ibnex_nvppa_comm_svcs; index++) {
		if (strcmp(ibnex.ibnex_vppa_comm_svc_names[index],
		    "ipib") == 0) {
			break;
		}
	}

	hca_guid = ibtl_ibnex_hcadip2guid(parent);
	if ((port_attr = ibdm_ibnex_probe_hcaport(
	    hca_guid, port_num)) == NULL) {
		IBTF_DPRINTF_L2("ibnex",
		    "\tconfig_port_node: Port does not exist");
		return (NULL);
	}

	/* Wait until "port is up" */
	while (port_attr->pa_state != IBT_PORT_ACTIVE) {
		ibdm_ibnex_free_port_attr(port_attr);
		delay(drv_usectohz(10000));
		if ((port_attr = ibdm_ibnex_probe_hcaport(
		    hca_guid, port_num)) == NULL) {
			return (NULL);
		}
		if (iter++ == 400) {
			if (displayed == B_FALSE) {
				cmn_err(CE_NOTE, "\tWaiting for Port %d "
				    "initialization", port_attr->pa_port_num);
				displayed = B_TRUE;
			}
		}
	}
	IBTF_DPRINTF_L4("ibnex", "\tPort is initialized");

	mutex_enter(&ibnex.ibnex_mutex);
	port_guid = port_attr->pa_port_guid;
	rval = ibnex_get_dip_from_guid(port_guid, index, pkey, &cdip);
	if (rval == IBNEX_SUCCESS && cdip != NULL) {
		IBTF_DPRINTF_L4("ibnex", "\tconfig_port_node: Node exists");
		mutex_exit(&ibnex.ibnex_mutex);
		ibdm_ibnex_free_port_attr(port_attr);
		return (cdip);
	}
	for (ii = 0; ii < port_attr->pa_npkeys; ii++) {
		if (pkey == port_attr->pa_pkey_tbl[ii].pt_pkey) {
			cdip = ibnex_commsvc_initnode(parent, port_attr,
			    index, IBNEX_VPPA_COMMSVC_NODE, pkey, &rval,
			    IBNEX_CFGADM_ENUMERATE);
			IBTF_DPRINTF_L5("ibnex",
			    "\t ibnex_commsvc_initnode rval %x", rval);
			break;
		}
	}
	mutex_exit(&ibnex.ibnex_mutex);

	ibdm_ibnex_free_port_attr(port_attr);
	return (cdip);
}


/*
 * ibnex_prom_devname_to_pkey_n_portnum()
 *	Parses the device node name and extracts "PKEY" and "port#"
 *	Returns IBNEX_SUCCESS/IBNEX_FAILURE
 */
static int
ibnex_prom_devname_to_pkey_n_portnum(
    char *devname, ib_pkey_t *pkey, uint8_t *port)
{
	int	ret = IBNEX_SUCCESS;
	char	*tmp, *tmp1;

	if ((tmp = strstr(devname, "port=")) != NULL) {
		if ((tmp = strchr(++tmp, '=')) != NULL)
			if ((tmp1 = strchr(++tmp, ',')) != NULL)
				*port = ibnex_str2int(tmp, (tmp1 - tmp), &ret);
	} else
		ret = IBNEX_FAILURE;

	if ((ret == IBNEX_SUCCESS) &&
	    (tmp = strstr(devname, "pkey=")) != NULL) {
		if ((tmp = strchr(++tmp, '=')) != NULL)
			if ((tmp1 = strchr(++tmp, ',')) != NULL)
				*pkey = ibnex_str2hex(tmp, (tmp1 - tmp), &ret);
	} else
		ret = IBNEX_FAILURE;

	return (ret);
}

static ibnex_node_data_t *
ibnex_get_cdip_info(dev_info_t *parent,
    char *devname, dev_info_t **cdip, ibnex_node_type_t *type)
{
	char 			*device_name, *cname = NULL, *caddr = NULL;
	int			len;
	ibnex_node_data_t	*node_data = NULL;

	len = strlen((char *)devname) + 1;
	device_name = i_ddi_strdup(devname, KM_SLEEP);
	i_ddi_parse_name(device_name, &cname, &caddr, NULL);

	IBTF_DPRINTF_L4("ibnex",
	    "\tfind_child_dip: cname %s addr %s", cname, caddr);

	if (strncmp(cname, IBNEX_IOC_CNAME, 3) ==  0)
		*type = IBNEX_IOC_NODE;
	else if (strncmp(cname, IBNEX_IBPORT_CNAME, 3) ==  0)
		*type = IBNEX_HCA_CHILD_NODE;
	else
		*type = IBNEX_PSEUDO_NODE;

	*cdip = ndi_devi_findchild(parent, devname);

	IBTF_DPRINTF_L4("ibnex",
	    "\tfind_child_dip: cdip %p type %x", *cdip, *type);

	if (*cdip)
		node_data = ddi_get_parent_data(*cdip);
	kmem_free(device_name, len);

	return (node_data);
}

static int
ibnex_hca_bus_config_one(dev_info_t *parent, void *devname,
ddi_bus_config_op_t op, uint_t *flag, dev_info_t **child)
{
	int			ret = IBNEX_SUCCESS, len, circ, need_bus_config;
	char 			*device_name, *caddr, *cname;
	dev_info_t		*cdip;
	ibnex_node_data_t	*node_data;
	ibnex_node_type_t	node_type;
	int			index;
	uint8_t			port_num;
	ib_pkey_t		pkey;

	len = strlen((char *)devname) + 1;
	device_name = i_ddi_strdup(devname, KM_SLEEP);
	i_ddi_parse_name(device_name, &cname, &caddr, NULL);

	if (caddr == NULL || (strlen(caddr) == 0)) {
		IBTF_DPRINTF_L2("ibnex",
		    "\thca_bus_config: Invalid device node address");
		kmem_free(device_name, len);
		return (IBNEX_FAILURE);
	}

	ndi_devi_enter(parent, &circ);
	node_data = ibnex_get_cdip_info(
	    parent, devname, &cdip, &node_type);
	ndi_devi_exit(parent, circ);

	if (cdip) {
		if ((node_data) && (node_data->node_type ==
		    IBNEX_PORT_COMMSVC_NODE)) {
			if (node_data->node_dip == NULL) {
				node_data->node_dip = cdip;
				node_data->node_data.port_node.port_pdip =
				    parent;
			}
		}
	}

	/*
	 * If child dip is present, just return
	 * from here.
	 */
	if (cdip != NULL || (node_data != NULL &&
	    node_data->node_dip != NULL)) {
		goto end;
	}

	switch (node_type) {

	case IBNEX_IOC_NODE:
		ret = ibnex_ioc_bus_config_one(&parent, *flag,
		    op, devname, child, &need_bus_config);
		if (!need_bus_config) {
			kmem_free(device_name, len);
			return (ret);
		}
		break;

	case IBNEX_PSEUDO_NODE:
		ret = IBNEX_SUCCESS;
		mdi_devi_enter(parent, &circ);
		ibnex_pseudo_initnodes();
		mutex_enter(&ibnex.ibnex_mutex);
		ret = ibnex_pseudo_config_one(NULL,
		    caddr, parent);
		mutex_exit(&ibnex.ibnex_mutex);
		mdi_devi_exit(parent, circ);
		break;

	default:
		if (ibnex_get_pkey_commsvc_index_portnum(devname,
		    &index, &pkey, &port_num) != IBNEX_SUCCESS) {
			IBTF_DPRINTF_L2("ibnex",
			    "\tconfig_port_node: Invalid Service Name");
			kmem_free(device_name, len);
			return (IBNEX_FAILURE);
		}

		if ((pkey != 0) && (port_num != 0)) {
			if (strcmp("ipib",
			    ibnex.ibnex_vppa_comm_svc_names[index]) == 0) {
				IBTF_DPRINTF_L2("ibnex",
				    "Skipping IBD devices... ");
				break;
			}
		}

		ndi_devi_enter(parent, &circ);
		cdip = ibnex_config_port_node(parent, devname);
		if (cdip)
			ret = IBNEX_SUCCESS;
		else
			ret = IBNEX_FAILURE;
		ndi_devi_exit(parent, circ);
		break;
	}
end:
	if (node_type == IBNEX_HCA_CHILD_NODE) {
		/* Allows enumeration under PHCI */
		*flag |= NDI_MDI_FALLBACK;
	}
	kmem_free(device_name, len);
	return (ret);
}

void
ibnex_handle_hca_attach(void *cb_arg)
{
	ib_guid_t hca_guid = *((ib_guid_t *)cb_arg);
	dev_info_t	*phci;
	int		ii, circ;
	ibdm_hca_list_t		*hca_list;

	IBTF_DPRINTF_L4("ibnex", "handle_hca_attach(%llx)", hca_guid);

	phci = ibtl_ibnex_hcaguid2dip(hca_guid);

	/*
	 * Enumerate children of this HCA, port nodes,
	 * VPPA & HCA_SVC nodes. Use ndi_devi_enter() for
	 * locking. IB Nexus is enumerating the children
	 * of HCA, not MPXIO clients.
	 */
	ndi_devi_enter(phci, &circ);
	ibdm_ibnex_port_settle_wait(hca_guid, ibnex_port_settling_time);
	hca_list = ibdm_ibnex_get_hca_info_by_guid(hca_guid);
	if (hca_list == NULL) {
		ndi_devi_exit(phci, circ);
		kmem_free(cb_arg, sizeof (ib_guid_t));
		return;
	}
	ibnex_create_hcasvc_nodes(phci, hca_list->hl_hca_port_attr);
	for (ii = 0; ii < hca_list->hl_nports; ii++) {
		ibnex_create_vppa_nodes(
		    phci, &hca_list->hl_port_attr[ii]);
	}
	ibdm_ibnex_free_hca_list(hca_list);
	ndi_devi_exit(phci, circ);
	kmem_free(cb_arg, sizeof (ib_guid_t));
}