// Copyright(c) 2017-2020, Intel Corporation
//
// Redistribution  and  use  in source  and  binary  forms,  with  or  without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of  source code  must retain the  above copyright notice,
//   this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
//   this list of conditions and the following disclaimer in the documentation
//   and/or other materials provided with the distribution.
// * Neither the name  of Intel Corporation  nor the names of its contributors
//   may be used to  endorse or promote  products derived  from this  software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,  BUT NOT LIMITED TO,  THE
// IMPLIED WARRANTIES OF  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT  SHALL THE COPYRIGHT OWNER  OR CONTRIBUTORS BE
// LIABLE  FOR  ANY  DIRECT,  INDIRECT,  INCIDENTAL,  SPECIAL,  EXEMPLARY,  OR
// CONSEQUENTIAL  DAMAGES  (INCLUDING,  BUT  NOT LIMITED  TO,  PROCUREMENT  OF
// SUBSTITUTE GOODS OR SERVICES;  LOSS OF USE,  DATA, OR PROFITS;  OR BUSINESS
// INTERRUPTION)  HOWEVER CAUSED  AND ON ANY THEORY  OF LIABILITY,  WHETHER IN
// CONTRACT,  STRICT LIABILITY,  OR TORT  (INCLUDING NEGLIGENCE  OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,  EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

/*
 * @file fpgaconf.c
 *
 * @brief FPGA configure command line tool
 *
 * fpgaconf allows you to program green bitstream files to an FPGA supported by
 * the intel-fpga driver and API.
 *
 * Features:
 *   * Auto-discovery of compatible slots for supplied bitstream
 *   * Dry-run mode ("what would happen if...?")
 */
#define _GNU_SOURCE
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif // HAVE_CONFIG_H
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/stat.h>

#include <uuid/uuid.h>

#include <opae/fpga.h>
#include <libbitstream/bitstream.h>

/*
 * macro to check FPGA return codes, print error message, and goto cleanup label
 * NOTE: this changes the program flow (uses goto)!
 */
#define ON_ERR_GOTO(res, label, desc)                                          \
	do {                                                                   \
		if ((res) != FPGA_OK) {                                        \
			print_err((desc), (res));                              \
			goto label;                                            \
		}                                                              \
	} while (0)
/*
 * Global configuration, set during parse_args()
 */
#define MAX_FILENAME_LEN 256
struct config {
	unsigned int verbosity;
	bool dry_run;
	enum { INTERACTIVE, /* ask if ambiguous */
	       NORMAL,      /* stop if ambiguous */
	       AUTOMATIC    /* choose if ambiguous */
	} mode;
	int flags;
	struct target {
		int segment;
		int bus;
		int device;
		int function;
		int socket;
	} target;
	char *filename;
} config = {.verbosity = 0,
	    .dry_run = false,
	    .mode = NORMAL,
	    .flags = 0,
	    .target = {.segment = -1, .bus = -1, .device = -1, .function = -1, .socket = -1},
	    .filename = NULL };

/*
 * Print readable error message for fpga_results
 */
void print_err(const char *s, fpga_result res)
{
	fprintf(stderr, "Error %s: %s\n", s, fpgaErrStr(res));
}

/*
 * Print message depending on verbosity
 */
void print_msg(unsigned int verbosity, const char *s)
{
	if (config.verbosity >= verbosity)
		printf("%s\n", s);
}

/*
 * Print help
 * TODO: uncomment options as they are implemented
 */
void help(void)
{
	printf("\n"
	       "fpgaconf\n"
	       "FPGA configuration utility\n"
	       "\n"
	       "Usage:\n"
	       //"        fpgaconf [-hvnAIQ] [-B <bus>] [-D <device>] [-F
	       //<function>] [-S <socket-id>] <gbs>\n"
	       "        fpgaconf [-hvn] [-B <bus>] [-D <device>] [-F <function>] [-S <socket-id>] <gbs>\n"
	       "\n"
	       "                -h,--help           Print this help\n"
	       "                -V,--verbose        Increase verbosity\n"
	       "                -n,--dry-run        Don't actually perform actions\n"
	       "                --force             Don't try to open accelerator resource\n"
	       "                --segment           Set target segment number\n"
	       "                -B,--bus            Set target bus number\n"
	       "                -D,--device         Set target device number\n"
	       "                -F,--function       Set target function number\n"
	       "                -S,--socket-id      Set target socket number\n"
	       "                 -v,--version       Print version info and exit\n"
	       /* "                -A,--auto           Automatically choose
		  target slot if\n" */
	       /* "                                    multiple valid slots are
		  available\n" */
	       /* "                -I,--interactive    Prompt user to choose
		  target slot if\n" */
	       /* "                                    multiple valid slots are
		  available\n" */
	       /* "                -Q,--quiet          Don't print any messages
		  except errors\n" */
	       "\n");
}

/*
 * Parse command line arguments
 * TODO: uncomment options as they are implemented
 */
#define GETOPT_STRING ":hVnB:D:F:S:AIQv"
int parse_args(int argc, char *argv[])
{
	struct option longopts[] = {
		{"help",      no_argument,       NULL, 'h'},
		{"verbose",   no_argument,       NULL, 'V'},
		{"dry-run",   no_argument,       NULL, 'n'},
		{"segment",   required_argument, NULL, 0xe},
		{"bus",       required_argument, NULL, 'B'},
		{"device",    required_argument, NULL, 'D'},
		{"function",  required_argument, NULL, 'F'},
		{"socket-id", required_argument, NULL, 'S'},
		{"force",     no_argument,       NULL, 0xf},
		{"version",   no_argument,       NULL, 'v'},
		/* {"auto",          no_argument,       NULL, 'A'}, */
		/* {"interactive",   no_argument,       NULL, 'I'}, */
		/* {"quiet",         no_argument,       NULL, 'Q'}, */
		{0, 0, 0, 0} };

	int getopt_ret;
	int option_index;
	char *endptr = NULL;

	while (-1
	       != (getopt_ret = getopt_long(argc, argv, GETOPT_STRING, longopts,
					    &option_index))) {
		const char *tmp_optarg = optarg;

		if ((optarg) && ('=' == *tmp_optarg)) {
			++tmp_optarg;
		}

		switch (getopt_ret) {
		case 'h': /* help */
			help();
			return -1;

		case 'V': /* verbose */
			config.verbosity++;
			break;

		case 'n': /* dry-run */
			config.dry_run = true;
			break;

		case 0xf: /* force */
			config.flags |= FPGA_RECONF_FORCE;
			break;

		case 0xe: /* segment */
			if (NULL == tmp_optarg)
				break;
			endptr = NULL;
			config.target.segment =
				(int)strtoul(tmp_optarg, &endptr, 0);
			if (endptr != tmp_optarg + strlen(tmp_optarg)) {
				fprintf(stderr, "invalid segment: %s\n",
					tmp_optarg);
				return -1;
			}
			break;

		case 'B': /* bus */
			if (NULL == tmp_optarg)
				break;
			endptr = NULL;
			config.target.bus =
				(int)strtoul(tmp_optarg, &endptr, 0);
			if (endptr != tmp_optarg + strlen(tmp_optarg)) {
				fprintf(stderr, "invalid bus: %s\n",
					tmp_optarg);
				return -1;
			}
			break;

		case 'D': /* device */
			if (NULL == tmp_optarg)
				break;
			endptr = NULL;
			config.target.device =
				(int)strtoul(tmp_optarg, &endptr, 0);
			if (endptr != tmp_optarg + strlen(tmp_optarg)) {
				fprintf(stderr, "invalid device: %s\n",
					tmp_optarg);
				return -1;
			}
			break;

		case 'F': /* function */
			if (NULL == tmp_optarg)
				break;
			endptr = NULL;
			config.target.function =
				(int)strtoul(tmp_optarg, &endptr, 0);
			if (endptr != tmp_optarg + strlen(tmp_optarg)) {
				fprintf(stderr, "invalid function: %s\n",
					tmp_optarg);
				return -1;
			}
			break;

		case 'S': /* socket */
			if (NULL == tmp_optarg)
				break;
			endptr = NULL;
			config.target.socket =
				(int)strtoul(tmp_optarg, &endptr, 0);
			if (endptr != tmp_optarg + strlen(tmp_optarg)) {
				fprintf(stderr, "invalid socket: %s\n",
					tmp_optarg);
				return -1;
			}
			break;

		case 'A': /* auto */
			config.mode = AUTOMATIC;
			break;

		case 'I': /* interactive */
			config.mode = INTERACTIVE;
			break;

		case 'Q': /* quiet */
			config.verbosity = 0;
			break;

		case 'v': /* version */
			fprintf(stdout, "fpgaconf %s %s%s\n",
					OPAE_VERSION,
					OPAE_GIT_COMMIT_HASH,
					OPAE_GIT_SRC_TREE_DIRTY ? "*":"");
			return -1;

		case ':': /* missing option argument */
			fprintf(stderr, "Missing option argument\n");
			return -1;

		case '?':
		default: /* invalid option */
			fprintf(stderr, "Invalid cmdline options\n");
			return -1;
		}
	}

	/* use first non-option argument as GBS filename */
	if (optind == argc) {
		fprintf(stderr, "No GBS file\n");
		return -1;
	}
	config.filename = canonicalize_file_name(argv[optind]);
	if (config.filename) {
		return 0;
	} else {
		fprintf(stderr, "Error locating GBS file specified: \"%s\"\n", strerror(errno));
		return -1;
	}
}

fpga_result get_fpga_interface_id(fpga_token token, fpga_guid interface_id)
{
	fpga_result result = FPGA_OK;
	fpga_result resval = FPGA_OK;
	fpga_properties filter = NULL;
	fpga_objtype objtype;
	fpga_guid guid;

	result = fpgaGetProperties(token, &filter);
	if (result != FPGA_OK) {
		OPAE_ERR("Failed to get Token Properties Object");
		goto out;
	}

	result = fpgaPropertiesGetObjectType(filter, &objtype);
	if (result != FPGA_OK) {
		OPAE_ERR("Failed to get Token Properties Object");
		goto out_destroy;
	}

	if (objtype != FPGA_DEVICE) {
		OPAE_ERR("Invalid FPGA object type");
		result = FPGA_EXCEPTION;
		goto out_destroy;
	}

	result = fpgaPropertiesGetGUID(filter, &guid);
	if (result != FPGA_OK) {
		OPAE_ERR("Failed to get PR guid");
		goto out_destroy;
	}

	memcpy(interface_id, guid, sizeof(fpga_guid));

out_destroy:
	resval = (result != FPGA_OK) ? result : resval;
	result = fpgaDestroyProperties(&filter);
	if (result != FPGA_OK) {
		OPAE_ERR("Failed to destroy properties");
	}

out:
	resval = (result != FPGA_OK) ? result : resval;
	return resval;
}

/*
 * Prints Actual and Expected Interface id
 */
int print_interface_id(fpga_guid actual_interface_id)
{
	fpga_properties filter = NULL;
	uint32_t num_matches = 0;
	int retval = -1;
	fpga_handle fpga_handle = NULL;
	fpga_result res = -1;
	fpga_token fpga_token = NULL;
	fpga_guid expt_interface_id = {0};
	char guid_str[37] = {0};

	res = fpgaGetProperties(NULL, &filter);
	ON_ERR_GOTO(res, out_err, "creating properties object");

	res = fpgaPropertiesSetObjectType(filter, FPGA_DEVICE);
	ON_ERR_GOTO(res, out_destroy, "setting object type");

	if (-1 != config.target.segment) {
		res = fpgaPropertiesSetSegment(filter, config.target.segment);
		ON_ERR_GOTO(res, out_destroy, "setting segment");
	}

	if (-1 != config.target.bus) {
		res = fpgaPropertiesSetBus(filter, config.target.bus);
		ON_ERR_GOTO(res, out_destroy, "setting bus");
	}

	if (-1 != config.target.device) {
		res = fpgaPropertiesSetDevice(filter, config.target.device);
		ON_ERR_GOTO(res, out_destroy, "setting device");
	}

	if (-1 != config.target.function) {
		res = fpgaPropertiesSetFunction(filter, config.target.function);
		ON_ERR_GOTO(res, out_destroy, "setting function");
	}

	if (-1 != config.target.socket) {
		res = fpgaPropertiesSetSocketID(filter, config.target.socket);
		ON_ERR_GOTO(res, out_destroy, "setting socket id");
	}

	res = fpgaEnumerate(&filter, 1, &fpga_token, 1, &num_matches);
	ON_ERR_GOTO(res, out_destroy, "enumerating FPGAs");

	if (num_matches > 0) {
		retval = (int)num_matches; /* FPGA found */
	} else {
		retval = 0; /* no FPGA found */
		goto out_destroy;
	}

	res = fpgaOpen(fpga_token, &fpga_handle, 0);
	ON_ERR_GOTO(res, out_destroy, "opening fpga");

	res = get_fpga_interface_id(fpga_token, expt_interface_id);
	ON_ERR_GOTO(res, out_close, "interfaceid get");

	uuid_unparse(expt_interface_id, guid_str);
	printf("Expected Interface id:  %s\n", guid_str);

	uuid_unparse(actual_interface_id, guid_str);
	printf("Actual Interface id:    %s\n", guid_str);


out_close:
	res = fpgaClose(fpga_handle);
	ON_ERR_GOTO(res, out_destroy, "closing fme");

out_destroy:
	if (fpga_token)
		fpgaDestroyToken(&fpga_token);
	res = fpgaDestroyProperties(&filter); /* not needed anymore */
	ON_ERR_GOTO(res, out_err, "destroying properties object");
out_err:
	return retval;
}

/*
 * Find first FPGA matching the interface ID of the GBS
 *
 * @returns the total number of FPGAs matching the interface ID
 */
int find_fpga(fpga_guid interface_id, fpga_token *fpga)
{
	fpga_properties filter = NULL;
	uint32_t num_matches;
	fpga_result res;
	int retval = -1;

	/* Get number of FPGAs in system */
	res = fpgaGetProperties(NULL, &filter);
	ON_ERR_GOTO(res, out_err, "creating properties object");

	res = fpgaPropertiesSetObjectType(filter, FPGA_DEVICE);
	ON_ERR_GOTO(res, out_destroy, "setting object type");

	res = fpgaPropertiesSetGUID(filter, interface_id);
	ON_ERR_GOTO(res, out_destroy, "setting interface ID");

	if (-1 != config.target.segment) {
		res = fpgaPropertiesSetSegment(filter, config.target.segment);
		ON_ERR_GOTO(res, out_destroy, "setting segment");
	}

	if (-1 != config.target.bus) {
		res = fpgaPropertiesSetBus(filter, config.target.bus);
		ON_ERR_GOTO(res, out_destroy, "setting bus");
	}

	if (-1 != config.target.device) {
		res = fpgaPropertiesSetDevice(filter, config.target.device);
		ON_ERR_GOTO(res, out_destroy, "setting device");
	}

	if (-1 != config.target.function) {
		res = fpgaPropertiesSetFunction(filter, config.target.function);
		ON_ERR_GOTO(res, out_destroy, "setting function");
	}

	if (-1 != config.target.socket) {
		res = fpgaPropertiesSetSocketID(filter, config.target.socket);
		ON_ERR_GOTO(res, out_destroy, "setting socket id");
	}

	res = fpgaEnumerate(&filter, 1, fpga, 1, &num_matches);
	ON_ERR_GOTO(res, out_destroy, "enumerating FPGAs");

	if (num_matches > 0) {
		retval = (int)num_matches; /* FPGA found */
	} else {
		retval = 0; /* no FPGA found */
	}

out_destroy:
	res = fpgaDestroyProperties(&filter); /* not needed anymore */
	ON_ERR_GOTO(res, out_err, "destroying properties object");
out_err:
	return retval;
}

int program_bitstream(fpga_token token, uint32_t slot_num,
		      opae_bitstream_info *info, int flags)
{
	fpga_handle handle;
	fpga_result res;

	print_msg(2, "Opening FPGA");
	res = fpgaOpen(token, &handle, 0);
	ON_ERR_GOTO(res, out_err, "opening FPGA");

	print_msg(1, "Writing bitstream");
	if (config.dry_run) {
		print_msg(1, "[--dry-run] Skipping reconfiguration");
	} else {
		res = fpgaReconfigureSlot(handle, slot_num, info->data,
					  info->data_len, flags);
		ON_ERR_GOTO(res, out_close, "writing bitstream to FPGA");
	}

	print_msg(2, "Closing FPGA");
	res = fpgaClose(handle);
	ON_ERR_GOTO(res, out_err, "closing FPGA");
	return 1;

out_close:
	res = fpgaClose(handle);
	ON_ERR_GOTO(res, out_err, "closing FPGA");
out_err:
	return -1;
}


int main(int argc, char *argv[])
{
	int res;
	fpga_result result;
	int retval = 0;
	opae_bitstream_info info;
	fpga_token token;
	uint32_t slot_num = 0; /* currently, we don't support multiple slots */

	/* parse command line arguments */
	res = parse_args(argc, argv);
	if (res < 0) {
		retval = 1;
		goto out_exit;
	}

	if (config.dry_run)
		printf("--dry-run is set\n");

	/* allocate memory and read bitstream data */
	print_msg(1, "Reading bitstream");
	result = opae_load_bitstream(config.filename, &info);
	if (result != FPGA_OK) {
		retval = 2;
		goto out_exit;
	}

	/* find suitable slot */
	print_msg(1, "Looking for slot");
	res = find_fpga(info.pr_interface_id, &token);
	if (res < 0) {
		retval = 3;
		goto out_free;
	}
	if (res == 0) {
		fprintf(stderr, "No suitable slots found.\n");
		retval = 4;
		if (config.verbosity > 0)
			print_interface_id(info.pr_interface_id);
		goto out_free;
	}
	if (res > 1) {
		fprintf(stderr,
			"Found more than one suitable slot, please be more specific.\n");
		retval = 5;
		goto out_destroy;
	}
	print_msg(1, "Found slot");

	/* program bitstream */
	print_msg(1, "Programming bitstream");
	res = program_bitstream(token, slot_num, &info, config.flags);
	if (res < 0) {
		retval = 5;
		goto out_destroy;
	}
	print_msg(1, "Done");

	/* clean up */
out_destroy:
	fpgaDestroyToken(&token);
out_free:
	opae_unload_bitstream(&info);
out_exit:
	if (config.filename) {
		free(config.filename);
		config.filename = NULL;
	}
	return retval;
}
