package com.hervian.lambda;

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;

import org.paukov.combinatorics.Factory;
import org.paukov.combinatorics.Generator;
import org.paukov.combinatorics.ICombinatoricsVector;

/**
 * Copyright 2016 Anders Granau Høfft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * END OF NOTICE
 * 
 * This AbstractProcessor only exists in order to auto-generate source code, namely an interface with multiple signatures.
 * Since the processor is part of the project, where the annotation it processes i located, it is important the compilation
 * proceeds in wel defined rounds, i.e. the processor must be compiled, before the annotated class, which triggers the processor.
 * Being a Maven project, this is currently handled in the pom.xml file, by the maven-compiler-plugin.
 * 
 * @author Anders Granau Høfft
 */
public class GenerateLambdaProcessor extends AbstractProcessor {

	private static final String END_OF_SIGNATURE = ");";
	private static final String NEWLINE_TAB = "\n\t";
	private static final String NEWLINE = "\n";
	private static final String INTERFACE_NAME = "Lambda";
	private static final String PACKAGE = "com.hervian.lambda";
	
	static final String METHOD_NAME = "invoke_for_";	
	private static final String METHOD_NAME_BOOLEAN 	= METHOD_NAME+boolean.class.getSimpleName();
	private static final String METHOD_NAME_CHAR 			= METHOD_NAME+char.class.getSimpleName();
	private static final String METHOD_NAME_BYTE 			= METHOD_NAME+byte.class.getSimpleName();
	private static final String METHOD_NAME_SHORT 		= METHOD_NAME+short.class.getSimpleName();
	private static final String METHOD_NAME_INT 			= METHOD_NAME+int.class.getSimpleName();
	private static final String METHOD_NAME_FLOAT 		= METHOD_NAME+float.class.getSimpleName();
	private static final String METHOD_NAME_LONG 			= METHOD_NAME+long.class.getSimpleName();
	private static final String METHOD_NAME_DOUBLE 		= METHOD_NAME+double.class.getSimpleName();
	private static final String METHOD_NAME_OBJECT 		= METHOD_NAME+Object.class.getSimpleName();
	private static final String METHOD_NAME_VOID 			= METHOD_NAME+void.class.getSimpleName();
	
	private static final String METHOD_NAME_PART_BOOLEAN 	= " "+METHOD_NAME_BOOLEAN	+"(";
	private static final String METHOD_NAME_PART_CHAR 		= " "+METHOD_NAME_CHAR		+"(";
	private static final String METHOD_NAME_PART_BYTE 		= " "+METHOD_NAME_BYTE		+"(";
	private static final String METHOD_NAME_PART_SHORT 		= " "+METHOD_NAME_SHORT		+"(";
	private static final String METHOD_NAME_PART_INT 			= " "+METHOD_NAME_INT			+"(";
	private static final String METHOD_NAME_PART_FLOAT 		= " "+METHOD_NAME_FLOAT		+"(";
	private static final String METHOD_NAME_PART_LONG 		= " "+METHOD_NAME_LONG		+"(";
	private static final String METHOD_NAME_PART_DOUBLE 	= " "+METHOD_NAME_DOUBLE	+"(";
	private static final String METHOD_NAME_PART_OBJECT 	= " "+METHOD_NAME_OBJECT	+"(";
	private static final String METHOD_NAME_PART_VOID 		= " "+METHOD_NAME_VOID		+"(";
	
	private Filer filer;
	private static boolean fileCreated;

	@Override
	public SourceVersion getSupportedSourceVersion() {
		return SourceVersion.latestSupported();
	}

	@Override
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotataions = new LinkedHashSet<String>();
    annotataions.add(GenerateLambda.class.getCanonicalName());
    return annotataions;
  }
	
	@Override
	public void init(ProcessingEnvironment processingEnv) {
		filer = processingEnv.getFiler();
	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
		if (!roundEnv.processingOver() && !fileCreated) {
			Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(GenerateLambda.class);
			for (Element element : elements) {
				if (element.getKind() == ElementKind.CLASS) {
					GenerateLambda generateSignatureContainerAnnotation = element.getAnnotation(GenerateLambda.class);
					generateCode(PACKAGE, INTERFACE_NAME, generateSignatureContainerAnnotation);
					return true;
				}
				break;
			}
		}
		return true;
	}

	private void generateCode(String packageOfMarkerClass, String className, GenerateLambda generateSignatureContainerAnnotation) {
		String fqcn = packageOfMarkerClass + "." + className;
		try (Writer writer = filer.createSourceFile(fqcn).openWriter()) {
			StringBuilder javaFile = new StringBuilder();
			javaFile.append("package ").append(packageOfMarkerClass).append(";");
			javaFile.append("\n\n/**\n * Copyright 2016 Anders Granau Høfft"
					+ "\n * The invocation methods throws an AbstractMethodError, if arguments provided does not match "
					+ "\n * the type defined by the Method over which the lambda was created."
					+ "\n * A typical example of this is that the caller forget to cast a primitive number to its proper type. "
					+ "\n * Fx. forgetting to explicitly cast a number as a short, byte etc. "
					+ "\n * The AbstractMethodException will also be thrown if the caller does not provide"
					+ "\n * an Object instance as the first argument to a non-static method, and vice versa."
					+ "\n * @author Anders Granau Høfft").append("\n */")
							.append("\[email protected](value=\"com.hervian.lambda.GenerateLambdaProcessor\", date=\"").append(new Date()).append("\")")
							.append("\npublic interface " + className + "{\n");
			generateAbstractandConcreteMethods(javaFile, generateSignatureContainerAnnotation);
			javaFile.append("\n}");
			writer.write(javaFile.toString());
			fileCreated = true;
		} catch (IOException e) {
			throw new RuntimeException("An exception occurred while generating the source file "+METHOD_NAME, e);
		}
	}

	private void generateAbstractandConcreteMethods(StringBuilder javaFile, GenerateLambda generateSignatureContainerAnnotation) {
		MethodParameter[] types = generateSignatureContainerAnnotation.paramTypes();
		int maxNumberOfParams = generateSignatureContainerAnnotation.maxNumberOfParameters();
		generateAbstractMethods(javaFile, types, maxNumberOfParams);
	}

	private void generateAbstractMethods(StringBuilder javaFile, MethodParameter[] types, int maxNumberOfParams) {
		List<String> returnTypes = Arrays.asList(types).stream().map(type -> type.getTypeAsSourceCodeString()).collect(Collectors.toList());
		returnTypes.add("void");
		ICombinatoricsVector<MethodParameter> originalVector = Factory.createVector(types);
		generateInterfaceMethodsForStaticCallsWithMaxNumOfArgs(javaFile, originalVector, returnTypes, maxNumberOfParams + 1);
		generateInterfaceMethodCombinationsRecursively(javaFile, originalVector, returnTypes, maxNumberOfParams);
	}

	private void generateInterfaceMethodsForStaticCallsWithMaxNumOfArgs(StringBuilder javaFile,
			ICombinatoricsVector<MethodParameter> originalVector, List<String> returnTypes, int numberOfParams) {
		Generator<MethodParameter> gen = Factory.createPermutationWithRepetitionGenerator(originalVector, numberOfParams);
		for (String returnTypeAsString : returnTypes) {
			for (ICombinatoricsVector<MethodParameter> paramType : gen) {
				if (paramType.getVector().get(0) == MethodParameter.OBJECT) {
					String parameters = getParametersString(paramType, javaFile);
					javaFile.append(NEWLINE_TAB).append(returnTypeAsString).append(getSignatureExclArgsAndReturn(returnTypeAsString)).append(parameters).append(END_OF_SIGNATURE);
				}
			}
		}
	}

	private void generateInterfaceMethodCombinationsRecursively(StringBuilder javaFile,
			ICombinatoricsVector<MethodParameter> originalVector, List<String> returnTypes, int numberOfParams) {
		if (numberOfParams >= 0) {
			javaFile.append(NEWLINE);
			Generator<MethodParameter> gen = Factory.createPermutationWithRepetitionGenerator(originalVector, numberOfParams);
			for (String returnTypeAsString : returnTypes) {
				generateInterfaceMethods(gen, returnTypeAsString, javaFile);
			}
			generateInterfaceMethodCombinationsRecursively(javaFile, originalVector, returnTypes, --numberOfParams);
		}
	}

	private void generateInterfaceMethods(Generator<MethodParameter> gen, String returnTypeAsString, StringBuilder javaFile) {
		javaFile.append(NEWLINE);
		for (ICombinatoricsVector<MethodParameter> paramType : gen) {
			String parameters = getParametersString(paramType, javaFile);
			javaFile.append(NEWLINE_TAB).append(returnTypeAsString).append(getSignatureExclArgsAndReturn(returnTypeAsString)).append(parameters).append(END_OF_SIGNATURE);
		}
	}

	private String getParametersString(ICombinatoricsVector<MethodParameter> paramType, StringBuilder javaFile) {
		AtomicInteger atomicInteger = new AtomicInteger(1);
		return paramType.getVector().stream()
				.map(t -> t.getTypeAsSourceCodeString() + " arg" + atomicInteger.getAndIncrement())
				.collect(Collectors.joining(", "));
	}
	
	private static String getSignatureExclArgsAndReturn(String returnType){
		switch (returnType){
		case "boolean": return METHOD_NAME_PART_BOOLEAN;
		case "byte": return METHOD_NAME_PART_BYTE;
		case "char": return METHOD_NAME_PART_CHAR;
		case "double": return METHOD_NAME_PART_DOUBLE;
		case "float": return METHOD_NAME_PART_FLOAT;
		case "int": return METHOD_NAME_PART_INT;
		case "long": return METHOD_NAME_PART_LONG;
		case "Object": return METHOD_NAME_PART_OBJECT;
		case "short": return METHOD_NAME_PART_SHORT;
		case "void" : return METHOD_NAME_PART_VOID;
		default: return METHOD_NAME_PART_OBJECT;
		}
	}
	
	static String getMethodName(String returnType){
		switch (returnType){
		case "boolean": return METHOD_NAME_BOOLEAN;
		case "byte": return METHOD_NAME_BYTE;
		case "char": return METHOD_NAME_CHAR;
		case "double": return METHOD_NAME_DOUBLE;
		case "float": return METHOD_NAME_FLOAT;
		case "int": return METHOD_NAME_INT;
		case "long": return METHOD_NAME_LONG;
		case "Object": return METHOD_NAME_OBJECT;
		case "short": return METHOD_NAME_SHORT;
		case "void" : return METHOD_NAME_VOID;
		default: return METHOD_NAME_OBJECT;
		}
	}


}