package de.fernflower.modules.renamer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import de.fernflower.code.CodeConstants;
import de.fernflower.main.DecompilerContext;
import de.fernflower.main.extern.IFernflowerPreferences;
import de.fernflower.main.extern.IIdentifierRenamer;
import de.fernflower.struct.StructClass;
import de.fernflower.struct.StructContext;
import de.fernflower.struct.StructField;
import de.fernflower.struct.StructMethod;
import de.fernflower.struct.gen.FieldDescriptor;
import de.fernflower.struct.gen.MethodDescriptor;
import de.fernflower.struct.gen.VarType;
import de.fernflower.util.VBStyleCollection;

public class IdentifierConverter {

	private StructContext context;
	
	private IIdentifierRenamer helper; 

	private PoolInterceptor interceptor;
	
	private List<ClassWrapperNode> rootClasses = new ArrayList<ClassWrapperNode>(); 

	private List<ClassWrapperNode> rootInterfaces = new ArrayList<ClassWrapperNode>(); 
	
	private HashMap<String, HashMap<String, String>> interfaceNameMaps = new HashMap<String, HashMap<String, String>>(); 
	
	public void rename(StructContext context) {
		
		try {
			this.context = context; 
						
			String user_class = (String)DecompilerContext.getProperty(IFernflowerPreferences.USER_RENAMER_CLASS);
			if(user_class != null) {
				try {
					helper = (IIdentifierRenamer)IdentifierConverter.class.getClassLoader().loadClass(user_class).newInstance();
				} catch(Exception ex) {
					; // ignore errors
				}
			}
			
			if(helper == null) {
				helper = new ConverterHelper();
			}
			
			interceptor = new PoolInterceptor(helper);
			
			buildInheritanceTree(); 
			
			renameAllClasses();
			
			renameInterfaces();
	
			renameClasses(); 
			
			DecompilerContext.setPoolInterceptor(interceptor);
			context.reloadContext();
			
		} catch(IOException ex){
			throw new RuntimeException("Renaming failed!"); 
		}
		
	}
	
	private void renameClasses() {
		
		List<ClassWrapperNode> lstClasses = getReversePostOrderListIterative(rootClasses);
		
		HashMap<String, HashMap<String, String>> classNameMaps = new HashMap<String, HashMap<String, String>>(); 
		
		for(ClassWrapperNode node : lstClasses) {
			
			StructClass cl = node.getClassStruct();
			HashMap<String, String> names = new HashMap<String, String>();

			// merge informations on super class
			if(cl.superClass != null) {
				HashMap<String, String> mapClass = classNameMaps.get(cl.superClass.getString());
				if(mapClass != null) {
					names.putAll(mapClass);
				}
			}
			
			// merge informations on interfaces
			for(String intrName : cl.getInterfaceNames()) {
				HashMap<String, String> mapInt = interfaceNameMaps.get(intrName);
				if(mapInt != null) {
					names.putAll(mapInt);
				} else {
					StructClass clintr = context.getClass(intrName);
					if(clintr!=null) {
						names.putAll(processExternalInterface(clintr));
					}
				}
			}
			
			renameClassIdentifiers(cl, names);

			if(!node.getSubclasses().isEmpty()) {
				classNameMaps.put(cl.qualifiedName, names);
			}
		}
		
	}
	
	private HashMap<String, String> processExternalInterface(StructClass cl) {
		
		HashMap<String, String> names = new HashMap<String, String>();
		
		for(String intrName : cl.getInterfaceNames()) {

			HashMap<String, String> mapInt = interfaceNameMaps.get(intrName);
			if(mapInt != null) {
				names.putAll(mapInt);
			} else {
				StructClass clintr = context.getClass(intrName);
				if(clintr!=null) {
					names.putAll(processExternalInterface(clintr));
				}
			}
		}
		
		renameClassIdentifiers(cl, names);
		
		return names; 
	}
	
	private void renameInterfaces() {

		List<ClassWrapperNode> lstInterfaces = getReversePostOrderListIterative(rootInterfaces);
		
		HashMap<String, HashMap<String, String>> interfaceNameMaps = new HashMap<String, HashMap<String, String>>(); 
		
		// rename methods and fields
		for(ClassWrapperNode node : lstInterfaces) {
			
			StructClass cl = node.getClassStruct();
			HashMap<String, String> names = new HashMap<String, String>();

			// merge informations on super interfaces
			for(String intrName : cl.getInterfaceNames()) {
				HashMap<String, String> mapInt = interfaceNameMaps.get(intrName);
				if(mapInt != null) {
					names.putAll(mapInt);
				}
			}
			
			renameClassIdentifiers(cl, names);

			interfaceNameMaps.put(cl.qualifiedName, names);
		}
		
		this.interfaceNameMaps = interfaceNameMaps;
	}

	private void renameAllClasses() {

		// order not important
		List<ClassWrapperNode> lstAllClasses = new ArrayList<ClassWrapperNode>(getReversePostOrderListIterative(rootInterfaces));
		lstAllClasses.addAll(getReversePostOrderListIterative(rootClasses));
		
		// rename all interfaces and classes
		for(ClassWrapperNode node : lstAllClasses) {
			renameClass(node.getClassStruct());
		}
	}
	
	private void renameClass(StructClass cl) {

		if(!cl.isOwn()) {
			return;
		}
		
		String classOldFullName = cl.qualifiedName;
		String classNewFullName = classOldFullName;

		// TODO: rename packages 
		String clsimplename = ConverterHelper.getSimpleClassName(classOldFullName); 
		if(helper.toBeRenamed(IIdentifierRenamer.ELEMENT_CLASS, clsimplename, null, null)) {
			do {
				classNewFullName = ConverterHelper.replaceSimpleClassName(classOldFullName, 
						helper.getNextClassname(classOldFullName, ConverterHelper.getSimpleClassName(classOldFullName)));
			} while(context.getClasses().containsKey(classNewFullName));
			
			interceptor.addName(classOldFullName, classNewFullName);
		} 

	}
	
	private void renameClassIdentifiers(StructClass cl, HashMap<String, String> names) {

		// all classes are already renamed
		String classOldFullName = cl.qualifiedName;
		String classNewFullName = interceptor.getName(classOldFullName);

		if(classNewFullName == null) {
			classNewFullName = classOldFullName; 
		}
		
		// methods
		HashSet<String> setMethodNames = new HashSet<String>();
		for(StructMethod md : cl.getMethods()) {
			setMethodNames.add(md.getName());
		}
		
		VBStyleCollection<StructMethod, String> methods = cl.getMethods(); 
		for(int i=0;i<methods.size();i++) {
			
			StructMethod mt = methods.get(i);
			String key = methods.getKey(i);
			
			int access_flags = mt.getAccessFlags();
			boolean isPrivate = ((access_flags & CodeConstants.ACC_PRIVATE) != 0);

			String name = mt.getName();
			if(!cl.isOwn() || (access_flags & CodeConstants.ACC_NATIVE) != 0) {
				// external and native methods must not be renamed
				if(!isPrivate) {
					names.put(key, name);
				}
			} else if(helper.toBeRenamed(IIdentifierRenamer.ELEMENT_METHOD, classOldFullName, name, mt.getDescriptor())) {
				if(isPrivate || !names.containsKey(key)) {
					do {
						name = helper.getNextMethodname(classOldFullName, name, mt.getDescriptor());
					} while(setMethodNames.contains(name));
					
					if(!isPrivate) {
						names.put(key, name);
					}
				} else {
					name = names.get(key);
				}

				interceptor.addName(classOldFullName+" "+mt.getName()+" "+mt.getDescriptor(), 
										classNewFullName+" "+name+" "+buildNewDescriptor(false, mt.getDescriptor()));
			}
		}
		
		// external fields are not being renamed
		if(!cl.isOwn()) {
			return;
		}
		
		// fields
		// FIXME: should overloaded fields become the same name?
		HashSet<String> setFieldNames = new HashSet<String>();
		for(StructField fd : cl.getFields()) {
			setFieldNames.add(fd.getName());
		}
		
		for(StructField fd : cl.getFields()) {
			if(helper.toBeRenamed(IIdentifierRenamer.ELEMENT_FIELD, classOldFullName, fd.getName(), fd.getDescriptor())) {
				String newname;
				
				do {
					newname = helper.getNextFieldname(classOldFullName, fd.getName(), fd.getDescriptor());
				} while(setFieldNames.contains(newname));

				interceptor.addName(classOldFullName+" "+fd.getName()+" "+fd.getDescriptor(), 
						classNewFullName+" "+newname+" "+buildNewDescriptor(true, fd.getDescriptor()));
			}
		}

	}
	
	private String buildNewDescriptor(boolean isField, String descriptor) {

		boolean updated = false;
		
		if(isField) {
			FieldDescriptor fd = FieldDescriptor.parseDescriptor(descriptor);

			VarType ftype = fd.type;
			if(ftype.type == CodeConstants.TYPE_OBJECT) {
				String newclname = interceptor.getName(ftype.value);
				if(newclname != null) {
					ftype.value = newclname;
					updated = true;
				}
			}
			
			if(updated) {
				return fd.getDescriptor();
			}
			
		} else {
			
			MethodDescriptor md = MethodDescriptor.parseDescriptor(descriptor);
			// params 
			for(VarType partype : md.params) {
				if(partype.type == CodeConstants.TYPE_OBJECT) {
					String newclname = interceptor.getName(partype.value);
					if(newclname != null) {
						partype.value = newclname;
						updated = true;
					}
				}
			}
	
			// return value
			if(md.ret.type == CodeConstants.TYPE_OBJECT) {
				String newclname = interceptor.getName(md.ret.value);
				if(newclname!=null) {
					md.ret.value = newclname;
					updated = true;
				}
			}
	
			if(updated) {
				return md.getDescriptor();
			}
		}
		
		return descriptor;
	}
	
	private List<ClassWrapperNode> getReversePostOrderListIterative(List<ClassWrapperNode> roots) {
		
		List<ClassWrapperNode> res = new ArrayList<ClassWrapperNode>();
		
		LinkedList<ClassWrapperNode> stackNode = new LinkedList<ClassWrapperNode>();
		LinkedList<Integer> stackIndex = new LinkedList<Integer>();
		
		HashSet<ClassWrapperNode> setVisited = new HashSet<ClassWrapperNode>(); 
		
		for(ClassWrapperNode root : roots) {
			stackNode.add(root);
			stackIndex.add(0);
		}
		
		while(!stackNode.isEmpty()) {
			
			ClassWrapperNode node = stackNode.getLast();
			int index = stackIndex.removeLast();

			setVisited.add(node);
			
			List<ClassWrapperNode> lstSubs = node.getSubclasses();
			
			for(; index < lstSubs.size(); index++) {
				ClassWrapperNode sub = lstSubs.get(index);
				if(!setVisited.contains(sub)) {
					stackIndex.add(index+1);
					
					stackNode.add(sub);
					stackIndex.add(0);
					
					break;
				}
			}

			if(index == lstSubs.size()) {
				res.add(0, node);
				
				stackNode.removeLast();
			}
		}
		
		return res;
	}
	
	
	private void buildInheritanceTree() {
		
		HashMap<String, ClassWrapperNode> nodes = new HashMap<String, ClassWrapperNode>();
		HashMap<String, StructClass> classes = context.getClasses(); 

		List<ClassWrapperNode> rootClasses = new ArrayList<ClassWrapperNode>();
		List<ClassWrapperNode> rootInterfaces = new ArrayList<ClassWrapperNode>();

		for(StructClass cl : classes.values()) {
			
			if(!cl.isOwn()) {
				continue;
			}

			LinkedList<StructClass> stack = new LinkedList<StructClass>();  
			LinkedList<ClassWrapperNode> stackSubnodes = new LinkedList<ClassWrapperNode>();  
			
			stack.add(cl);
			stackSubnodes.add(null);
			
			while(!stack.isEmpty()) {
				
				StructClass clstr = stack.removeFirst();
				ClassWrapperNode child = stackSubnodes.removeFirst();
				
				ClassWrapperNode node = nodes.get(clstr.qualifiedName);
				boolean isNewNode = (node == null);
				
				if(isNewNode) {
					nodes.put(clstr.qualifiedName, node = new ClassWrapperNode(clstr));
				}
				
				if(child!=null) {
					node.addSubclass(child);
				}
				
				if(!isNewNode) {
					break;
				} else {

					boolean isInterface = ((clstr.access_flags & CodeConstants.ACC_INTERFACE) != 0);
					boolean found_parent = false;
					
					if(isInterface) {
						for(String intrName : clstr.getInterfaceNames()) {
							StructClass clparent = classes.get(intrName);
							if(clparent != null) {
								stack.add(clparent);
								stackSubnodes.add(node);
								found_parent = true;
							}
						}
					} else {
						if(clstr.superClass != null) { // null iff java/lang/Object
							StructClass clparent = classes.get(clstr.superClass.getString());

							if(clparent != null) {
								stack.add(clparent);
								stackSubnodes.add(node);
								found_parent = true;
							}
						}
					}

					if(!found_parent) { // no super class or interface
						(isInterface?rootInterfaces:rootClasses).add(node);
					}
				}
			}			
		}
		
		this.rootClasses = rootClasses;
		this.rootInterfaces = rootInterfaces; 
	}
	
}
