/*
 * Copyright (c) 2007, 2008 University of Tsukuba
 * 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.
 * 3. Neither the name of the University of Tsukuba nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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.
 */
/*
 * Copyright (c) 2010-2012 Yuichi Watanabe
 */

#include <core/assert.h>
#include <core/initfunc.h>
#include <core/printf.h>
#include <core/spinlock.h>
#include <core/string.h>
#include <core/vmmerr.h>
#include "asm.h"
#include "constants.h"
#include "cpu_interpreter.h"
#include "cpu_mmu.h"
#include "current.h"
#include "mm.h"
#include "mmio.h"
#include "panic.h"

struct call_flush_tlb_data {
	phys_t start;
	phys_t end;
	bool ret;
};

static int
rangecheck (struct mmio_handle *h, phys_t gphys, uint len, phys_t *gphys2,
	    uint *len2)
{
	phys_t start, end, h_start, h_end;

	if (len == 0) {
		return 0;
	}
	start = gphys;
	end = gphys + len - 1;
	h_start = h->gphys;
	h_end = h->gphys + h->len - 1;
	if (start > h_end || end < h_start) {
		return 0;
	}
	if (start < h_start) {
		start = h_start;
	}
	if (end > h_end) {
		end = h_end;
	}
	if (gphys2) {
		*gphys2 = start;
	}
	if (len2) {
		*len2 = end - start + 1;
	}
	return 1;
}

void *
mmio_memcpy(void *dest, const void *src, int len)
{
	switch (len) {
	case 1:
		*(u8*)dest = *(u8*)src;
		break;
	case 2:
		*(u16*)dest = *(u16*)src;
		break;
	case 4:
		*(u32*)dest = *(u32*)src;
		break;
	case 8:
		*(u64*)dest = *(u64*)src;
		break;
	default:
		return memcpy(dest, src, len);
	}
	return dest;
}

static void
mmio_gphys_access (phys_t gphysaddr, bool wr, void *buf, uint len, u32 flags)
{
	void *virt;

	ASSERT (!(flags & ~MAPMEM_VALID_FLAG));

	if (!len)
		return;
	virt = mapmem_gphys (gphysaddr, len, (wr ? MAPMEM_WRITE : 0) | flags);
	if (virt == NULL) {
		printf("mmio_gphys_access: Failed to map. "
		       "gphysaddr=0x%llx, len=0x%x, wr=%d\n",
		       gphysaddr, len, wr);
		if (!wr) {
			memset(buf, -1, len);
		}
		return;
	}
	if (wr)
		mmio_memcpy(virt, buf, len);
	else
		mmio_memcpy(buf, virt, len);
	unmapmem (virt, len);
}

static void
call_mmio(struct mmio_handle *handle, phys_t gphys, bool wr,
	  void *buf, uint length, u32 flags)
{
	int emulated;
	uint len;

	while (length > 0) {
		for (len = 1; len < length; len <<= 1) {
			if (gphys & ((len << 1) - 1)) {
				break;
			}
		}
		emulated = handle->handler(handle->data, gphys, wr, buf, len, flags);
		if (!emulated) {
			mmio_gphys_access(gphys, wr, buf, len, flags);
		}
		gphys += len;
		length -= len;
	}
}

/*
 * Return 1 if this function accesses memory.
 * Return 0 if this function does not access memory.l
 */
int
mmio_access_memory (phys_t gphysaddr, bool wr, void *buffer, uint length,
		    u32 flags)
{
	struct mmio_handle *h;
	int ret = 0;
	phys_t gphys2;
	uint len1, len2;
	u8 *buf = buffer;

	ASSERT (!(flags & ~MAPMEM_VALID_FLAG));
        ASSERT((length & (length - 1)) == 0);

	LIST1_FOREACH (current->vm->mmio.mmio[MMIO_INDEX(gphysaddr)], h) {
		if (!length)
			break;
		if (rangecheck(h, gphysaddr, length, &gphys2, &len2)) {
			ret = 1;
			len1 = gphys2 - gphysaddr;
			mmio_gphys_access(gphysaddr, wr, buf, len1, flags);
			buf += len1;
			length -= len1;
			call_mmio(h, gphys2, wr, buf, len2, flags);
			gphysaddr = gphys2 + len2;
			buf += len2;
			length -= len2;
		}
	}
	if (ret)
		mmio_gphys_access(gphysaddr, wr, buf, length, flags);
	return ret;
}

vmmerr_t
mmio_pagefault(phys_t gphysaddr)
{
	vmmerr_t e;
	struct mmio_handle *h;

	gphysaddr &= ~PAGESIZE_MASK;
	LIST1_FOREACH (current->vm->mmio.mmio[MMIO_INDEX(gphysaddr)], h) {
		if (rangecheck (h, gphysaddr, PAGESIZE, NULL, NULL)) {
			e = cpu_interpreter ();
			return e;
		}
	}
	return VMMERR_NODEV;
}

static void
mmio_add (int index, struct mmio_handle *handle)
{
	struct mmio_handle *next;
	phys_t gphys;

	gphys = handle->gphys;
	LIST1_FOREACH (current->vm->mmio.mmio[index], next) {
		if (next->gphys > gphys)
			break;
	}
	if (next)
		LIST1_INSERT(current->vm->mmio.mmio[index], next, handle);
	else
		LIST1_ADD(current->vm->mmio.mmio[index], handle);
}

static void
mmio_del (int index, struct mmio_handle *handle)
{
	LIST1_DEL (current->vm->mmio.mmio[index], handle);
}

int
mmio_do_nothing(void *data, phys_t gphys, bool wr, void *buf,
		uint len, u32 flags)
{
	if (!wr) {
		memset(buf, -1, len);
	}
	return 1; /* emulated */
}

int
mmio_pass(void *data, phys_t gphys, bool wr, void *buf,
		uint len, u32 flags)
{
	return 0; /* pass */
}

void *
mmio_register (phys_t gphys, uint len, mmio_handler_t *handler, void *data)
{
	struct mmio_handle *handle;
	int index;

	index = MMIO_INDEX(gphys);

	rw_spinlock_lock_ex (&current->vm->mmio_rwlock);
	LIST1_FOREACH (current->vm->mmio.mmio[index], handle) {
		if (rangecheck (handle, gphys, len, NULL, NULL)) {
			panic("MMIO regions are conflicted.");

		}
	}
	handle = alloc (sizeof *handle);
	if (handle == NULL) {
		panic("Failed to allocate memory for mmio_handle.");
	}
	handle->gphys = gphys;
	handle->len = len;
	handle->data = data;
	handle->handler = handler;
	mmio_add(index, handle);
	rw_spinlock_unlock_ex (&current->vm->mmio_rwlock);
	return handle;
}

void
mmio_unregister (void *handle)
{
	struct mmio_handle *hdl;
	int index;

	hdl = handle;
	index = MMIO_INDEX(hdl->gphys);
	rw_spinlock_lock_ex (&current->vm->mmio_rwlock);
	mmio_del(index, hdl);
	free (hdl);
	rw_spinlock_unlock_ex (&current->vm->mmio_rwlock);
}

void
mmio_lock (void)
{
	rw_spinlock_lock_sh (&current->vm->mmio_rwlock);
}

void
mmio_unlock (void)
{
	rw_spinlock_unlock_sh (&current->vm->mmio_rwlock);
}

static void
mmio_init (void)
{
	int i;

	rw_spinlock_init (&current->vm->mmio_rwlock);
	for (i = 0; i < 17; i++)
		LIST1_HEAD_INIT (current->vm->mmio.mmio[i]);
}

INITFUNC ("gvm0", mmio_init);

