/**
  *
  *                      OOAS Compiler
  *
  *       Copyright 2015, AIT Austrian Institute of Technology.
  * This code is based on the C# Version of the OOAS Compiler, which is
  * copyright 2015 by the Institute of Software Technology, Graz University
  * of Technology with portions copyright by the AIT Austrian Institute of
  * Technology. All rights reserved.
  *
  * SEE THE "LICENSE" FILE FOR THE TERMS UNDER WHICH THIS FILE IS PROVIDED.
  *
  * If you modify the file please update the list of contributors below to in-
  * clude your name. Please also stick to the coding convention of using TABs
  * to do the basic (block-level) indentation and spaces for anything after
  * that. (Enable the display of special chars and it should be pretty obvious
  * what this means.) Also, remove all trailing whitespace.
  *
  * Contributors:
  *               Willibald Krenn (AIT)
  *               Stephan Zimmerer (AIT)
  *               Markus Demetz (AIT)
  *               Christoph Czurda (AIT)
  *
  */


package org.momut.ooas.visitors;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.IdentityHashMap;

import org.momut.ooas.ast.expressions.CallExpression;
import org.momut.ooas.ast.expressions.Expression;
import org.momut.ooas.ast.expressions.IdentifierExpression;
import org.momut.ooas.ast.identifiers.FunctionIdentifier;
import org.momut.ooas.ast.identifiers.Identifier;
import org.momut.ooas.ast.identifiers.IdentifierKind;
import org.momut.ooas.ast.identifiers.MainModule;
import org.momut.ooas.ast.identifiers.MethodIdentifier;
import org.momut.ooas.ast.identifiers.NamedActionIdentifier;
import org.momut.ooas.ast.statements.Assignment;
import org.momut.ooas.ast.types.ListType;
import org.momut.ooas.ast.types.MapType;
import org.momut.ooas.ast.types.OoActionSystemType;
import org.momut.ooas.ast.types.TupleType;
import org.momut.ooas.ast.types.Type;
import org.momut.ooas.parser.ParserError;
import org.momut.ooas.parser.ParserMessage;
import org.momut.ooas.parser.ParserState;

/**
 *
 * @author KrennW
 *
 */
public final class OoaCheckObjectRefsConstant extends OoaCompleteAstTraversalVisitor {

	private static class CallGraphNode {
		public final FunctionIdentifier m_functionIdentifier;
		public final HashSet<CallGraphNode> m_calledFunctions;
		public final HashSet<CallGraphNode> m_callees;
		public boolean m_mayChangeObjectReferences;


		@Override
		public int hashCode() {
			return m_functionIdentifier == null
				?  0
				:  m_functionIdentifier.tokenText().hashCode();
		}

		@Override
		public boolean equals(Object obj) {
			if (obj instanceof CallGraphNode) {
				final CallGraphNode other = (CallGraphNode) obj;
				return m_functionIdentifier != null
					? m_functionIdentifier.tokenText().equals(other.m_functionIdentifier.tokenText())
					: m_functionIdentifier == other.m_functionIdentifier;
			}
			return false;
		}

		@Override
		public String toString() {
			return m_functionIdentifier == null ? "<root>" : m_functionIdentifier.tokenText();
		}

		public CallGraphNode(FunctionIdentifier id) {
			m_functionIdentifier = id;
			m_calledFunctions = new HashSet<>();
			m_callees = new HashSet<>();
			m_mayChangeObjectReferences = false;
		}
	}


	/** null (root-do/od block), or an action or method that is currently being parsed. */
	private final ArrayList<FunctionIdentifier> m_currentFunction;
	/** Maps FunctionIdentifiers to the CallGraphNode data structure. */
	private final IdentityHashMap<String, CallGraphNode> m_callGraph;


	/** Adds a new CallGraphNode to the callgraph if necessary. Returns the node. */
	private CallGraphNode addCallGraphNode(FunctionIdentifier anIdentifier) {
		if (anIdentifier != null) {
			final String token = anIdentifier.tokenText().intern();
			if (!m_callGraph.containsKey(token)) {
				m_callGraph.put(token, new CallGraphNode(anIdentifier));
			}
			return m_callGraph.get(token);
		}

		if (!m_callGraph.containsKey(null)) {
			m_callGraph.put(null, new CallGraphNode(null));
		}
		return m_callGraph.get(null);
	}

	/** adds a link to the callgraph */
	private void addCalledFunction(CallGraphNode aNode) {
		final FunctionIdentifier fi = m_currentFunction.get(m_currentFunction.size() -1);
		if (fi != null && !m_callGraph.containsKey(fi.tokenText().intern()))
			addCallGraphNode(fi);
		final CallGraphNode currNode = fi == null ? m_callGraph.get(null) : m_callGraph.get(fi.tokenText().intern());
		aNode.m_callees.add(currNode);
		currNode.m_calledFunctions.add(aNode);
	}

	private void setMayChangeObjectRefs() {
		final FunctionIdentifier fi = m_currentFunction.get(m_currentFunction.size() -1);
		CallGraphNode node = m_callGraph.get(fi);
		if (node == null)
			node = addCallGraphNode(fi);
		node.m_mayChangeObjectReferences = true;
	}

	private void check(TupleType type) {
		for (final Type t: type.innerTypes())
			switch (t.kind()) {
			case ListType:
				check( (ListType) t);
				break;
			case MapType:
				check( (MapType) t);
				break;
			case TupleType:
				check( (TupleType) t);
				break;
			case OoActionSystemType:
				setMayChangeObjectRefs();
				return;
			default:
				break;
			}
	}

	private void check(MapType type) {
		throw new RuntimeException("OoaCheckObjectRefsConstant pass does not support MapTypes.");
	}

	private void check(ListType type) {
		switch (type.innerType().kind()) {
		case ListType:
			check( (ListType) type.innerType());
			break;
		case MapType:
			check( (MapType) type.innerType());
			break;
		case TupleType:
			check( (TupleType) type.innerType());
			break;
		case OoActionSystemType:
			setMayChangeObjectRefs();
			return;
		default:
			break;
		}
	}

	private IdentifierExpression m_lastIdentifier = null;

	@Override
	public void visit(IdentifierExpression identifierExpression) {
		m_lastIdentifier = identifierExpression;
	}

	@Override
	public void visit(Assignment assignment) {
		for (final Expression e: assignment.values())
			VisitSub(e, assignment);
		for (final Expression e: assignment.places()) {
			m_lastIdentifier = null;
			VisitSub(e, assignment);
			if (m_lastIdentifier.identifier().kind() == IdentifierKind.AttributeIdentifier)
				switch(e.type().kind()) {
					case ListType:
						check( (ListType) e.type());
						break;
					case MapType:
						check( (MapType) e.type());
						break;
					case TupleType:
						check( (TupleType) e.type() );
						break;
					case OoActionSystemType:
						setMayChangeObjectRefs();
						break;
					default:
						break;
				}
		}
	}

	@Override
	public void visit(CallExpression callExpression) {
		for (final FunctionIdentifier f: callExpression.callTargets())
			addCalledFunction(addCallGraphNode(f));
		super.visit(callExpression);
	}


	@Override
	public void visit(MethodIdentifier methodIdentifier) {
		m_currentFunction.add(methodIdentifier);
		try {
			addCallGraphNode(methodIdentifier);
			super.visit(methodIdentifier);
		} finally {
			m_currentFunction.remove(m_currentFunction.size() -1);
		}
	}

	@Override
	public void visit(NamedActionIdentifier actionIdentifier){
		m_currentFunction.add(actionIdentifier);
		try {
			addCallGraphNode(actionIdentifier);
			super.visit(actionIdentifier);
		} finally {
			m_currentFunction.remove(m_currentFunction.size() -1);
		}
	}

	@Override
	public void visit(OoActionSystemType ooActionSystemType) {
		for (final Identifier s: ooActionSystemType.symbols().symbolList())
			VisitSub(s, ooActionSystemType);

		m_currentFunction.add(null);
		try {
			VisitSub(ooActionSystemType.doOdBlock(), ooActionSystemType);
		} finally {
			m_currentFunction.remove(m_currentFunction.size() -1);
		}
	}

	@Override
	public void visit(MainModule mainModule) {
		super.visit(mainModule);
		// forward information..
		for (final CallGraphNode n: m_callGraph.values())
			promoteChangeInfo(n);
		final CallGraphNode root = m_callGraph.get(null);
		if (root.m_mayChangeObjectReferences) {
			// check that only the initialize function changes object refs..
			final StringBuilder b = new StringBuilder();
			b.append("The following functions (all action systems merged) potentially change object references:");
			b.append(System.lineSeparator());
			printCandidates(root, 1,b);
			m_ParserState.AddMessage(new ParserMessage("",0,0, b.toString()));
			for (final CallGraphNode n: root.m_calledFunctions)
				if (n.m_mayChangeObjectReferences && !n.toString().equals("initialize")) {
					m_ParserState.AddErrorMessage(new ParserError("", 0, 0,
						"Only \"initialize\" is allowed to change object references when using the symbolic solver."));
//					System.out.println(b.toString());
					break;
				}
		}
	}

	private void printCandidates(CallGraphNode root, int level, StringBuilder msg) {
		for (final CallGraphNode n: root.m_calledFunctions) {
			if (n.m_mayChangeObjectReferences) {
				for (int i = 0; i < level; i++)
					msg.append("\t");
				msg.append(n.m_functionIdentifier.toString());
				msg.append(System.lineSeparator());
				printCandidates(n, level + 1, msg);
			}
		}
	}

	private void promoteChangeInfo(CallGraphNode n) {
		if (n.m_mayChangeObjectReferences) {
			for (final CallGraphNode c: n.m_callees) {
				if (!c.m_mayChangeObjectReferences) {
					c.m_mayChangeObjectReferences = true;
					if (!c.m_calledFunctions.contains(n))
						System.out.println("Bug.");
					promoteChangeInfo(c);
				}
			}
		}
	}

	public OoaCheckObjectRefsConstant(ParserState aState) {
		super(aState);
		m_callGraph = new IdentityHashMap<>();
		m_currentFunction = new ArrayList<>();
		m_currentFunction.add(null);
		addCallGraphNode(null);  // add root elem.
	}
}
