/* Copyright (c) 2002,2008-2011, Code Aurora Forum. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 and
 * only version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <linux/delay.h>
#include <linux/debugfs.h>
#include <linux/uaccess.h>
#include <linux/io.h>

#include "kgsl.h"
#include "adreno_postmortem.h"
#include "adreno.h"

#include "a200_reg.h"

unsigned int kgsl_cff_dump_enable;
int kgsl_pm_regs_enabled;

static uint32_t kgsl_ib_base;
static uint32_t kgsl_ib_size;

static struct dentry *pm_d_debugfs;

static int pm_dump_set(void *data, u64 val)
{
	struct kgsl_device *device = data;

	if (val) {
		mutex_lock(&device->mutex);
		adreno_postmortem_dump(device, 1);
		mutex_unlock(&device->mutex);
	}

	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(pm_dump_fops,
			NULL,
			pm_dump_set, "%llu\n");

static int pm_regs_enabled_set(void *data, u64 val)
{
	kgsl_pm_regs_enabled = val ? 1 : 0;
	return 0;
}

static int pm_regs_enabled_get(void *data, u64 *val)
{
	*val = kgsl_pm_regs_enabled;
	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(pm_regs_enabled_fops,
			pm_regs_enabled_get,
			pm_regs_enabled_set, "%llu\n");


static int kgsl_cff_dump_enable_set(void *data, u64 val)
{
#ifdef CONFIG_MSM_KGSL_CFF_DUMP
	kgsl_cff_dump_enable = (val != 0);
	return 0;
#else
	return -EINVAL;
#endif
}

static int kgsl_cff_dump_enable_get(void *data, u64 *val)
{
	*val = kgsl_cff_dump_enable;
	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(kgsl_cff_dump_enable_fops, kgsl_cff_dump_enable_get,
			kgsl_cff_dump_enable_set, "%llu\n");

static int kgsl_dbgfs_open(struct inode *inode, struct file *file)
{
	file->f_mode &= ~(FMODE_PREAD | FMODE_PWRITE);
	file->private_data = inode->i_private;
	return 0;
}

static int kgsl_dbgfs_release(struct inode *inode, struct file *file)
{
	return 0;
}

static int kgsl_hex_dump(const char *prefix, int c, uint8_t *data,
	int rowc, int linec, char __user *buff)
{
	int ss;
	/* Prefix of 20 chars max, 32 bytes per row, in groups of four - that's
	 * 8 groups at 8 chars per group plus a space, plus new-line, plus
	 * ending character */
	char linebuf[20 + 64 + 1 + 1];

	ss = snprintf(linebuf, sizeof(linebuf), prefix, c);
	hex_dump_to_buffer(data, linec, rowc, 4, linebuf+ss,
		sizeof(linebuf)-ss, 0);
	strncat(linebuf, "\n", sizeof(linebuf));
	linebuf[sizeof(linebuf)-1] = 0;
	ss = strlen(linebuf);
	if (copy_to_user(buff, linebuf, ss+1))
		return -EFAULT;
	return ss;
}

static ssize_t kgsl_ib_dump_read(
	struct file *file,
	char __user *buff,
	size_t buff_count,
	loff_t *ppos)
{
	int i, count = kgsl_ib_size, remaining, pos = 0, tot = 0, ss;
	struct kgsl_device *device = file->private_data;
	const int rowc = 32;
	unsigned int pt_base, ib_memsize;
	uint8_t *base_addr;
	char linebuf[80];

	if (!ppos || !device || !kgsl_ib_base)
		return 0;

	kgsl_regread(device, MH_MMU_PT_BASE, &pt_base);
	base_addr = kgsl_sharedmem_convertaddr(device, pt_base, kgsl_ib_base,
		&ib_memsize);

	if (!base_addr)
		return 0;

	pr_info("%s ppos=%ld, buff_count=%d, count=%d\n", __func__, (long)*ppos,
		buff_count, count);
	ss = snprintf(linebuf, sizeof(linebuf), "IB: base=%08x(%08x"
		"), size=%d, memsize=%d\n", kgsl_ib_base,
		(uint32_t)base_addr, kgsl_ib_size, ib_memsize);
	if (*ppos == 0) {
		if (copy_to_user(buff, linebuf, ss+1))
			return -EFAULT;
		tot += ss;
		buff += ss;
		*ppos += ss;
	}
	pos += ss;
	remaining = count;
	for (i = 0; i < count; i += rowc) {
		int linec = min(remaining, rowc);

		remaining -= rowc;
		ss = kgsl_hex_dump("IB: %05x: ", i, base_addr, rowc, linec,
			buff);
		if (ss < 0)
			return ss;

		if (pos >= *ppos) {
			if (tot+ss >= buff_count) {
				ss = copy_to_user(buff, "", 1);
				return tot;
			}
			tot += ss;
			buff += ss;
			*ppos += ss;
		}
		pos += ss;
		base_addr += linec;
	}

	return tot;
}

static ssize_t kgsl_ib_dump_write(
	struct file *file,
	const char __user *buff,
	size_t count,
	loff_t *ppos)
{
	char local_buff[64];

	if (count >= sizeof(local_buff))
		return -EFAULT;

	if (copy_from_user(local_buff, buff, count))
		return -EFAULT;

	local_buff[count] = 0;	/* end of string */
	sscanf(local_buff, "%x %d", &kgsl_ib_base, &kgsl_ib_size);

	pr_info("%s: base=%08X size=%d\n", __func__, kgsl_ib_base,
		kgsl_ib_size);

	return count;
}

static const struct file_operations kgsl_ib_dump_fops = {
	.open = kgsl_dbgfs_open,
	.release = kgsl_dbgfs_release,
	.read = kgsl_ib_dump_read,
	.write = kgsl_ib_dump_write,
};

static int kgsl_regread_nolock(struct kgsl_device *device,
	unsigned int offsetwords, unsigned int *value)
{
	unsigned int *reg;

	if (offsetwords*sizeof(uint32_t) >= device->regspace.sizebytes) {
		KGSL_DRV_ERR(device, "invalid offset %d\n", offsetwords);
		return -ERANGE;
	}

	reg = (unsigned int *)(device->regspace.mmio_virt_base
				+ (offsetwords << 2));
	*value = __raw_readl(reg);
	return 0;
}

#define KGSL_ISTORE_START 0x5000
#define KGSL_ISTORE_LENGTH 0x600
static ssize_t kgsl_istore_read(
	struct file *file,
	char __user *buff,
	size_t buff_count,
	loff_t *ppos)
{
	int i, count = KGSL_ISTORE_LENGTH, remaining, pos = 0, tot = 0;
	struct kgsl_device *device = file->private_data;
	const int rowc = 8;

	if (!ppos || !device)
		return 0;

	remaining = count;
	for (i = 0; i < count; i += rowc) {
		unsigned int vals[rowc];
		int j, ss;
		int linec = min(remaining, rowc);
		remaining -= rowc;

		if (pos >= *ppos) {
			for (j = 0; j < linec; ++j)
				kgsl_regread_nolock(device,
					KGSL_ISTORE_START+i+j, vals+j);
		} else
			memset(vals, 0, sizeof(vals));

		ss = kgsl_hex_dump("IS: %04x: ", i, (uint8_t *)vals, rowc*4,
			linec*4, buff);
		if (ss < 0)
			return ss;

		if (pos >= *ppos) {
			if (tot+ss >= buff_count)
				return tot;
			tot += ss;
			buff += ss;
			*ppos += ss;
		}
		pos += ss;
	}

	return tot;
}

static const struct file_operations kgsl_istore_fops = {
	.open = kgsl_dbgfs_open,
	.release = kgsl_dbgfs_release,
	.read = kgsl_istore_read,
	.llseek = default_llseek,
};

typedef void (*reg_read_init_t)(struct kgsl_device *device);
typedef void (*reg_read_fill_t)(struct kgsl_device *device, int i,
	unsigned int *vals, int linec);
static ssize_t kgsl_reg_read(struct kgsl_device *device, int count,
	reg_read_init_t reg_read_init,
	reg_read_fill_t reg_read_fill, const char *prefix, char __user *buff,
	loff_t *ppos)
{
	int i, remaining;
	const int rowc = 8;

	if (!ppos || *ppos || !device)
		return 0;

	mutex_lock(&device->mutex);
	reg_read_init(device);
	remaining = count;
	for (i = 0; i < count; i += rowc) {
		unsigned int vals[rowc];
		int ss;
		int linec = min(remaining, rowc);
		remaining -= rowc;

		reg_read_fill(device, i, vals, linec);
		ss = kgsl_hex_dump(prefix, i, (uint8_t *)vals, rowc*4, linec*4,
			buff);
		if (ss < 0) {
			mutex_unlock(&device->mutex);
			return ss;
		}
		buff += ss;
		*ppos += ss;
	}
	mutex_unlock(&device->mutex);

	return *ppos;
}


static void kgsl_sx_reg_read_init(struct kgsl_device *device)
{
	kgsl_regwrite(device, REG_RBBM_PM_OVERRIDE2, 0xFF);
	kgsl_regwrite(device, REG_RBBM_DEBUG_CNTL, 0);
}

static void kgsl_sx_reg_read_fill(struct kgsl_device *device, int i,
	unsigned int *vals, int linec)
{
	int j;

	for (j = 0; j < linec; ++j) {
		kgsl_regwrite(device, REG_RBBM_DEBUG_CNTL, 0x1B00 | i);
		kgsl_regread(device, REG_RBBM_DEBUG_OUT, vals+j);
	}
}

static ssize_t kgsl_sx_debug_read(
	struct file *file,
	char __user *buff,
	size_t buff_count,
	loff_t *ppos)
{
	struct kgsl_device *device = file->private_data;
	return kgsl_reg_read(device, 0x1B, kgsl_sx_reg_read_init,
			     kgsl_sx_reg_read_fill, "SX: %02x: ", buff, ppos);
}

static const struct file_operations kgsl_sx_debug_fops = {
	.open = kgsl_dbgfs_open,
	.release = kgsl_dbgfs_release,
	.read = kgsl_sx_debug_read,
};

static void kgsl_cp_reg_read_init(struct kgsl_device *device)
{
	kgsl_regwrite(device, REG_RBBM_DEBUG_CNTL, 0);
}

static void kgsl_cp_reg_read_fill(struct kgsl_device *device, int i,
	unsigned int *vals, int linec)
{
	int j;

	for (j = 0; j < linec; ++j) {
		kgsl_regwrite(device, REG_RBBM_DEBUG_CNTL, 0x1628);
		kgsl_regread(device, REG_RBBM_DEBUG_OUT, vals+j);
		msleep(100);
	}
}

static ssize_t kgsl_cp_debug_read(
	struct file *file,
	char __user *buff,
	size_t buff_count,
	loff_t *ppos)
{
	struct kgsl_device *device = file->private_data;
	return kgsl_reg_read(device, 20, kgsl_cp_reg_read_init,
		kgsl_cp_reg_read_fill,
		"CP: %02x: ", buff, ppos);
}

static const struct file_operations kgsl_cp_debug_fops = {
	.open = kgsl_dbgfs_open,
	.release = kgsl_dbgfs_release,
	.read = kgsl_cp_debug_read,
};

static void kgsl_mh_reg_read_init(struct kgsl_device *device)
{
	kgsl_regwrite(device, REG_RBBM_DEBUG_CNTL, 0);
}

static void kgsl_mh_reg_read_fill(struct kgsl_device *device, int i,
	unsigned int *vals, int linec)
{
	int j;

	for (j = 0; j < linec; ++j) {
		kgsl_regwrite(device, MH_DEBUG_CTRL, i+j);
		kgsl_regread(device, MH_DEBUG_DATA, vals+j);
	}
}

static ssize_t kgsl_mh_debug_read(
	struct file *file,
	char __user *buff,
	size_t buff_count,
	loff_t *ppos)
{
	struct kgsl_device *device = file->private_data;
	return kgsl_reg_read(device, 0x40, kgsl_mh_reg_read_init,
		kgsl_mh_reg_read_fill,
		"MH: %02x: ", buff, ppos);
}

static const struct file_operations kgsl_mh_debug_fops = {
	.open = kgsl_dbgfs_open,
	.release = kgsl_dbgfs_release,
	.read = kgsl_mh_debug_read,
};

void adreno_debugfs_init(struct kgsl_device *device)
{
	if (!device->d_debugfs || IS_ERR(device->d_debugfs))
		return;

	debugfs_create_file("ib_dump",  0600, device->d_debugfs, device,
			    &kgsl_ib_dump_fops);
	debugfs_create_file("istore",   0400, device->d_debugfs, device,
			    &kgsl_istore_fops);
	debugfs_create_file("sx_debug", 0400, device->d_debugfs, device,
			    &kgsl_sx_debug_fops);
	debugfs_create_file("cp_debug", 0400, device->d_debugfs, device,
			    &kgsl_cp_debug_fops);
	debugfs_create_file("mh_debug", 0400, device->d_debugfs, device,
			    &kgsl_mh_debug_fops);
	debugfs_create_file("cff_dump", 0644, device->d_debugfs, device,
			    &kgsl_cff_dump_enable_fops);

	/* Create post mortem control files */

	pm_d_debugfs = debugfs_create_dir("postmortem", device->d_debugfs);

	if (IS_ERR(pm_d_debugfs))
		return;

	debugfs_create_file("dump",  0600, pm_d_debugfs, device,
			    &pm_dump_fops);
	debugfs_create_file("regs_enabled", 0644, pm_d_debugfs, device,
			    &pm_regs_enabled_fops);
}
