#include <sys/modctl.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/conf.h>
#include <sys/devops.h>
#include <sys/stat.h>
#include <sys/zfs_znode.h>
#include <sys/time.h>
#include <sys/sa.h>
#include <sys/zap.h>
#include <sys/time.h>
#include <sys/dsl_dataset.h>
#include <sys/zfs_vfsops.h>
#include <sys/dmu.h>
#include <sys/dmu_objset.h>
#include <sys/dsl_dir.h>

#include <getgen/getgen.h>

/* per-instance context */
typedef struct gg_state {
	kmutex_t	mutex;
	dev_info_t	*dip;
	boolean_t	busy;
} gg_state_t;

static void		*statep;
static kmutex_t		gg_mutex;
static int		gg_instances = 0;

int
gg_ioc_get_gen(intptr_t arg, int mode)
{
	gg_ioctl_get_generation_t gg;
	file_t *fp;
	uint64_t gen;
	uint64_t crtime[2];
	int ret = 0;
	zfsvfs_t *zfsvfs = NULL;
	objset_t *osp;
	sa_attr_type_t *sa_table;
	sa_handle_t *hdl;
	dmu_buf_t *db;
	sa_bulk_attr_t bulk[2];
	int count = 0;
	dmu_object_info_t doi;
	timestruc_t crtime_s;
	znode_t *zp;

	if (ddi_copyin((void *)arg, &gg, sizeof(gg), mode) != 0)
		return EFAULT;
	fp = getf(gg.fd);
	if (fp == NULL)
		return EBADF;
	if (fp->f_vnode->v_vfsp->vfs_fstype != zfsfstype) {
		ret = EINVAL;
		goto out;
	}

	zp = (znode_t *)fp->f_vnode->v_data;
	zfsvfs = zp->z_zfsvfs;
	/* modified version of ZFS_ENTER() macro - we need to clean up fp */
	zfsvfs = zp->z_zfsvfs;
	rrm_enter_read(&zfsvfs->z_teardown_lock, FTAG);
	if (zp->z_zfsvfs->z_unmounted) {
		ret = EIO;
		goto out;
	}
	/* modified version of ZFS_VERIFY_ZP() macro */
	if (zp->z_sa_hdl == NULL) {
		ret = EIO;
		goto out;
	}

	/* get dataset name */
	dsl_dataset_name(zfsvfs->z_os->os_dsl_dataset, gg.dataset);

	/* get guid */
	gg.guid = dsl_dataset_phys(zfsvfs->z_os->os_dsl_dataset)->ds_guid;

	/* get generation and crtime */
	osp = zfsvfs->z_os;
	ret = sa_setup(osp, gg.inode, zfs_attr_table, ZPL_END, &sa_table);
	if (ret)
		goto out;
	ret = sa_buf_hold(osp, gg.inode, FTAG, &db);
	if (ret)
		goto out;
	dmu_object_info_from_db(db, &doi);
	if ((doi.doi_bonus_type != DMU_OT_SA &&
	    doi.doi_bonus_type != DMU_OT_ZNODE) ||
	    doi.doi_bonus_type == DMU_OT_ZNODE &&
	    doi.doi_bonus_size < sizeof (znode_phys_t)) {
		sa_buf_rele(db, FTAG);
		ret = ENOTSUP;
		goto out;
	}
	ret = sa_handle_get(osp, gg.inode, NULL, SA_HDL_PRIVATE, &hdl);
	if (ret) {
		sa_buf_rele(db, FTAG);
		goto out;
	}
	SA_ADD_BULK_ATTR(bulk, count, sa_table[ZPL_GEN], NULL,
		&gen, sizeof(gen));
	SA_ADD_BULK_ATTR(bulk, count, sa_table[ZPL_CRTIME], NULL,
		&crtime, sizeof(crtime));
	ret = sa_bulk_lookup(hdl, bulk, count);
	sa_handle_destroy(hdl);
	sa_buf_rele(db, FTAG);
	if (ret)
		goto out;
	ZFS_TIME_DECODE(&crtime_s, crtime);
	gg.generation = gen;
	gg.crtime = crtime_s.tv_sec;

	ddi_copyout(&gg, (void *)arg, sizeof(gg), mode);
out:
	if (zfsvfs)
		ZFS_EXIT(zfsvfs);
	releasef(gg.fd);
	return ret;
}

/* ARGSUSED */
static int
gg_ioctl(dev_t dev, int cmd, intptr_t arg, int mode, cred_t *credp, int *rvalp)
{
	int instance;

	instance = getminor(dev);
	if (ddi_get_soft_state(statep, instance) == NULL)
		return (ENXIO);
	/*
	 * all structures passed between kernel and userspace
	 * are compatible between 64 and 32 bit.  Model
	 * conversion can be ignored.
	 */
	switch (cmd) {
	case GG_IOC_GET_GENERATION:
		return gg_ioc_get_gen(arg, mode);
	default:
		/* generic "ioctl unknown" error */
		return ENOTTY;
	}
	return (0);
}

/* gg_close() is called when the final fd/reference is removed. */
/* ARGSUSED */
static int
gg_close(dev_t dev, int flag, int otyp, cred_t *crepd)
{
	gg_state_t *sp;
	int instance;

	instance = getminor(dev);
	if ((sp = ddi_get_soft_state(statep, instance)) == NULL)
		return (ENXIO);
	if (otyp != OTYP_CHR)
		return (EINVAL);
	mutex_enter(&sp->mutex);
	if (sp->busy != B_TRUE) {
		mutex_exit(&sp->mutex);
		return (EINVAL);
	}
	sp->busy = B_FALSE;
	mutex_exit(&sp->mutex);
	return (0);
}

/* gg_open() is called every time the device is opened/mounted/duped. */
/* ARGSUSED */
static int
gg_open(dev_t *devp, int flag, int otyp, cred_t *credp)
{
	gg_state_t *sp;
	int instance;

	instance = getminor(*devp);
	if ((sp = ddi_get_soft_state(statep, instance)) == NULL)
		return (ENXIO);
	if (otyp != OTYP_CHR)
		return (EINVAL);
	if (drv_priv(credp) != 0)
		return (EPERM);
	mutex_enter(&sp->mutex);
	if ((flag & FEXCL) && (sp->busy == B_TRUE)) {
		mutex_exit(&sp->mutex);
		return (EBUSY);
	}
	sp->busy = B_TRUE;
	mutex_exit(&sp->mutex);
	return (0);
}

static struct cb_ops gg_cb_ops = {
	gg_open,		/* open */
	gg_close,		/* close */
	nodev,			/* strategy */
	nodev,			/* print */
	nodev,			/* dump */
	nodev,			/* read */
	nodev,			/* write */
	gg_ioctl,		/* ioctl */
	nodev,			/* devmap */
	nodev,			/* mmap */
	nodev,			/* segmap */
	nochpoll,		/* chpoll */
	ddi_prop_op,		/* prop_op */
	NULL,			/* streamtab */
	D_MP | D_64BIT,		/* cb_flag */
	CB_REV,			/* cb_rev */
	nodev,			/* aread */
	nodev,			/* awrite */
};

static int
gg_detach(dev_info_t *dip, ddi_detach_cmd_t cmd)
{
	int instance;
	gg_state_t *sp;
	/* called once per instance with DDI_DETACH,
	   may be called to suspend */
	switch (cmd) {
	case DDI_DETACH:
		/* instance busy? */
		instance = ddi_get_instance(dip);
		if ((sp = ddi_get_soft_state(statep, instance)) == NULL)
			return (DDI_FAILURE);
		mutex_enter(&sp->mutex);
		if (sp->busy == B_TRUE) {
			mutex_exit(&sp->mutex);
			return (DDI_FAILURE);
		}
		mutex_exit(&sp->mutex);
		/* free resources allocated for this instance */
		mutex_destroy(&sp->mutex);
		ddi_remove_minor_node(dip, NULL);
		ddi_soft_state_free(statep, instance);
		mutex_enter(&gg_mutex);
		gg_instances--;
		mutex_exit(&gg_mutex);
		return (DDI_SUCCESS);
	case DDI_SUSPEND:
		return (DDI_FAILURE);
	default:
		return (DDI_FAILURE);
	}
}

static int
gg_attach(dev_info_t *dip, ddi_attach_cmd_t cmd)
{
	/* called once per instance with DDI_ATTACH,
	   may be called to resume */
	int instance;
	gg_state_t *sp;
	switch (cmd) {
	case DDI_ATTACH:
		instance = ddi_get_instance(dip);
		if (ddi_soft_state_zalloc(statep, instance) != DDI_SUCCESS) {
			return (DDI_FAILURE);
		}
		sp = ddi_get_soft_state(statep, instance);
		ddi_set_driver_private(dip, sp);
		sp->dip = dip;
		sp->busy = B_FALSE;
		if (ddi_create_minor_node(dip, ddi_get_name(dip),
		    S_IFCHR, instance, DDI_PSEUDO, 0) == DDI_FAILURE) {
			ddi_soft_state_free(statep, instance);
			return (DDI_FAILURE);
		}
		mutex_init(&sp->mutex, NULL, MUTEX_DRIVER, NULL);
		ddi_report_dev(dip);
		mutex_enter(&gg_mutex);
		gg_instances++;
		mutex_exit(&gg_mutex);
		return (DDI_SUCCESS);
	case DDI_RESUME:
		return (DDI_SUCCESS);
	default:
		return (DDI_FAILURE);
	}
}

/* ARGSUSED */
static int
gg_getinfo(dev_info_t *dip, ddi_info_cmd_t infocmd, void *arg, void **resultp)
{
	int instance;
	gg_state_t *sp;
	switch (infocmd) {
	case DDI_INFO_DEVT2DEVINFO:
		/* arg is dev_t */
		instance = getminor((dev_t)arg);
		if ((sp = ddi_get_soft_state(statep, instance)) != NULL) {
			mutex_enter(&sp->mutex);
			*resultp = sp->dip;
			mutex_exit(&sp->mutex);
			return (DDI_SUCCESS);
		}
		*resultp = NULL;
		return (DDI_FAILURE);
	case DDI_INFO_DEVT2INSTANCE:
		/* arg is dev_t */
		instance = getminor((dev_t)arg);
		*resultp = (void *)(uintptr_t)instance;
		return (DDI_FAILURE);
	}
	return (DDI_FAILURE);
}

static struct dev_ops gg_dev_ops = {
	DEVO_REV,			/* driver build revision */
	0,				/* driver reference count */
	gg_getinfo,			/* getinfo */
	nulldev,			/* identify (obsolete) */
	nulldev,			/* probe (search for devices) */
	gg_attach,			/* attach */
	gg_detach,			/* detach */
	nodev,				/* reset (obsolete, use quiesce) */
	&gg_cb_ops,			/* character and block device ops */
	NULL,				/* bus driver ops */
	NULL,				/* power management, not needed */
	ddi_quiesce_not_needed,		/* quiesce */
};

static struct modldrv gg_modldrv = {
	&mod_driverops,			/* all loadable modules use this */
	"getgen - znode info, v1.0",	/* driver name and version info */
	&gg_dev_ops			/* ops method pointers */
};

static struct modlinkage gg_modlinkage = {
	MODREV_1,	/* fixed value */
	{
		&gg_modldrv,	/* driver linkage structure */
		NULL		/* list terminator */
	}
};

int
_init(void)
{
	int error;

	if ((error = ddi_soft_state_init(&statep, sizeof(gg_state_t), 1)) != 0)
		return (error);
	gg_instances = 0;

	mutex_init(&gg_mutex, NULL, MUTEX_DRIVER, NULL);

	if ((error = mod_install(&gg_modlinkage)) != 0) {
		cmn_err(CE_WARN, "getgen: could not install module");
		mutex_destroy(&gg_mutex);
		ddi_soft_state_fini(&statep);
		return (error);
	}
	return (0);
}

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

int
_fini(void)
{
	int error = 0;

	mutex_enter(&gg_mutex);
	if (gg_instances > 0) {
		mutex_exit(&gg_mutex);
		return (SET_ERROR(EBUSY));
	}
	mutex_exit(&gg_mutex);

	if ((error = mod_remove(&gg_modlinkage)) != 0) {
		cmn_err(CE_WARN, "mod_remove failed: %d", error);
		return (error);
	}

	/* free resources */
	ddi_soft_state_fini(&statep);
	mutex_destroy(&gg_mutex);

	return (0);
}