/*
 * Copyright (C) 2015 The Project Lombok Authors.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package lombok.eclipse.handlers.singulars;

import static lombok.eclipse.Eclipse.*;
import static lombok.eclipse.handlers.EclipseHandlerUtil.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.Block;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.ForeachStatement;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.MessageSend;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ReturnStatement;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.ThisReference;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
import org.mangosdk.spi.ProviderFor;

import lombok.core.LombokImmutableList;
import lombok.core.handlers.HandlerUtil;
import lombok.eclipse.EclipseNode;
import lombok.eclipse.handlers.EclipseSingularsRecipes.EclipseSingularizer;
import lombok.eclipse.handlers.EclipseSingularsRecipes.SingularData;

@ProviderFor(EclipseSingularizer.class)
public class EclipseJavaUtilMapSingularizer extends EclipseJavaUtilSingularizer {
	@Override public LombokImmutableList<String> getSupportedTypes() {
		return LombokImmutableList.of("java.util.Map", "java.util.SortedMap", "java.util.NavigableMap");
	}
	
	@Override public List<char[]> listFieldsToBeGenerated(SingularData data, EclipseNode builderType) {
		if (useGuavaInstead(builderType)) {
			return guavaMapSingularizer.listFieldsToBeGenerated(data, builderType);
		}
		
		char[] p = data.getPluralName();
		int len = p.length;
		char[] k = new char[len + 4];
		char[] v = new char[len + 6];
		System.arraycopy(p, 0, k, 0, len);
		System.arraycopy(p, 0, v, 0, len);
		k[len] = '$';
		k[len + 1] = 'k';
		k[len + 2] = 'e';
		k[len + 3] = 'y';
		v[len] = '$';
		v[len + 1] = 'v';
		v[len + 2] = 'a';
		v[len + 3] = 'l';
		v[len + 4] = 'u';
		v[len + 5] = 'e';
		return Arrays.asList(k, v);
	}
	
	@Override public List<char[]> listMethodsToBeGenerated(SingularData data, EclipseNode builderType) {
		if (useGuavaInstead(builderType)) {
			return guavaMapSingularizer.listFieldsToBeGenerated(data, builderType);
		} else {
			return super.listMethodsToBeGenerated(data, builderType);
		}
	}
	
	@Override public List<EclipseNode> generateFields(SingularData data, EclipseNode builderType) {
		if (useGuavaInstead(builderType)) {
			return guavaMapSingularizer.generateFields(data, builderType);
		}
		
		char[] keyName = (new String(data.getPluralName()) + "$key").toCharArray();
		char[] valueName = (new String(data.getPluralName()) + "$value").toCharArray();
		FieldDeclaration buildKeyField; {
			TypeReference type = new QualifiedTypeReference(JAVA_UTIL_ARRAYLIST, NULL_POSS);
			type = addTypeArgs(1, false, builderType, type, data.getTypeArgs());
			buildKeyField = new FieldDeclaration(keyName, 0, -1);
			buildKeyField.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG;
			buildKeyField.modifiers = ClassFileConstants.AccPrivate;
			buildKeyField.declarationSourceEnd = -1;
			buildKeyField.type = type;
		}
		FieldDeclaration buildValueField; {
			TypeReference type = new QualifiedTypeReference(JAVA_UTIL_ARRAYLIST, NULL_POSS);
			List<TypeReference> tArgs = data.getTypeArgs();
			if (tArgs != null && tArgs.size() > 1) tArgs = Collections.singletonList(tArgs.get(1));
			else tArgs = Collections.emptyList();
			type = addTypeArgs(1, false, builderType, type, tArgs);
			buildValueField = new FieldDeclaration(valueName, 0, -1);
			buildValueField.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG;
			buildValueField.modifiers = ClassFileConstants.AccPrivate;
			buildValueField.declarationSourceEnd = -1;
			buildValueField.type = type;
		}
		data.setGeneratedByRecursive(buildKeyField);
		data.setGeneratedByRecursive(buildValueField);
		EclipseNode keyFieldNode = injectFieldAndMarkGenerated(builderType, buildKeyField);
		EclipseNode valueFieldNode = injectFieldAndMarkGenerated(builderType, buildValueField);
		return Arrays.asList(keyFieldNode, valueFieldNode);
	}
	
	@Override public void generateMethods(SingularData data, EclipseNode builderType, boolean fluent, boolean chain) {
		if (useGuavaInstead(builderType)) {
			guavaMapSingularizer.generateMethods(data, builderType, fluent, chain);
			return;
		}
		
		TypeReference returnType = chain ? cloneSelfType(builderType) : TypeReference.baseTypeReference(TypeIds.T_void, 0);
		Statement returnStatement = chain ? new ReturnStatement(new ThisReference(0, 0), 0, 0) : null;
		generateSingularMethod(returnType, returnStatement, data, builderType, fluent);
		
		returnType = chain ? cloneSelfType(builderType) : TypeReference.baseTypeReference(TypeIds.T_void, 0);
		returnStatement = chain ? new ReturnStatement(new ThisReference(0, 0), 0, 0) : null;
		generatePluralMethod(returnType, returnStatement, data, builderType, fluent);
	}
	
	private void generateSingularMethod(TypeReference returnType, Statement returnStatement, SingularData data, EclipseNode builderType, boolean fluent) {
		MethodDeclaration md = new MethodDeclaration(((CompilationUnitDeclaration) builderType.top().get()).compilationResult);
		md.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG;
		md.modifiers = ClassFileConstants.AccPublic;
		
		List<Statement> statements = new ArrayList<Statement>();
		statements.add(createConstructBuilderVarIfNeeded(data, builderType, true));
		
		String sN = new String(data.getSingularName());
		String pN = new String(data.getPluralName());
		char[] keyParamName = (sN + "Key").toCharArray();
		char[] valueParamName = (sN + "Value").toCharArray();
		char[] keyFieldName = (pN + "$key").toCharArray();
		char[] valueFieldName = (pN + "$value").toCharArray();
		
		/* this.pluralname$key.add(singularnameKey); */ {
			FieldReference thisDotKeyField = new FieldReference(keyFieldName, 0L);
			thisDotKeyField.receiver = new ThisReference(0, 0);
			MessageSend thisDotKeyFieldDotAdd = new MessageSend();
			thisDotKeyFieldDotAdd.arguments = new Expression[] {new SingleNameReference(keyParamName, 0L)};
			thisDotKeyFieldDotAdd.receiver = thisDotKeyField;
			thisDotKeyFieldDotAdd.selector = "add".toCharArray();
			statements.add(thisDotKeyFieldDotAdd);
		}
		
		/* this.pluralname$value.add(singularnameValue); */ {
			FieldReference thisDotValueField = new FieldReference(valueFieldName, 0L);
			thisDotValueField.receiver = new ThisReference(0, 0);
			MessageSend thisDotValueFieldDotAdd = new MessageSend();
			thisDotValueFieldDotAdd.arguments = new Expression[] {new SingleNameReference(valueParamName, 0L)};
			thisDotValueFieldDotAdd.receiver = thisDotValueField;
			thisDotValueFieldDotAdd.selector = "add".toCharArray();
			statements.add(thisDotValueFieldDotAdd);
		}
		if (returnStatement != null) statements.add(returnStatement);
		
		md.statements = statements.toArray(new Statement[statements.size()]);
		TypeReference keyParamType = cloneParamType(0, data.getTypeArgs(), builderType);
		Argument keyParam = new Argument(keyParamName, 0, keyParamType, 0);
		TypeReference valueParamType = cloneParamType(1, data.getTypeArgs(), builderType);
		Argument valueParam = new Argument(valueParamName, 0, valueParamType, 0);
		md.arguments = new Argument[] {keyParam, valueParam};
		md.returnType = returnType;
		md.selector = fluent ? data.getSingularName() : HandlerUtil.buildAccessorName("put", new String(data.getSingularName())).toCharArray();
		
		data.setGeneratedByRecursive(md);
		injectMethod(builderType, md);
	}
	
	private void generatePluralMethod(TypeReference returnType, Statement returnStatement, SingularData data, EclipseNode builderType, boolean fluent) {
		MethodDeclaration md = new MethodDeclaration(((CompilationUnitDeclaration) builderType.top().get()).compilationResult);
		md.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG;
		md.modifiers = ClassFileConstants.AccPublic;
		
		String pN = new String(data.getPluralName());
		char[] keyFieldName = (pN + "$key").toCharArray();
		char[] valueFieldName = (pN + "$value").toCharArray();
		
		List<Statement> statements = new ArrayList<Statement>();
		statements.add(createConstructBuilderVarIfNeeded(data, builderType, true));
		
		char[] entryName = "$lombokEntry".toCharArray();
		
		TypeReference forEachType = new QualifiedTypeReference(JAVA_UTIL_MAP_ENTRY, NULL_POSS);
		forEachType = addTypeArgs(2, true, builderType, forEachType, data.getTypeArgs());
		
		MessageSend keyArg = new MessageSend();
		keyArg.receiver = new SingleNameReference(entryName, 0L);
		keyArg.selector = "getKey".toCharArray();
		MessageSend addKey = new MessageSend();
		FieldReference thisDotKeyField = new FieldReference(keyFieldName, 0L);
		thisDotKeyField.receiver = new ThisReference(0, 0);
		addKey.receiver = thisDotKeyField;
		addKey.selector = new char[] {'a', 'd', 'd'};
		addKey.arguments = new Expression[] {keyArg};
		
		MessageSend valueArg = new MessageSend();
		valueArg.receiver = new SingleNameReference(entryName, 0L);
		valueArg.selector = "getValue".toCharArray();
		MessageSend addValue = new MessageSend();
		FieldReference thisDotValueField = new FieldReference(valueFieldName, 0L);
		thisDotValueField.receiver = new ThisReference(0, 0);
		addValue.receiver = thisDotValueField;
		addValue.selector = new char[] {'a', 'd', 'd'};
		addValue.arguments = new Expression[] {valueArg};
		
		LocalDeclaration elementVariable = new LocalDeclaration(entryName, 0, 0);
		elementVariable.type = forEachType;
		ForeachStatement forEach = new ForeachStatement(elementVariable, 0);
		MessageSend invokeEntrySet = new MessageSend();
		invokeEntrySet.selector = new char[] { 'e', 'n', 't', 'r', 'y', 'S', 'e', 't'};
		invokeEntrySet.receiver = new SingleNameReference(data.getPluralName(), 0L);
		forEach.collection = invokeEntrySet;
		Block forEachContent = new Block(0);
		forEachContent.statements = new Statement[] {addKey, addValue};
		forEach.action = forEachContent;
		statements.add(forEach);
		if (returnStatement != null) statements.add(returnStatement);
		
		md.statements = statements.toArray(new Statement[statements.size()]);
		
		TypeReference paramType = new QualifiedTypeReference(JAVA_UTIL_MAP, NULL_POSS);
		paramType = addTypeArgs(2, true, builderType, paramType, data.getTypeArgs());
		Argument param = new Argument(data.getPluralName(), 0, paramType, 0);
		md.arguments = new Argument[] {param};
		md.returnType = returnType;
		md.selector = fluent ? data.getPluralName() : HandlerUtil.buildAccessorName("putAll", new String(data.getPluralName())).toCharArray();
		
		data.setGeneratedByRecursive(md);
		injectMethod(builderType, md);
	}
	
	@Override public void appendBuildCode(SingularData data, EclipseNode builderType, List<Statement> statements, char[] targetVariableName) {
		if (useGuavaInstead(builderType)) {
			guavaMapSingularizer.appendBuildCode(data, builderType, statements, targetVariableName);
			return;
		}
		
		if (data.getTargetFqn().equals("java.util.Map")) {
			statements.addAll(createJavaUtilSetMapInitialCapacitySwitchStatements(data, builderType, true, "emptyMap", "singletonMap", "LinkedHashMap"));
		} else {
			statements.addAll(createJavaUtilSimpleCreationAndFillStatements(data, builderType, true, true, false, true, "TreeMap"));
		}
	}
}
