ProvidesMatchersWithSameValueHelper.java

package ch.powerunit.extensions.matchers.provideprocessor.helper;

import static ch.powerunit.extensions.matchers.common.CommonUtils.addPrefix;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.of;
import static java.util.stream.Collectors.joining;

import java.util.Collection;
import java.util.Map.Entry;
import java.util.Optional;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.tools.Diagnostic.Kind;

import ch.powerunit.extensions.matchers.common.RessourceLoaderHelper;
import ch.powerunit.extensions.matchers.provideprocessor.Matchable;
import ch.powerunit.extensions.matchers.provideprocessor.ProvidesMatchersAnnotatedElementMirror;
import ch.powerunit.extensions.matchers.provideprocessor.dsl.DSLMethod;
import ch.powerunit.extensions.matchers.provideprocessor.dsl.lang.DSLMethodArgument;
import ch.powerunit.extensions.matchers.provideprocessor.fields.AbstractFieldDescription;

public final class ProvidesMatchersWithSameValueHelper {

	private static final String HAS_SAME_VALUE_IGNORE_CYCLE = RessourceLoaderHelper
			.loadRessource(ProvidesMatchersWithSameValueHelper.class, "DSLHasSameValueIgnoreAndCycle.txt");

	private static final String JAVADOC_OTHER = "other the other object to be used as a reference.";

	private static final String JAVDOC_PREVIOUS = "previous the previous object of the call stack of matcher.";

	private static final String JAVADOC_IGNORE = "ignoredFields fields name that must be ignored.";

	private static final String JAVADOC_POSTPROCESSOR = "postProcessor Function to be applied to modify, if necessary, the matchers.";

	private static final String JAVADOC_OTHER_IGNORE = JAVADOC_OTHER + "\n" + JAVADOC_IGNORE;

	private static final String JAVADOC_OTHER_IGNORE_POSTPROCESSOR = JAVADOC_OTHER + "\n" + JAVADOC_IGNORE + "\n"
			+ JAVADOC_POSTPROCESSOR;

	private static final String JAVADOC_OTHER_PREVIOUS_IGNORE_POST = JAVADOC_OTHER + "\n" + JAVDOC_PREVIOUS + "\n"
			+ JAVADOC_IGNORE + "\n" + JAVADOC_POSTPROCESSOR;

	private ProvidesMatchersWithSameValueHelper() {
	}

	private static DSLMethod generateWithSameValueWithParentMatcherIgnoreAndCycle(
			ProvidesMatchersAnnotatedElementMirror target, boolean hasSuper) {
		return generateHasSameValueDeclaration(target)
				.addOneArgument(target.getFullyQualifiedNameOfClassAnnotatedWithProvideMatcherWithGeneric(), "other")
				.addOneArgument("java.util.Set<java.lang.Object>", "previous")
				.addOneArgument(
						"java.util.function.BiFunction<org.hamcrest.Matcher<?>,java.lang.Object,org.hamcrest.Matcher<?>>",
						"postProcessor")
				.addOneArgument("String...", "ignoredFields")
				.withImplementation(asList(format(HAS_SAME_VALUE_IGNORE_CYCLE,
						target.getSimpleNameOfGeneratedInterfaceMatcherWithGenericNoParent(),
						target.getSimpleNameOfGeneratedImplementationMatcherWithGenericNoParent(),
						generateParentMatcher(target, hasSuper), copyFields(target),
						target.getSimpleNameOfGeneratedInterfaceMatcher()).split("\n")))
				.withJavadoc(target.generateDefaultJavaDocWithoutDSLStarter(of(JAVADOC_OTHER_PREVIOUS_IGNORE_POST),
						"the DSL matcher", false));
	}

	private static String generateParentMatcher(ProvidesMatchersAnnotatedElementMirror target, boolean hasSuper) {
		return hasSuper ? (target.getParentMirror()
				.map(p -> "(" + p.getFullyQualifiedNameOfGeneratedClass() + "."
						+ p.getSimpleNameOfGeneratedInterfaceMatcher()
						+ ")java.util.Objects.requireNonNull(postProcessor,\"postProcessor can't be null\").apply("
						+ p.getWithSameValue(false) + "(other"
						+ (p.supportSameValueWithParentPostPrecessor() ? ",postProcessor" : "")
						+ (p.supportIgnore() ? ",ignoredFields" : "") + "),other)")
				.orElse("org.hamcrest.Matchers.anything()")) : "";
	}

	private static String copyFields(ProvidesMatchersAnnotatedElementMirror target) {
		return target.getFields().stream().flatMap(f -> stream(copyField(f).split("\n"))).collect(joining("\n"));
	}

	private static String copyField(AbstractFieldDescription f) {
		String args = f
				.getTargetAsMatchable().filter(Matchable::supportCycleDetectionV1).map(x -> ",nPrevious"
						+ (x.supportSameValueWithParentPostPrecessor() ? ",postProcessor" : "") + ",localIgnored")
				.orElse(",localIgnored");
		return format(
				"if(!ignored.contains(\"%1$s\")) {\n  String localIgnored[] = ignored.stream().filter(s->s.startsWith(\"%1$s.\")).map(s->s.replaceFirst(\"%1$s\\\\.\",\"\")).toArray(String[]::new);\n%2$s\n}",
				f.getFieldName(), addPrefix("  ", f.getFieldCopy("m", "other", args)));
	}

	private static DSLMethod generateWithSameValueWithParentMatcherIgnoreAndPostProcessor(
			ProvidesMatchersAnnotatedElementMirror target) {
		return generateHasSameValueDeclaration(target)
				.addOneArgument(target.getFullyQualifiedNameOfClassAnnotatedWithProvideMatcherWithGeneric(), "other")
				.addOneArgument(
						"java.util.function.BiFunction<org.hamcrest.Matcher<?>,java.lang.Object,org.hamcrest.Matcher<?>>",
						"postProcessor")
				.addOneArgument("String...", "ignoredFields")
				.withImplementation(generateReturnOther(target, "postProcessor,ignoredFields"))
				.withJavadoc(target.generateDefaultJavaDocWithoutDSLStarter(of(JAVADOC_OTHER_IGNORE_POSTPROCESSOR),
						"the DSL matcher", false));
	}

	private static DSLMethod generateWithSameValueWithParentMatcherIgnore(
			ProvidesMatchersAnnotatedElementMirror target) {
		return generateHasSameValueDeclaration(target)
				.addOneArgument(target.getFullyQualifiedNameOfClassAnnotatedWithProvideMatcherWithGeneric(), "other")
				.addOneArgument("String...", "ignoredFields")
				.withImplementation(generateReturnOther(target, "(m,o)->m,ignoredFields")).withJavadoc(target
						.generateDefaultJavaDocWithoutDSLStarter(of(JAVADOC_OTHER_IGNORE), "the DSL matcher", false));
	}

	private static DSLMethod generateWithSameValueWithParentMatcherAndNoIgnore(
			ProvidesMatchersAnnotatedElementMirror target) {
		return generateHasSameValueDeclaration(target)
				.withOneArgument(target.getFullyQualifiedNameOfClassAnnotatedWithProvideMatcherWithGeneric(), "other")
				.withImplementation(generateReturnOther(target, "(m,o)->m,new String[]{}")).withJavadoc(
						target.generateDefaultJavaDocWithoutDSLStarter(of(JAVADOC_OTHER), "the DSL matcher", false));
	}

	private static String generateReturnOther(ProvidesMatchersAnnotatedElementMirror target, String complement) {
		return format("return %1$s(other,java.util.Collections.emptySet(),%2$s);",
				target.getMethodNameDSLWithSameValue(), complement);
	}

	private static DSLMethodArgument generateHasSameValueDeclaration(ProvidesMatchersAnnotatedElementMirror target) {
		return DSLMethod.of(
				format("%1$s %2$s.%3$s %4$s", target.getFullGeneric(), target.getFullyQualifiedNameOfGeneratedClass(),
						target.getSimpleNameOfGeneratedInterfaceMatcherWithGenericNoParent(),
						target.getMethodNameDSLWithSameValue()));
	}

	private static boolean isWeakAllowed(ProvidesMatchersAnnotatedElementMirror target) {
		return target.getRealAnnotation().allowWeakWithSameValue();
	}

	private static void logWeak(ProvidesMatchersAnnotatedElementMirror target) {
		Optional<AnnotationMirror> am = target.getAnnotationMirror();
		Optional<? extends AnnotationValue> av = am.map(a -> a.getElementValues().entrySet().stream()
				.filter(kv -> kv.getKey().getSimpleName().toString().equals("allowWeakWithSameValue"))
				.map(Entry::getValue).findAny().orElse(null));
		target.getMessager().printMessage(Kind.MANDATORY_WARNING,
				"This class use the option allowWeakWithSameValue and a weak WithSameValue is detected. The generated WithSameValue DSL may not be able to fully control all the field of this class",
				target.getElement(), am.orElse(null), av.orElse(null));
	}

	public static Collection<DSLMethod> generateParentValueDSLStarter(ProvidesMatchersAnnotatedElementMirror target) {
		return target.getParentMirror()
				.map(parentMirror -> asList(generateWithSameValueWithParentMatcherIgnoreAndCycle(target, true),
						generateWithSameValueWithParentMatcherIgnoreAndPostProcessor(target),
						generateWithSameValueWithParentMatcherIgnore(target),
						generateWithSameValueWithParentMatcherAndNoIgnore(target)))
				.orElseGet(() -> {
					if (isWeakAllowed(target)) {
						logWeak(target);
						return asList(generateWithSameValueWithParentMatcherIgnoreAndCycle(target, true),
								generateWithSameValueWithParentMatcherIgnoreAndPostProcessor(target),
								generateWithSameValueWithParentMatcherIgnore(target),
								generateWithSameValueWithParentMatcherAndNoIgnore(target));
					} else {
						return emptyList();
					}
				});
	}

	public static Collection<DSLMethod> generateNoParentValueDSLStarter(ProvidesMatchersAnnotatedElementMirror target) {
		return asList(generateWithSameValueWithParentMatcherIgnoreAndCycle(target, false),
				generateWithSameValueWithParentMatcherIgnoreAndPostProcessor(target),
				generateWithSameValueWithParentMatcherIgnore(target),
				generateWithSameValueWithParentMatcherAndNoIgnore(target));
	}

}