const std = @import("std");
const zollections = @import("zollections");
const _database = @import("database.zig");
const _repository = @import("repository.zig");
const _relations = @import("relations.zig");

/// Structure of a model with its metadata.
pub fn ModelWithMetadata(comptime Model: type, comptime MetadataShape: ?type) type {
	if (MetadataShape) |MetadataType| {
		return struct {
			model: Model,
			metadata: MetadataType,
		};
	} else {
		return Model;
	}
}

/// Type of a retrieved table data, with its retrieved relations.
pub fn TableWithRelations(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.Relation) type {
	if (optionalRelations) |relations| {
		const tableType = @typeInfo(TableShape);

		// Build fields list: copy the existing table type fields and add those for relations.
		var fields: [tableType.Struct.fields.len + relations.len + (if (MetadataShape) |_| 1 else 0)]std.builtin.Type.StructField = undefined;
		// Copy base table fields.
		@memcpy(fields[0..tableType.Struct.fields.len], tableType.Struct.fields);

		// For each relation, create a new struct field in the table shape.
		for (relations, fields[tableType.Struct.fields.len..(tableType.Struct.fields.len+relations.len)]) |relation, *field| {
			// Get relation field type (optional TableShape of the related value).
			const relationFieldType = @Type(std.builtin.Type{
				.Optional = .{
					.child = relation.TableShape
				},
			});

			// Create the new field from relation data.
			field.* = std.builtin.Type.StructField{
				.name = relation.field ++ [0:0]u8{},
				.type = relationFieldType,
				.default_value = null,
				.is_comptime = false,
				.alignment = @alignOf(relationFieldType),
			};
		}

		if (MetadataShape) |MetadataType| {
			// Add metadata field.
			fields[tableType.Struct.fields.len + relations.len] = std.builtin.Type.StructField{
				.name = "_zrm_metadata",
				.type = MetadataType,
				.default_value = null,
				.is_comptime = false,
				.alignment = @alignOf(MetadataType),
			};
		}

		// Build the new type.
		return @Type(std.builtin.Type{
			.Struct = .{
				.layout = tableType.Struct.layout,
				.fields = &fields,
				.decls = &[0]std.builtin.Type.Declaration{},
				.is_tuple = tableType.Struct.is_tuple,
				.backing_integer = tableType.Struct.backing_integer,
			},
		});
	} else {
		return TableShape;
	}
}

/// Convert a value of the fully retrieved type to the TableShape type.
pub fn toTableShape(comptime TableShape: type, comptime MetadataShape: ?type, comptime optionalRelations: ?[]const _relations.Relation, value: TableWithRelations(TableShape, MetadataShape, optionalRelations)) TableShape {
	if (optionalRelations) |_| {
		// Make a structure of TableShape type.
		var tableValue: TableShape = undefined;

		// Copy all fields of the table shape in the new structure.
		inline for (std.meta.fields(TableShape)) |field| {
			@field(tableValue, field.name) = @field(value, field.name);
		}

		// Return the simplified structure.
		return tableValue;
	} else {
		// No relations, it should already be of type TableShape.
		return value;
	}
}

/// Generic interface of a query result reader.
pub fn QueryResultReader(comptime TableShape: type, comptime MetadataShape: ?type, comptime inlineRelations: ?[]const _relations.Relation) type {
	return struct {
		const Self = @This();

		/// Generic interface of a query result reader instance.
		pub const Instance = struct {
			__interface: struct {
				instance: *anyopaque,
				next: *const fn (self: *anyopaque) anyerror!?TableWithRelations(TableShape, MetadataShape, inlineRelations),
			},

			allocator: std.mem.Allocator,

			pub fn next(self: Instance) !?TableWithRelations(TableShape, MetadataShape, inlineRelations) {
				return self.__interface.next(self.__interface.instance);
			}
		};

		_interface: struct {
			instance: *anyopaque,
			init: *const fn (self: *anyopaque, allocator: std.mem.Allocator) anyerror!Instance,
		},

		/// Initialize a reader instance.
		pub fn init(self: Self, allocator: std.mem.Allocator) !Instance {
			return self._interface.init(self._interface.instance, allocator);
		}
	};
}

/// Map query result to repository model structures, and load the given relations.
pub fn ResultMapper(comptime Model: type, comptime TableShape: type, comptime MetadataShape: ?type, comptime repositoryConfig: _repository.RepositoryConfiguration(Model, TableShape), comptime inlineRelations: ?[]const _relations.Relation, comptime relations: ?[]const _relations.Relation) type {
	return struct {
		/// Map the query result to a repository result, with all the required relations.
		pub fn map(comptime withMetadata: bool, allocator: std.mem.Allocator, connector: _database.Connector, queryReader: QueryResultReader(TableShape, MetadataShape, inlineRelations)) !_repository.RepositoryResult(if (withMetadata) ModelWithMetadata(Model, MetadataShape) else Model) {
			// Get result type depending on metadata
			const ResultType = if (withMetadata) ModelWithMetadata(Model, MetadataShape) else Model;

			// Create an arena for mapper data.
			var mapperArena = std.heap.ArenaAllocator.init(allocator);

			// Initialize query result reader.
			const reader = try queryReader.init(mapperArena.allocator());

			// Initialize models list.
			var models = std.ArrayList(*ResultType).init(allocator);
			defer models.deinit();

			// Get all raw models from the result reader.
			while (try reader.next()) |rawModel| {
				// Parse each raw model from the reader.
				const model = try allocator.create(ResultType);
				(if (withMetadata) model.model else model.*) = try repositoryConfig.fromSql(toTableShape(TableShape, MetadataShape, inlineRelations, rawModel));

				// Map inline relations.
				if (inlineRelations) |_inlineRelations| {
					// If there are loaded inline relations, map them to the result.
					inline for (_inlineRelations) |relation| {
						// Set the read inline relation value.
						@field(model.*, relation.field) = (
							if (@field(rawModel, relation.field)) |relationVal|
								try relation.repositoryConfiguration().fromSql(relationVal)
							else null
						);
					}
				}

				if (withMetadata) {
					// Set model metadata.
					model.metadata = rawModel._zrm_metadata;
				}

				try models.append(model);
			}

			if (relations) |relationsToLoad| {
				inline for (relationsToLoad) |relation| {
					// Build query for the relation to get.
					const query: *relation.QueryType = @ptrCast(@alignCast(
						try relation.buildQuery(@ptrCast(models.items), allocator, connector)
					));
					defer {
						query.deinit();
						allocator.destroy(query);
					}

					// Get related models.
					const relatedModels = try query.getWithMetadata(mapperArena.allocator());

					// Create a map with related models.
					const RelatedModelsListType = std.ArrayList(@TypeOf(relatedModels.models[0].model));
					const RelatedModelsMapType = std.AutoHashMap(std.meta.FieldType(@TypeOf(relatedModels.models[0].metadata), .__zrm_relation_key), RelatedModelsListType);
					var relatedModelsMap = RelatedModelsMapType.init(allocator);
					defer relatedModelsMap.deinit();

					// Fill the map of related models, indexing them by the relation key.
					for (relatedModels.models) |relatedModel| {
						// For each related model, put it in the map at the relation key.
						var modelsList = try relatedModelsMap.getOrPut(relatedModel.metadata.__zrm_relation_key);

						if (!modelsList.found_existing) {
							// Initialize the related models list.
							modelsList.value_ptr.* = RelatedModelsListType.init(mapperArena.allocator());
						}

						// Add the current related model to the list.
						try modelsList.value_ptr.append(relatedModel.model);
					}

					// For each model, at the grouped related models if there are some.
					for (models.items) |model| {
						@field(model, relation.field) = (
							if (relatedModelsMap.getPtr(@field(model, repositoryConfig.key[0]))) |relatedModelsList|
								// There are related models, set them.
								try relatedModelsList.toOwnedSlice()
							else
								// No related models, set an empty array.
								&[0](@TypeOf(relatedModels.models[0].model)){}
						);
					}
				}
			}

			// Return a result with the models.
			return _repository.RepositoryResult(ResultType).init(allocator,
				zollections.Collection(ResultType).init(allocator, try models.toOwnedSlice()),
				mapperArena,
			);
		}
	};
}