/*
 * Copyright (c) 2013-2021 Chris Newland.
 * Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
 * Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
 */
package org.adoptopenjdk.jitwatch.model;

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_CLOSE_PARENTHESES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_COMMA;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_DOT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_HAT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_OPEN_PARENTHESES;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_OPEN_SQUARE_BRACKET;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.C_SPACE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING_ASSEMBLY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.DEBUG_LOGGING_SIG_MATCH;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.REGEX_GROUP_ANY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.REGEX_UNICODE_PACKAGE_NAME;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_CLOSE_SQUARE_BRACKET;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DOT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_ESCAPED_CLOSE_SQUARE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_ESCAPED_OPEN_SQUARE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_OPEN_SQUARE_BRACKET;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_POLYMORPHIC_SIGNATURE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_TYPE_NAME_VOID;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.adoptopenjdk.jitwatch.model.assembly.AssemblyMethod;
import org.adoptopenjdk.jitwatch.model.bytecode.BytecodeInstruction;
import org.adoptopenjdk.jitwatch.model.bytecode.ClassBC;
import org.adoptopenjdk.jitwatch.model.bytecode.MemberBytecode;
import org.adoptopenjdk.jitwatch.util.ParseUtil;
import org.adoptopenjdk.jitwatch.util.StringUtil;
import com.chrisnewland.freelogj.Logger;
import com.chrisnewland.freelogj.LoggerFactory;

public abstract class AbstractMetaMember implements IMetaMember, Comparable<IMetaMember>
{
	protected static final Logger logger = LoggerFactory.getLogger(AbstractMetaMember.class);

	protected MetaClass metaClass;
	private List<Compilation> compilations;
	private int selectedCompilationIndex;

	private boolean isCompiled = false;

	protected boolean isVarArgs = false;
	protected boolean isPolymorphicSignature = false;
	protected int modifier; // bitset
	private String memberName;
	protected Class<?> returnType;
	protected List<Class<?>> paramTypes;

	public AbstractMetaMember(String memberName)
	{
		this.memberName = memberName;

		compilations = new ArrayList<>();
	}

	protected void checkPolymorphicSignature(Method method)
	{
		for (Annotation anno : method.getAnnotations())
		{
			if (S_POLYMORPHIC_SIGNATURE.equals(anno.annotationType().getSimpleName()))
			{
				isPolymorphicSignature = true;
				break;
			}
		}
	}

	@Override
	public String getMemberName()
	{
		return memberName;
	}

	@Override
	public String getFullyQualifiedMemberName()
	{
		return metaClass.getFullyQualifiedName() + C_DOT + memberName;
	}

	@Override
	public String getAbbreviatedFullyQualifiedMemberName()
	{
		return metaClass.getAbbreviatedFullyQualifiedName() + C_DOT + memberName;
	}

	@Override
	public int getModifier()
	{
		return modifier;
	}

	@Override
	public String getModifierString()
	{
		return Modifier.toString(modifier);
	}

	private boolean nameMatches(MemberSignatureParts msp)
	{
		if (DEBUG_LOGGING_SIG_MATCH)
		{
			logger.debug("nameMatches this.memberName: '{}' fq: '{}' other '{}' fq: '{}'", memberName,
					getFullyQualifiedMemberName(), msp.getMemberName(), msp.getFullyQualifiedClassName());
		}

		boolean match = memberName.equals(msp.getMemberName());

		if (DEBUG_LOGGING_SIG_MATCH)
		{
			logger.debug("nameMatches {}", match);
		}

		return match;
	}

	private boolean returnTypeMatches(MemberSignatureParts msp) throws ClassNotFoundException
	{
		boolean matched = false;

		String returnTypeClassName = msp.applyGenericSubstitutionsForClassLoading(msp.getReturnType());

		if (returnTypeClassName != null)
		{
			Class<?> sigReturnType = ParseUtil.findClassForLogCompilationParameter(returnTypeClassName);
			matched = returnType.equals(sigReturnType);

			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("Return: '{}' === '{}' ? {}", returnType.getName(), sigReturnType.getName(), matched);
			}
		}
		else
		{
			matched = (isConstructor());

			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("Constructor found");
			}
		}

		return matched;
	}

	@Override
	public boolean matchesSignature(MemberSignatureParts msp, boolean matchTypesExactly)
	{
		boolean result = false;

		if (nameMatches(msp))
		{
			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("Comparing:\n--------------\n{}\n--------------\n{}\n--------------", this, msp);
			}

			if (isPolymorphicSignature)
			{
				// assumption: method overloading not possible
				// with polymorphic signatures so this is a match
				if (DEBUG_LOGGING_SIG_MATCH)
				{
					logger.debug("Member has PolymorphicSignature");
				}

				result = true;
			}
			else
			{
				try
				{
					if (returnTypeMatches(msp))
					{
						List<Class<?>> mspClassTypes = getClassesForParamTypes(msp);

						if (ParseUtil.paramClassesMatch(isVarArgs, this.paramTypes, mspClassTypes, matchTypesExactly))
						{
							result = true;
						}
					}
				}
				catch (ClassNotFoundException cnfe)
				{
					logger.error("Class not found while matching signature:\n{}", msp, cnfe);
				}
			}

			if (DEBUG_LOGGING_SIG_MATCH)
			{
				logger.debug("Match: {}", result);
			}
		}

		return result;
	}

	private List<Class<?>> getClassesForParamTypes(MemberSignatureParts msp) throws ClassNotFoundException
	{
		List<Class<?>> result = new ArrayList<>();

		for (String param : msp.getParamTypes())
		{
			String paramClassName = msp.applyGenericSubstitutionsForClassLoading(param);

			Class<?> clazz = ParseUtil.findClassForLogCompilationParameter(paramClassName);

			result.add(clazz);
		}

		return result;
	}

	@Override
	public String getReturnTypeName()
	{
		String result = null;

		if (isConstructor() || returnType == null)
		{
			result = S_TYPE_NAME_VOID;
		}
		else
		{
			result = ParseUtil.expandParameterType(returnType.getName());
		}

		return result;
	}

	@Override
	public String[] getParamTypeNames()
	{
		List<String> typeNames = new ArrayList<>();

		for (Class<?> paramClass : paramTypes)
		{
			typeNames.add(ParseUtil.expandParameterType(paramClass.getName()));
		}

		return typeNames.toArray(new String[typeNames.size()]);
	}

	@Override
	public MemberBytecode getMemberBytecode()
	{
		MemberBytecode result = null;

		if (metaClass != null)
		{
			ClassBC classBytecode = metaClass.getClassBytecode();

			if (classBytecode != null)
			{
				result = classBytecode.getMemberBytecode(this);
			}
		}

		return result;
	}

	@Override
	public List<BytecodeInstruction> getInstructions()
	{
		List<BytecodeInstruction> result = null;

		MemberBytecode memberBytecode = getMemberBytecode();

		if (memberBytecode != null)
		{
			result = memberBytecode.getInstructions();
		}
		else
		{
			result = new ArrayList<>();
		}

		return result;
	}

	@Override
	public MetaClass getMetaClass()
	{
		return metaClass;
	}

	@Override
	public String getQueuedAttribute(String key)
	{
		return getLastCompilation() == null ? null : getLastCompilation().getQueuedAttribute(key);
	}

	@Override
	public String getCompiledAttribute(String key)
	{
		return getLastCompilation() == null ? null : getLastCompilation().getCompiledAttribute(key);
	}

	@Override
	public Compilation getCompilationByCompileID(String compileID)
	{
		Compilation result = null;

		for (Compilation compilation : compilations)
		{
			if (compileID.equals(compilation.getCompileID()))
			{
				result = compilation;
				break;
			}
		}

		return result;
	}

	@Override
	public Compilation getCompilationByAddress(AssemblyMethod asmMethod)
	{
		Compilation result = null;

		String entryAddress = asmMethod.getEntryAddress();

		String nativeAddress = asmMethod.getNativeAddress();

		for (Compilation compilation : compilations)
		{
			if (entryAddress != null && entryAddress.equals(compilation.getEntryAddress()))
			{
				result = compilation;
				break;
			}
			else if (nativeAddress != null && nativeAddress.equals(compilation.getNativeAddress()))
			{
				result = compilation;
				break;
			}
		}

		return result;
	}


	@Override
	public void storeCompilation(Compilation compilation)
	{
		compilations.add(compilation);

		selectedCompilationIndex = compilations.size() - 1;
	}

	@Override
	public boolean isCompiled()
	{
		return isCompiled;
	}
	
	@Override
	public void setCompiled(boolean compiled)
	{
		isCompiled = compiled;
	}

	@Override
	public Map<String, String> getQueuedAttributes()
	{
		return getLastCompilation() == null ? null : getLastCompilation().getQueuedAttributes();
	}

	@Override
	public Map<String, String> getCompiledAttributes()
	{
		return getLastCompilation() == null ? null : getLastCompilation().getCompiledAttributes();
	}

	@Override
	public String toStringUnqualifiedMethodName(boolean visibilityAndReturnType, boolean fqParamTypes)
	{
		StringBuilder builder = new StringBuilder();

		if (visibilityAndReturnType)
		{
			if (modifier != 0)
			{
				builder.append(Modifier.toString(modifier)).append(C_SPACE);
			}

			if (!isConstructor() && returnType != null)
			{
				builder.append(expandParam(returnType.getName(), fqParamTypes)).append(C_SPACE);
			}
		}

		builder.append(memberName);
		builder.append(C_OPEN_PARENTHESES);

		if (paramTypes.size() > 0)
		{
			for (Class<?> paramClass : paramTypes)
			{
				builder.append(expandParam(paramClass.getName(), fqParamTypes)).append(C_COMMA);
			}

			builder.deleteCharAt(builder.length() - 1);
		}

		builder.append(C_CLOSE_PARENTHESES);

		return builder.toString();
	}

	@Override
	public void addAssembly(AssemblyMethod asmMethod)
	{
		if (DEBUG_LOGGING_ASSEMBLY)
		{
			logger.debug("setAssembly on member {}", getFullyQualifiedMemberName());
		}

		Compilation compilation = getCompilationByAddress(asmMethod);

		if (compilation != null)
		{
			compilation.setAssembly(asmMethod);
		}
		else
		{
			logger.warn("{} Didn't find compilation to attach assembly for nativeAddress:{} or entryAddress:{}", getFullyQualifiedMemberName(),
					asmMethod.getNativeAddress(), asmMethod.getEntryAddress());
		}
	}

	@Override
	public List<Compilation> getCompilations()
	{
		return compilations;
	}

	@Override
	public boolean isConstructor()
	{
		return (this instanceof MetaConstructor);
	}

	@Override
	public String getSourceMethodSignatureRegEx()
	{
		StringBuilder builder = new StringBuilder();

		builder.append(C_HAT);
		builder.append(REGEX_GROUP_ANY);

		String modifiers = Modifier.toString(modifier);

		if (modifiers.length() > 0)
		{
			builder.append(modifiers).append(C_SPACE);
		}

		// return type of constructor is not declared in signature
		if (!isConstructor() && returnType != null)
		{
			String rt = expandParamRegEx(returnType.getName());

			builder.append(rt);
			builder.append(C_SPACE);
		}

		if (isConstructor())
		{
			builder.append(REGEX_UNICODE_PACKAGE_NAME);
			builder.append(StringUtil.getUnqualifiedClassName(memberName));
		}
		else
		{
			builder.append(memberName);
		}

		// TODO return type and name should uniquely identify a method

		/*
		 * builder.append(REGEX_ZERO_OR_MORE_SPACES);
		 * 
		 * builder.append(S_ESCAPED_OPEN_PARENTHESES);
		 * 
		 * if (paramTypes.size() > 0) { for (Class<?> paramClass : paramTypes) {
		 * builder.append(REGEX_ZERO_OR_MORE_SPACES);
		 * 
		 * String paramType = expandParamRegEx(paramClass.getName());
		 * 
		 * builder.append(paramType); builder.append(REGEX_ONE_OR_MORE_SPACES);
		 * builder.append(REGEX_UNICODE_PARAM_NAME); builder.append(S_COMMA); }
		 * 
		 * builder.deleteCharAt(builder.length() - 1); }
		 * 
		 * builder.append(REGEX_ZERO_OR_MORE_SPACES);
		 * builder.append(S_ESCAPED_CLOSE_PARENTHESES);
		 * builder.append(REGEX_GROUP_ANY); builder.append(C_DOLLAR);
		 */

		return builder.toString();
	}

	public static String expandParam(String inParamType, boolean fullyQualifiedType)
	{
		String paramType = inParamType;

		if (paramType.charAt(0) == C_OPEN_SQUARE_BRACKET)
		{
			paramType = ParseUtil.expandParameterType(paramType);
		}

		if (paramType.contains(S_DOT) && !fullyQualifiedType)
		{
			paramType = StringUtil.getUnqualifiedClassName(paramType);
		}

		return paramType;
	}

	public static String expandParamRegEx(String inParamType)
	{
		String paramType = inParamType;
		if (paramType.charAt(0) == C_OPEN_SQUARE_BRACKET)
		{
			paramType = ParseUtil.expandParameterType(paramType);

			paramType = paramType.replace(S_OPEN_SQUARE_BRACKET, S_ESCAPED_OPEN_SQUARE).replace(S_CLOSE_SQUARE_BRACKET,
					S_ESCAPED_CLOSE_SQUARE);
		}

		if (paramType.contains(S_DOT))
		{
			paramType = REGEX_UNICODE_PACKAGE_NAME + StringUtil.getUnqualifiedClassName(paramType);
		}

		return paramType;
	}

	@Override
	public Compilation getLastCompilation()
	{
		int compilationCount = compilations.size();

		Compilation result = null;

		if (compilationCount > 0)
		{
			result = compilations.get(compilationCount - 1);
		}

		return result;
	}

	private int makeSafeIndex(int index)
	{
		return Math.max(0, Math.min(index, compilations.size() - 1));
	}

	@Override
	public Compilation getCompilation(int index)
	{
		Compilation result = null;

		if (index >= 0 && index < compilations.size())
		{
			result = compilations.get(index);
		}

		return result;
	}

	@Override
	public void setSelectedCompilation(int index)
	{
		this.selectedCompilationIndex = makeSafeIndex(index);
	}

	@Override
	public Compilation getSelectedCompilation()
	{
		return getCompilation(selectedCompilationIndex);
	}

	@Override
	public int compareTo(IMetaMember other)
	{
		if (other == null)
		{
			return -1;
		}
		else
		{
			return getMemberName().compareTo(other.getMemberName());
		}
	}
}
