/*
 * Copyright (C) ST-Ericsson SA 2010
 * Author: Stefan Nilsson <stefan.xk.nilsson@stericsson.com> for ST-Ericsson.
 * Author: Martin Persson <martin.persson@stericsson.com> for ST-Ericsson.
 * License terms: GNU General Public License (GPL), version 2.
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/slab.h>

#include <mach/id.h>

#define MODEM_INTCON_BASE_ADDR 0xBFFD3000
#define MODEM_INTCON_SIZE 0xFFF

#define DEST_IRQ41_OFFSET 0x2A4
#define DEST_IRQ43_OFFSET 0x2AC
#define DEST_IRQ45_OFFSET 0x2B4

#define PRIO_IRQ41_OFFSET 0x6A4
#define PRIO_IRQ43_OFFSET 0x6AC
#define PRIO_IRQ45_OFFSET 0x6B4

#define ALLOW_IRQ_OFFSET 0x104

#define MODEM_INTCON_CPU_NBR 0x1
#define MODEM_INTCON_PRIO_HIGH 0x0

#define MODEM_INTCON_ALLOW_IRQ41 0x0200
#define MODEM_INTCON_ALLOW_IRQ43 0x0800
#define MODEM_INTCON_ALLOW_IRQ45 0x2000

#define MODEM_IRQ_REG_OFFSET 0x4

struct modem_irq {
	void __iomem *modem_intcon_base;
};


static void setup_modem_intcon(void __iomem *modem_intcon_base)
{
	/* IC_DESTINATION_BASE_ARRAY - Which CPU to receive the IRQ */
	writel(MODEM_INTCON_CPU_NBR, modem_intcon_base + DEST_IRQ41_OFFSET);
	writel(MODEM_INTCON_CPU_NBR, modem_intcon_base + DEST_IRQ43_OFFSET);
	writel(MODEM_INTCON_CPU_NBR, modem_intcon_base + DEST_IRQ45_OFFSET);

	/* IC_PRIORITY_BASE_ARRAY - IRQ priority in modem IRQ controller */
	writel(MODEM_INTCON_PRIO_HIGH, modem_intcon_base + PRIO_IRQ41_OFFSET);
	writel(MODEM_INTCON_PRIO_HIGH, modem_intcon_base + PRIO_IRQ43_OFFSET);
	writel(MODEM_INTCON_PRIO_HIGH, modem_intcon_base + PRIO_IRQ45_OFFSET);

	/* IC_ALLOW_ARRAY - IRQ enable */
	writel(MODEM_INTCON_ALLOW_IRQ41 |
		   MODEM_INTCON_ALLOW_IRQ43 |
		   MODEM_INTCON_ALLOW_IRQ45,
		   modem_intcon_base + ALLOW_IRQ_OFFSET);
}

static irqreturn_t modem_cpu_irq_handler(int irq, void *data)
{
	int real_irq;
	int virt_irq;
	struct modem_irq *mi = (struct modem_irq *)data;

	/* Read modem side IRQ number from modem IRQ controller */
	real_irq = readl(mi->modem_intcon_base + MODEM_IRQ_REG_OFFSET) & 0xFF;
	virt_irq = IRQ_MODEM_EVENTS_BASE + real_irq;

	pr_debug("modem_irq: Worker read addr 0x%X and got value 0x%X "
		 "which will be 0x%X (%d) which translates to "
		 "virtual IRQ 0x%X (%d)!\n",
		   (u32)mi->modem_intcon_base + MODEM_IRQ_REG_OFFSET,
		   real_irq,
		   real_irq & 0xFF,
		   real_irq & 0xFF,
		   virt_irq,
		   virt_irq);

	if (virt_irq != 0)
		generic_handle_irq(virt_irq);

	pr_debug("modem_irq: Done handling virtual IRQ %d!\n", virt_irq);

	return IRQ_HANDLED;
}

static void create_virtual_irq(int irq, struct irq_chip *modem_irq_chip)
{
	irq_set_chip_and_handler(irq, modem_irq_chip, handle_simple_irq);
	set_irq_flags(irq, IRQF_VALID);

	pr_debug("modem_irq: Created virtual IRQ %d\n", irq);
}

static int modem_irq_init(void)
{
	int err;
	static struct irq_chip  modem_irq_chip;
	struct modem_irq *mi;

	if (!cpu_is_u5500())
		return -ENODEV;

	pr_info("modem_irq: Set up IRQ handler for incoming modem IRQ %d\n",
		   IRQ_DB5500_MODEM);

	mi = kmalloc(sizeof(struct modem_irq), GFP_KERNEL);
	if (!mi) {
		pr_err("modem_irq: Could not allocate device\n");
		return -ENOMEM;
	}

	mi->modem_intcon_base =
		ioremap(MODEM_INTCON_BASE_ADDR, MODEM_INTCON_SIZE);
	pr_debug("modem_irq: ioremapped modem_intcon_base from "
		 "phy 0x%x to virt 0x%x\n", MODEM_INTCON_BASE_ADDR,
		 (u32)mi->modem_intcon_base);

	setup_modem_intcon(mi->modem_intcon_base);

	modem_irq_chip = dummy_irq_chip;
	modem_irq_chip.name = "modem_irq";

	/* Create the virtual IRQ:s needed */
	create_virtual_irq(MBOX_PAIR0_VIRT_IRQ, &modem_irq_chip);
	create_virtual_irq(MBOX_PAIR1_VIRT_IRQ, &modem_irq_chip);
	create_virtual_irq(MBOX_PAIR2_VIRT_IRQ, &modem_irq_chip);

	err = request_threaded_irq(IRQ_DB5500_MODEM, NULL,
				   modem_cpu_irq_handler, IRQF_ONESHOT,
				   "modem_irq", mi);
	if (err)
		pr_err("modem_irq: Could not register IRQ %d\n",
		       IRQ_DB5500_MODEM);

	return 0;
}

arch_initcall(modem_irq_init);