﻿using System;
using System.Collections.Generic;
using System.Linq;
using EasyApi.Metadata;
using EasyApi.Utils;
using EasyApi.Values;

namespace EasyApi.Mapping
{
	internal class ComplexParameterListMapping : IParameterListMapping
	{
		private readonly IDictionary<IParameterMetadata, IReadOnlyList<ParameterMapping>> _parameterMappings;
		private readonly IDictionary<IParameterMetadata, IReadOnlyList<ParameterMapping>> _parameterDtoMappings;

		public ComplexParameterListMapping(IReadOnlyList<ParameterMapping> parameterMappings)
		{
			Argument.IsNotNull(parameterMappings, nameof(parameterMappings));

			_parameterMappings = parameterMappings.GroupBy(m => m.ParameterMetadata).ToDictionary(g => g.Key, g => g.ToArray().AsReadOnly());
			_parameterDtoMappings = parameterMappings.GroupBy(m => m.ParameterDtoMetadata).ToDictionary(g => g.Key, g => g.ToArray().AsReadOnly());
			ParameterMetadataList = parameterMappings.Select(m => m.ParameterMetadata).Distinct().ToArray().AsReadOnly();
			ParameterDtoMetadataList = parameterMappings.Select(m => m.ParameterDtoMetadata).Distinct().ToArray().AsReadOnly();
		}

		public IReadOnlyList<ActionArgument> MapParametersToDtos(IReadOnlyList<ActionArgument> arguments)
		{
			Argument.IsNotNull(arguments, nameof(arguments));

			IDictionary<IParameterMetadata, ActionArgument> argumentDtos = new Dictionary<IParameterMetadata, ActionArgument>(ParameterDtoMetadataList.Count);
			var result = new List<ActionArgument>(ParameterDtoMetadataList.Count);
			var mappedArguments = new HashSet<IParameterMetadata>();

			ParameterMapping currentParameterMapping = null;
			try
			{
				foreach (var argument in arguments)
				{
					if (!_parameterMappings.TryGetValue(argument.Metadata, out var mappings))
					{
						throw new InvalidOperationException("Given argument is not supported by this mapping.")
							.SetData("FullParameterName", argument.Metadata.MetadataName);
					}

					if (!mappedArguments.Add(argument.Metadata))
					{
						throw new InvalidOperationException("Duplicate argument.")
							.SetData("ParameterName", argument.Metadata.Name);
					}

					foreach (var mapping in mappings)
					{
						currentParameterMapping = mapping;

						if (!argumentDtos.TryGetValue(mapping.ParameterDtoMetadata, out var argumentDto))
						{
							argumentDto = new ActionArgument(mapping.ParameterDtoMetadata);

							argumentDtos[argumentDto.Metadata] = argumentDto;
							result.Add(argumentDto);
						}

						mapping.MapParameterToDto(argument, argumentDto);
					}
				}
			}
			catch (Exception e)
			{
				if (currentParameterMapping != null)
				{
					e.SetData("ParameterName", currentParameterMapping.ParameterMetadata.Name);
					e.SetData("ParameterDtoName", currentParameterMapping.ParameterDtoMetadata.Name);
				}

				throw;
			}

			if (mappedArguments.Count != ParameterMetadataList.Count)
			{
				throw new InvalidOperationException("Too few arguments.");
			}

			return result.AsReadOnly();
		}

		public IReadOnlyList<ActionArgument> MapDtosToParameters(IReadOnlyList<ActionArgument> argumentDtos)
		{
			Argument.IsNotNull(argumentDtos, nameof(argumentDtos));

			IDictionary<IParameterMetadata, ActionArgument> arguments = new Dictionary<IParameterMetadata, ActionArgument>(ParameterMetadataList.Count);
			var result = new List<ActionArgument>(ParameterMetadataList.Count);
			var mappedDtos = new HashSet<IParameterMetadata>();

			ParameterMapping currentParameterMapping = null;
			try
			{
				foreach (var argumentDto in argumentDtos)
				{
					if (!_parameterDtoMappings.TryGetValue(argumentDto.Metadata, out var mappings))
					{
						throw new InvalidOperationException("Given data-transfer argument is not supported by this mapping.")
							.SetData("FullParameterName", argumentDto.Metadata.MetadataName);
					}

					if (!mappedDtos.Add(argumentDto.Metadata))
					{
						throw new InvalidOperationException("Duplicate data-transfer argument.")
							.SetData("ParameterName", argumentDto.Metadata.Name);
					}

					foreach (var mapping in mappings)
					{
						currentParameterMapping = mapping;

						if (!arguments.TryGetValue(mapping.ParameterMetadata, out var argument))
						{
							argument = new ActionArgument(mapping.ParameterMetadata);

							arguments[argument.Metadata] = argument;
							result.Add(argument);
						}

						mapping.MapDtoToParameter(argumentDto, argument);
					}
				}
			}
			catch (Exception e)
			{
				if (currentParameterMapping != null)
				{
					e.SetData("ParameterName", currentParameterMapping.ParameterMetadata.Name);
					e.SetData("ParameterDtoName", currentParameterMapping.ParameterDtoMetadata.Name);
				}

				throw;
			}

			if (mappedDtos.Count != ParameterDtoMetadataList.Count)
			{
				throw new InvalidOperationException("Too few data-transfer arguments.");
			}

			return result.AsReadOnly();
		}

		public IReadOnlyList<IParameterMetadata> ParameterMetadataList { get; }

		public IReadOnlyList<IParameterMetadata> ParameterDtoMetadataList { get; }
	}
}