/*
 * ObjectBox Generator - a build time tool for ObjectBox
 * Copyright (C) 2018-2024 ObjectBox Ltd. All rights reserved.
 * https://objectbox.io
 *
 * This file is part of ObjectBox Generator.
 *
 * ObjectBox Generator is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * ObjectBox Generator is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ObjectBox Generator.  If not, see <http://www.gnu.org/licenses/>.
 */

package templates

import (
	"text/template"
)

// CBindingTemplate is used to generated the binding code
var CBindingTemplate = template.Must(template.New("binding-c").Funcs(funcMap).Parse(
	`// Code generated by ObjectBox; DO NOT EDIT.

#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include "flatcc/flatcc.h"
#include "flatcc/flatcc_builder.h"
#include "objectbox.h"

/// Internal function used in other generated functions to put (write) explicitly typed objects.
static obx_id {{.FileIdentifier}}_put_object(OBX_box* box, void* object,
                             bool (*to_flatbuffer)(flatcc_builder_t*, const void*, void**, size_t*), OBXPutMode mode);

/// Internal function used in other generated functions to get (read) explicitly typed objects.
static void* {{.FileIdentifier}}_get_object(OBX_box* box, obx_id id, void* (*from_flatbuffer)(const void*, size_t));

/// Internal function used in other generated functions to get a vTable offset for a given field.
static flatbuffers_voffset_t {{.FileIdentifier}}_fb_field_offset(flatbuffers_voffset_t vs, const flatbuffers_voffset_t* vt, size_t field);

{{range $entity := .Model.EntitiesWithMeta}}
{{PrintComments 0 $entity.Comments}}typedef struct {{$entity.Meta.CName}} {
	{{range $property := $entity.Properties}}{{$propType := PropTypeName $property.Type -}}
	{{PrintComments 1 $property.Comments}}{{if $property.Meta.FbIsVector}}{{$property.Meta.CElementType}}* {{$property.Meta.CppName}};
	{{- if or (or (eq $propType "StringVector") (eq $propType "ByteVector")) (eq $propType "FloatVector")}}
	size_t {{$property.Meta.CppName}}_len;{{end}}
	{{else}}{{$property.Meta.CppType}}{{if $property.Meta.Optional}}*{{end}} {{$property.Meta.CppName}};
	{{end}}{{end}}
} {{$entity.Meta.CName}};

enum {{$entity.Meta.CName}}_ {
	{{$entity.Meta.CName}}_ENTITY_ID = {{$entity.Id.GetId}},
{{- range $property := $entity.Properties}}
	{{$entity.Meta.CName}}_PROP_ID_{{$property.Meta.CppName}} = {{$property.Id.GetId}},
{{- end}}
{{- range $relation := $entity.Relations}}
	{{$entity.Meta.CName}}_REL_ID_{{$relation.Meta.CppName}} = {{$relation.Id.GetId}},
{{- end}}
};

/// Write given object to the FlatBufferBuilder
static bool {{$entity.Meta.CName}}_to_flatbuffer(flatcc_builder_t* B, const {{$entity.Meta.CName}}* object, void** out_buffer, size_t* out_size);

/// Read an object from a valid FlatBuffer.
/// If the read object contains vectors or strings, those are allocated on heap and must be freed after use by calling {{$entity.Meta.CName}}_free_pointers().
/// Thus, when calling this function multiple times on the same object, ensure to call {{$entity.Meta.CName}}_free_pointers() before subsequent calls to avoid leaks. 
/// @returns true if the object was deserialized successfully or false on (allocation) error in which case any memory 
///          allocated by this function will also be freed before returning, allowing you to retry.
static bool {{$entity.Meta.CName}}_from_flatbuffer(const void* data, size_t size, {{$entity.Meta.CName}}* out_object);

/// Read an object from a valid FlatBuffer, allocating the object on heap. 
/// The object must be freed after use by calling {{$entity.Meta.CName}}_free();
static {{$entity.Meta.CName}}* {{$entity.Meta.CName}}_new_from_flatbuffer(const void* data, size_t size);

/// Free memory allocated for vector and string properties, setting the freed pointers to NULL.  
static void {{$entity.Meta.CName}}_free_pointers({{$entity.Meta.CName}}* object);

/// Free {{$entity.Meta.CName}}* object pointer and all its property pointers (vectors and strings).
/// Equivalent to calling {{$entity.Meta.CName}}_free_pointers() followed by free();
static void {{$entity.Meta.CName}}_free({{$entity.Meta.CName}}* object);
{{end}}
{{- range $entity := .Model.EntitiesWithMeta}}
static bool {{$entity.Meta.CName}}_to_flatbuffer(flatcc_builder_t* B, const {{$entity.Meta.CName}}* object, void** out_buffer, size_t* out_size) {
    assert(B);
    assert(object);
    assert(out_buffer);
    assert(out_size);

    flatcc_builder_reset(B);
	flatcc_builder_start_buffer(B, 0, 0, 0);
	{{range $property := $entity.Properties}}{{$propType := PropTypeName $property.Type}}
	{{- if eq $propType "String"}}
	flatcc_builder_ref_t offset_{{$property.Meta.CppName}} = !object->{{$property.Meta.CppName}} ? 0 : flatcc_builder_create_string_str(B, object->{{$property.Meta.CppName}});
	{{- else if eq $propType "ByteVector"}}
	flatcc_builder_ref_t offset_{{$property.Meta.CppName}} = !object->{{$property.Meta.CppName}} ? 0 : flatcc_builder_create_vector(B, object->{{$property.Meta.CppName}}, object->{{$property.Meta.CppName}}_len, sizeof({{$property.Meta.CElementType}}), sizeof({{$property.Meta.CElementType}}), FLATBUFFERS_COUNT_MAX(sizeof({{$property.Meta.CElementType}})));
	{{- else if eq $propType "FloatVector"}}
	flatcc_builder_ref_t offset_{{$property.Meta.CppName}} = !object->{{$property.Meta.CppName}} ? 0 : flatcc_builder_create_vector(B, object->{{$property.Meta.CppName}}, object->{{$property.Meta.CppName}}_len, sizeof({{$property.Meta.CElementType}}), sizeof({{$property.Meta.CElementType}}), FLATBUFFERS_COUNT_MAX(sizeof({{$property.Meta.CElementType}})));
	{{- else if eq $propType "StringVector"}}
	flatcc_builder_ref_t offset_{{$property.Meta.CppName}} = 0;
	if (object->{{$property.Meta.CppName}}) {
		flatcc_builder_start_offset_vector(B);
		for (size_t i = 0; i < object->{{$property.Meta.CppName}}_len; i++) {
			flatcc_builder_ref_t ref = !object->{{$property.Meta.CppName}}[i] ? 0 : flatcc_builder_create_string_str(B, object->{{$property.Meta.CppName}}[i]);
			if (ref) flatcc_builder_offset_vector_push(B, ref);
		}
		offset_{{$property.Meta.CppName}} = flatcc_builder_end_offset_vector(B);
	}
	{{- end}}{{end}}

    if (flatcc_builder_start_table(B, {{len $entity.Properties}}) != 0) return false;

    void* p;
	flatcc_builder_ref_t* _p;
	{{range $property := $entity.Properties}}
	{{- if $property.Meta.FbIsVector}}
	if (offset_{{$property.Meta.CppName}}) {
        if (!(_p = flatcc_builder_table_add_offset(B, {{$property.FbSlot}}))) return false;
        *_p = offset_{{$property.Meta.CppName}};
    }
	{{- else}}
	{{if $property.Meta.Optional}}if (object->{{$property.Meta.CppName}}) {{end}}{
		if (!(p = flatcc_builder_table_add(B, {{$property.FbSlot}}, {{$property.Meta.FbTypeSize}}, {{$property.Meta.FbTypeSize}}))) return false;
    	{{$property.Meta.FlatccFnPrefix}}_write_to_pe(p, {{if $property.Meta.Optional}}*{{end}}object->{{$property.Meta.CppName}});
	}{{- end}}
	{{end}}
    flatcc_builder_ref_t ref;
	if (!(ref = flatcc_builder_end_table(B))) return false;
	if (!flatcc_builder_end_buffer(B, ref)) return false;
    return (*out_buffer = flatcc_builder_finalize_aligned_buffer(B, out_size)) != NULL;
}

static bool {{$entity.Meta.CName}}_from_flatbuffer(const void* data, size_t size, {{$entity.Meta.CName}}* out_object) {
	assert(data);
	assert(size > 0);
	assert(out_object);

	const uint8_t* table = (const uint8_t*) data + __flatbuffers_uoffset_read_from_pe(data);
	assert(table);
	const flatbuffers_voffset_t* vt = (const flatbuffers_voffset_t*) (table - __flatbuffers_soffset_read_from_pe(table));
	flatbuffers_voffset_t vs = __flatbuffers_voffset_read_from_pe(vt);

	// variables reused when reading strings and vectors
	flatbuffers_voffset_t offset;
	const flatbuffers_uoffset_t* val;
	size_t len;

	// reset so that dangling pointers are freed properly on malloc() failures
#ifdef __cplusplus
	*out_object = {};
#else
	*out_object = ({{$entity.Meta.CName}}){0};{{/* Only works in C99, fails to compile on MSVC in a C++ project*/}}
#endif
	{{range $property := $entity.Properties}}{{$propType := PropTypeName $property.Type -}}
	if ((offset = {{$.FileIdentifier}}_fb_field_offset(vs, vt, {{$property.FbSlot}}))) {
	{{- if $property.Meta.FbIsVector}}
		val = (const flatbuffers_uoffset_t*)(table + offset + sizeof(flatbuffers_uoffset_t) + __flatbuffers_uoffset_read_from_pe(table + offset));
		len = (size_t) __flatbuffers_uoffset_read_from_pe(val - 1);
		out_object->{{$property.Meta.CppName}} = ({{$property.Meta.CElementType}}*) malloc({{if eq $propType "String"}}(len+1){{else}}len{{end}} * sizeof({{$property.Meta.CElementType}}));
		if (out_object->{{$property.Meta.CppName}} == NULL) {
			{{$entity.Meta.CName}}_free_pointers(out_object);
			return false;
		}
		{{- if not (eq $propType "String")}}
		out_object->{{$property.Meta.CppName}}_len = len;
		{{- end -}}
		{{/*Note: direct copy for string and byte vectors*/}}
		{{if eq $propType "String"}}memcpy((void*)out_object->{{$property.Meta.CppName}}, (const void*)val, len+1);
		{{else if eq $propType "ByteVector"}}memcpy((void*)out_object->{{$property.Meta.CppName}}, (const void*)val, len);
		{{else if eq $propType "FloatVector"}}memcpy((void*)out_object->{{$property.Meta.CppName}}, (const void*)val, sizeof(float)*len);
		{{else}}{{/* StringVector - FB vector contains offsets to strings, each must be read separately*/ -}}
		for (size_t i = 0; i < len; i++, val++) {
			const uint8_t* str = (const uint8_t*) val + (size_t)__flatbuffers_uoffset_read_from_pe(val) + sizeof(val[0]);
			out_object->{{$property.Meta.CppName}}[i] = (char*) malloc((strlen((const char*)str) + 1) * sizeof(char));
			if (out_object->{{$property.Meta.CppName}}[i] == NULL) {
				out_object->{{$property.Meta.CppName}}_len = i; // only free() indexes before the current "i"
				{{$entity.Meta.CName}}_free_pointers(out_object);
				return false;
			}
			strcpy((char*)out_object->{{$property.Meta.CppName}}[i], (const char*)str);
		}{{end}}
	} else {
		out_object->{{$property.Meta.CppName}} = NULL;
		{{- if not (eq $propType "String")}}
		out_object->{{$property.Meta.CppName}}_len = 0;
		{{- end}}
	{{- else}}
		{{if $property.Meta.Optional -}}
		out_object->{{$property.Meta.CppName}} = ({{$property.Meta.CppType}}*) malloc(sizeof({{$property.Meta.CppType}}));
		if (out_object->{{$property.Meta.CppName}} == NULL) {
			{{$entity.Meta.CName}}_free_pointers(out_object);
			return false;
		}
		*{{end}}out_object->{{$property.Meta.CppName}} = {{$property.Meta.FlatccFnPrefix}}_read_from_pe(table + offset);
	{{- end}}
	}
	{{end}}return true;
}

static {{$entity.Meta.CName}}* {{$entity.Meta.CName}}_new_from_flatbuffer(const void* data, size_t size) {
	{{$entity.Meta.CName}}* object = ({{$entity.Meta.CName}}*) malloc(sizeof({{$entity.Meta.CName}}));
	if (object) {
		if (!{{$entity.Meta.CName}}_from_flatbuffer(data, size, object)) {
			free(object);
			object = NULL;
		}
	}
	return object;
}

static void {{$entity.Meta.CName}}_free_pointers({{$entity.Meta.CName}}* object) {
	if (object == NULL) return;
	{{range $property := $entity.Properties}}{{$propType := PropTypeName $property.Type}}{{if $property.Meta.FbIsVector -}}
	if (object->{{$property.Meta.CppName}}) {
		{{- if eq $propType "StringVector"}}
		for (size_t i = 0; i < object->{{$property.Meta.CppName}}_len; i++) {
			if (object->{{$property.Meta.CppName}}[i]) free(object->{{$property.Meta.CppName}}[i]);
		}{{end}}
		free(object->{{$property.Meta.CppName}});
		object->{{$property.Meta.CppName}} = NULL;
	{{- if not (eq $propType "String")}}
		object->{{$property.Meta.CppName}}_len = 0;
	} else {
		assert(object->{{$property.Meta.CppName}}_len == 0);
	{{- end}}
	}
	{{else if $property.Meta.Optional -}}
	if (object->{{$property.Meta.CppName}}) {
		free(object->{{$property.Meta.CppName}});
		object->{{$property.Meta.CppName}} = NULL;
	}
	{{end}}
	{{- end}}
}

static void {{$entity.Meta.CName}}_free({{$entity.Meta.CName}}* object) {
	{{$entity.Meta.CName}}_free_pointers(object);
	free(object);
}

/// Insert or update the given object in the database.
/// @param object (in & out) will be updated with a newly inserted ID if the one specified previously was zero. If an ID 
/// was already specified (non-zero), it will remain unchanged.
/// @return object ID from the object param (see object param docs) or a zero on error. If a zero was returned, you can
/// check obx_last_error_*() to get the error details. In an unlikely event that those functions return no error
/// code/message, the error occurred in FlatBuffers serialization, e.g. due to memory allocation issues.
static obx_id {{$entity.Meta.CName}}_put(OBX_box* box, {{$entity.Meta.CName}}* object) {
    obx_id id = {{$.FileIdentifier}}_put_object(box, object,
                               (bool (*)(flatcc_builder_t*, const void*, void**, size_t*)) {{$entity.Meta.CName}}_to_flatbuffer,
                               OBXPutMode_PUT);
    if (id != 0) {
        object->{{$entity.IdProperty.Meta.CppName}} = id;  // update the ID property on new objects for convenience
    }
    return id;
}

/// Read an object from the database, returning a pointer.
/// @return an object pointer or NULL if an object with the given ID doesn't exist or any other error occurred. You can
/// check obx_last_error_*() if NULL is returned to get the error details. In an unlikely event that those functions
/// return no error code/message, the error occurred in FlatBuffers serialization, e.g. due to memory allocation issues.
/// @note: The returned object must be freed after use by calling {{$entity.Meta.CName}}_free();
static {{$entity.Meta.CName}}* {{$entity.Meta.CName}}_get(OBX_box* box, obx_id id) {
	return ({{$entity.Meta.CName}}*) {{$.FileIdentifier}}_get_object(box, id, (void* (*) (const void*, size_t)) {{$entity.Meta.CName}}_new_from_flatbuffer);
}
{{end}}
static obx_id {{.FileIdentifier}}_put_object(OBX_box* box, void* object,
                             bool (*to_flatbuffer)(flatcc_builder_t*, const void*, void**, size_t*), OBXPutMode mode) {
    flatcc_builder_t builder;
    flatcc_builder_init(&builder);

    obx_id id = 0;
    size_t size = 0;
    void* buffer = NULL;
    if (!to_flatbuffer(&builder, object, &buffer, &size)) {
        obx_last_error_set(OBX_ERROR_STD_OTHER, 0, "FlatBuffer serialization failed");
    } else {
        id = obx_box_put_object4(box, buffer, size, mode);  // 0 on error
    }

    flatcc_builder_clear(&builder);
    if (buffer) flatcc_builder_aligned_free(buffer);

    return id;
}

static void* {{.FileIdentifier}}_get_object(OBX_box* box, obx_id id, void* (*from_flatbuffer)(const void*, size_t)) {
    // We need an explicit TX - read data lifecycle is bound to the open TX.
    OBX_txn* tx = obx_txn_read(obx_box_store(box));
    if (!tx) return NULL;

    void* result = NULL;
    const void* data;
    size_t size;
    if (obx_box_get(box, id, &data, &size) == OBX_SUCCESS) {
        result = from_flatbuffer(data, size);
        if (result == NULL) {
			obx_last_error_set(OBX_ERROR_STD_OTHER, 0, "FlatBuffer deserialization failed");
        }
    }

    obx_txn_close(tx);
    return result;
}

static flatbuffers_voffset_t {{.FileIdentifier}}_fb_field_offset(flatbuffers_voffset_t vs, const flatbuffers_voffset_t* vt, size_t field) {
    return (vs < sizeof(vt[0]) * (field + 3)) ? 0 : __flatbuffers_voffset_read_from_pe(vt + field + 2);
}
`))
