/*
 * Copyright (c) 2002-2008 LWJGL Project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'LWJGL' nor the names of
 *   its contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.lwjgl.util.generator;

/**
 *
 * This class generates the functions in the native source files.
 *
 * @author elias_naur <elias_naur@users.sourceforge.net>
 * @version $Revision$
 * $Id$
 */

import org.lwjgl.PointerBuffer;

import java.io.PrintWriter;
import java.nio.Buffer;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

public final class NativeMethodStubsGenerator {
	private static final String BUFFER_ADDRESS_POSTFIX  = "_address";
	public static final  String BUFFER_POSITION_POSTFIX = "_position";
	private static final String STRING_LIST_NAME        = "_str";
	private static final String POINTER_LIST_NAME       = "_ptr";

	public static void generateNativeMethodStubs(ProcessingEnvironment env, TypeMap type_map, PrintWriter writer, TypeElement d, boolean generate_error_checks, boolean context_specific) {
		for ( ExecutableElement method : Utils.getMethods(d) ) {
			Alternate alt_annotation = method.getAnnotation(Alternate.class);
			if ( (alt_annotation != null && (!alt_annotation.nativeAlt() || alt_annotation.skipNative())) || method.getAnnotation(Reuse.class) != null )
				continue;
			generateMethodStub(env, type_map, writer, Utils.getQualifiedClassName(d), method, Mode.NORMAL, generate_error_checks, context_specific);
			if ( Utils.hasMethodBufferObjectParameter(method) )
				generateMethodStub(env, type_map, writer, Utils.getQualifiedClassName(d), method, Mode.BUFFEROBJECT, generate_error_checks, context_specific);
		}
	}

	private static void generateParameters(PrintWriter writer, List<? extends VariableElement> params, Mode mode) {
		for ( VariableElement param : params ) {
			if ( param.getAnnotation(Result.class) != null || (param.getAnnotation(Helper.class) != null && !param.getAnnotation(Helper.class).passToNative()) )
				continue;
			final Constant constant_annotation = param.getAnnotation(Constant.class);
			if ( constant_annotation == null || !constant_annotation.isNative() )
				generateParameter(writer, param, mode);
		}
	}

	private static void generateParameter(PrintWriter writer, VariableElement param, Mode mode) {
		writer.print(", ");
		if ( mode == Mode.BUFFEROBJECT && param.getAnnotation(BufferObject.class) != null ) {
			writer.append("jlong ").append(param.getSimpleName()).append(Utils.BUFFER_OBJECT_PARAMETER_POSTFIX);
		} else if ( param.getAnnotation(PointerWrapper.class) != null ) {
			writer.append("jlong ").append(param.getSimpleName());
		} else {
			JNITypeTranslator translator = new JNITypeTranslator();
			param.asType().accept(translator, null);
			writer.append(translator.getSignature()).append(' ').append(param.getSimpleName());
		}
	}

	private static void generateMethodStub(ProcessingEnvironment env, TypeMap type_map, PrintWriter writer, String interface_name, ExecutableElement method, Mode mode, boolean generate_error_checks, boolean context_specific) {
		if ( !context_specific && method.getAnnotation(Alternate.class) == null )
			writer.print("static ");
		else
			writer.print("JNIEXPORT ");

		final TypeMirror result_type = Utils.getMethodReturnType(method);
		final CachedResult cached_result_annotation = method.getAnnotation(CachedResult.class);
		final AutoSize auto_size_annotation = method.getAnnotation(AutoSize.class);

		if ( method.getAnnotation(PointerWrapper.class) != null ) {
			writer.print("jlong");
		} else {
			JNITypeTranslator translator = new JNITypeTranslator();
			result_type.accept(translator, null);
			writer.print(translator.getReturnSignature());
		}
		writer.print(" JNICALL ");

		writer.print(Utils.getQualifiedNativeMethodName(interface_name, method, generate_error_checks, context_specific));
		if ( mode == Mode.BUFFEROBJECT )
			writer.print(Utils.BUFFER_OBJECT_METHOD_POSTFIX);
		writer.print("(JNIEnv *env, jclass clazz");
		generateParameters(writer, method.getParameters(), mode);
		if ( Utils.getNIOBufferType(result_type) != null ) {
			if ( (cached_result_annotation == null || !cached_result_annotation.isRange()) && (auto_size_annotation == null || !auto_size_annotation.isNative()) )
				writer.print(", jlong " + Utils.RESULT_SIZE_NAME);
			if ( cached_result_annotation != null )
				writer.print(", jobject " + Utils.CACHED_BUFFER_NAME);
		}
		if ( context_specific ) {
			writer.print(", jlong " + Utils.FUNCTION_POINTER_VAR_NAME);
		}
		writer.append(") {\n");

		generateBufferParameterAddresses(type_map, writer, method, mode);
		Alternate alt_annotation = method.getAnnotation(Alternate.class);
		if ( context_specific ) {
			String typedef_name = Utils.getTypedefName(method);
			writer.append('\t').append(typedef_name).append(' ').append(alt_annotation == null ? method.getSimpleName() : alt_annotation.value());
			writer.append(" = (").append(typedef_name).append(")((intptr_t)");
			writer.append(Utils.FUNCTION_POINTER_VAR_NAME + ");\n");
		}

		final Code code_annotation = method.getAnnotation(Code.class);

		final boolean hasResult = !result_type.equals(env.getTypeUtils().getNoType(TypeKind.VOID));
		final boolean resultPreDeclare = hasResult && (hasPointerArrayInits(method.getParameters()) || (code_annotation != null && (code_annotation.nativeAfterVars().length() > 0 || code_annotation.nativeBeforeCall().length() > 0)));
		if ( resultPreDeclare )
			printResultParam(type_map, writer, method, result_type, true);

		if ( code_annotation != null && code_annotation.nativeAfterVars().length() > 0 )
			writer.append(code_annotation.nativeAfterVars()).append('\n');

		generatePointerArrayInits(type_map, writer, method.getParameters());

		if ( code_annotation != null && code_annotation.nativeBeforeCall().length() > 0 )
			writer.append(code_annotation.nativeBeforeCall()).append('\n');

		writer.print("\t");
		if ( resultPreDeclare )
			writer.print(Utils.RESULT_VAR_NAME + " = ");
		else if ( hasResult )
			printResultParam(type_map, writer, method, result_type, false);
		writer.append(alt_annotation == null ? method.getSimpleName() : alt_annotation.value()).append('(');
		generateCallParameters(writer, type_map, method.getParameters());
		writer.append(");\n");

		if ( code_annotation != null && code_annotation.nativeAfterCall().length() > 0 )
			writer.append(code_annotation.nativeAfterCall()).append('\n');

		generateStringDeallocations(writer, method.getParameters());
		if ( !result_type.equals(env.getTypeUtils().getNoType(TypeKind.VOID)) ) {
			writer.append("\treturn ");
			Class java_result_type = Utils.getJavaType(result_type);
			if ( Buffer.class.isAssignableFrom(java_result_type) ) {
				if ( cached_result_annotation != null )
					writer.print("safeNewBufferCached(env, ");
				else
					writer.print("safeNewBuffer(env, ");
			} else if ( String.class.equals(java_result_type) ) {
				writer.print("NewStringNativeUnsigned(env, ");
			} else if ( method.getAnnotation(PointerWrapper.class) != null ) {
				writer.print("(intptr_t)");
			}
			writer.print(Utils.RESULT_VAR_NAME);
			if ( Buffer.class.isAssignableFrom(java_result_type) ) {
				final String size_parameter_name;
				if ( auto_size_annotation != null && (auto_size_annotation.isNative() || (cached_result_annotation != null && cached_result_annotation.isRange())) )
					size_parameter_name = auto_size_annotation.value();
				else
					size_parameter_name = Utils.RESULT_SIZE_NAME;

				writer.print(", ");
				Utils.printExtraCallArguments(writer, method, size_parameter_name);
			}
			if ( Buffer.class.isAssignableFrom(java_result_type) ||
			     String.class.equals(java_result_type) )
				writer.print(")");
			writer.append(";\n");
		}
		writer.append("}\n");
	}

	private static void printResultParam(final TypeMap type_map, final PrintWriter writer, final ExecutableElement method, final TypeMirror result_type, final boolean preDeclare) {
		final VariableElement result_param = Utils.getResultParameter(method);
		final Element return_declaration = result_param == null ? method : result_param;
		final NativeTypeTranslator result_translator = new NativeTypeTranslator(type_map, return_declaration);
		result_type.accept(result_translator, null);
		if ( preDeclare )
			writer.print("\t");
		writer.append(result_translator.getSignature()).append(' ').append(Utils.RESULT_VAR_NAME);
		if ( preDeclare )
			writer.append(";\n");
		else
			writer.append(result_param == null ? " = " : ";\n\t");
	}

	private static void generateCallParameters(PrintWriter writer, TypeMap type_map, List<? extends VariableElement> params) {
		if ( params.size() > 0 ) {
			boolean first = true;
			for ( VariableElement param : params ) {
				if ( param.getAnnotation(Helper.class) != null )
					continue;

				if ( first )
					first = false;
				else
					writer.append(", ");

				generateCallParameter(writer, type_map, param);
			}
		}
	}

	private static void generateCallParameter(PrintWriter writer, TypeMap type_map, VariableElement param) {
		if ( param.getAnnotation(Helper.class) != null )
			return;

		final Constant constant_annotation = param.getAnnotation(Constant.class);
		if ( constant_annotation != null && constant_annotation.isNative() ) {
			writer.append(constant_annotation.value());
			return;
		}

		boolean is_indirect = param.getAnnotation(Indirect.class) != null;
		if ( is_indirect || param.getAnnotation(PointerArray.class) != null ) {
			writer.append('(');
			final NativeTypeTranslator translator = new NativeTypeTranslator(type_map, param);
			param.asType().accept(translator, null);
			writer.append(translator.getSignature());
			writer.append("*)");
		}
		if ( param.getAnnotation(PointerWrapper.class) != null )
			writer.append('(').append(param.getAnnotation(PointerWrapper.class).value()).append(")(intptr_t)");
		if ( param.getAnnotation(Result.class) != null || is_indirect )
			writer.append('&');

		if ( param.getAnnotation(Result.class) != null ) {
			writer.append(Utils.RESULT_VAR_NAME);
		} else {
			writer.append(param.getSimpleName());
			if ( param.getAnnotation(PointerArray.class) != null )
				writer.append(getPointerArrayName(Utils.getJavaType(param.asType())));
			else if ( Utils.isAddressableType(param.asType()) )
				writer.append(BUFFER_ADDRESS_POSTFIX);
		}
	}

	private static void generateStringDeallocations(PrintWriter writer, List<? extends VariableElement> params) {
		for ( VariableElement param : params ) {
			final Class java_type = Utils.getJavaType(param.asType());
			if ( java_type.equals(String.class) && param.getAnnotation(Result.class) == null )
				writer.append("\tfree(" + param.getSimpleName() + BUFFER_ADDRESS_POSTFIX + ");\n");
			else if ( param.getAnnotation(PointerArray.class) != null ) // Free the string array mem
				writer.append("\tfree(" + param.getSimpleName() + getPointerArrayName(java_type) + ");\n");
		}
	}

	private static void generateBufferParameterAddresses(TypeMap type_map, PrintWriter writer, ExecutableElement method, Mode mode) {
		strLoopDeclared = false;
		ptrLoopDeclared = false;
		for ( VariableElement param : method.getParameters() ) {
			final Constant constant_annotation = param.getAnnotation(Constant.class);
			if ( param.getAnnotation(Result.class) == null && (constant_annotation == null || !constant_annotation.isNative()) && Utils.isAddressableType(param.asType()) )
				generateBufferParameterAddress(type_map, writer, param, mode);
		}
	}

	private static boolean strLoopDeclared;
	private static boolean ptrLoopDeclared;

	private static void generateBufferParameterAddress(TypeMap type_map, PrintWriter writer, VariableElement param, Mode mode) {
		final Check check_annotation = param.getAnnotation(Check.class);
		final PointerArray array_annotation = param.getAnnotation(PointerArray.class);
		final Class java_type = Utils.getJavaType(param.asType());

		final NativeTypeTranslator translator = new NativeTypeTranslator(type_map, param);
		param.asType().accept(translator, null);
		final String native_type = translator.getSignature();

		if ( !java_type.isArray() || CharSequence.class.isAssignableFrom(java_type.getComponentType()) ) {
			writer.append('\t').append(native_type).append(param.getSimpleName());
			writer.append(BUFFER_ADDRESS_POSTFIX + " = (");
			writer.append(native_type);
			writer.append(")(intptr_t)");

			if ( mode == Mode.BUFFEROBJECT && param.getAnnotation(BufferObject.class) != null ) {
				writer.append("offsetToPointer(").append(param.getSimpleName()).append(Utils.BUFFER_OBJECT_PARAMETER_POSTFIX + ')');
			} else {
				if ( Buffer.class.isAssignableFrom(java_type) || java_type.equals(CharSequence.class) || java_type.equals(CharSequence[].class) || PointerBuffer.class.isAssignableFrom(java_type) ) {
					writer.append(param.getSimpleName());
				} else if ( java_type.equals(String.class) ) {
					writer.append("GetStringNativeChars(env, ").append(param.getSimpleName()).append(')');
				} else if ( array_annotation == null )
					throw new RuntimeException("Illegal type " + java_type);
			}
			writer.append(";\n");
		}

		if ( array_annotation != null ) {
			final String n = getPointerArrayName(java_type);
			final String arrayType;
			if ( POINTER_LIST_NAME.equals(n) ) {
				if ( n.equals(param.getSimpleName().toString()) )
					throw new RuntimeException("The name '" + n + "' is not valid for object array arguments annotated with PointerArray");

				arrayType = translator.getSignature(true) + (org.lwjgl.PointerWrapper.class.isAssignableFrom(java_type.getComponentType()) ? " " : "");

				// Declare loop counters and allocate object array
				if ( !ptrLoopDeclared ) {
					writer.append("\tint ").append(n).append("_i;\n");
					writer.append("\tjobject ").append(n).append("_object;\n");
					ptrLoopDeclared = true;
				}
			} else {
				if ( n.equals(param.getSimpleName().toString()) )
					throw new RuntimeException("The name '" + n + "' is not valid for arguments annotated with PointerArray");

				arrayType = translator.getSignature(true);

				// Declare loop counters and allocate string array
				if ( !strLoopDeclared ) {
					writer.append("\tint ").append(n).append("_i;\n");
					writer.append("\t").append(arrayType).append(n).append("_address;\n");
					strLoopDeclared = true;
				}
			}

			writer.append('\t').append(arrayType).append('*').append(param.getSimpleName()).append(n).append(" = ");
			if ( check_annotation != null && check_annotation.canBeNull() )
				writer.append(array_annotation.value()).append(" == 0 ? NULL : ");
			writer.append('(').append(arrayType).append("*) malloc(").append(array_annotation.value()).append(" * sizeof(" + arrayType + "));\n");
		}
	}

	private static String getPointerArrayName(final Class java_type) {
		final Class<?> component_type = java_type.getComponentType();
		if ( component_type != null && (Buffer.class.isAssignableFrom(component_type) || org.lwjgl.PointerWrapper.class.isAssignableFrom(component_type)) )
			return POINTER_LIST_NAME;
		else
			return STRING_LIST_NAME;
	}

	private static boolean hasPointerArrayInits(List<? extends VariableElement> params) {
		for ( VariableElement param : params ) {
			PointerArray pointerArray_annotation = param.getAnnotation(PointerArray.class);
			if ( pointerArray_annotation != null )
				return true;
		}
		return false;
	}

	private static void generatePointerArrayInits(TypeMap type_map, PrintWriter writer, List<? extends VariableElement> params) {
		for ( VariableElement param : params ) {
			PointerArray pointerArray_annotation = param.getAnnotation(PointerArray.class);
			if ( pointerArray_annotation != null ) {
				final Class java_type = Utils.getJavaType(param.asType());
				final Class<?> component_type = java_type.isArray() ? java_type.getComponentType() : null;
				final NativeTypeTranslator translator = new NativeTypeTranslator(type_map, param);
				param.asType().accept(translator, null);

				final String n = getPointerArrayName(java_type);
				if ( POINTER_LIST_NAME.equals(n) ) {
					// Init vars
					writer.append('\t').append(n).append("_i = 0;\n");
					// Fill pointer array with the buffer pointers
					writer.append("\twhile ( ").append(n).append("_i < ").append(pointerArray_annotation.value()).append(" ) {\n");
					writer.append("\t\t").append(n).append("_object = (*env)->GetObjectArrayElement(env, ").append(param.getSimpleName()).append(", ").append(n).append("_i);\n");
					writer.append("\t\t").append(param.getSimpleName()).append(n).append('[').append(n).append("_i++] = (").append(translator.getSignature(true)).append(')');
					if ( Buffer.class.isAssignableFrom(component_type) )
						writer.append("(*env)->GetDirectBufferAddress(env, ");
					else
						writer.append("(intptr_t)getPointerWrapperAddress(env, ");
					writer.append(n).append("_object);\n")
						.append("\t}\n");
				} else {
					final String lengths = pointerArray_annotation.lengths();

					// Init vars
					writer.append('\t').append(n).append("_i = 0;\n");
					writer.append('\t').append(n).append("_address = (").append(translator.getSignature(true)).append(')').append(param.getSimpleName()).append(BUFFER_ADDRESS_POSTFIX + ";\n");
					// Fill string array with the string pointers
					writer.append("\twhile ( ").append(n).append("_i < ").append(pointerArray_annotation.value()).append(" ) {\n");
					if ( lengths.length() == 0 ) {
						writer.append("\t\t").append(param.getSimpleName()).append(n).append('[').append(n).append("_i++] = ").append(n).append("_address;\n");
						writer.append("\t\t").append(n).append("_address += strlen((const char *)").append(n).append("_address) + 1;\n");
					} else {
						writer.append("\t\t").append(param.getSimpleName()).append(n).append('[').append(n).append("_i] = ").append(n).append("_address;\n");
						writer.append("\t\t").append(n).append("_address += ").append(lengths).append(BUFFER_ADDRESS_POSTFIX + "[").append(n).append("_i++];\n");
					}
					writer.append("\t}\n");
				}
			}
		}
	}

}
