/* $NetBSD: $ */

/*-
  * Copyright (c) 2010 UPMC/LIP6
  * All rights reserved.
  * This software is distributed under the following condiions
  * compliant with the NetBSD foundation policy.
  *
  * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
  */

/* driver for soclib's block device */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: machdep.c,v 1.223 2008/07/02 17:28:56 ad Exp $");

#include <sys/param.h>
#include <sys/buf.h>
#include <sys/bufq.h>
#include <sys/disk.h>
#include <sys/disklabel.h>
#include <sys/ioctl.h>
#include <sys/proc.h>
#include <sys/systm.h>
#include <sys/device.h>
#include <sys/conf.h>
#include <sys/kauth.h>

#include <dev/dkvar.h>

#include <uvm/uvm.h>

#include <machine/autoconf.h>
#include <machine/bus.h>

#undef BD_DEBUG
#ifdef BD_DEBUG
#define DPRINTF(x) printf x;
#else
#define DPRINTF(x)
#endif

/* block device registers */
#define BLOCK_DEVICE_BUFFER	0x0
#define BLOCK_DEVICE_LBA	0x1
#define BLOCK_DEVICE_COUNT	0x2
#define BLOCK_DEVICE_OP		0x3
#define BLOCK_DEVICE_STATUS	0x4
#define BLOCK_DEVICE_IRQ_ENABLE	0x5
#define BLOCK_DEVICE_SIZE	0x6
#define BLOCK_DEVICE_BLOCK_SIZE	0x7
#define BLOCK_DEVICE_BUFFER_EXT	0x8

#define BLOCK_DEVICE_REG_READ(sc, reg) \
	bus_space_read_4((sc)->sc_tag, (sc)->sc_handle, (reg) * 4)
#define BLOCK_DEVICE_REG_WRITE(sc, reg, val) \
	bus_space_write_4((sc)->sc_tag, (sc)->sc_handle, (reg) * 4, (val))

/* operations for BLOCK_DEVICE_OP */
#define BLOCK_DEVICE_OP_NOOP	0x0
#define BLOCK_DEVICE_OP_READ	0x1
#define BLOCK_DEVICE_OP_WRITE	0x2

/* BLOCK_DEVICE_STATUS */
#define BLOCK_DEVICE_IDLE		0x0
#define BLOCK_DEVICE_BUSY		0x1
#define BLOCK_DEVICE_READ_SUCCESS	0x2
#define BLOCK_DEVICE_WRITE_SUCCESS	0x3
#define BLOCK_DEVICE_READ_ERROR		0x4
#define BLOCK_DEVICE_WRITE_ERROR	0x5
#define BLOCK_DEVICE_ERROR		0x6

static int vcibd_match(device_t, cfdata_t, void *);
static void vcibd_attach(device_t, device_t, void *);
static void vcibd_attach_intr(device_t);
static int vcibd_intr(void *);
static int vcibdstart(struct dk_softc *, struct buf *);

struct vcibd_softc {
	device_t sc_dev;
	struct dk_softc sc_dksc;
	struct dk_intf *sc_di;
	bus_space_tag_t sc_tag;
	bus_space_handle_t sc_handle;
	irq_t sc_ih;
	bus_dma_tag_t sc_dmat;
	bus_dmamap_t  sc_dmamap;
	bus_dma_segment_t sc_bouncesegs;
	void *sc_bounce;
	uint64_t sc_sectors;
	u_long sc_secsize;
	uint64_t sc_bdsize;
	struct buf *sc_bp; /* xfer being handled */
	u_long sc_bcount; /* real byte count of xfer */
	int sc_flags;
#define BD_FLAGS_BOUNCE 0x01
};

CFATTACH_DECL_NEW(vcibd, sizeof(struct vcibd_softc),
    vcibd_match, vcibd_attach, NULL, NULL);

dev_type_open(vcibdopen);
dev_type_close(vcibdclose);
dev_type_read(vcibdread);
dev_type_write(vcibdwrite);
dev_type_ioctl(vcibdioctl);
dev_type_strategy(vcibdstrategy);
dev_type_dump(vcibddump);
dev_type_size(vcibdsize);

const struct bdevsw vcibd_bdevsw = {
	vcibdopen, vcibdclose, vcibdstrategy, vcibdioctl,
	vcibddump, vcibdsize, D_DISK
};

const struct cdevsw vcibd_cdevsw = {
	vcibdopen, vcibdclose, vcibdread, vcibdwrite, vcibdioctl,
	nostop, notty, nopoll, nommap, nokqfilter, D_DISK
};

extern struct cfdriver vcibd_cd;

/* Pseudo-disk Interface */
static struct dk_intf dkintf_esdi = {
	DTYPE_ESDI,
	"Soclib Virtual disk",
	vcibdopen,
	vcibdclose,
	vcibdstrategy,
	vcibdstart,
};

static struct dkdriver vcibddkdriver = {
	.d_strategy = vcibdstrategy,
	.d_minphys = minphys,
};

int
vcibd_match(device_t parent, cfdata_t match, void *aux)
{
	struct tsardevs_attach_args *tsd = aux;
	if (strcmp(tsd->tsd_devtype, "soclib:blockdevice") != 0)
		return 0;
	return 1;
}

void
vcibd_attach(device_t parent, device_t self, void *aux)
{
	struct vcibd_softc *sc = device_private(self);
	struct tsardevs_attach_args *tsd = aux;
	int error;
	int nsegs;
	char buf[40];

	aprint_normal(": VCI block device\n");

	sc->sc_dev = self;
	sc->sc_tag = tsd->tsd_tag;
	sc->sc_dmat = tsd->tsd_dmatag;

	/* map the device */
	if (bus_space_map(sc->sc_tag,
	    tsd->tsd_reg[0].reg_addr,tsd->tsd_reg[0].reg_size, 0,
	    &sc->sc_handle)) {
			aprint_error_dev(self, "couldn't map registers\n");
			return;
	}
	sc->sc_ih = intr_establish(tsd->tsd_irq, IPL_BIO,
	    device_xname(self), buf, vcibd_intr, sc, NULL);
	if (sc->sc_ih == NULL) {
		aprint_error_dev(self, "can't establish interrupt\n");
		return;
	} else {
		aprint_normal_dev(self, "interrupting at %s\n", buf);
	}
	error = bus_dmamap_create(sc->sc_dmat, MAXPHYS, 1, MAXPHYS,
	    0, BUS_DMA_NOWAIT | BUS_DMA_ALLOCNOW, &sc->sc_dmamap);
	if (error) {
		aprint_error_dev(self, "can't create DMA map (%d)\n",
		    error);
		return;
	}
	error = bus_dmamem_alloc(sc->sc_dmat, MAXPHYS, PAGE_SIZE, 0,
	    &sc->sc_bouncesegs, 1, &nsegs, BUS_DMA_NOWAIT | BUS_DMA_STREAMING);
	if (error) {
		aprint_error_dev(self, "can't allocate bounce memory (%d)\n",
		    error);
		goto err1;
	}
	error = bus_dmamem_map(sc->sc_dmat, &sc->sc_bouncesegs, nsegs,
	    MAXPHYS, &sc->sc_bounce, BUS_DMA_NOWAIT);
	if (error) {
		aprint_error_dev(self, "can't map bounce memory (%d)\n",
		    error);
		goto err2;
	}
	dk_sc_init(&sc->sc_dksc, sc, device_xname(self));
	disk_init(&sc->sc_dksc.sc_dkdev, device_xname(self), &vcibddkdriver);
	sc->sc_di = &dkintf_esdi;
	sc->sc_secsize = BLOCK_DEVICE_REG_READ(sc, BLOCK_DEVICE_BLOCK_SIZE);
	sc->sc_sectors = BLOCK_DEVICE_REG_READ(sc, BLOCK_DEVICE_SIZE);
	DPRINTF(("sc_sectors %d sc_secsize %d DEV_BSIZE %d\n",
	    (int)sc->sc_sectors, (int)sc->sc_secsize, (int)DEV_BSIZE));
	sc->sc_dksc.sc_size = sc->sc_bdsize =
	    sc->sc_sectors * (uint64_t)sc->sc_secsize / DEV_BSIZE;
	DPRINTF(("sc_bdsize %d\n", (int)sc->sc_bdsize));
	bufq_alloc(&sc->sc_dksc.sc_bufq, "fcfs", 0);
	sc->sc_dksc.sc_flags |= DKF_INITED;
	disk_attach(&sc->sc_dksc.sc_dkdev);
	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_IRQ_ENABLE, 1);
	config_pending_incr(); /* wait for interrupts to be enabled */
	config_interrupts(self, vcibd_attach_intr);
	return;
err2:
	bus_dmamem_free(sc->sc_dmat, &sc->sc_bouncesegs, nsegs);
err1:
	bus_dmamap_destroy(sc->sc_dmat, sc->sc_dmamap);
	return;
}

static void
vcibd_attach_intr(device_t self)
{
	struct vcibd_softc *sc = device_private(self);
	char buf[9];
	struct dk_geom *pdg;
	prop_dictionary_t disk_info, odisk_info, geom;
	int s = splbio();

	pdg = &sc->sc_dksc.sc_geom;
	pdg->pdg_secsize = DEV_BSIZE;
	pdg->pdg_ntracks = 1;
	pdg->pdg_nsectors = 1024 * (1024 / pdg->pdg_secsize);
	DPRINTF(("pdg_nsectors %d\n", pdg->pdg_nsectors));
	pdg->pdg_ncylinders = sc->sc_dksc.sc_size / pdg->pdg_nsectors;
	DPRINTF(("pdg_ncylinders %d\n", pdg->pdg_ncylinders));

	/* try to read the disklabel */
	dk_getdisklabel(sc->sc_di, &sc->sc_dksc, 0 /* XXX ? */);
	format_bytes(buf, sizeof(buf), sc->sc_sectors * sc->sc_secsize);
	aprint_normal_dev(sc->sc_dev,
	    "%s, %d bytes/sect x %" PRIu64 " sectors\n",
	    buf, DEV_BSIZE, sc->sc_bdsize);
	/* Discover wedges on this disk. */
	dkwedge_discover(&sc->sc_dksc.sc_dkdev);

	disk_info = prop_dictionary_create();
	geom = prop_dictionary_create();
	prop_dictionary_set_uint64(geom, "sectors-per-unit",
	    sc->sc_dksc.sc_size);
	prop_dictionary_set_uint32(geom, "sector-size",
	    pdg->pdg_secsize);
	prop_dictionary_set_uint16(geom, "sectors-per-track",
	    pdg->pdg_nsectors);
	prop_dictionary_set_uint16(geom, "tracks-per-cylinder",
	    pdg->pdg_ntracks);
	prop_dictionary_set_uint64(geom, "cylinders-per-unit",
	    pdg->pdg_ncylinders);
	prop_dictionary_set(disk_info, "geometry", geom);
	prop_object_release(geom);
	prop_dictionary_set(device_properties(sc->sc_dev),
	    "disk-info", disk_info);
	/*
	 * Don't release disk_info here; we keep a reference to it.
	 * disk_detach() will release it when we go away.
	 */
	odisk_info = sc->sc_dksc.sc_dkdev.dk_info;
	sc->sc_dksc.sc_dkdev.dk_info = disk_info;
	if (odisk_info)
		prop_object_release(odisk_info);
	config_pending_decr(); /* wait for interrupts to be enabled */
	splx(s);
	return;
}

static int
vcibd_intr(void *p)
{
	struct vcibd_softc *sc = p;
	int status = BLOCK_DEVICE_REG_READ(sc, BLOCK_DEVICE_STATUS);
	struct buf *bp = sc->sc_bp;

	DPRINTF(("vcibd_intr(%p) bp %p status %d\n", p, bp, status));
	if (bp == NULL) {
		aprint_error_dev(sc->sc_dev, "unexpected status %d without "
		    "buf\n", status);
		return 0;
	}

	switch(status) {
	case BLOCK_DEVICE_READ_SUCCESS: /* completed without error */
	case BLOCK_DEVICE_WRITE_SUCCESS: /* completed without error */
		bus_dmamap_sync(sc->sc_dmat, sc->sc_dmamap, 0, sc->sc_bcount,
		    (bp->b_flags & B_READ) ?
		     BUS_DMASYNC_POSTREAD : BUS_DMASYNC_POSTWRITE);
		if (sc->sc_flags & BD_FLAGS_BOUNCE) {
			if (bp->b_flags & B_READ) {
				memcpy(bp->b_data, sc->sc_bounce,
				    sc->sc_bcount);
			}
			sc->sc_flags &= ~BD_FLAGS_BOUNCE;
		}
		bp->b_error = 0;
		/* bp->b_resid has already been set */
		break;
	case BLOCK_DEVICE_BUSY:
		aprint_error_dev(sc->sc_dev,
		    "unexpected interrupt while BUSY\n");
		return 0; /* maybe we'll get interrupted again when done */
	case BLOCK_DEVICE_IDLE:
	default:
		aprint_error_dev(sc->sc_dev, "unexpected status %d\n",
			status);
		bp->b_error = EIO;
		bp->b_resid = bp->b_bcount;
		break;
	case BLOCK_DEVICE_READ_ERROR:
	case BLOCK_DEVICE_WRITE_ERROR:
	case BLOCK_DEVICE_ERROR:
		bp->b_error = EIO;
		bp->b_resid = bp->b_bcount;
		break;
	}
	bus_dmamap_unload(sc->sc_dmat, sc->sc_dmamap);
	disk_unbusy(&sc->sc_dksc.sc_dkdev,
	    (bp->b_bcount - bp->b_resid),
	    (bp->b_flags & B_READ));
	DPRINTF(("vcibd_intr biodone b_resid %d b_error %d\n",
	    bp->b_resid, bp->b_error));
	sc->sc_bp = NULL;
	biodone(bp);
	dk_iodone(sc->sc_di, &sc->sc_dksc);
	return 1;
}

int
vcibdopen(dev_t dev, int flags, int fmt, struct lwp *l)
{
	struct	vcibd_softc *sc;

	sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	if (__predict_false(sc == NULL))
		return (ENXIO);

	DPRINTF(("vcibdopen(0x%04x, %d)\n", dev, flags));
	return dk_open(sc->sc_di, &sc->sc_dksc, dev, flags, fmt, l);
}

int
vcibdclose(dev_t dev, int flags, int fmt, struct lwp *l)
{
	struct vcibd_softc *sc;

	sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));

	DPRINTF(("vcibdclose(%d, %d)\n", dev, flags));
	return dk_close(sc->sc_di, &sc->sc_dksc, dev, flags, fmt, l);
}

void
vcibdstrategy(struct buf *bp)
{
	struct vcibd_softc *sc;

	sc = device_lookup_private(&vcibd_cd, DISKUNIT(bp->b_dev));

	DPRINTF(("vcibdstrategy(%p): b_bcount = %ld\n", bp,
	    (long)bp->b_bcount));

	if (__predict_false(sc == NULL)) {
		bp->b_error = EIO;
		biodone(bp);
		return;
	}
	dk_strategy(sc->sc_di, &sc->sc_dksc, bp);
	return;
}

int
vcibdsize(dev_t dev)
{
	struct	vcibd_softc *sc;

	DPRINTF(("vcibdsize(%d)\n", dev));

	sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	if (__predict_false(sc == NULL))
		return -1;
	return dk_size(sc->sc_di, &sc->sc_dksc, dev);
}

int
vcibdread(dev_t dev, struct uio *uio, int flags)
{
	struct vcibd_softc *sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	struct  dk_softc *dksc = &sc->sc_dksc;

	if (__predict_false(((dksc->sc_flags & DKF_INITED) == 0)))
		return ENXIO;
	return physio(vcibdstrategy, NULL, dev, B_READ, minphys, uio);
}

int
vcibdwrite(dev_t dev, struct uio *uio, int flags)
{
	struct vcibd_softc *sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	struct  dk_softc *dksc = &sc->sc_dksc;

	if (__predict_false((dksc->sc_flags & DKF_INITED) == 0))
		return ENXIO;
	return physio(vcibdstrategy, NULL, dev, B_WRITE, minphys, uio);
}

int
vcibdioctl(dev_t dev, u_long cmd, void *data, int flag, struct lwp *l)
{
	struct vcibd_softc *sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	struct	dk_softc *dksc;
	int	error;
	struct	disk *dk;

	DPRINTF(("vcibdioctl(%d, %08lx, %p, %d, %p)\n",
	    dev, cmd, data, flag, l));
	dksc = &sc->sc_dksc;
	dk = &dksc->sc_dkdev;

	error = disk_ioctl(&sc->sc_dksc.sc_dkdev, cmd, data, flag, l);
	if (error != EPASSTHROUGH)
		return (error);

	switch (cmd) {
	case DIOCSSTRATEGY:
		error = EOPNOTSUPP;
		break;
	case DIOCCACHESYNC:
		error = EOPNOTSUPP;
		break;
	default:
		error = dk_ioctl(sc->sc_di, dksc, dev, cmd, data, flag, l);
		break;
	}

	return error;
}

int
vcibddump(dev_t dev, daddr_t blkno, void *va, size_t size)
{
	struct vcibd_softc *sc;

	sc  = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	if (sc == NULL)
		return (ENXIO);

	DPRINTF(("vcibddump(%d, %" PRId64 ", %p, %lu)\n", dev, blkno, va,
	    (unsigned long)size));
	return dk_dump(sc->sc_di, &sc->sc_dksc, dev, blkno, va, size);
}

static int
vcibdstart(struct dk_softc *dksc, struct buf *bp)
{
	struct vcibd_softc *sc;
	int ret = 0;

	DPRINTF(("vcibdstart(%p): b_bcount = %ld\n", bp, (long)bp->b_bcount));

	sc = device_lookup_private(&vcibd_cd, DISKUNIT(bp->b_dev));
	if (__predict_false(sc == NULL)) {
		bp->b_error = EIO;
		goto err;
	}

	if (bp->b_rawblkno < 0 || bp->b_rawblkno > sc->sc_bdsize) {
		/* invalid block number */
		bp->b_error = EINVAL;
		goto err;
	}

	if (bp->b_rawblkno == sc->sc_bdsize) {
		/* at end of disk; return short read */
		bp->b_resid = bp->b_bcount;
		bp->b_error = 0;
		DPRINTF(("vcibd_start done biodone b_resid %d\n",
		    bp->b_resid));
		biodone(bp);
		return 0;
	}
		
	if (sc->sc_bp != NULL) {
		ret = -1;
		goto out;
	}

	dksc = &sc->sc_dksc;

	sc->sc_bp = bp;
	/* now we're sure we'll send this buf */
	disk_busy(&dksc->sc_dkdev);
	if (bp->b_rawblkno + bp->b_bcount / DEV_BSIZE >= sc->sc_bdsize) {
		sc->sc_bcount =
		    (sc->sc_bdsize - bp->b_rawblkno) * DEV_BSIZE;
		bp->b_resid = bp->b_bcount - sc->sc_bcount;
	} else {
		sc->sc_bcount = bp->b_bcount;
		bp->b_resid = 0;
	}
	DPRINTF(("vcibdstart: real bcount = %ld\n",(long)sc->sc_bcount));
	bp->b_error = bus_dmamap_load(sc->sc_dmat, sc->sc_dmamap, bp->b_data,
	    sc->sc_bcount, NULL, BUS_DMA_NOWAIT | BUS_DMA_STREAMING |
	    (bp->b_flags & B_READ) ? BUS_DMA_READ : BUS_DMA_WRITE);
	if (bp->b_error && bp->b_error != EFBIG) {
		aprint_error_dev(sc->sc_dev, "bus_dmamap_load failed (%d)\n",
		    bp->b_error);
		goto err;
	}
	if (bp->b_error) {
		DPRINTF(("vcibdstart: use bounce\n"));
		/* copy to contigous buffer */
		sc->sc_flags |= BD_FLAGS_BOUNCE;
		if ((bp->b_flags & B_READ) == 0)
			memcpy(sc->sc_bounce, bp->b_data, sc->sc_bcount);
		bp->b_error = bus_dmamap_load(sc->sc_dmat, sc->sc_dmamap,
		    sc->sc_bounce, sc->sc_bcount, NULL,
		    BUS_DMA_NOWAIT | BUS_DMA_STREAMING |
		    (bp->b_flags & B_READ) ? BUS_DMA_READ : BUS_DMA_WRITE);
		if (bp->b_error) {
			aprint_error_dev(sc->sc_dev,
			    "bus_dmamap_load failed (%d)\n",
			    bp->b_error);
			goto err;
		}
	}
	bus_dmamap_sync(sc->sc_dmat, sc->sc_dmamap, 0, sc->sc_bcount,
	    (bp->b_flags & B_READ) ? BUS_DMASYNC_PREREAD : BUS_DMASYNC_PREWRITE);
	DPRINTF(("vcibdstart: buffer 0x%lx, blkno 0x%llx, count 0x%lx, op %d\n",
	    (u_long)sc->sc_dmamap->dm_segs[0].ds_addr,
	    (unsigned long long)bp->b_rawblkno,
	    (u_int)sc->sc_bcount / sc->sc_secsize,
	    (bp->b_flags & B_READ) ? BLOCK_DEVICE_OP_READ : BLOCK_DEVICE_OP_WRITE));

	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_BUFFER,
	    sc->sc_dmamap->dm_segs[0].ds_addr);
	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_BUFFER_EXT,
	    (sc->sc_dmamap->dm_segs[0].ds_addr >> 32));
	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_LBA,
	    bp->b_rawblkno);
	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_COUNT,
	    sc->sc_bcount / sc->sc_secsize);
	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_OP,
	    (bp->b_flags & B_READ) ? BLOCK_DEVICE_OP_READ : BLOCK_DEVICE_OP_WRITE);
out:
	return ret;

err:
	bp->b_resid = bp->b_bcount;
	DPRINTF(("vcibd_start err biodone b_resid %d b_error %d\n",
	    bp->b_resid, bp->b_error));
	biodone(bp);
	return 0;
}
