/* Copyright 2013-2014 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
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <limits.h>

#include <ccan/container_of/container_of.h>

#include <mtd/mtd-abi.h>

#include "libflash.h"
#include "libflash/file.h"
#include "blocklevel.h"

struct file_data {
	int fd;
	char *name;
	char *path;
	struct blocklevel_device bl;
};

static int file_release(struct blocklevel_device *bl)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	close(file_data->fd);
	file_data->fd = -1;
	return 0;
}

static int file_reacquire(struct blocklevel_device *bl)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	int fd;

	fd = open(file_data->path, O_RDWR);
	if (fd == -1)
		return FLASH_ERR_PARM_ERROR;
	file_data->fd = fd;
	return 0;
}

static int file_read(struct blocklevel_device *bl, uint64_t pos, void *buf, uint64_t len)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	int rc, count = 0;

	rc = lseek(file_data->fd, pos, SEEK_SET);
	/* errno should remain set */
	if (rc != pos)
		return FLASH_ERR_PARM_ERROR;

	while (count < len) {
		rc = read(file_data->fd, buf, len - count);
		/* errno should remain set */
		if (rc == -1 || rc == 0)
			return FLASH_ERR_BAD_READ;

		buf += rc;
		count += rc;
	}

	return 0;
}

static int file_write(struct blocklevel_device *bl, uint64_t dst, const void *src,
		uint64_t len)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	int rc, count = 0;

	rc = lseek(file_data->fd, dst, SEEK_SET);
	/* errno should remain set */
	if (rc != dst)
		return FLASH_ERR_PARM_ERROR;

	while (count < len) {
		rc = write(file_data->fd, src, len - count);
		/* errno should remain set */
		if (rc == -1)
			return FLASH_ERR_VERIFY_FAILURE;

		src += rc;
		count += rc;
	}

	return 0;
}

/*
 * Due to to the fact these interfaces are ultimately supposed to deal with
 * flash, an erase function must be implemented even when the flash images
 * are backed by regular files.
 * Also, erasing flash leaves all the bits set to 1. This may be expected
 * by higher level functions so this function should also emulate that
 */
static int file_erase(struct blocklevel_device *bl, uint64_t dst, uint64_t len)
{
	static char buf[4096];
	int i = 0;
	int rc;

	memset(buf, ~0, sizeof(buf));

	while (len - i > 0) {
		rc = file_write(bl, dst + i, buf, len - i > sizeof(buf) ? sizeof(buf) : len - i);
		if (rc)
			return rc;
		i += (len - i > sizeof(buf)) ? sizeof(buf) : len - i;
	}

	return 0;
}

static int mtd_erase(struct blocklevel_device *bl, uint64_t dst, uint64_t len)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	int err;

	FL_DBG("%s: dst: 0x%" PRIx64 ", len: 0x%" PRIx64 "\n", __func__, dst, len);

	/*
	 * Some kernels that pflash supports do not know about the 64bit
	 * version of the ioctl() therefore we'll just use the 32bit (which
	 * should always be supported...) unless we MUST use the 64bit and
	 * then lets just hope the kernel knows how to deal with it. If it
	 * is unsupported the ioctl() will fail and we'll report that -
	 * there is no other option.
	 *
	 * Furthermore, even very recent MTD layers and drivers aren't
	 * particularly good at not blocking in the kernel. This creates
	 * unexpected behaviour in userspace tools using these functions.
	 * In the absence of significant work inside the kernel, we'll just
	 * split stuff up here for convenience.
	 * We can assume everything is aligned here.
	 */
	while (len) {
		if (dst > UINT_MAX || len > UINT_MAX) {
			struct erase_info_user64 erase_info = {
				.start = dst,
				.length = file_data->bl.erase_mask + 1
			};

			if (ioctl(file_data->fd, MEMERASE64, &erase_info) == -1) {
				err = errno;
				if (err == 25) /* Kernel doesn't do 64bit MTD erase ioctl() */
					FL_DBG("Attempted a 64bit erase on a kernel which doesn't support it\n");
				FL_ERR("%s: IOCTL to kernel failed! %s\n", __func__, strerror(err));
				errno = err;
				return FLASH_ERR_PARM_ERROR;
			}
		} else {
			struct erase_info_user erase_info = {
				.start = dst,
				.length = file_data->bl.erase_mask + 1
			};
			if (ioctl(file_data->fd, MEMERASE, &erase_info) == -1) {
				err = errno;
				FL_ERR("%s: IOCTL to kernel failed! %s\n", __func__, strerror(err));
				errno = err;
				return FLASH_ERR_PARM_ERROR;
			}
		}
		dst += file_data->bl.erase_mask + 1;
		len -= file_data->bl.erase_mask + 1;
	}
	return 0;
}

static int get_info_name(struct file_data *file_data, char **name)
{
	char *path, *lpath;
	int len;
	struct stat st;

	if (asprintf(&path, "/proc/self/fd/%d", file_data->fd) == -1)
		return FLASH_ERR_MALLOC_FAILED;

	if (lstat(path, &st)) {
		free(path);
		return FLASH_ERR_PARM_ERROR;
	}

	lpath = malloc(st.st_size + 1);
	if (!lpath) {
		free(path);
		return FLASH_ERR_MALLOC_FAILED;
	}

	len = readlink(path, lpath, st.st_size +1);
	if (len == -1) {
		free(path);
		free(lpath);
		return FLASH_ERR_PARM_ERROR;
	}
	lpath[len] = '\0';

	*name = lpath;

	free(path);
	return 0;
}


static int mtd_get_info(struct blocklevel_device *bl, const char **name,
		uint64_t *total_size, uint32_t *erase_granule)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	struct mtd_info_user mtd_info;
	int rc;

	rc = ioctl(file_data->fd, MEMGETINFO, &mtd_info);
	if (rc == -1)
		return FLASH_ERR_BAD_READ;

	if (total_size)
		*total_size = mtd_info.size;

	if (erase_granule)
		*erase_granule = mtd_info.erasesize;

	if (name) {
		rc = get_info_name(file_data, &(file_data->name));
		if (rc)
			return rc;
		*name = file_data->name;
	}

	return 0;
}

static int file_get_info(struct blocklevel_device *bl, const char **name,
		uint64_t *total_size, uint32_t *erase_granule)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	struct stat st;
	int rc;

	if (fstat(file_data->fd, &st))
		return FLASH_ERR_PARM_ERROR;

	if (total_size)
		*total_size = st.st_size;

	if (erase_granule)
		*erase_granule = 1;

	if (name) {
		rc = get_info_name(file_data, &(file_data->name));
		if (rc)
			return rc;
		*name = file_data->name;
	}

	return 0;
}

int file_init(int fd, struct blocklevel_device **bl)
{
	struct file_data *file_data;
	struct stat sbuf;

	if (!bl)
		return FLASH_ERR_PARM_ERROR;

	*bl = NULL;

	file_data = calloc(1, sizeof(struct file_data));
	if (!file_data)
		return FLASH_ERR_MALLOC_FAILED;

	file_data->fd = fd;
	file_data->bl.reacquire = &file_reacquire;
	file_data->bl.release = &file_release;
	file_data->bl.read = &file_read;
	file_data->bl.write = &file_write;
	file_data->bl.erase = &file_erase;
	file_data->bl.get_info = &file_get_info;
	file_data->bl.erase_mask = 0;

	/*
	 * If the blocklevel_device is only inited with file_init() then keep
	 * alive is assumed, as fd will change otherwise and this may break
	 * callers assumptions.
	 */
	file_data->bl.keep_alive = 1;

	/*
	 * Unfortunately not all file descriptors are created equal...
	 * Here we check to see if the file descriptor is to an MTD device, in
	 * which case we have to erase and get the size of it differently.
	 */
	if (fstat(file_data->fd, &sbuf) == -1)
		goto out;

	/* Won't be able to handle other than MTD devices for now */
	if (S_ISCHR(sbuf.st_mode)) {
		file_data->bl.erase = &mtd_erase;
		file_data->bl.get_info = &mtd_get_info;
		file_data->bl.flags = WRITE_NEED_ERASE;
		mtd_get_info(&file_data->bl, NULL, NULL, &(file_data->bl.erase_mask));
		file_data->bl.erase_mask--;
	} else if (!S_ISREG(sbuf.st_mode)) {
		/* If not a char device or a regular file something went wrong */
		goto out;
	}

	*bl = &(file_data->bl);
	return 0;
out:
	free(file_data);
	return FLASH_ERR_PARM_ERROR;
}

int file_init_path(const char *path, int *r_fd, bool keep_alive,
		struct blocklevel_device **bl)
{
	int fd, rc;
	char *path_ptr = NULL;
	struct file_data *file_data;

	if (!path || !bl)
		return FLASH_ERR_PARM_ERROR;

	fd = open(path, O_RDWR);
	if (fd == -1)
		return FLASH_ERR_PARM_ERROR;

	/*
	 * strdup() first so don't have to deal with malloc failure after
	 * file_init()
	 */
	path_ptr = strdup(path);
	if (!path_ptr) {
		rc = FLASH_ERR_MALLOC_FAILED;
		goto out;
	}

	rc = file_init(fd, bl);
	if (rc)
		goto out;

	file_data = container_of(*bl, struct file_data, bl);
	file_data->bl.keep_alive = keep_alive;
	file_data->path = path_ptr;

	if (r_fd)
		*r_fd = fd;

	return rc;
out:
	free(path_ptr);
	close(fd);
	return rc;
}

void file_exit(struct blocklevel_device *bl)
{
	struct file_data *file_data;
	if (bl) {
		free(bl->ecc_prot.prot);
		file_data = container_of(bl, struct file_data, bl);
		free(file_data->name);
		free(file_data->path);
		free(file_data);
	}
}

void file_exit_close(struct blocklevel_device *bl)
{
	struct file_data *file_data;
	if (bl) {
		file_data = container_of(bl, struct file_data, bl);
		close(file_data->fd);
		file_exit(bl);
	}
}
