/* 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 <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 "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, uint32_t pos, void *buf, uint32_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);
		/* errno should remain set */
		if (rc == -1 || rc == 0)
			return FLASH_ERR_BAD_READ;

		count += rc;
	}

	return 0;
}

static int file_write(struct blocklevel_device *bl, uint32_t dst, const void *src,
		uint32_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);
		/* errno should remain set */
		if (rc == -1)
			return FLASH_ERR_VERIFY_FAILURE;

		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, uint32_t dst, uint32_t len)
{
	unsigned long long int d = ULLONG_MAX;
	int i = 0;
	int rc;

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

	return 0;
}

static int mtd_erase(struct blocklevel_device *bl, uint32_t dst, uint32_t len)
{
	struct file_data *file_data = container_of(bl, struct file_data, bl);
	struct erase_info_user erase_info = {
		.start = dst,
		.length = len
	};

	if (ioctl(file_data->fd, MEMERASE, &erase_info) == -1)
		return FLASH_ERR_PARM_ERROR;

	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,
		uint32_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,
		uint32_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);
	}
}
