package de.metas.ui.web.quickinput;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.adempiere.exceptions.AdempiereException;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;

import de.metas.cache.CCache;
import de.metas.lang.SOTrx;
import de.metas.logging.LogManager;
import de.metas.ui.web.exceptions.EntityNotFoundException;
import de.metas.ui.web.window.datatypes.DocumentId;
import de.metas.ui.web.window.datatypes.DocumentType;
import de.metas.ui.web.window.descriptor.DetailId;
import de.metas.ui.web.window.descriptor.DocumentEntityDescriptor;
import lombok.NonNull;

/*
 * #%L
 * metasfresh-webui-api
 * %%
 * Copyright (C) 2017 metas GmbH
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

@Service
public class QuickInputDescriptorFactoryService
{
	private static final Logger logger = LogManager.getLogger(QuickInputDescriptorFactoryService.class);

	private final ImmutableListMultimap<IQuickInputDescriptorFactory.MatchingKey, IQuickInputDescriptorFactory> factories;
	private final CCache<QuickInputDescriptorKey, QuickInputDescriptor> descriptors = CCache.newCache("QuickInputDescriptors", 10, 0);

	@Autowired
	private QuickInputDescriptorFactoryService(final Optional<List<IQuickInputDescriptorFactory>> factoriesList)
	{
		factories = createFactoriesFromContext(factoriesList.orElse(null));

		//
		if (logger.isInfoEnabled())
		{
			factories.forEach((matchingKey, factory) -> logger.info("Registered: {} -> {}", matchingKey, factory));
		}
	}

	private static ImmutableListMultimap<IQuickInputDescriptorFactory.MatchingKey, IQuickInputDescriptorFactory> createFactoriesFromContext(final List<IQuickInputDescriptorFactory> factoriesList)
	{
		if (factoriesList == null || factoriesList.isEmpty())
		{
			return ImmutableListMultimap.of();
		}

		final ImmutableListMultimap.Builder<IQuickInputDescriptorFactory.MatchingKey, IQuickInputDescriptorFactory> factoriesMap = ImmutableListMultimap.builder();
		for (final IQuickInputDescriptorFactory factory : factoriesList)
		{
			final Set<IQuickInputDescriptorFactory.MatchingKey> matchingKeys = factory.getMatchingKeys();
			if (matchingKeys == null || matchingKeys.isEmpty())
			{
				logger.warn("Ignoring {} because it provides no matching keys", factory);
				break;
			}

			for (final IQuickInputDescriptorFactory.MatchingKey matchingKey : matchingKeys)
			{
				factoriesMap.put(matchingKey, factory);
			}
		}

		return factoriesMap.build();
	}

	public QuickInputDescriptor getQuickInputEntityDescriptor(final DocumentEntityDescriptor includedDocumentDescriptor)
	{
		if (!includedDocumentDescriptor.isAllowQuickInput())
		{
			throw new AdempiereException("Quick input is not supported for " + includedDocumentDescriptor.getInternalName());
		}

		final QuickInputDescriptorKey key = createQuickInputDescriptorKey(includedDocumentDescriptor);
		final QuickInputDescriptor descriptor = getQuickInputEntityDescriptorOrNull(key);
		if (descriptor == null)
		{
			throw new EntityNotFoundException("Batch input not available; found no QuickInputDescriptor for the given parameters")
					.appendParametersToMessage()
					.setParameter("key", key);
		}
		return descriptor;
	}

	public boolean hasQuickInputEntityDescriptor(
			@NonNull final DocumentType documentType,
			@NonNull final DocumentId documentTypeId,
			@NonNull final Optional<String> includedTableName,
			@NonNull final DetailId includedTabId,
			@NonNull final Optional<SOTrx> soTrx)
	{
		final QuickInputDescriptorKey key = QuickInputDescriptorKey.builder()
				.documentType(documentType)
				.documentTypeId(documentTypeId)
				.includedTableName(includedTableName)
				.includedTabId(includedTabId)
				.soTrx(soTrx)
				.build();

		return hasQuickInputEntityDescriptor(key);
	}

	public boolean hasQuickInputEntityDescriptor(@NonNull final DocumentEntityDescriptor includedDocumentDescriptor)
	{
		final QuickInputDescriptorKey key = createQuickInputDescriptorKey(includedDocumentDescriptor);
		return hasQuickInputEntityDescriptor(key);
	}

	private boolean hasQuickInputEntityDescriptor(final QuickInputDescriptorKey key)
	{
		return getQuickInputEntityDescriptorOrNull(key) != null;
	}

	private static final QuickInputDescriptorKey createQuickInputDescriptorKey(final DocumentEntityDescriptor includedDocumentDescriptor)
	{
		return QuickInputDescriptorKey.builder()
				.documentType(includedDocumentDescriptor.getDocumentType())
				.documentTypeId(includedDocumentDescriptor.getDocumentTypeId())
				.includedTableName(Optional.ofNullable(includedDocumentDescriptor.getTableNameOrNull()))
				.includedTabId(includedDocumentDescriptor.getDetailId())
				.soTrx(includedDocumentDescriptor.getSOTrx())
				.build();
	}

	private QuickInputDescriptor getQuickInputEntityDescriptorOrNull(final QuickInputDescriptorKey key)
	{
		return descriptors.getOrLoad(key, this::createQuickInputDescriptorOrNull);
	}

	private QuickInputDescriptor createQuickInputDescriptorOrNull(final QuickInputDescriptorKey key)
	{
		final IQuickInputDescriptorFactory quickInputDescriptorFactory = getQuickInputDescriptorFactory(key);
		if (quickInputDescriptorFactory == null)
		{
			return null;
		}

		return quickInputDescriptorFactory.createQuickInputDescriptor(
				key.getDocumentType(),
				key.getDocumentTypeId(),
				key.getIncludedTabId(),
				key.getSoTrx());
	}

	private IQuickInputDescriptorFactory getQuickInputDescriptorFactory(final QuickInputDescriptorKey key)
	{
		return getQuickInputDescriptorFactory(
				//
				// factory for included document:
				IQuickInputDescriptorFactory.MatchingKey.includedDocument(
						key.getDocumentType(),
						key.getDocumentTypeId(),
						key.getIncludedTableName().orElse(null)),
				//
				// factory for table:
				IQuickInputDescriptorFactory.MatchingKey.ofTableName(key.getIncludedTableName().orElse(null)));
	}

	private IQuickInputDescriptorFactory getQuickInputDescriptorFactory(final IQuickInputDescriptorFactory.MatchingKey... matchingKeys)
	{
		for (final IQuickInputDescriptorFactory.MatchingKey matchingKey : matchingKeys)
		{
			final IQuickInputDescriptorFactory factory = getQuickInputDescriptorFactory(matchingKey);
			if (factory != null)
			{
				return factory;
			}
		}

		return null;
	}

	private IQuickInputDescriptorFactory getQuickInputDescriptorFactory(final IQuickInputDescriptorFactory.MatchingKey matchingKey)
	{
		final ImmutableList<IQuickInputDescriptorFactory> matchingFactories = factories.get(matchingKey);
		if (matchingFactories.isEmpty())
		{
			return null;
		}

		if (matchingFactories.size() > 1)
		{
			logger.warn("More than one factory found for {}. Using the first one: {}", matchingFactories);
		}

		return matchingFactories.get(0);
	}

	@lombok.Builder
	@lombok.Value
	private static class QuickInputDescriptorKey
	{
		@NonNull
		DocumentType documentType;

		@NonNull
		DocumentId documentTypeId;

		@NonNull
		Optional<String> includedTableName;

		@NonNull
		DetailId includedTabId;

		@NonNull
		Optional<SOTrx> soTrx;
	}
}
