/* $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>

//#define 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 *);

struct vcibd_softc {
	struct dk_softc sc_dksc;
	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 */
	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 = {
        .d_open = vcibdopen,
	.d_close = vcibdclose,
	.d_strategy = vcibdstrategy,
	.d_ioctl = vcibdioctl,
	.d_dump = vcibddump,
	.d_psize = vcibdsize,
	.d_discard = nodiscard,
	.d_flag = D_DISK
};

const struct cdevsw vcibd_cdevsw = {
        .d_open = vcibdopen,
        .d_close = vcibdclose,
        .d_read = vcibdread,
        .d_write = vcibdwrite,
        .d_ioctl = vcibdioctl,
        .d_stop = nostop,
        .d_tty = notty,
        .d_poll = nopoll,
        .d_mmap = nommap,
        .d_kqfilter = nokqfilter,
        .d_discard = nodiscard,
        .d_flag = D_DISK
};

extern struct cfdriver vcibd_cd;

static int vcibdstart(device_t, struct buf *);

static struct dkdriver vcibddkdriver = {
        .d_open = vcibdopen,
        .d_close = vcibdclose,
        .d_strategy = vcibdstrategy,
        .d_minphys = minphys,
        .d_diskstart = vcibdstart,
        .d_dumpblocks = NULL,
        .d_iosize = NULL,
        .d_firstopen = NULL,
        .d_lastclose = NULL,
        .d_label = NULL,
};

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_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_init(&sc->sc_dksc, self, DKTYPE_UNKNOWN);
	disk_init(&sc->sc_dksc.sc_dkdev, device_xname(self), &vcibddkdriver);
	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_bdsize = sc->sc_sectors * (uint64_t)sc->sc_secsize / DEV_BSIZE;
	dk_attach(&sc->sc_dksc);
	disk_attach(&sc->sc_dksc.sc_dkdev);
	bufq_alloc(&sc->sc_dksc.sc_bufq, "fcfs", 0);
	BLOCK_DEVICE_REG_WRITE(sc, BLOCK_DEVICE_IRQ_ENABLE, 1);
	config_pending_incr(self); /* 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 disk_geom *pdg;
	int s = splbio();

	pdg = &sc->sc_dksc.sc_dkdev.dk_geom;
	pdg->dg_secperunit = sc->sc_bdsize;
	pdg->dg_secsize = DEV_BSIZE;
	pdg->dg_ntracks = 1;
	pdg->dg_nsectors = 1024 * (1024 / pdg->dg_secsize);
	DPRINTF(("pdg_nsectors %d\n", pdg->dg_nsectors));
	pdg->dg_ncylinders = sc->sc_bdsize / pdg->dg_nsectors;
	DPRINTF(("pdg_ncylinders %d\n", pdg->dg_ncylinders));

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

	config_pending_decr(self); /* 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_dksc.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, bp->b_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,
				    bp->b_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_dksc.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_dksc.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);
	DPRINTF(("vcibd_intr biodone b_resid %d b_error %d\n",
	    bp->b_resid, bp->b_error));
	sc->sc_bp = NULL;
	dk_done(&sc->sc_dksc, bp);
	dk_start(&sc->sc_dksc, NULL);
	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%04" PRIx64 ", %d)\n", dev, flags));
	return dk_open(&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(%" PRId64 ", %d)\n", dev, flags));
	return dk_close(&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_dksc, bp);
	return;
}

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

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

	sc = device_lookup_private(&vcibd_cd, DISKUNIT(dev));
	if (__predict_false(sc == NULL))
		return -1;
	return dk_size(&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;

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

	switch (cmd) {
	case DIOCCACHESYNC:
		error = EOPNOTSUPP;
		break;
	default:
		error = dk_ioctl(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(%" PRId64 ", %" PRId64 ", %p, %lu)\n", dev, blkno, va,
	    (unsigned long)size));
	return dk_dump(&sc->sc_dksc, dev, blkno, va, size);
}

static int
vcibdstart(device_t dev, struct buf *bp)
{
	struct vcibd_softc *sc = device_private(dev);
	int ret = 0;

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

	if (sc->sc_bp != NULL) {
		ret = EAGAIN;
		goto out;
	}

	sc->sc_bp = bp;
	
	ret = bus_dmamap_load(sc->sc_dmat, sc->sc_dmamap, bp->b_data,
	    bp->b_bcount, NULL, BUS_DMA_NOWAIT | BUS_DMA_STREAMING |
	    (bp->b_flags & B_READ) ? BUS_DMA_READ : BUS_DMA_WRITE);
	if (ret && ret != EFBIG) {
		aprint_error_dev(sc->sc_dksc.sc_dev, "bus_dmamap_load failed (%d)\n",
		    ret);
		goto out;
	}
	if (ret) {
		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, bp->b_bcount);
		ret = bus_dmamap_load(sc->sc_dmat, sc->sc_dmamap,
		    sc->sc_bounce, bp->b_bcount, NULL,
		    BUS_DMA_NOWAIT | BUS_DMA_STREAMING |
		    (bp->b_flags & B_READ) ? BUS_DMA_READ : BUS_DMA_WRITE);
		if (ret) {
			aprint_error_dev(sc->sc_dksc.sc_dev,
			    "bus_dmamap_load failed (%d)\n",
			    ret);
			goto out;
		}
	}
	bus_dmamap_sync(sc->sc_dmat, sc->sc_dmamap, 0, bp->b_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)bp->b_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,
	    bp->b_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;
}
