/*
 *    Fernflower - The Analytical Java Decompiler
 *    http://www.reversed-java.com
 *
 *    (C) 2008 - 2010, Stiver
 *
 *    This software is NEITHER public domain NOR free software 
 *    as per GNU License. See license.txt for more details.
 *
 *    This software is distributed WITHOUT ANY WARRANTY; without 
 *    even the implied warranty of MERCHANTABILITY or FITNESS FOR 
 *    A PARTICULAR PURPOSE. 
 */

package de.fernflower.main.rels;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;

import de.fernflower.code.CodeConstants;
import de.fernflower.main.DecompilerContext;
import de.fernflower.main.ClassesProcessor.ClassNode;
import de.fernflower.main.collectors.CounterContainer;
import de.fernflower.main.collectors.VarNamesCollector;
import de.fernflower.main.extern.IFernflowerPreferences;
import de.fernflower.modules.decompiler.exps.AssignmentExprent;
import de.fernflower.modules.decompiler.exps.ExitExprent;
import de.fernflower.modules.decompiler.exps.Exprent;
import de.fernflower.modules.decompiler.exps.FieldExprent;
import de.fernflower.modules.decompiler.exps.InvocationExprent;
import de.fernflower.modules.decompiler.exps.VarExprent;
import de.fernflower.modules.decompiler.sforms.DirectGraph;
import de.fernflower.modules.decompiler.sforms.DirectNode;
import de.fernflower.modules.decompiler.vars.VarVersionPaar;
import de.fernflower.struct.StructMethod;
import de.fernflower.struct.gen.MethodDescriptor;
import de.fernflower.util.InterpreterUtil;

public class NestedMemberAccess {
	
	private static final int METHOD_ACCESS_NORMAL = 1;
	private static final int METHOD_ACCESS_FIELDGET = 2;
	private static final int METHOD_ACCESS_FIELDSET = 3;
	private static final int METHOD_ACCESS_METHOD = 4;

	private boolean notSetSync; 
	
	private HashMap<MethodWrapper, Integer> mapMethodType = new HashMap<MethodWrapper, Integer>();
	
	
	public void propagateMemberAccess(ClassNode root) {
		
		if(root.nested.isEmpty()) {
			return;
		}

		notSetSync = DecompilerContext.getOption(IFernflowerPreferences.SYNTHETIC_NOT_SET);
		
		computeMethodTypes(root);
		
		eliminateStaticAccess(root);
	}
	
	
	private void computeMethodTypes(ClassNode node) {
		
		if(node.type == ClassNode.CLASS_LAMBDA) {
			return;
		}
		
		for(ClassNode nd : node.nested) {
			computeMethodTypes(nd);
		}
		
		for(MethodWrapper meth : node.wrapper.getMethods()) {
			computeMethodType(node, meth);
		}
		
	}
	
	private void computeMethodType(ClassNode node, MethodWrapper meth) {

		int type = METHOD_ACCESS_NORMAL;

		if(meth.root != null) {

			DirectGraph graph = meth.getOrBuildGraph();

			int flags = meth.methodStruct.getAccessFlags();
			if(((flags & CodeConstants.ACC_SYNTHETIC) != 0 || meth.methodStruct.getAttributes().containsKey("Synthetic") || notSetSync) && 
					(flags & CodeConstants.ACC_STATIC) != 0) {
				if(graph.nodes.size() == 2) {  // incl. dummy exit node
					if(graph.first.exprents.size() == 1) {
						Exprent exprent = graph.first.exprents.get(0);

						MethodDescriptor mtdesc = MethodDescriptor.parseDescriptor(meth.methodStruct.getDescriptor());
						int parcount = mtdesc.params.length;

						Exprent exprCore = exprent;

						if(exprent.type == Exprent.EXPRENT_EXIT) {
							ExitExprent exexpr = (ExitExprent)exprent;
							if(exexpr.getExittype() == ExitExprent.EXIT_RETURN && exexpr.getValue() != null) {
								exprCore = exexpr.getValue();
							}
						}

						switch(exprCore.type) {
						case Exprent.EXPRENT_FIELD:
							FieldExprent fexpr = (FieldExprent)exprCore;
							if((parcount == 1 && !fexpr.isStatic()) ||
									(parcount == 0 && fexpr.isStatic())) {
								if(fexpr.getClassname().equals(node.classStruct.qualifiedName)) {  // FIXME: check for private flag of the field
									if(fexpr.isStatic() || (fexpr.getInstance().type == Exprent.EXPRENT_VAR && ((VarExprent)fexpr.getInstance()).getIndex() == 0)) {
										type = METHOD_ACCESS_FIELDGET;
									}
								}
							}
							break;
						case Exprent.EXPRENT_VAR:  // qualified this
							if(parcount == 1) {
								// this or final variable
								if(((VarExprent)exprCore).getIndex() != 0) {
									type = METHOD_ACCESS_FIELDGET;
								}
							}

							break;
						case Exprent.EXPRENT_INVOCATION:
							type = METHOD_ACCESS_METHOD;
							break;
						case Exprent.EXPRENT_ASSIGNMENT:
							AssignmentExprent asexpr = (AssignmentExprent)exprCore;
							if(asexpr.getLeft().type == Exprent.EXPRENT_FIELD && asexpr.getRight().type == Exprent.EXPRENT_VAR) {
								FieldExprent fexpras = (FieldExprent)asexpr.getLeft();
								if((parcount == 2 && !fexpras.isStatic()) ||
										(parcount == 1 && fexpras.isStatic())) {
									if(fexpras.getClassname().equals(node.classStruct.qualifiedName)) { // FIXME: check for private flag of the field
										if(fexpras.isStatic() || (fexpras.getInstance().type == Exprent.EXPRENT_VAR && ((VarExprent)fexpras.getInstance()).getIndex() == 0)) {
											if(((VarExprent)asexpr.getRight()).getIndex() == parcount - 1) {
												type = METHOD_ACCESS_FIELDSET;
											}
										}
									}
								}
							}
						}


						if(type == METHOD_ACCESS_METHOD) { // FIXME: check for private flag of the method

							type = METHOD_ACCESS_NORMAL;

							InvocationExprent invexpr = (InvocationExprent)exprCore;

							if((invexpr.isStatic() && invexpr.getLstParameters().size() == parcount) || (!invexpr.isStatic() && invexpr.getInstance().type == Exprent.EXPRENT_VAR 
									&& ((VarExprent)invexpr.getInstance()).getIndex() == 0 && invexpr.getLstParameters().size() == parcount-1)) {

								boolean equalpars = true;

								for(int i=0;i<invexpr.getLstParameters().size();i++) {
									Exprent parexpr = invexpr.getLstParameters().get(i);
									if(parexpr.type != Exprent.EXPRENT_VAR ||
											((VarExprent)parexpr).getIndex() != i + (invexpr.isStatic()?0:1)) {
										equalpars = false;
										break;
									}
								}

								if(equalpars) {
									type = METHOD_ACCESS_METHOD;
								}
							}
						}
					} else if(graph.first.exprents.size() == 2) {
						Exprent exprentFirst = graph.first.exprents.get(0);
						Exprent exprentSecond = graph.first.exprents.get(1);

						if(exprentFirst.type == Exprent.EXPRENT_ASSIGNMENT && 
								exprentSecond.type == Exprent.EXPRENT_EXIT) {
							
							MethodDescriptor mtdesc = MethodDescriptor.parseDescriptor(meth.methodStruct.getDescriptor());
							int parcount = mtdesc.params.length;

							AssignmentExprent asexpr = (AssignmentExprent)exprentFirst;
							if(asexpr.getLeft().type == Exprent.EXPRENT_FIELD && asexpr.getRight().type == Exprent.EXPRENT_VAR) {
								FieldExprent fexpras = (FieldExprent)asexpr.getLeft();
								if((parcount == 2 && !fexpras.isStatic()) ||
										(parcount == 1 && fexpras.isStatic())) {
									if(fexpras.getClassname().equals(node.classStruct.qualifiedName)) { // FIXME: check for private flag of the field
										if(fexpras.isStatic() || (fexpras.getInstance().type == Exprent.EXPRENT_VAR && ((VarExprent)fexpras.getInstance()).getIndex() == 0)) {
											if(((VarExprent)asexpr.getRight()).getIndex() == parcount - 1) {
												
												ExitExprent exexpr = (ExitExprent)exprentSecond;
												if(exexpr.getExittype() == ExitExprent.EXIT_RETURN && exexpr.getValue() != null) {
													if(exexpr.getValue().type == Exprent.EXPRENT_VAR &&
															((VarExprent)asexpr.getRight()).getIndex() == parcount - 1) {
														type = METHOD_ACCESS_FIELDSET;
													}
												}
											}
										}
									}
								}
							}
						}
					}
					
					
				}
			}
		}

		if(type != METHOD_ACCESS_NORMAL) {
			mapMethodType.put(meth, type);
		} else {
			mapMethodType.remove(meth);
		}
	}
	
	
	
	private void eliminateStaticAccess(ClassNode node) {
		
		if(node.type == ClassNode.CLASS_LAMBDA) {
			return;
		}
		
		for(MethodWrapper meth : node.wrapper.getMethods()) {
			
			if(meth.root != null) {
				
				boolean replaced = false;
				
				DirectGraph graph = meth.getOrBuildGraph();
				
				HashSet<DirectNode> setVisited = new HashSet<DirectNode>(); 
				LinkedList<DirectNode> stack = new LinkedList<DirectNode>();
				stack.add(graph.first);
				
				while(!stack.isEmpty()) {  // TODO: replace with interface iterator?
					
					DirectNode nd = stack.removeFirst();
					
					if(setVisited.contains(nd)) {
						continue;
					}
					setVisited.add(nd);
					
					for(int i=0;i<nd.exprents.size();i++) {
						Exprent exprent = nd.exprents.get(i);
	
						replaced |= replaceInvocations(node, meth, exprent);
						
						if(exprent.type == Exprent.EXPRENT_INVOCATION) {
							Exprent ret = replaceAccessExprent(node, meth, (InvocationExprent)exprent);
							
							if(ret != null) {
								nd.exprents.set(i, ret);
								replaced = true;
							}
						}
					}
					
					for(DirectNode ndx: nd.succs) {
						stack.add(ndx);
					}
				}
				
				if(replaced) {
					computeMethodType(node, meth);
				}
				
			}
		}
		
		for(ClassNode child : node.nested) {
			eliminateStaticAccess(child);
		}
		
	}
	
	
	private boolean replaceInvocations(ClassNode caller, MethodWrapper meth, Exprent exprent) {
		
		boolean res = false;
		
		for(Exprent expr : exprent.getAllExprents()) {
			res |= replaceInvocations(caller, meth, expr);
		}
		
		for(;;) {
			
			boolean found = false;
			
			for(Exprent expr : exprent.getAllExprents()) {
				if(expr.type == Exprent.EXPRENT_INVOCATION) {
					Exprent newexpr = replaceAccessExprent(caller, meth, (InvocationExprent)expr);
					if(newexpr != null) {
						exprent.replaceExprent(expr, newexpr);
						found = true;
						res = true;
						break;
					}
				}
			}
			
			if(!found) {
				break;
			}
		}
		
		return res;
	}
	
	private boolean sameTree(ClassNode caller, ClassNode callee) {
		
		if(caller.classStruct.qualifiedName.equals(callee.classStruct.qualifiedName)) {
			return false;
		}
		
		while(caller.parent != null) {
			caller = caller.parent;
		}
		
		while(callee.parent != null) {
			callee = callee.parent;
		}
		
		return caller == callee;
	}

	private Exprent replaceAccessExprent(ClassNode caller, MethodWrapper methdest, InvocationExprent invexpr) {
		
		ClassNode node = DecompilerContext.getClassprocessor().getMapRootClasses().get(invexpr.getClassname());
		
		MethodWrapper methsource = null;
		if(node != null && node.wrapper != null) {
			methsource = node.wrapper.getMethodWrapper(invexpr.getName(), invexpr.getStringDescriptor());
		}
		
		if(methsource == null || !mapMethodType.containsKey(methsource)) {
			return null;
		}
		
		// if same method, return
		if(node.classStruct.qualifiedName.equals(caller.classStruct.qualifiedName) &&
				methsource.methodStruct.getName().equals(methdest.methodStruct.getName()) &&
				methsource.methodStruct.getDescriptor().equals(methdest.methodStruct.getDescriptor())) {
			// no recursive invocations permitted!
			return null;
		}
		
		int type = mapMethodType.get(methsource);
		
//		// FIXME: impossible case. METHOD_ACCESS_NORMAL is not saved in the map 
//		if(type == METHOD_ACCESS_NORMAL) {
//			return null;
//		}
		
		if(!sameTree(caller, node)) {
			return null;
		}
		
		DirectGraph graph = methsource.getOrBuildGraph();
		Exprent source = graph.first.exprents.get(0);
		
		Exprent retexprent = null;
		
		switch(type) {
		case METHOD_ACCESS_FIELDGET:
			ExitExprent exsource = (ExitExprent)source;
			if(exsource.getValue().type == Exprent.EXPRENT_VAR) { // qualified this
				VarExprent var = (VarExprent)exsource.getValue();
				String varname = methsource.varproc.getVarName(new VarVersionPaar(var));
				
				if(!methdest.setOuterVarNames.contains(varname)) {
					VarNamesCollector vnc = new VarNamesCollector();
					vnc.addName(varname);
					
					methdest.varproc.refreshVarNames(vnc);
					methdest.setOuterVarNames.add(varname);
				}
				
				int index = methdest.counter.getCounterAndIncrement(CounterContainer.VAR_COUNTER);
				VarExprent ret = new VarExprent(index, var.getVartype(), methdest.varproc);
				methdest.varproc.setVarName(new VarVersionPaar(index, 0), varname);
				
				retexprent = ret;
			} else { // field
				FieldExprent ret = (FieldExprent)exsource.getValue().copy();
				if(!ret.isStatic()) {
					ret.replaceExprent(ret.getInstance(), invexpr.getLstParameters().get(0));
				}
				retexprent = ret;
			}
			break;
		case METHOD_ACCESS_FIELDSET:
			AssignmentExprent ret;
			if(source.type == Exprent.EXPRENT_EXIT) {
				ExitExprent extex = (ExitExprent)source;
				ret = (AssignmentExprent)((AssignmentExprent)extex.getValue()).copy();
			} else {
				ret = (AssignmentExprent)((AssignmentExprent)source).copy();
			}
			FieldExprent fexpr = (FieldExprent)ret.getLeft();
			
			if(fexpr.isStatic()) {
				ret.replaceExprent(ret.getRight(), invexpr.getLstParameters().get(0));
			} else {
				ret.replaceExprent(ret.getRight(), invexpr.getLstParameters().get(1));
				fexpr.replaceExprent(fexpr.getInstance(), invexpr.getLstParameters().get(0));
			}
			retexprent = ret;
			break;
		case METHOD_ACCESS_METHOD:
			if(source.type == Exprent.EXPRENT_EXIT) {
				source = ((ExitExprent)source).getValue();
			}
			
			InvocationExprent invret = (InvocationExprent)source.copy();
			
			int index = 0;
			if(!invret.isStatic()) {
				invret.replaceExprent(invret.getInstance(), invexpr.getLstParameters().get(0));
				index = 1;
			}
			
			for(int i=0;i<invret.getLstParameters().size();i++) {
				invret.replaceExprent(invret.getLstParameters().get(i), invexpr.getLstParameters().get(i + index));
			}
			
			retexprent = invret;
		}
		
		
		if(retexprent != null) {
			// hide synthetic access method
			boolean hide = true;
			
			if(node.type == ClassNode.CLASS_ROOT || (node.access & CodeConstants.ACC_STATIC) != 0) {
				StructMethod mt = methsource.methodStruct;
				if((mt.getAccessFlags() & CodeConstants.ACC_SYNTHETIC) == 0 && !mt.getAttributes().containsKey("Synthetic")) {
					hide = false;
				}
			}
			if(hide) {
				node.wrapper.getHideMembers().add(InterpreterUtil.makeUniqueKey(invexpr.getName(), invexpr.getStringDescriptor()));
			}
		}
		
		return retexprent; 
	}
	
	
}
