/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (C) 2018-2020 Michael Drake <tlsa@netsurf-browser.org>
 */

/**
 * \file
 * \brief Save client's data structure to YAML, using schema.
 *
 * This uses `libyaml` to emit YAML documents, it uses the client-provided
 * schema to access the client data, and validates it before emitting the YAML.
 */

#include <inttypes.h>
#include <stdbool.h>
#include <assert.h>
#include <limits.h>

#include <yaml.h>

#include "mem.h"
#include "data.h"
#include "util.h"

/**
 * A CYAML save state machine stack entry.
 */
typedef struct cyaml_state {
	/** Current save state machine state. */
	enum cyaml_state_e state;
	/** Schema for the expected value in this state. */
	const cyaml_schema_value_t *schema;
	/** Anonymous union for schema type specific state. */
	union {
		/**
		 * Additional state for \ref CYAML_STATE_IN_MAP_KEY and
		 *  \ref CYAML_STATE_IN_MAP_VALUE states.
		 */
		struct {
			const cyaml_schema_field_t *field;
		} mapping;
		/**  Additional state for \ref CYAML_STATE_IN_SEQUENCE state. */
		struct {
			uint64_t entry;
			uint64_t count;
		} sequence;
	};
	const uint8_t *data; /**< Start of client value data for this state. */
	bool done; /**< Whether the state has been handled. */
} cyaml_state_t;

/**
 * Internal YAML saving context.
 */
typedef struct cyaml_ctx {
	const cyaml_config_t *config; /**< Settings provided by client. */
	cyaml_state_t *state;   /**< Current entry in state stack, or NULL. */
	cyaml_state_t *stack;   /**< State stack */
	uint32_t stack_idx;     /**< Next (empty) state stack slot */
	uint32_t stack_max;     /**< Current stack allocation limit. */
	unsigned seq_count;     /**< Top-level sequence count. */
	yaml_emitter_t *emitter;  /**< Internal libyaml parser object. */
} cyaml_ctx_t;

/**
 * Ensure that the CYAML save context has space for a new stack entry.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__stack_ensure(
		cyaml_ctx_t *ctx)
{
	cyaml_state_t *temp;
	uint32_t max = ctx->stack_max + 16;

	if (ctx->stack_idx < ctx->stack_max) {
		return CYAML_OK;
	}

	temp = cyaml__realloc(ctx->config, ctx->stack, 0,
			sizeof(*ctx->stack) * max, false);
	if (temp == NULL) {
		return CYAML_ERR_OOM;
	}

	ctx->stack = temp;
	ctx->stack_max = max;
	ctx->state = ctx->stack + ctx->stack_idx - 1;

	return CYAML_OK;
}

/**
 * Helper to simplify emitting libyaml events.
 *
 * This is a slightly peculiar function, but it is intended to reduce the
 * boilerplate required to emit events.
 *
 * Intended usage is something like:
 *
 * ```
 * cyaml_err_t err;
 * yaml_event_t event;
 * int ret = yaml_mapping_end_event_initialize(&event);
 * err = cyaml__emit_event_helper(ctx, ret, &event);
 * ```
 *
 * \param[in]  ctx    The CYAML saving context.
 * \param[in]  valid  Whether the event pointer is valid.  Typically this
 *                    will be the value returned by the libyaml call that
 *                    initialised the event.  As such, if `valid` is non-zero,
 *                    the helper will try to emit the event, otherwise, it
 *                    will return \ref CYAML_ERR_LIBYAML_EVENT_INIT.
 * \param[in]  event  The event to try to emit, if valid.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__emit_event_helper(
		const cyaml_ctx_t *ctx,
		int valid,
		yaml_event_t *event)
{
	if (valid == 0) {
		cyaml__log(ctx->config, CYAML_LOG_ERROR,
				"Save: LibYAML: Failed to initialise event\n");
		return CYAML_ERR_LIBYAML_EVENT_INIT;
	}

	/* Emit event and update save state stack. */
	valid = yaml_emitter_emit(ctx->emitter, event);
	if (valid == 0) {
		cyaml__log(ctx->config, CYAML_LOG_ERROR,
				"Save: LibYAML: Failed to emit event: %s\n",
				ctx->emitter->problem);
		return CYAML_ERR_LIBYAML_EMITTER;
	}

	return CYAML_OK;
}

/** The style to use when emitting mappings and sequences. */
enum cyaml_emit_style {
	CYAML_EMIT_STYLE_DEFAULT,
	CYAML_EMIT_STYLE_BLOCK,
	CYAML_EMIT_STYLE_FLOW,
};

/**
 * Get the style to use for mappings/sequences from value flags and config.
 *
 * As described in the API, schema flags take priority over config flags, and
 * block has precedence over flow, if both flags are set at the same level.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The CYAML schema for the value expected in state.
 * \return The generic style to emit the value with.
 */
static inline enum cyaml_emit_style cyaml__get_emit_style(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema)
{
	if (schema->flags & CYAML_FLAG_BLOCK) {
		return CYAML_EMIT_STYLE_BLOCK;

	} else if (schema->flags & CYAML_FLAG_FLOW) {
		return CYAML_EMIT_STYLE_FLOW;

	} else if (ctx->config->flags & CYAML_CFG_STYLE_BLOCK) {
		return CYAML_EMIT_STYLE_BLOCK;

	} else if (ctx->config->flags & CYAML_CFG_STYLE_FLOW) {
		return CYAML_EMIT_STYLE_FLOW;
	}

	return CYAML_EMIT_STYLE_DEFAULT;
}

/**
 * Get the style to use for sequences from value flags and config.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The CYAML schema for the value expected in state.
 * \return The libyaml sequence style to emit the value with.
 */
static inline yaml_sequence_style_t cyaml__get_emit_style_seq(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema)
{
	switch (cyaml__get_emit_style(ctx, schema)) {
	case CYAML_EMIT_STYLE_BLOCK: return YAML_BLOCK_SEQUENCE_STYLE;
	case CYAML_EMIT_STYLE_FLOW:  return YAML_FLOW_SEQUENCE_STYLE;
	default: break;
	}

	return YAML_ANY_SEQUENCE_STYLE;
}

/**
 * Get the style to use for mappings from value flags and config.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The CYAML schema for the value expected in state.
 * \return The libyaml mapping style to emit the value with.
 */
static inline yaml_mapping_style_t cyaml__get_emit_style_map(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema)
{
	switch (cyaml__get_emit_style(ctx, schema)) {
	case CYAML_EMIT_STYLE_BLOCK: return YAML_BLOCK_MAPPING_STYLE;
	case CYAML_EMIT_STYLE_FLOW:  return YAML_FLOW_MAPPING_STYLE;
	default: break;
	}

	return YAML_ANY_MAPPING_STYLE;
}

/**
 * Get the style to use for scalar values from value flags.
 *
 * \param[in]  schema  The CYAML schema for the scaler value to be emitted.
 * \return The libyaml scalar style to emit the value with.
 */
static inline yaml_scalar_style_t cyaml__get_emit_style_scalar(
		const cyaml_schema_value_t *schema)
{
	/* Consult flags in order of decreasing priority. */
	if (schema->flags & CYAML_FLAG_SCALAR_QUOTE_DOUBLE) {
		return YAML_DOUBLE_QUOTED_SCALAR_STYLE;

	} else if (schema->flags & CYAML_FLAG_SCALAR_QUOTE_SINGLE) {
		return YAML_SINGLE_QUOTED_SCALAR_STYLE;

	} else if (schema->flags & CYAML_FLAG_SCALAR_LITERAL) {
		return YAML_LITERAL_SCALAR_STYLE;

	} else if (schema->flags & CYAML_FLAG_SCALAR_FOLDED) {
		return YAML_FOLDED_SCALAR_STYLE;

	} else if (schema->flags & CYAML_FLAG_SCALAR_PLAIN) {
		return YAML_PLAIN_SCALAR_STYLE;
	}

	return YAML_ANY_SCALAR_STYLE;
}

/**
 * Helper to discern whether to emit document delimiting marks.
 *
 * These are "---" for document start, and "..." for document end.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \return true if delimiters should be emitted, false otherwise.
 */
static inline bool cyaml__emit_doc_delim(
		const cyaml_ctx_t *ctx)
{
	return ctx->config->flags & CYAML_CFG_DOCUMENT_DELIM;
}

/**
 * Emit a YAML start event for the state being pushed to the stack.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  state   The CYAML save state we're pushing a stack entry for.
 * \param[in]  schema  The CYAML schema for the value expected in state.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__stack_push_write_event(
		const cyaml_ctx_t *ctx,
		enum cyaml_state_e state,
		const cyaml_schema_value_t *schema)
{
	yaml_event_t event;
	int ret;

	/* Create any appropriate event for the new state. */
	switch (state) {
	case CYAML_STATE_START:
		ret = yaml_stream_start_event_initialize(&event,
				YAML_UTF8_ENCODING);
		break;
	case CYAML_STATE_IN_STREAM:
		ret = yaml_document_start_event_initialize(&event,
				NULL, NULL, NULL, !cyaml__emit_doc_delim(ctx));
		break;
	case CYAML_STATE_IN_DOC:
		return CYAML_OK;
	case CYAML_STATE_IN_MAP_KEY:
		ret = yaml_mapping_start_event_initialize(&event, NULL,
				(yaml_char_t *)YAML_MAP_TAG, 1,
				cyaml__get_emit_style_map(ctx, schema));
		break;
	case CYAML_STATE_IN_SEQUENCE:
		ret = yaml_sequence_start_event_initialize(&event, NULL,
				(yaml_char_t *)YAML_SEQ_TAG, 1,
				cyaml__get_emit_style_seq(ctx, schema));
		break;
	default:
		return CYAML_ERR_INTERNAL_ERROR;
	}

	return cyaml__emit_event_helper(ctx, ret, &event);
}

/**
 * Push a new entry onto the CYAML save context's stack.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  state   The CYAML save state we're pushing a stack entry for.
 * \param[in]  schema  The CYAML schema for the value expected in state.
 * \param[in]  data    Pointer to where value's data should be read from.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__stack_push(
		cyaml_ctx_t *ctx,
		enum cyaml_state_e state,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data)
{
	cyaml_err_t err;
	cyaml_state_t s = {
		.data = data,
		.state = state,
		.schema = schema,
	};

	err = cyaml__stack_push_write_event(ctx, state, schema);
	if (err != CYAML_OK) {
		return err;
	}

	err = cyaml__stack_ensure(ctx);
	if (err != CYAML_OK) {
		return err;
	}

	switch (state) {
	case CYAML_STATE_IN_MAP_KEY:
		assert(schema->type == CYAML_MAPPING);
		s.mapping.field = schema->mapping.fields;
		break;
	default:
		break;
	}

	cyaml__log(ctx->config, CYAML_LOG_DEBUG,
			"Save: PUSH[%u]: %s\n", ctx->stack_idx,
			cyaml__state_to_str(state));

	ctx->stack[ctx->stack_idx] = s;
	ctx->state = ctx->stack + ctx->stack_idx;
	ctx->stack_idx++;

	return CYAML_OK;
}

/**
 * Emit a YAML end event for the state being popped from the stack.
 *
 * This frees any resources owned by the stack entry.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  state   The CYAML save state we're popping from the stack.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__stack_pop_write_event(
		const cyaml_ctx_t *ctx,
		enum cyaml_state_e state)
{
	yaml_event_t event;
	int ret;

	/* Create any appropriate event for the new state. */
	switch (state) {
	case CYAML_STATE_START:
		return CYAML_OK;
	case CYAML_STATE_IN_STREAM:
		ret = yaml_stream_end_event_initialize(&event);
		break;
	case CYAML_STATE_IN_DOC:
		ret = yaml_document_end_event_initialize(&event,
				!cyaml__emit_doc_delim(ctx));
		break;
	case CYAML_STATE_IN_MAP_KEY:
		ret = yaml_mapping_end_event_initialize(&event);
		break;
	case CYAML_STATE_IN_SEQUENCE:
		ret = yaml_sequence_end_event_initialize(&event);
		break;
	default:
		return CYAML_ERR_INTERNAL_ERROR;
	}

	return cyaml__emit_event_helper(ctx, ret, &event);
}

/**
 * Pop the current entry on the CYAML save context's stack.
 *
 * This frees any resources owned by the stack entry.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  emit    Whether end events should be emitted.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__stack_pop(
		cyaml_ctx_t *ctx,
		bool emit)
{
	uint32_t idx = ctx->stack_idx;

	assert(idx != 0);

	if (emit) {
		cyaml_err_t err;
		err = cyaml__stack_pop_write_event(ctx, ctx->state->state);
		if (err != CYAML_OK) {
			return err;
		}
	}

	idx--;

	cyaml__log(ctx->config, CYAML_LOG_DEBUG, "Save: POP[%u]: %s\n", idx,
			cyaml__state_to_str(ctx->state->state));

	ctx->state = (idx == 0) ? NULL : &ctx->stack[idx - 1];
	ctx->stack_idx = idx;

	return CYAML_OK;
}

/**
 * Find address of actual value.
 *
 * If the value has the pointer flag, the pointer is read, otherwise the
 * address is returned unchanged.
 *
 * \param[in]  config     The CYAML client configuration object.
 * \param[in]  schema     CYAML schema for the expected value.
 * \param[in]  data_in    The address to read from.
 * \return New address or for \ref CYAML_FLAG_POINTER, or data_in.
 */
static const uint8_t * cyaml__data_handle_pointer(
		const cyaml_config_t *config,
		const cyaml_schema_value_t *schema,
		const uint8_t *data_in)
{
	if (schema->flags & CYAML_FLAG_POINTER) {
		const uint8_t *data = cyaml_data_read_pointer(data_in);

		cyaml__log(config, CYAML_LOG_DEBUG,
				"Save: Handle pointer: %p --> %p\n",
				data_in, data);

		return data;
	}

	return data_in;
}

/**
 * Dump a backtrace to the log.
 *
 * \param[in]  ctx     The CYAML saving context.
 */
static void cyaml__backtrace(
		const cyaml_ctx_t *ctx)
{
	if (ctx->stack_idx > 1) {
		cyaml__log(ctx->config, CYAML_LOG_ERROR, "Save: Backtrace:\n");
	} else {
		return;
	}

	for (uint32_t idx = ctx->stack_idx - 1; idx != 0; idx--) {
		cyaml_state_t *state = ctx->stack + idx;
		switch (state->state) {
		case CYAML_STATE_IN_MAP_KEY: /* Fall through. */
		case CYAML_STATE_IN_MAP_VALUE:
			assert(state->mapping.field != NULL);
			cyaml__log(ctx->config, CYAML_LOG_ERROR,
					"  in mapping field: %s\n",
					state->mapping.field->key);
			break;
		case CYAML_STATE_IN_SEQUENCE:
			cyaml__log(ctx->config, CYAML_LOG_ERROR,
					"  in sequence entry: %"PRIu32"\n",
					state->sequence.count);
			break;
		default:
			/** \todo \ref CYAML_STATE_IN_DOC handling for multi
			 *        document streams.
			 */
			break;
		}
	}
}

/**
 * Helper to emit YAML scalar events, using libyaml.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to emit.
 * \param[in]  value   The value to emit as a null-terminated C string.
 * \param[in]  tag     YAML tag to use for output.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__emit_scalar(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const char *value,
		const char *tag)
{
	int ret;
	yaml_event_t event;

	if (schema->type == CYAML_MAPPING) {
		cyaml__log(ctx->config, CYAML_LOG_INFO, "Save: [%s]\n", value);
	} else {
		cyaml__log(ctx->config, CYAML_LOG_INFO,
				"Save:   <%s>\n", value);
	}

	ret = yaml_scalar_event_initialize(&event, NULL,
			(yaml_char_t *)tag,
			(yaml_char_t *)value,
			(int)strlen(value),
			1, 1, cyaml__get_emit_style_scalar(schema));

	return cyaml__emit_event_helper(ctx, ret, &event);
}

/**
 * Convert signed integer to string.
 *
 * \param[in]  value  The integer to convert.
 * \return String conversion of the value.
 */
static const char * cyaml__get_int(
		int64_t value)
{
	static char string[32];

	sprintf(string, "%"PRIi64, value);

	return string;
}

/**
 * Convert unsigned integer to string.
 *
 * \param[in]  value  The integer to convert.
 * \param[in]  hex    Whether to render the number as hexadecimal.
 * \return String conversion of the value.
 */
static const char * cyaml__get_uint(
		uint64_t value, bool hex)
{
	static char string[32];

	if (hex) {
		sprintf(string, "0x%"PRIx64, value);
	} else {
		sprintf(string, "%"PRIu64, value);
	}

	return string;
}

/**
 * Get string representation for single precision floating point values.
 *
 * \param[in]  value  The value to convert.
 * \return String conversion of the value.
 */
static const char * cyaml__get_float(
		double value)
{
	static char string[64];

	sprintf(string, "%g", value);

	return string;
}

/**
 * Get string representation for double precision floating point values.
 *
 * \param[in]  value  The value to convert.
 * \return String conversion of the value.
 */
static const char * cyaml__get_double(
		double value)
{
	static char string[64];

	sprintf(string, "%.16e", value);

	return string;
}

/**
 * Pad a signed value that's smaller than 64-bit to an int64_t.
 *
 * This sets all the bits in the padded region.
 *
 * \param[in]  raw   Contains a signed value of size bytes.
 * \param[in]  size  Number of bytes used in raw.
 * \return Value padded to 64-bit signed.
 */
static int64_t cyaml_sign_pad(uint64_t raw, size_t size)
{
	uint64_t sign_bit = (size == 0) ?
			UINT64_MAX : ((uint64_t)1) << (size * CHAR_BIT - 1);
	unsigned padding = ((unsigned)(sizeof(raw) - size)) * CHAR_BIT;

	if ((sign_bit & raw) && (padding != 0)) {
		raw |= (((uint64_t)1 << padding) - 1) << (size * CHAR_BIT);
	}

	return (int64_t)raw;
}

/**
 * Write a value of type \ref CYAML_INT.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_int(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data)
{
	cyaml_err_t err;
	int64_t number;
	uint64_t raw;

	raw = cyaml_data_read(schema->data_size, data, &err);
	if (err != CYAML_OK) {
		return err;
	}

	number = cyaml_sign_pad(raw, schema->data_size);

	return cyaml__emit_scalar(ctx, schema,
			cyaml__get_int(number),
			YAML_INT_TAG);
}

/**
 * Write a value of type \ref CYAML_UINT.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_uint(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data)
{
	uint64_t number;
	cyaml_err_t err;

	number = cyaml_data_read(schema->data_size, data, &err);
	if (err == CYAML_OK) {
		const char *string = cyaml__get_uint(number, false);
		err = cyaml__emit_scalar(ctx, schema, string, YAML_INT_TAG);
	}

	return err;
}

/**
 * Write a value of type \ref CYAML_BOOL.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_bool(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data)
{
	uint64_t number;
	cyaml_err_t err;

	number = cyaml_data_read(schema->data_size, data, &err);
	if (err == CYAML_OK) {
		err = cyaml__emit_scalar(ctx, schema,
			number ? "true" : "false", YAML_BOOL_TAG);
	}

	return err;
}

/**
 * Write a value of type \ref CYAML_ENUM.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_enum(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data)
{
	int64_t number;
	cyaml_err_t err;

	number = (int64_t)cyaml_data_read(schema->data_size, data, &err);
	if (err == CYAML_OK) {
		const cyaml_strval_t *strings = schema->enumeration.strings;
		const char *string = NULL;
		for (uint32_t i = 0; i < schema->enumeration.count; i++) {
			if (number == strings[i].val) {
				string = strings[i].str;
				break;
			}
		}
		if (string == NULL) {
			if (schema->flags & CYAML_FLAG_STRICT) {
				return CYAML_ERR_INVALID_VALUE;
			} else {
				return cyaml__write_int(ctx, schema, data);
			}
		}
		err = cyaml__emit_scalar(ctx, schema, string, YAML_STR_TAG);
	}

	return err;
}

/**
 * Write a value of type \ref CYAML_FLOAT.
 *
 * The `data_size` of the schema entry must be the size of a known
 * floating point C type.
 *
 * \note The `long double` type was causing problems, so it isn't currently
 *       supported.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_float(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data)
{
	const char *string = NULL;

	if (schema->data_size == sizeof(float)) {
		float number;
		memcpy(&number, data, schema->data_size);
		string = cyaml__get_float(number);

	} else if (schema->data_size == sizeof(double)) {
		double number;
		memcpy(&number, data, schema->data_size);
		string = cyaml__get_double(number);
	} else {
		return CYAML_ERR_INVALID_DATA_SIZE;
	}

	return cyaml__emit_scalar(ctx, schema, string, YAML_FLOAT_TAG);
}

/**
 * Write a value of type \ref CYAML_STRING.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_string(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data)
{
	return cyaml__emit_scalar(ctx, schema, (const char *)data,
			YAML_STR_TAG);
}

/**
 * Write a scalar value.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_scalar_value(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data)
{
	typedef cyaml_err_t (*cyaml_read_scalar_fn)(
			const cyaml_ctx_t *ctx,
			const cyaml_schema_value_t *schema,
			const uint8_t *data_target);
	static const cyaml_read_scalar_fn fn[CYAML__TYPE_COUNT] = {
		[CYAML_INT]    = cyaml__write_int,
		[CYAML_UINT]   = cyaml__write_uint,
		[CYAML_BOOL]   = cyaml__write_bool,
		[CYAML_ENUM]   = cyaml__write_enum,
		[CYAML_FLOAT]  = cyaml__write_float,
		[CYAML_STRING] = cyaml__write_string,
	};

	assert(fn[schema->type] != NULL);

	return fn[schema->type](ctx, schema, data);
}

/**
 * Emit a sequence of flag values.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  number  The value of the flag data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__emit_flags_sequence(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		uint64_t number)
{
	yaml_event_t event;
	cyaml_err_t err;
	int ret;

	ret = yaml_sequence_start_event_initialize(&event, NULL,
			(yaml_char_t *)YAML_SEQ_TAG, 1,
			YAML_ANY_SEQUENCE_STYLE);

	err = cyaml__emit_event_helper(ctx, ret, &event);
	if (err != CYAML_OK) {
		return err;
	}

	for (uint32_t i = 0; i < schema->enumeration.count; i++) {
		const cyaml_strval_t *strval = &schema->enumeration.strings[i];
		uint64_t flag = (uint64_t)strval->val;
		if (number & flag) {
			err = cyaml__emit_scalar(ctx, schema, strval->str,
					YAML_STR_TAG);
			if (err != CYAML_OK) {
				return err;
			}
			number &= ~flag;
		}
	}
	if (number != 0) {
		if (schema->flags & CYAML_FLAG_STRICT) {
			return CYAML_ERR_INVALID_VALUE;
		} else {
			const char *string = cyaml__get_uint(number, false);
			err = cyaml__emit_scalar(ctx, schema, string,
					YAML_STR_TAG);
			if (err != CYAML_OK) {
				return err;
			}
		}
	}

	ret = yaml_sequence_end_event_initialize(&event);

	return cyaml__emit_event_helper(ctx, ret, &event);
}

/**
 * Write a value of type \ref CYAML_FLAGS.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_flags_value(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data)
{
	uint64_t number;
	cyaml_err_t err;

	number = cyaml_data_read(schema->data_size, data, &err);
	if (err == CYAML_OK) {
		err = cyaml__emit_flags_sequence(ctx, schema, number);
	}

	return err;
}

/**
 * Emit a mapping of bitfield values.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  number  The value of the bitfield data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__emit_bitfield_mapping(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		uint64_t number)
{
	const cyaml_bitdef_t *bitdef = schema->bitfield.bitdefs;
	yaml_event_t event;
	cyaml_err_t err;
	int ret;

	ret = yaml_mapping_start_event_initialize(&event, NULL,
			(yaml_char_t *)YAML_MAP_TAG, 1,
			cyaml__get_emit_style_map(ctx, schema));

	err = cyaml__emit_event_helper(ctx, ret, &event);
	if (err != CYAML_OK) {
		return err;
	}

	for (uint32_t i = 0; i < schema->bitfield.count; i++) {
		const char *value_str;
		uint64_t value;
		uint64_t mask;

		if (bitdef[i].bits + bitdef[i].offset > schema->data_size * 8) {
			return CYAML_ERR_BAD_BITVAL_IN_SCHEMA;
		}

		mask = ((~(uint64_t)0) >>
		        ((8 * sizeof(uint64_t)) - bitdef[i].bits)
		       ) << bitdef[i].offset;

		if ((number & mask) == 0) {
			continue;
		}

		value = (number & mask) >> bitdef[i].offset;

		/* Emit bitfield value's name */
		err = cyaml__emit_scalar(ctx, schema, bitdef[i].name,
				YAML_STR_TAG);
		if (err != CYAML_OK) {
			return err;
		}

		/* Emit bitfield value's value */
		value_str = cyaml__get_uint(value, true);
		err = cyaml__emit_scalar(ctx, schema, value_str, YAML_INT_TAG);
		if (err != CYAML_OK) {
			return err;
		}
	}

	ret = yaml_mapping_end_event_initialize(&event);

	return cyaml__emit_event_helper(ctx, ret, &event);
}

/**
 * Write a value of type \ref CYAML_BITFIELD.
 *
 * \param[in]  ctx     The CYAML saving context.
 * \param[in]  schema  The schema for the value to be written.
 * \param[in]  data    The place to read the value from in the client data.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_bitfield_value(
		const cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data)
{
	uint64_t number;
	cyaml_err_t err;

	number = cyaml_data_read(schema->data_size, data, &err);
	if (err == CYAML_OK) {
		err = cyaml__emit_bitfield_mapping(ctx, schema, number);
	}

	return err;
}

/**
 * Emit the YAML events required for a CYAML value.
 *
 * \param[in]  ctx        The CYAML saving context.
 * \param[in]  schema     CYAML schema for the expected value.
 * \param[in]  data       The place to read the value from in the client data.
 * \param[in]  seq_count  Entry count for sequence values.  Unused for
 *                        non-sequence values.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_value(
		cyaml_ctx_t *ctx,
		const cyaml_schema_value_t *schema,
		const uint8_t *data,
		uint64_t seq_count)
{
	cyaml_err_t err;

	cyaml__log(ctx->config, CYAML_LOG_DEBUG,
			"Save: Writing value of type '%s'%s\n",
			cyaml__type_to_str(schema->type),
			schema->flags & CYAML_FLAG_POINTER ? " (pointer)" : "");

	data = cyaml__data_handle_pointer(ctx->config, schema, data);

	if (data == NULL) {
		if (cyaml__flag_check_all(schema->flags,
				CYAML_FLAG_POINTER_NULL_STR)) {
			return cyaml__emit_scalar(ctx, schema, "null",
					YAML_STR_TAG);
		} else if (cyaml__flag_check_all(schema->flags,
				CYAML_FLAG_POINTER_NULL)) {
			return cyaml__emit_scalar(ctx, schema, "",
					YAML_STR_TAG);
		} else {
			return CYAML_ERR_INVALID_VALUE;
		}
	}

	switch (schema->type) {
	case CYAML_INT:   /* Fall through. */
	case CYAML_UINT:  /* Fall through. */
	case CYAML_BOOL:  /* Fall through. */
	case CYAML_ENUM:  /* Fall through. */
	case CYAML_FLOAT: /* Fall through. */
	case CYAML_STRING:
		err = cyaml__write_scalar_value(ctx, schema, data);
		break;
	case CYAML_FLAGS:
		err = cyaml__write_flags_value(ctx, schema, data);
		break;
	case CYAML_MAPPING:
		err = cyaml__stack_push(ctx, CYAML_STATE_IN_MAP_KEY,
				schema, data);
		break;
	case CYAML_BITFIELD:
		err = cyaml__write_bitfield_value(ctx, schema, data);
		break;
	case CYAML_SEQUENCE_FIXED:
		if (schema->sequence.min != schema->sequence.max) {
			return CYAML_ERR_SEQUENCE_FIXED_COUNT;
		}
		/* Fall through. */
	case CYAML_SEQUENCE:
		err = cyaml__stack_push(ctx, CYAML_STATE_IN_SEQUENCE,
				schema, data);
		if (err == CYAML_OK) {
			ctx->state->sequence.count = seq_count;
		}
		break;
	default:
		err = CYAML_ERR_BAD_TYPE_IN_SCHEMA;
		break;
	}

	return err;
}

/**
 * YAML saving handler for the \ref CYAML_STATE_START state.
 *
 * \param[in]  ctx  The CYAML saving context.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_start(
		cyaml_ctx_t *ctx)
{
	return cyaml__stack_push(ctx, CYAML_STATE_IN_STREAM,
			ctx->state->schema, ctx->state->data);
}

/**
 * YAML saving handler for the \ref CYAML_STATE_IN_STREAM state.
 *
 * \param[in]  ctx  The CYAML saving context.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_stream(
		cyaml_ctx_t *ctx)
{
	cyaml_err_t err;

	if (ctx->state->done) {
		err = cyaml__stack_pop(ctx, true);
	} else {
		ctx->stack[CYAML_STATE_IN_STREAM].done = true;
		err = cyaml__stack_push(ctx, CYAML_STATE_IN_DOC,
				ctx->state->schema, ctx->state->data);
	}
	return err;
}

/**
 * YAML saving handler for the \ref CYAML_STATE_IN_DOC state.
 *
 * \param[in]  ctx  The CYAML saving context.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_doc(
		cyaml_ctx_t *ctx)
{
	cyaml_err_t err;

	if (ctx->state->done) {
		err = cyaml__stack_pop(ctx, true);
	} else {
		unsigned seq_count = ctx->seq_count;
		if (ctx->state->schema->type == CYAML_SEQUENCE_FIXED) {
			seq_count = ctx->state->schema->sequence.max;
		}
		ctx->stack[CYAML_STATE_IN_DOC].done = true;
		err = cyaml__write_value(ctx,
				ctx->state->schema,
				ctx->state->data,
				seq_count);
	}
	return err;
}

/**
 * YAML saving handler for the \ref CYAML_STATE_IN_MAP_KEY and \ref
 * CYAML_STATE_IN_MAP_VALUE states.
 *
 * \param[in]  ctx  The CYAML saving context.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_mapping(
		cyaml_ctx_t *ctx)
{
	const cyaml_schema_field_t *field = ctx->state->mapping.field;
	cyaml_err_t err = CYAML_OK;

	if (field != NULL && field->key != NULL) {
		uint64_t seq_count = 0;

		if (field->value.type == CYAML_IGNORE) {
			ctx->state->mapping.field++;
			return CYAML_OK;
		}

		if ((field->value.flags & CYAML_FLAG_OPTIONAL) &&
		    (field->value.flags & CYAML_FLAG_POINTER)) {
			const void *ptr = cyaml_data_read_pointer(
					ctx->state->data + field->data_offset);
			if (ptr == NULL) {
				ctx->state->mapping.field++;
				return CYAML_OK;
			}
		}

		err = cyaml__emit_scalar(ctx, ctx->state->schema,
				field->key, YAML_STR_TAG);
		if (err != CYAML_OK) {
			return err;
		}

		/* Advance the field before writing value, since writing the
		 * value can put a new state entry on the stack. */
		ctx->state->mapping.field++;

		if (field->value.type == CYAML_SEQUENCE) {
			seq_count = cyaml_data_read(field->count_size,
					ctx->state->data + field->count_offset,
					&err);
			if (err != CYAML_OK) {
				return err;
			}
			cyaml__log(ctx->config, CYAML_LOG_INFO,
					"Save: Sequence entry count: "
					"%"PRIu64"\n", seq_count);

		} else if (field->value.type == CYAML_SEQUENCE_FIXED) {
			seq_count = field->value.sequence.min;
		}

		err = cyaml__write_value(ctx,
				&field->value,
				ctx->state->data + field->data_offset,
				seq_count);
	} else {
		err = cyaml__stack_pop(ctx, true);
	}

	return err;
}

/**
 * YAML saving handler for the \ref CYAML_STATE_IN_SEQUENCE state.
 *
 * \param[in]  ctx  The CYAML saving context.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__write_sequence(
		cyaml_ctx_t *ctx)
{
	cyaml_err_t err = CYAML_OK;

	if (ctx->state->sequence.entry < ctx->state->sequence.count) {
		const cyaml_schema_value_t *schema = ctx->state->schema;
		const cyaml_schema_value_t *value = schema->sequence.entry;
		unsigned seq_count = 0;
		size_t data_size;
		size_t offset;

		if (value->type == CYAML_SEQUENCE) {
			return CYAML_ERR_SEQUENCE_IN_SEQUENCE;

		} else if (value->type == CYAML_SEQUENCE_FIXED) {
			seq_count = value->sequence.max;
		}

		if (value->flags & CYAML_FLAG_POINTER) {
			data_size = sizeof(NULL);
		} else {
			data_size = value->data_size;
			if (value->type == CYAML_SEQUENCE_FIXED) {
				data_size *= seq_count;
			}
		}
		offset = data_size * ctx->state->sequence.entry;

		cyaml__log(ctx->config, CYAML_LOG_INFO,
				"Save: Sequence entry %u of %u\n",
				ctx->state->sequence.entry + 1,
				ctx->state->sequence.count);

		/* Advance the entry before writing value, since writing the
		 * value can put a new state entry on the stack. */
		ctx->state->sequence.entry++;

		err = cyaml__write_value(ctx, value,
				ctx->state->data + offset,
				seq_count);
	} else {
		err = cyaml__stack_pop(ctx, true);
	}

	return err;
}

/**
 * Check that common save params from client are valid.
 *
 * \param[in] config     The client's CYAML library config.
 * \param[in] schema     The schema describing the content of data.
 * \param[in] data       Points to client's data.
 * \param[in] seq_count  Top level sequence count.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static inline cyaml_err_t cyaml__validate_save_params(
		const cyaml_config_t *config,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data,
		unsigned seq_count)
{
	if (config == NULL) {
		return CYAML_ERR_BAD_PARAM_NULL_CONFIG;
	}
	if (config->mem_fn == NULL) {
		return CYAML_ERR_BAD_CONFIG_NULL_MEMFN;
	}
	if (schema == NULL) {
		return CYAML_ERR_BAD_PARAM_NULL_SCHEMA;
	}
	if ((schema->type == CYAML_SEQUENCE) != (seq_count != 0)) {
		return CYAML_ERR_BAD_PARAM_SEQ_COUNT;
	}
	if (!(schema->flags & CYAML_FLAG_POINTER)) {
		return CYAML_ERR_TOP_LEVEL_NON_PTR;
	}
	if (data == NULL) {
		return CYAML_ERR_BAD_PARAM_NULL_DATA;
	}
	return CYAML_OK;
}

/**
 * The main YAML saving function.
 *
 * The public interfaces are wrappers around this.
 *
 * \param[in] config     Client's CYAML configuration structure.
 * \param[in] schema     CYAML schema for the YAML to be saved.
 * \param[in] data       The caller-owned data to be saved.
 * \param[in] seq_count  If top level type is sequence, this should be the
 *                       entry count, otherwise it is ignored.
 * \param[in] emitter    An initialised `libyaml` emitter object
 *                       with its output set.
 * \return \ref CYAML_OK on success, or appropriate error code otherwise.
 */
static cyaml_err_t cyaml__save(
		const cyaml_config_t *config,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data,
		unsigned seq_count,
		yaml_emitter_t *emitter)
{
	cyaml_ctx_t ctx = {
		.config = config,
		.emitter = emitter,
		.seq_count = seq_count,
	};
	typedef cyaml_err_t (* const cyaml_write_fn)(
			cyaml_ctx_t *ctx);
	static const cyaml_write_fn fn[CYAML_STATE__COUNT] = {
		[CYAML_STATE_START]        = cyaml__write_start,
		[CYAML_STATE_IN_STREAM]    = cyaml__write_stream,
		[CYAML_STATE_IN_DOC]       = cyaml__write_doc,
		[CYAML_STATE_IN_MAP_KEY]   = cyaml__write_mapping,
		[CYAML_STATE_IN_MAP_VALUE] = cyaml__write_mapping,
		[CYAML_STATE_IN_SEQUENCE]  = cyaml__write_sequence,
	};
	cyaml_err_t err = CYAML_OK;

	err = cyaml__validate_save_params(config, schema, data, seq_count);
	if (err != CYAML_OK) {
		return err;
	}

	err = cyaml__stack_push(&ctx, CYAML_STATE_START, schema, &data);
	if (err != CYAML_OK) {
		goto out;
	}

	do {
		cyaml__log(ctx.config, CYAML_LOG_DEBUG,
				"Save: Handle state %s\n",
				cyaml__state_to_str(ctx.state->state));
		err = fn[ctx.state->state](&ctx);
		if (err != CYAML_OK) {
			goto out;
		}
	} while (ctx.stack_idx > 1);

	cyaml__stack_pop(&ctx, true);

	assert(ctx.stack_idx == 0);

	if (!yaml_emitter_flush(emitter)) {
		cyaml__log(config, CYAML_LOG_ERROR,
				"Save: LibYAML: Failed to flush emitter: %s\n",
				emitter->problem);
		err = CYAML_ERR_LIBYAML_EMITTER;
	}

out:
	if (err != CYAML_OK) {
		cyaml__backtrace(&ctx);
	}
	while (ctx.stack_idx > 0) {
		cyaml__stack_pop(&ctx, false);
	}
	cyaml__free(config, ctx.stack);
	return err;
}

/* Exported function, documented in include/cyaml/cyaml.h */
cyaml_err_t cyaml_save_file(
		const char *path,
		const cyaml_config_t *config,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data,
		unsigned seq_count)
{
	FILE *file;
	cyaml_err_t err;
	yaml_emitter_t emitter;

	/* Initialize emitter */
	if (!yaml_emitter_initialize(&emitter)) {
		return CYAML_ERR_LIBYAML_EMITTER_INIT;
	}

	/* Open output file. */
	file = fopen(path, "w");
	if (file == NULL) {
		yaml_emitter_delete(&emitter);
		return CYAML_ERR_FILE_OPEN;
	}

	/* Set output file */
	yaml_emitter_set_output_file(&emitter, file);

	/* Serialise to the output */
	err = cyaml__save(config, schema, data, seq_count, &emitter);
	if (err != CYAML_OK) {
		yaml_emitter_delete(&emitter);
		fclose(file);
		return err;
	}

	/* Cleanup */
	yaml_emitter_delete(&emitter);
	fclose(file);

	return CYAML_OK;
}

/** CYAML save buffer context. */
typedef struct cyaml_buffer_ctx {
	/** Client's CYAML configuration structure. */
	const cyaml_config_t *config;
	size_t len;      /**< Current length of `data` allocation. */
	size_t used;     /**< Current number of bytes used in `data`. */
	char *data;      /**< Current allocation for serialised output. */
	cyaml_err_t err; /**< Any error encounted in buffer handling. */
} cyaml_buffer_ctx_t;

/**
 * Write handler for libyaml.
 *
 * The write handler is called when the emitter needs to flush the accumulated
 * characters to the output.  The handler should write size bytes of the
 * buffer to the output.
 *
 * \todo Could be more efficient about this, but for now, this is fine.
 *
 * \param[in]  data    A pointer to cyaml buffer context struture.
 * \param[in]  buffer  The buffer with bytes to be written.
 * \param[in]  size    The number of bytes to be written.
 * \return 1 on sucess, 0 otherwise.
 */
static int cyaml__buffer_handler(
		void *data,
		unsigned char *buffer,
		size_t size)
{
	cyaml_buffer_ctx_t *buffer_ctx = data;
	enum {
		RETURN_SUCCESS = 1,
		RETURN_FAILURE = 0,
	};

	if (size > (buffer_ctx->len - buffer_ctx->used)) {
		char *temp = cyaml__realloc(
				buffer_ctx->config,
				buffer_ctx->data,
				buffer_ctx->len,
				buffer_ctx->used + size,
				false);
		if (temp == NULL) {
			buffer_ctx->err = CYAML_ERR_OOM;
			return RETURN_FAILURE;
		}
		buffer_ctx->data = temp;
		buffer_ctx->len = buffer_ctx->used + size;
	}

	memcpy(buffer_ctx->data + buffer_ctx->used, buffer, size);
	buffer_ctx->used += size;

	return RETURN_SUCCESS;
}

/* Exported function, documented in include/cyaml/cyaml.h */
cyaml_err_t cyaml_save_data(
		char **output,
		size_t *len,
		const cyaml_config_t *config,
		const cyaml_schema_value_t *schema,
		const cyaml_data_t *data,
		unsigned seq_count)
{
	cyaml_err_t err;
	yaml_emitter_t emitter;
	cyaml_buffer_ctx_t buffer_ctx = {
		.config = config,
		.err = CYAML_OK,
	};

	/* Initialize emitter */
	if (!yaml_emitter_initialize(&emitter)) {
		return CYAML_ERR_LIBYAML_EMITTER_INIT;
	}

	/* Set output buffer */
	yaml_emitter_set_output(&emitter, cyaml__buffer_handler, &buffer_ctx);

	/* Serialise to the output */
	err = cyaml__save(config, schema, data, seq_count, &emitter);
	if (err != CYAML_OK) {
		yaml_emitter_delete(&emitter);
		if ((config != NULL) && (config->mem_fn != NULL)) {
			cyaml__free(config, buffer_ctx.data);
		}
		if (buffer_ctx.err != CYAML_OK) {
			err = buffer_ctx.err;
		}
		return err;
	}

	/* Cleanup */
	yaml_emitter_delete(&emitter);

	*output = buffer_ctx.data;
	*len = buffer_ctx.used;

	return CYAML_OK;
}
