/*-
 * Copyright (c) 2008 Hans-Christian Esperer <hc@hcesperer.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * This is the FreeBSD version of the mgdrive kernel module.
 * Hopefully it will be less a mess than the linux version.
 * ;-)
 */


#include <sys/cdefs.h>
__FBSDID("$Id: g_mg.c 186 2008-05-09 10:35:18Z hc $");

#include <sys/param.h>
#include <sys/bio.h>
#include <sys/kernel.h>
#include <sys/limits.h>
#include <sys/malloc.h>
#include <sys/queue.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <sys/proc.h>
#include <sys/condvar.h>


#include <geom/geom.h>

#include "g_mg.h"

#define	G_MG_CLASS_NAME	"MG"
#define	G_MGCTL_CLASS_NAME	"MGCTL"

SYSCTL_DECL(_kern_geom);
SYSCTL_NODE(_kern_geom, OID_AUTO, mg, CTLFLAG_RW, 0, "GEOM_MG mgdrive FreeBSD implementation");

static int g_mg_debug = 0;
SYSCTL_INT(_kern_geom_mg, OID_AUTO, clear, CTLFLAG_RW, &g_mg_debug, 0,
    "If set, g_mg issues more verbose debugging messages");

static MALLOC_DEFINE(G_MG, "mgdrive data", "GEOM_MG Data");

static struct mtx g_mg_reqlock;

static struct cv g_mg_reqwqueue;
static int g_mg_reqwaiting = 0;


/*
 * Request queue
 */
STAILQ_HEAD(g_mg_bio_request_head, g_mg_bio_request) request_head =
		STAILQ_HEAD_INITIALIZER(request_head);
// static struct g_mg_bio_request_head *request_headp;
struct g_mg_bio_request {
	STAILQ_ENTRY(g_mg_bio_request) entries;
	struct bio *bp;
	char* data;
	long nbytes;
	enum g_mg_request request;
};
/** END REQUEST QUEUE **/

struct g_provider *mgdrive0_provider;

static void
g_mg_start(struct bio *bp)
{
	struct g_mg_bio_request *request;

	switch (bp->bio_cmd) {
	case BIO_READ:
		request = (struct g_mg_bio_request*) malloc(sizeof(struct
				g_mg_bio_request), G_MG, M_WAITOK);
		if (!request) {
			g_io_deliver(bp, ENOMEM);
			break;
		}
		request->bp = bp;
		request->request = G_MG_READ;
		request->data = bp->bio_data;
		request->nbytes = bp->bio_length;
		mtx_lock(&g_mg_reqlock);
		STAILQ_INSERT_TAIL(&request_head, request, entries);
        cv_signal(&g_mg_reqwqueue);
		mtx_unlock(&g_mg_reqlock);
		break;

	case BIO_WRITE:
		request = (struct g_mg_bio_request*) malloc(sizeof(struct
				g_mg_bio_request), G_MG, M_WAITOK);
		if (!request) {
			g_io_deliver(bp, ENOMEM);
			break;
		}
		request->bp = bp;
		request->request = G_MG_WRITE;
		request->data = bp->bio_data;
		request->nbytes = bp->bio_length;

		mtx_lock(&g_mg_reqlock);
		STAILQ_INSERT_TAIL(&request_head, request, entries);
        cv_signal(&g_mg_reqwqueue);
		mtx_unlock(&g_mg_reqlock);
		break;

	default:
		g_io_deliver(bp, EOPNOTSUPP);
		break;
	}
}

static void
g_mg_init(struct g_class *mp)
{
	struct g_geom *gp;
	struct g_provider *pp;

	STAILQ_INIT(&request_head);

	mtx_init(&g_mg_reqlock, "g_mg_reqlock", "reqlock", MTX_DEF);
    cv_init(&g_mg_reqwqueue, "g_mg_reqwqueue");

	g_topology_assert();
	gp = g_new_geomf(mp, "mgdrive0");
	gp->start = g_mg_start;
	gp->access = g_std_access;
	pp = g_new_providerf(gp, "%s", gp->name);
	mgdrive0_provider = pp;
	pp->mediasize = 0LLU;
	pp->sectorsize = 512;
	g_error_provider(pp, 0);
}


static void
g_mgctl_start(struct bio *bp)
{
	g_io_deliver(bp, EOPNOTSUPP);
}

static void
g_mgctl_init(struct g_class *mp)
{
	struct g_geom *gp;
	struct g_provider *pp;

	g_topology_assert();
	gp = g_new_geomf(mp, "mgctl0");
	gp->start = g_mgctl_start;
	gp->access = g_std_access;
	pp = g_new_providerf(gp, "%s", gp->name);
	pp->mediasize = 0LLU;
	pp->sectorsize = 512;
	g_error_provider(pp, 0);
}

static int
g_mgctl_destroy_geom(struct gctl_req *req __unused, struct g_class *mp __unused,
    struct g_geom *gp)
{
	struct g_provider *pp;

	g_topology_assert();
	if (gp == NULL) {
		return (0);
	}
	pp = LIST_FIRST(&gp->provider);
	if (pp == NULL) {
		return (0);
	}	
	if (pp->acr > 0 || pp->acw > 0 || pp->ace > 0) {
		return EBUSY;
	}
	g_wither_geom(gp, ENXIO);
	return EBUSY;
}

static int
g_mg_destroy_geom(struct gctl_req *req __unused, struct g_class *mp __unused,
    struct g_geom *gp)
{
	struct g_provider *pp;
	static int called = 0;

	g_topology_assert();
	if (gp == NULL) {
		return (0);
	}
	pp = LIST_FIRST(&gp->provider);
	if (pp == NULL) {
		return (0);
	}
	if (pp->acr > 0 || pp->acw > 0 || pp->ace > 0) {
		return (EBUSY);
	}
	g_wither_geom(gp, ENXIO);

    cv_destroy(&g_mg_reqwqueue);

	if (called > 10) {
		return (EBUSY);
	}
	called++;
	printf("geom_mg: unload: called: %d\n", called);
	return (0);
}

static int
g_mgctl_ioctl(struct g_provider *pp, u_long cmd, void *data, int fflag, struct thread *td)
{
	struct g_mg_bio_request *request;
	struct bio *bp;
	struct g_provider *pp2 = mgdrive0_provider;
	struct g_mg_bio bio_req;
	long len;

	switch(cmd & 65535) {
	case 23:
		mtx_lock(&g_mg_reqlock);
        if (STAILQ_EMPTY(&request_head)) {
            cv_wait_sig(&g_mg_reqwqueue, &g_mg_reqlock);
        }
		if (STAILQ_EMPTY(&request_head)) {
			mtx_unlock(&g_mg_reqlock);
			return (EBUSY);
		}
        if (!(cmd & IOC_OUT)) {
			mtx_unlock(&g_mg_reqlock);
            printf("geom_mg: invalid IOCTL req; should be IOC_OUT\n");
		    return (ENOIOCTL);
        }
        if (IOCPARM_LEN(cmd) != sizeof(struct g_mg_bio)) {
			mtx_unlock(&g_mg_reqlock);
            printf("geom_mg: invalid length; should be sizeof(bio_req)\n");
		    return (ENOIOCTL);
        }
		request = STAILQ_FIRST(&request_head);
		mtx_unlock(&g_mg_reqlock);

		bio_req.request = request->request;
		bio_req.size = request->bp->bio_length;
		bio_req.offset = request->bp->bio_offset;
		memcpy(data, &bio_req, sizeof(bio_req));
		return 0;

	case 24:
		mtx_lock(&g_mg_reqlock);
		if (STAILQ_EMPTY(&request_head)) {
			mtx_unlock(&g_mg_reqlock);
			return (EBUSY);
		}

		request = STAILQ_FIRST(&request_head);
		bp = request->bp;
		len = request->nbytes;
		len = (len > 4096) ? 4096 : len;
        if (!(cmd & ((request->request == G_MG_READ) ? IOC_IN : IOC_OUT))) {
            printf("geom_mg: invalid IOCTL req; should be %s\n",
                    (request->request == G_MG_READ) ? "IOC_IN" : "IOC_OUT");
			g_io_deliver(bp, EINVAL);
			mtx_unlock(&g_mg_reqlock);
		    return (ENOIOCTL);
        }
        if (IOCPARM_LEN(cmd) != len) {
            printf("geom_mg: invalid length; should be %ld\n", len);
			g_io_deliver(bp, EINVAL);
			mtx_unlock(&g_mg_reqlock);
		    return (ENOIOCTL);
        }
		if (request->request == G_MG_READ) {
			memcpy(request->data, data, len);
		} else {
			memcpy(data, request->data, len);
		}
		request->nbytes -= len;
		request->data += len;

		if (!request->nbytes) {
			bp->bio_completed = bp->bio_length;
			g_io_deliver(bp, 0);
			STAILQ_REMOVE_HEAD(&request_head, entries);
			free(request, G_MG);
		}
		mtx_unlock(&g_mg_reqlock);
		return 0;

	case 42:
        if (!(cmd & IOC_IN)) {
            printf("geom_mg: invalid IOCTL req; should be IOC_IN\n");
		    return (ENOIOCTL);
        }
        if (IOCPARM_LEN(cmd) != sizeof(off_t)) {
            printf("geom_mg: invalid length; is %lu; should be %u\n",
								IOCPARM_LEN(cmd), sizeof(off_t));
		    return (ENOIOCTL);
        }
        if ((*(off_t*)data) <= 0) {
            printf("geom_mg: illegal size %llu\n", *(off_t*)data);
            return ENOIOCTL;
        }
		memcpy(&pp2->mediasize, data, sizeof(off_t));
        if (g_mg_debug) {
    		printf("geom_mg: size set to %llu\n", pp2->mediasize);
        }
		return 0;
	case 43:
        if (g_mg_debug) {
    		printf("geom_mg: size reset to 0\n");
        }
		pp2->mediasize = 0LLU;
		return 0;


	default:
		printf("geom_mg: illegal ioctl command: %ld\n", cmd);
		return (ENOIOCTL);
	}
}

static struct g_class g_mg_class = {
	.name = G_MG_CLASS_NAME,
	.version = G_VERSION,
	.init = g_mg_init,
	.destroy_geom = g_mg_destroy_geom
};

static struct g_class g_mgctl_class = {
	.name = G_MGCTL_CLASS_NAME,
	.version = G_VERSION,
	.ioctl = g_mgctl_ioctl,
	.init = g_mgctl_init,
	.destroy_geom = g_mgctl_destroy_geom
};

DECLARE_GEOM_CLASS(g_mg_class, g_mg);
DECLARE_GEOM_CLASS(g_mgctl_class, g_mgctl);
