/*
MIT License

Copyright (c) 2018-2019 Gang ZHANG

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 depends.entity;

import depends.entity.repo.EntityRepo;
import depends.relations.IBindingResolver;
import depends.relations.Relation;
import multilang.depends.util.file.TemporaryFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.lang.ref.WeakReference;
import java.util.*;

/**
 * ContainerEntity for example file, class, method, etc. they could contain
 * vars, functions, ecpressions, type parameters, etc.
 */
public abstract class ContainerEntity extends DecoratedEntity {
	private static final Logger logger = LoggerFactory.getLogger(ContainerEntity.class);

	private ArrayList<VarEntity> vars;
	private ArrayList<FunctionEntity> functions;
	WeakReference<HashMap<Object, Expression>> expressionWeakReference;
	private ArrayList<Expression> expressionList;
	private int expressionCount = 0;
	private Collection<GenericName> mixins;
	private Collection<ContainerEntity> resolvedMixins;

	private ArrayList<VarEntity> vars() {
		if (vars==null)
			vars = new ArrayList<>();
		return this.vars;
	}
	
	private Collection<GenericName> mixins() {
		if (mixins==null)
			mixins = new ArrayList<>();
		return this.mixins;
	}

	private ArrayList<FunctionEntity> functions() {
		if (functions==null)
			functions = new ArrayList<>();
		return this.functions;
	}
	
	public ContainerEntity() {
	}

	public ContainerEntity(GenericName rawName, Entity parent, Integer id) {
		super(rawName, parent, id);
	}

	public void addVar(VarEntity var) {
		if (logger.isDebugEnabled()) {
			logger.debug("var found: " + var.getRawName() + ":" + var.getRawType());
		}
		this.vars().add(var);
	}

	public ArrayList<VarEntity> getVars() {
		if (vars==null)
			return new ArrayList<>();
		return this.vars();
	}

	public void addFunction(FunctionEntity functionEntity) {
		this.functions().add(functionEntity);
	}

	public ArrayList<FunctionEntity> getFunctions() {
		if (functions==null)
			return new ArrayList<>();
		return this.functions;
	}

	public HashMap<Object, Expression> expressions() {
		if (expressionWeakReference==null)
			expressionWeakReference= new WeakReference<HashMap<Object, Expression>>(new HashMap<>());
		HashMap<Object, Expression> r = expressionWeakReference.get();
		if (r==null) return new HashMap<>();
		return r;
	}

	public void addExpression(Object key, Expression expression) {
		expressions().put(key, expression);
		expressionList().add(expression);
		expressionCount = expressionList.size();
	}

	public boolean containsExpression(Object key) {
		return 	expressions().containsKey(key);
	}
	/**
	 * For all data in the class, infer their types. Should be override in
	 * sub-classes
	 */
	public void inferLocalLevelEntities(IBindingResolver bindingResolver) {
		super.inferLocalLevelEntities(bindingResolver);
		for (VarEntity var : this.vars()) {
			if (var.getParent()!=this) {
				var.inferLocalLevelEntities(bindingResolver);
			}
		}
		for (FunctionEntity func : this.getFunctions()) {
			if (func.getParent()!=this) {
				func.inferLocalLevelEntities(bindingResolver);
			}
		}
		if (bindingResolver.isEagerExpressionResolve()) {
			reloadExpression(bindingResolver.getRepo());
			resolveExpressions(bindingResolver);
			cacheExpressions();
		}
		resolvedMixins = identiferToContainerEntity(bindingResolver, getMixins());
	}

	private Collection<GenericName> getMixins() {
		if (mixins==null)
			return new ArrayList<>();
		return mixins;
	}

	private Collection<ContainerEntity> identiferToContainerEntity(IBindingResolver bindingResolver, Collection<GenericName> identifiers) {
		if (identifiers.size()==0) return null;
		ArrayList<ContainerEntity> r = new ArrayList<>();
		for (GenericName identifier : identifiers) {
			Entity entity = bindingResolver.resolveName(this, identifier, true);
			if (entity == null) {
				continue;
			}
			if (entity instanceof ContainerEntity)
				r.add((ContainerEntity) entity);
		}
		return r;
	}

	/**
	 * Resolve all expression's type
	 * 
	 * @param bindingResolver
	 */
	public void resolveExpressions(IBindingResolver bindingResolver) {
		if (this instanceof FunctionEntity) {
			((FunctionEntity)this).linkReturnToLastExpression();
		}
		
		if (expressionList==null) return;
		if(expressionList.size()>10000) return;
		
		
		for (Expression expression : expressionList) {
			// 1. if expression's type existed, break;
			if (expression.getType() != null)
				continue;
			if (expression.isDot()) { // wait for previous
				continue;
			}
			if (expression.getRawType() == null && expression.getIdentifier() == null)
				continue;

			// 2. if expression's rawType existed, directly infer type by rawType
			// if expression's rawType does not existed, infer type based on identifiers
			if (expression.getRawType() != null) {
				expression.setType(bindingResolver.inferTypeFromName(this, expression.getRawType()), null, bindingResolver);
				if (expression.getType() != null) {
					continue;
				}
			}
			if (expression.getIdentifier() != null) {
				Entity entity = bindingResolver.resolveName(this, expression.getIdentifier(), true);
				String composedName = expression.getIdentifier().toString();
				Expression theExpr = expression;
				if (entity==null) {
					while(theExpr.getParent()!=null && theExpr.getParent().isDot()) {
						theExpr = theExpr.getParent();
						if (theExpr.getIdentifier()==null) break;
						composedName = composedName + "." + theExpr.getIdentifier().toString();
						entity = bindingResolver.resolveName(this, GenericName.build(composedName), true);
						if (entity!=null)
							break;
					}
				}
				if (entity != null) {
					expression.setType(entity.getType(), entity, bindingResolver);
					continue;
				}
				if (expression.isCall()) {
					List<Entity> funcs = this.lookupFunctionInVisibleScope(expression.getIdentifier());
					if (funcs != null) {
						for (Entity func:funcs) {
							expression.setType(func.getType(), func, bindingResolver);
						}
					}
				} else {

					Entity varEntity = this.lookupVarInVisibleScope(expression.getIdentifier());
					if (varEntity != null) {
						expression.setType(varEntity.getType(), varEntity, bindingResolver);
					}
				}
			}
		}
	}
	
	public void cacheChildExpressions() {
		cacheExpressions();
		for (Entity child:getChildren()) {
			if (child instanceof ContainerEntity) {
				((ContainerEntity)child).cacheChildExpressions();
			}
		}
	}


	public void cacheExpressions() {
		if (expressionWeakReference==null) return;
		if (expressionList==null) return;
		this.expressions().clear();
		this.expressionWeakReference.clear();
		cacheExpressionListToFile();
		this.expressionList.clear();
		this.expressionList=null;
		this.expressionList = new ArrayList<>();
	}

	public void clearExpressions() {
		if (expressionWeakReference==null) return;
		if (expressionList==null) return;
		this.expressions().clear();
		this.expressionWeakReference.clear();
		this.expressionList.clear();
		this.expressionList=null;
		this.expressionList = new ArrayList<>();
		this.expressionUseList = null;
	}
	
	private void cacheExpressionListToFile() {
		if (expressionCount ==0) return;
		try {
			FileOutputStream fileOut = new FileOutputStream(TemporaryFile.getInstance().exprPath(this.id));
			ObjectOutputStream out = new ObjectOutputStream(fileOut);
			out.writeObject(this.expressionList);
			out.close();
			fileOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	@SuppressWarnings("unchecked")
	public void reloadExpression(EntityRepo repo) {
		if (expressionCount ==0) return;
		try
	      {
	         FileInputStream fileIn = new FileInputStream(TemporaryFile.getInstance().exprPath(this.id));
	         ObjectInputStream in = new ObjectInputStream(fileIn);
	         expressionList = (ArrayList<Expression>) in.readObject();
	         if (expressionList==null) expressionList = new ArrayList<>();
	         for (Expression expr:expressionList) {
	        	 expr.reload(repo,expressionList);
	         }
	         in.close();
	         fileIn.close();
	      }catch(IOException | ClassNotFoundException i)
	      {
	         return;
	      }
	}
	

	public List<Expression> expressionList() {
		if (expressionList==null) 
			expressionList = new ArrayList<>();
		return expressionList;
	}

	public boolean containsExpression() {
		return expressions().size() > 0;
	}
	
	/**
	 * The entry point of lookup functions. It will treat multi-declare entities and
	 * normal entity differently. - for multiDeclare entity, it means to lookup all
	 * entities - for normal entity, it means to lookup entities from current scope
	 * still root
	 * 
	 * @param functionName
	 * @return
	 */
	public List<Entity> lookupFunctionInVisibleScope(GenericName functionName) {
		List<Entity> functions = new ArrayList<>();
		if (this.getMutliDeclare() != null) {
			for (Entity fromEntity : this.getMutliDeclare().getEntities()) {
				Entity f = lookupFunctionBottomUpTillTopContainer(functionName, fromEntity);
				if (f != null) {
					functions.add(f);
					return functions;
				}
			}
		} else {
			ContainerEntity fromEntity = this;
			Entity f = lookupFunctionBottomUpTillTopContainer(functionName, fromEntity);
			if (f != null) {
				functions.add(f);
				return functions;
			}
		}
		return null;
	}

	/**
	 * lookup function bottom up till the most outside container
	 * 
	 * @param functionName
	 * @param fromEntity
	 * @return
	 */
	private Entity lookupFunctionBottomUpTillTopContainer(GenericName functionName, Entity fromEntity) {
		while (fromEntity != null) {
			if (fromEntity instanceof ContainerEntity) {
				FunctionEntity func = ((ContainerEntity) fromEntity).lookupFunctionLocally(functionName);
				if (func != null)
					return func;
			}
			for (Entity child:this.getChildren()) {
				if (child instanceof AliasEntity) {
					if (child.getRawName().equals(functionName))
						return child;
				}
			}
			fromEntity = (ContainerEntity) this.getAncestorOfType(ContainerEntity.class);
		}
		return null;
	}

	/**
	 * lookup function in local entity. It could be override such as the type entity
	 * (it should also lookup the inherit/implemented types
	 * 
	 * @param functionName
	 * @return
	 */
	public FunctionEntity lookupFunctionLocally(GenericName functionName) {
		for (FunctionEntity func : getFunctions()) {
			if (func.getRawName().equals(functionName))
				return func;
		}
		return null;
	}

	/**
	 * The entry point of lookup var. It will treat multi-declare entities and
	 * normal entity differently. - for multiDeclare entity, it means to lookup all
	 * entities - for normal entity, it means to lookup entities from current scope
	 * still root
	 * 
	 * @param varName
	 * @return
	 */
	public Entity lookupVarInVisibleScope(GenericName varName) {
		ContainerEntity fromEntity = this;
		return lookupVarBottomUpTillTopContainer(varName, fromEntity);
	}

	/**
	 * To found the var.
	 * 
	 * @param fromEntity
	 * @param varName
	 * @return
	 */
	private Entity lookupVarBottomUpTillTopContainer(GenericName varName, ContainerEntity fromEntity) {
		while (fromEntity != null) {
			if (fromEntity instanceof ContainerEntity) {
				VarEntity var = ((ContainerEntity) fromEntity).lookupVarLocally(varName);
				if (var != null)
					return var;
			}
			for (Entity child:this.getChildren()) {
				if (child instanceof AliasEntity) {
					if (child.getRawName().equals(varName))
						return child;
				}
			}
			fromEntity = (ContainerEntity) this.getAncestorOfType(ContainerEntity.class);
		}
		return null;
	}

	public VarEntity lookupVarLocally(GenericName varName) {
		for (VarEntity var : getVars()) {
			if (var.getRawName().equals(varName))
				return var;
		}
		return null;
	}
	
	public VarEntity lookupVarLocally(String varName) {
		return this.lookupVarLocally(GenericName.build(varName));
	}

	public void addMixin(GenericName moduleName) {
		mixins().add(moduleName);
	}

	public Collection<ContainerEntity> getResolvedMixins() {
		if (resolvedMixins==null) return new ArrayList<>();
		return resolvedMixins;
	}

	HashMap<String,Set<Expression>> expressionUseList = null;
	public void addRelation(Expression expression, Relation relation) {
		String key = relation.getEntity().qualifiedName+relation.getType();
		if (this.expressionUseList==null)
			expressionUseList = new HashMap<>();
		if (expressionUseList.containsKey(key)){
			Set<Expression> expressions = expressionUseList.get(key);
			for (Expression expr:expressions){
				if (linkedExpr(expr,expression)) return;
			}
		}else{
			expressionUseList.put(key,new HashSet<>());
		}

		expressionUseList.get(key).add(expression);
		super.addRelation(relation);
	}

	private boolean linkedExpr(Expression a, Expression b) {
		Expression parent = a.getParent();
		while(parent!=null){
			if (parent==b) return true;
			parent = parent.getParent();
		}
		parent = b.getParent();
		while(parent!=null){
			if (parent==a) return true;
			parent = parent.getParent();
		}
		return  false;
	}
}
