// SPDX-License-Identifier: BSD-3-Clause
/*
 * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
 */
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#include "file.h"
#include "json.h"
#include "list.h"
#include "firehose.h"
#include "qdl.h"

enum {
	QDL_FILE_UNKNOWN,
	QDL_FILE_PATCH,
	QDL_FILE_PROGRAM,
};

static int flashmap_resolve_path(char *path, size_t path_size, const char *filename, const char *incdir)
{
	if (!filename) {
		ux_err("flashmap: filename is null\n");
		return -1;
	}

	if (incdir) {
		snprintf(path, path_size, "%s/%s", incdir, filename);
		if (access(path, F_OK))
			snprintf(path, path_size, "%s", filename);
	} else {
		snprintf(path, path_size, "%s", filename);
	}

	return 0;
}

static int flashmap_get_legacy_programmer(struct qdl_zip *zip, struct json_value *layout,
					  struct sahara_image *images, const char *incdir)
{
	struct json_value *programmers;
	const char *filename;
	char path[PATH_MAX];
	int count;

	programmers = json_get_child(layout, "programmer");
	if (!programmers) {
		ux_err("flashmap: parse error when decoding programmer\n");
		return -1;
	}

	count = json_count_children(programmers);
	if (count != 1) {
		ux_err("flashmap: single programmer expected, found %d\n", count);
		return -1;
	}

	filename = json_get_element_string(programmers, 0);
	if (!filename) {
		ux_err("flashmap: parse error when decoding programmer\n");
		return -1;
	}

	if (flashmap_resolve_path(path, sizeof(path), filename, incdir))
		return -1;

	ux_debug("flashmap: selected programmer: %s\n", path);

	return load_sahara_image(zip, path, &images[SAHARA_ID_EHOSTDL_IMG]);
}

static int flashmap_get_programmer_map(struct qdl_zip *zip, struct json_value *layout,
				       struct sahara_image *images, const char *incdir)
{
	struct json_value *programmers;
	struct json_value *entry;
	unsigned long image_id;
	const char *filename;
	char *end;
	char path[PATH_MAX];
	int count = 0;
	int ret;

	programmers = json_get_child(layout, "programmer");
	if (!programmers || programmers->type != JSON_TYPE_OBJECT) {
		ux_err("flashmap: programmer map must be an object for version 1.2.0-qdl\n");
		return -1;
	}

	for (entry = programmers->u.value; entry; entry = entry->next) {
		errno = 0;
		image_id = strtoul(entry->key, &end, 0);
		if (errno || end == entry->key || *end || image_id == 0 || image_id >= MAPPING_SZ) {
			ux_err("flashmap: invalid programmer image id \"%s\"\n", entry->key);
			return -1;
		}

		if (entry->type != JSON_TYPE_STRING || !entry->u.string) {
			ux_err("flashmap: programmer entry \"%s\" must be a filename string\n", entry->key);
			return -1;
		}

		filename = entry->u.string;
		ret = flashmap_resolve_path(path, sizeof(path), filename, incdir);
		if (ret)
			return ret;

		ux_debug("flashmap: selected programmer %lu: %s\n", image_id, path);

		ret = load_sahara_image(zip, path, &images[image_id]);
		if (ret)
			return ret;

		count++;
	}

	if (!count) {
		ux_err("flashmap: programmer map is empty\n");
		return -1;
	}

	return 0;
}

static int flashmap_get_programmers(struct qdl_zip *zip, struct json_value *layout,
				    struct sahara_image *images, const char *incdir,
				    bool uses_programmer_map)
{
	if (uses_programmer_map)
		return flashmap_get_programmer_map(zip, layout, images, incdir);

	return flashmap_get_legacy_programmer(zip, layout, images, incdir);
}

static int flashmap_load_xml(struct list_head *ops, struct qdl_zip *zip, const char *filename,
			     bool is_nand, const char *incdir)
{
	struct qdl_file file;
	xmlNode *node;
	xmlNode *root;
	xmlDoc *doc;
	size_t len;
	void *xml;
	int type = QDL_FILE_UNKNOWN;
	int ret;

	ret = qdl_file_open(zip, filename, &file);
	if (ret < 0) {
		ux_err("unable to parse XML: %s\n", filename);
		return -1;
	}

	xml = qdl_file_load(&file, &len);
	if (!xml) {
		ux_err("failed to load \"%s\"\n", filename);
		ret = -1;
		goto out_close_file;
	}

	if (len > INT_MAX) {
		ux_err("\"%s\" too large for XML parser\n", filename);
		ret = -1;
		goto out_free_xml;
	}

	doc = xmlReadMemory(xml, (int)len, filename, NULL, 0);
	if (!doc) {
		ux_err("failed to parse %s\n", filename);
		ret = -1;
		goto out_free_xml;
	}

	root = xmlDocGetRootElement(doc);
	if (!root) {
		ux_err("unable to find XML root in \"%s\"\n", filename);
		ret = -1;
		goto out_free_doc;
	}

	if (!xmlStrcmp(root->name, (xmlChar *)"patches")) {
		type = QDL_FILE_PATCH;
	} else if (!xmlStrcmp(root->name, (xmlChar *)"data")) {
		type = QDL_FILE_PROGRAM;
		for (node = root->children; node ; node = node->next) {
			if (node->type != XML_ELEMENT_NODE)
				continue;
			if (!xmlStrcmp(node->name, (xmlChar *)"program") ||
			    !xmlStrcmp(node->name, (xmlChar *)"erase")) {
				type = QDL_FILE_PROGRAM;
				break;
			}
		}
	}

	switch (type) {
	case QDL_FILE_PROGRAM:
		ret = program_load_xml(ops, doc, zip, filename, is_nand, false, NULL, incdir);
		break;
	case QDL_FILE_PATCH:
		ret = patch_load_xml(ops, doc, filename);
		break;
	default:
		ux_err("unknown file type: %s\n", filename);
		ret = -1;
		break;
	}

out_free_doc:
	xmlFreeDoc(doc);
out_free_xml:
	free(xml);
out_close_file:
	qdl_file_close(&file);

	return ret;
}

static int flashmap_enumerate_programmables(struct json_value *list, struct list_head *ops,
					    struct qdl_zip *zip, const char *incdir)
{
	struct json_value *programmable;
	struct json_value *files;
	struct firehose_op *op;
	char path[PATH_MAX];
	const char *memory;
	const char *file;
	int file_count;
	int file_idx;
	bool is_nand;
	double slot;
	int count;
	int ret;
	int i;

	count = json_count_children(list);
	if (count < 0) {
		ux_err("flashmap: programmable list is not an array\n");
		return -1;
	}
	if (count == 0) {
		ux_err("flashmap: programmable list is empty\n");
		return -1;
	}

	for (i = 0; i < count; i++) {
		programmable = json_get_element_object(list, i);

		memory = json_get_string(programmable, "memory");
		ret = json_get_number(programmable, "slot", &slot);
		files = json_get_child(programmable, "files");

		if (!memory || ret < 0 || !files) {
			ux_err("failed to parse flashmap\n");
			return -1;
		}

		is_nand = !strcmp(memory, "nand");

		op = firehose_alloc_op(FIREHOSE_OP_CONFIGURE);
		op->storage_type = decode_storage_type(memory);
		list_append(ops, &op->node);

		file_count = json_count_children(files);
		if (file_count < 0) {
			ux_err("flashmap: files list is not an array\n");
			return -1;
		}

		for (file_idx = 0; file_idx < file_count; file_idx++) {
			file = json_get_element_string(files, file_idx);
			if (!file) {
				ux_err("flashmap: parse error when decoding files\n");
				return -1;
			}

			if (flashmap_resolve_path(path, sizeof(path), file, incdir))
				return -1;

			ret = flashmap_load_xml(ops, zip, path, is_nand, incdir);
			if (ret)
				return ret;
		}
	}

	return 0;
}

int flashmap_load(struct list_head *ops, const char *filename, char *specifier,
		  struct sahara_image *images, const char *incdir)
{
	struct list_head flashmap_ops = LIST_INIT(flashmap_ops);
	enum qdl_storage_type current_type = QDL_STORAGE_UNKNOWN;
	struct json_value *programmable;
	struct json_value *product;
	struct json_value *layout;
	struct firehose_op *op;
	struct firehose_op *next;
	struct qdl_file flashmap;
	struct json_value *json;
	struct json_value *obj;
	struct qdl_zip *zip;
	unsigned int type_filter = 0;
	unsigned int matched_ops = 0;
	char *filter = specifier;
	char *save = NULL;
	char *tmp;
	enum qdl_storage_type type;
	bool uses_programmer_map = false;
	const char *version;
	const char *name;
	size_t json_size;
	void *json_blob;
	int count;
	int ret = -1;

	if (filter) {
		for (tmp = strtok_r(filter, ",", &save); tmp; tmp = strtok_r(NULL, ",", &save)) {
			type = decode_storage_type(tmp);
			if (type == QDL_STORAGE_UNKNOWN) {
				ux_err("unknown storage type \"%s\"\n", tmp);
				return -1;
			}

			type_filter |= 1U << type;
		}
	}

	if (!type_filter)
		type_filter = ~0U;

	ret = qdl_zip_open(filename, &zip);
	if (ret < 0) {
		ux_err("unable to create zip reference\n");
		return -1;
	}

	ret = qdl_file_open(zip, zip ? "flashmap.json" : filename, &flashmap);
	if (ret < 0) {
		ux_err("failed to open flashmap\n");
		goto out_put_zip;
	}

	json_blob = qdl_file_load(&flashmap, &json_size);
	qdl_file_close(&flashmap);
	if (!json_blob)
		goto out_put_zip;

	json = json_parse_buf(json_blob, json_size);
	if (!json) {
		ux_err("failed to parse flashmap json\n");
		goto out_free_blob;
	}

	version = json_get_string(json, "version");
	if (!version) {
		ux_err("unsupported flashmap version\n");
		ret = -1;
		goto out_free_json;
	}

	if (!strcmp(version, "1.2.0-qdl"))
		uses_programmer_map = true;
	else if (strcmp(version, "1.1.0")) {
		ux_err("unsupported flashmap version\n");
		ret = -1;
		goto out_free_json;
	}

	ux_debug("found flashmap of version: %s\n", version);

	obj = json_get_child(json, "products");

	count = json_count_children(obj);
	if (count != 1) {
		ux_err("flashmap: only single product map supported, found %d\n", count);
		ret = -1;
		goto out_free_json;
	}

	product = json_get_element_object(obj, 0);

	name = json_get_string(product, "name");
	ux_debug("product: %s\n", name);

	obj = json_get_child(product, "layouts");

	count = json_count_children(obj);
	if (count != 1) {
		ux_err("flashmap: only one layout supported, found %d\n", count);
		ret = -1;
		goto out_free_json;
	}

	layout = json_get_element_object(obj, 0);

	ret = flashmap_get_programmers(zip, layout, images, zip ? NULL : incdir, uses_programmer_map);
	if (ret)
		goto out_free_json;

	programmable = json_get_child(layout, "programmable");
	if (!programmable) {
		ux_err("flashmap: parse error when decoding programmables\n");
		sahara_images_free(images, MAPPING_SZ);
		ret = -1;
		goto out_free_json;
	}

	ret = flashmap_enumerate_programmables(programmable, &flashmap_ops, zip, zip ? NULL : incdir);
	if (ret < 0) {
		firehose_free_ops(&flashmap_ops);
		sahara_images_free(images, MAPPING_SZ);
		goto out_free_json;
	}

	list_for_each_entry_safe(op, next, &flashmap_ops, node) {
		if (op->storage_type != QDL_STORAGE_UNKNOWN)
			current_type = op->storage_type;

		if ((1U << current_type) & type_filter) {
			list_del(&op->node);
			list_append(ops, &op->node);
			matched_ops++;
		}
	}

	firehose_free_ops(&flashmap_ops);

	if (!matched_ops) {
		ux_err("loaded flashmap does not contain any operations for selected storage type\n");
		ret = -1;
	}

out_free_json:
	json_free(json);
out_free_blob:
	free(json_blob);
out_put_zip:
	qdl_zip_put(zip);

	return ret;
}
