/******************************************************************************
 * pcpu.c
 * Management physical cpu in dom0, get pcpu info and provide sys interface
 *
 * Copyright (c) 2012 Intel Corporation
 * Author: Liu, Jinsong <jinsong.liu@intel.com>
 * Author: Jiang, Yunhong <yunhong.jiang@intel.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation; or, when distributed
 * separately from the Linux kernel or incorporated into other
 * software packages, subject to the following license:
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this source file (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#define pr_fmt(fmt) "xen_cpu: " fmt

#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <linux/cpu.h>
#include <linux/stat.h>
#include <linux/capability.h>

#include <xen/xen.h>
#include <xen/acpi.h>
#include <xen/xenbus.h>
#include <xen/events.h>
#include <xen/interface/platform.h>
#include <asm/xen/hypervisor.h>
#include <asm/xen/hypercall.h>


/*
 * @cpu_id: Xen physical cpu logic number
 * @flags: Xen physical cpu status flag
 * - XEN_PCPU_FLAGS_ONLINE: cpu is online
 * - XEN_PCPU_FLAGS_INVALID: cpu is not present
 */
struct pcpu {
	struct list_head list;
	struct device dev;
	uint32_t cpu_id;
	uint32_t flags;
};

static struct bus_type xen_pcpu_subsys = {
	.name = "xen_cpu",
	.dev_name = "xen_cpu",
};

static DEFINE_MUTEX(xen_pcpu_lock);

static LIST_HEAD(xen_pcpus);

static int xen_pcpu_down(uint32_t cpu_id)
{
	struct xen_platform_op op = {
		.cmd			= XENPF_cpu_offline,
		.interface_version	= XENPF_INTERFACE_VERSION,
		.u.cpu_ol.cpuid		= cpu_id,
	};

	return HYPERVISOR_dom0_op(&op);
}

static int xen_pcpu_up(uint32_t cpu_id)
{
	struct xen_platform_op op = {
		.cmd			= XENPF_cpu_online,
		.interface_version	= XENPF_INTERFACE_VERSION,
		.u.cpu_ol.cpuid		= cpu_id,
	};

	return HYPERVISOR_dom0_op(&op);
}

static ssize_t show_online(struct device *dev,
			   struct device_attribute *attr,
			   char *buf)
{
	struct pcpu *cpu = container_of(dev, struct pcpu, dev);

	return sprintf(buf, "%u\n", !!(cpu->flags & XEN_PCPU_FLAGS_ONLINE));
}

static ssize_t __ref store_online(struct device *dev,
				  struct device_attribute *attr,
				  const char *buf, size_t count)
{
	struct pcpu *pcpu = container_of(dev, struct pcpu, dev);
	unsigned long long val;
	ssize_t ret;

	if (!capable(CAP_SYS_ADMIN))
		return -EPERM;

	if (kstrtoull(buf, 0, &val) < 0)
		return -EINVAL;

	switch (val) {
	case 0:
		ret = xen_pcpu_down(pcpu->cpu_id);
		break;
	case 1:
		ret = xen_pcpu_up(pcpu->cpu_id);
		break;
	default:
		ret = -EINVAL;
	}

	if (ret >= 0)
		ret = count;
	return ret;
}
static DEVICE_ATTR(online, S_IRUGO | S_IWUSR, show_online, store_online);

static bool xen_pcpu_online(uint32_t flags)
{
	return !!(flags & XEN_PCPU_FLAGS_ONLINE);
}

static void pcpu_online_status(struct xenpf_pcpuinfo *info,
			       struct pcpu *pcpu)
{
	if (xen_pcpu_online(info->flags) &&
	   !xen_pcpu_online(pcpu->flags)) {
		/* the pcpu is onlined */
		pcpu->flags |= XEN_PCPU_FLAGS_ONLINE;
		kobject_uevent(&pcpu->dev.kobj, KOBJ_ONLINE);
	} else if (!xen_pcpu_online(info->flags) &&
		    xen_pcpu_online(pcpu->flags)) {
		/* The pcpu is offlined */
		pcpu->flags &= ~XEN_PCPU_FLAGS_ONLINE;
		kobject_uevent(&pcpu->dev.kobj, KOBJ_OFFLINE);
	}
}

static struct pcpu *get_pcpu(uint32_t cpu_id)
{
	struct pcpu *pcpu;

	list_for_each_entry(pcpu, &xen_pcpus, list) {
		if (pcpu->cpu_id == cpu_id)
			return pcpu;
	}

	return NULL;
}

static void pcpu_release(struct device *dev)
{
	struct pcpu *pcpu = container_of(dev, struct pcpu, dev);

	list_del(&pcpu->list);
	kfree(pcpu);
}

static void unregister_and_remove_pcpu(struct pcpu *pcpu)
{
	struct device *dev;

	if (!pcpu)
		return;

	dev = &pcpu->dev;
	if (dev->id)
		device_remove_file(dev, &dev_attr_online);

	/* pcpu remove would be implicitly done */
	device_unregister(dev);
}

static int register_pcpu(struct pcpu *pcpu)
{
	struct device *dev;
	int err = -EINVAL;

	if (!pcpu)
		return err;

	dev = &pcpu->dev;
	dev->bus = &xen_pcpu_subsys;
	dev->id = pcpu->cpu_id;
	dev->release = pcpu_release;

	err = device_register(dev);
	if (err) {
		pcpu_release(dev);
		return err;
	}

	/*
	 * Xen never offline cpu0 due to several restrictions
	 * and assumptions. This basically doesn't add a sys control
	 * to user, one cannot attempt to offline BSP.
	 */
	if (dev->id) {
		err = device_create_file(dev, &dev_attr_online);
		if (err) {
			device_unregister(dev);
			return err;
		}
	}

	return 0;
}

static struct pcpu *create_and_register_pcpu(struct xenpf_pcpuinfo *info)
{
	struct pcpu *pcpu;
	int err;

	if (info->flags & XEN_PCPU_FLAGS_INVALID)
		return ERR_PTR(-ENODEV);

	pcpu = kzalloc(sizeof(struct pcpu), GFP_KERNEL);
	if (!pcpu)
		return ERR_PTR(-ENOMEM);

	INIT_LIST_HEAD(&pcpu->list);
	pcpu->cpu_id = info->xen_cpuid;
	pcpu->flags = info->flags;

	/* Need hold on xen_pcpu_lock before pcpu list manipulations */
	list_add_tail(&pcpu->list, &xen_pcpus);

	err = register_pcpu(pcpu);
	if (err) {
		pr_warn("Failed to register pcpu%u\n", info->xen_cpuid);
		return ERR_PTR(-ENOENT);
	}

	return pcpu;
}

/*
 * Caller should hold the xen_pcpu_lock
 */
static int sync_pcpu(uint32_t cpu, uint32_t *max_cpu)
{
	int ret;
	struct pcpu *pcpu = NULL;
	struct xenpf_pcpuinfo *info;
	struct xen_platform_op op = {
		.cmd                   = XENPF_get_cpuinfo,
		.interface_version     = XENPF_INTERFACE_VERSION,
		.u.pcpu_info.xen_cpuid = cpu,
	};

	ret = HYPERVISOR_dom0_op(&op);
	if (ret)
		return ret;

	info = &op.u.pcpu_info;
	if (max_cpu)
		*max_cpu = info->max_present;

	pcpu = get_pcpu(cpu);

	/*
	 * Only those at cpu present map has its sys interface.
	 */
	if (info->flags & XEN_PCPU_FLAGS_INVALID) {
		unregister_and_remove_pcpu(pcpu);
		return 0;
	}

	if (!pcpu) {
		pcpu = create_and_register_pcpu(info);
		if (IS_ERR_OR_NULL(pcpu))
			return -ENODEV;
	} else
		pcpu_online_status(info, pcpu);

	return 0;
}

/*
 * Sync dom0's pcpu information with xen hypervisor's
 */
static int xen_sync_pcpus(void)
{
	/*
	 * Boot cpu always have cpu_id 0 in xen
	 */
	uint32_t cpu = 0, max_cpu = 0;
	int err = 0;
	struct pcpu *pcpu, *tmp;

	mutex_lock(&xen_pcpu_lock);

	while (!err && (cpu <= max_cpu)) {
		err = sync_pcpu(cpu, &max_cpu);
		cpu++;
	}

	if (err)
		list_for_each_entry_safe(pcpu, tmp, &xen_pcpus, list)
			unregister_and_remove_pcpu(pcpu);

	mutex_unlock(&xen_pcpu_lock);

	return err;
}

static void xen_pcpu_work_fn(struct work_struct *work)
{
	xen_sync_pcpus();
}
static DECLARE_WORK(xen_pcpu_work, xen_pcpu_work_fn);

static irqreturn_t xen_pcpu_interrupt(int irq, void *dev_id)
{
	schedule_work(&xen_pcpu_work);
	return IRQ_HANDLED;
}

/* Sync with Xen hypervisor after cpu hotadded */
void xen_pcpu_hotplug_sync(void)
{
	schedule_work(&xen_pcpu_work);
}
EXPORT_SYMBOL_GPL(xen_pcpu_hotplug_sync);

/*
 * For hypervisor presented cpu, return logic cpu id;
 * For hypervisor non-presented cpu, return -ENODEV.
 */
int xen_pcpu_id(uint32_t acpi_id)
{
	int cpu_id = 0, max_id = 0;
	struct xen_platform_op op;

	op.cmd = XENPF_get_cpuinfo;
	while (cpu_id <= max_id) {
		op.u.pcpu_info.xen_cpuid = cpu_id;
		if (HYPERVISOR_dom0_op(&op)) {
			cpu_id++;
			continue;
		}

		if (acpi_id == op.u.pcpu_info.acpi_id)
			return cpu_id;
		if (op.u.pcpu_info.max_present > max_id)
			max_id = op.u.pcpu_info.max_present;
		cpu_id++;
	}

	return -ENODEV;
}
EXPORT_SYMBOL_GPL(xen_pcpu_id);

static int __init xen_pcpu_init(void)
{
	int irq, ret;

	if (!xen_initial_domain())
		return -ENODEV;

	irq = bind_virq_to_irqhandler(VIRQ_PCPU_STATE, 0,
				      xen_pcpu_interrupt, 0,
				      "xen-pcpu", NULL);
	if (irq < 0) {
		pr_warn("Failed to bind pcpu virq\n");
		return irq;
	}

	ret = subsys_system_register(&xen_pcpu_subsys, NULL);
	if (ret) {
		pr_warn("Failed to register pcpu subsys\n");
		goto err1;
	}

	ret = xen_sync_pcpus();
	if (ret) {
		pr_warn("Failed to sync pcpu info\n");
		goto err2;
	}

	return 0;

err2:
	bus_unregister(&xen_pcpu_subsys);
err1:
	unbind_from_irqhandler(irq, NULL);
	return ret;
}
arch_initcall(xen_pcpu_init);