/* Copyright 2013-2015 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define _GNU_SOURCE         /* for aspritnf */
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <stdint.h>
#include <dirent.h>
#include <sys/param.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
#include <ccan/list/list.h>

#include "opal-prd.h"
#include "module.h"
#include "i2c.h"

struct i2c_bus {
	uint32_t		chip_id;
	uint8_t			engine;
	uint8_t			port;
	const char		*devpath;
	int			fd;
	struct list_node	link;
};

static struct list_head bus_list = LIST_HEAD_INIT(bus_list);

static int i2c_get_dev(uint32_t chip, uint8_t eng, uint8_t port, uint16_t dev)
{
	struct i2c_bus *b, *bus = NULL;

	list_for_each(&bus_list, b, link) {
		if (b->chip_id == chip && b->engine == eng && b->port == port) {
			bus = b;
			break;
		}
	}
	if (!bus) {
		pr_log(LOG_WARNING, "I2C: Bus %08x/%d/%d not found",
				chip, eng, port);
		return -1;
	}
	if (bus->fd < 0) {
		bus->fd = open(bus->devpath, O_RDWR);
		if (bus->fd < 0) {
			pr_log(LOG_ERR, "I2C: Failed to open %s: %m",
				bus->devpath);
			return -1;
		}
	}

	/* XXX We could use the I2C_SLAVE ioctl to check if the device
	 * is currently in use by a kernel driver...
	 */

	return bus->fd;
}

int i2c_read(uint32_t chip_id, uint8_t engine, uint8_t port,
	     uint16_t device, uint32_t offset_size, uint32_t offset,
	     uint32_t length, void* data)
{
	struct i2c_rdwr_ioctl_data ioargs;
	struct i2c_msg	msgs[2];
	uint8_t obuf[4];
	int fd, i, midx = 0;

	if (offset_size > 4) {
		pr_log(LOG_ERR, "I2C: Invalid write offset_size %d",
				offset_size);
		return -1;
	}
	fd = i2c_get_dev(chip_id, engine, port, device);
	if (fd == -1)
		return -1;

	/* If we have an offset, build a message for it */
	if (offset_size) {
		/* The offset has a variable size so let's handle this properly
		 * as it has to be laid out in memory MSB first
		 */
		for (i = 0; i < offset_size; i++)
			obuf[i] = offset >> (8 * (offset_size - i - 1));
		msgs[0].addr = device;
		msgs[0].flags = 0;
		msgs[0].buf = obuf;
		msgs[0].len = offset_size;
		midx = 1;
	}

	/* Build the message for the data portion */
	msgs[midx].addr = device;
	msgs[midx].flags = I2C_M_RD;
	msgs[midx].buf = data;
	msgs[midx].len = length;
	midx++;

	ioargs.msgs = msgs;
	ioargs.nmsgs = midx;
	if (ioctl(fd, I2C_RDWR, &ioargs) < 0) {
		pr_log(LOG_ERR, "I2C: Read error: %m");
		return -1;
	}
	pr_debug("I2C: Read from %08x:%d:%d@%02x+0x%x %d bytes ok",
	       chip_id, engine, port, device, offset_size ? offset : 0, length);

	return 0;
}

int i2c_write(uint32_t chip_id, uint8_t engine, uint8_t port,
	      uint16_t device, uint32_t offset_size, uint32_t offset,
	      uint32_t length, void* data)
{
	struct i2c_rdwr_ioctl_data ioargs;
	struct i2c_msg msg;
	int fd, size, i, rc;
	uint8_t *buf;

	if (offset_size > 4) {
		pr_log(LOG_ERR, "I2C: Invalid write offset_size %d",
				offset_size);
		return -1;
	}
	fd = i2c_get_dev(chip_id, engine, port, device);
	if (fd == -1)
		return -1;

	/* Not all kernel driver versions support breaking up a write into
	 * two components (offset, data), so we coalesce them first and
	 * issue a single write. The offset is laid out in BE format.
	 */
	size = offset_size + length;
	buf = malloc(size);
	if (!buf) {
		pr_log(LOG_ERR, "I2C: Out of memory");
		return -1;
	}

	/* The offset has a variable size so let's handle this properly
	 * as it has to be laid out in memory MSB first
	 */
	for (i = 0; i < offset_size; i++)
		buf[i] = offset >> (8 * (offset_size - i - 1));

	/* Copy the remaining data */
	memcpy(buf + offset_size, data, length);

	/* Build the message */
	msg.addr = device;
	msg.flags = 0;
	msg.buf = buf;
	msg.len = size;
	ioargs.msgs = &msg;
	ioargs.nmsgs = 1;
	rc = ioctl(fd, I2C_RDWR, &ioargs);
	free(buf);
	if (rc < 0) {
		pr_log(LOG_ERR, "I2C: Write error: %m");
		return -1;
	}

	return 0;
}

static void i2c_add_bus(uint32_t chip, uint32_t engine, uint32_t port,
			const char *devname)
{
	struct i2c_bus *b = malloc(sizeof(struct i2c_bus));
	char *dn;

	if (asprintf(&dn, "/dev/%s", devname) < 0) {
		pr_log(LOG_ERR, "I2C: Error creating devpath for %s: %m",
			devname);
		return;
	}

	memset(b, 0, sizeof(*b));
	b->chip_id = chip;
	b->engine = engine;
	b->port = port;
	b->devpath = dn;
	b->fd = -1;
	list_add(&bus_list, &b->link);
}

void i2c_init(void)
{
#define SYSFS	"/sys"	/* XXX Find it ? */
	DIR *devsdir;
	struct dirent *devent;
	char dpath[NAME_MAX];
	char busname[256];
	char *s;
	FILE *f;
	unsigned int chip, engine, port;

	/* Ensure i2c-dev is loaded */
	insert_module("i2c-dev");

	/* Get directory of i2c char devs in sysfs */
	devsdir = opendir(SYSFS "/class/i2c-dev");
	if (!devsdir) {
		pr_log(LOG_ERR, "I2C: Error opening "
					SYSFS "/class/i2c-dev: %m");
		return;
	}
	while ((devent = readdir(devsdir)) != NULL) {
		if (!strcmp(devent->d_name, "."))
			continue;
		if (!strcmp(devent->d_name, ".."))
			continue;

		/* Get bus name */
		sprintf(dpath, SYSFS "/class/i2c-dev/%s/name", devent->d_name);
		f = fopen(dpath, "r");
		if (!f) {
			pr_log(LOG_NOTICE, "I2C: Can't open %s: %m, skipping.",
					dpath);
			continue;
		}
		s = fgets(busname, sizeof(busname), f);
		fclose(f);
		if (!s) {
			pr_log(LOG_NOTICE, "Failed to read %s, skipping.",
					dpath);
			continue;
		}

		/* Is this a P8 or Centaur i2c bus ? No -> move on */
		if (strncmp(s, "p8_", 3) == 0)
			sscanf(s, "p8_%x_e%dp%d", &chip, &engine, &port);
		else if (strncmp(s, "cen_", 4) == 0)
			sscanf(s, "cen_%x_e%dp%d", &chip, &engine, &port);
		else
			continue;

		pr_log(LOG_INFO, "I2C: Found Chip: %08x engine %d port %d",
		       chip, engine, port);
		i2c_add_bus(chip, engine, port, devent->d_name);
	}
	closedir(devsdir);
}

