package edu.udel.cis.vsl.civl.model.common;

import java.io.PrintStream;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;

import edu.udel.cis.vsl.civl.model.IF.Fragment;
import edu.udel.cis.vsl.civl.model.IF.ModelFactory;
import edu.udel.cis.vsl.civl.model.IF.expression.BinaryExpression.BINARY_OPERATOR;
import edu.udel.cis.vsl.civl.model.IF.expression.Expression;
import edu.udel.cis.vsl.civl.model.IF.location.Location;
import edu.udel.cis.vsl.civl.model.IF.statement.Statement;
import edu.udel.cis.vsl.civl.model.common.statement.StatementSet;

/**
 * A fragment of a CIVL model. Consists of a start location and a last
 * statement. Why not always generate next location.
 * 
 * @author Stephen F. Siegel (siegel)
 * @author Manchun Zheng (zmanchun)
 * 
 */
public class CommonFragment implements Fragment {

	/* ************************** Instance Fields ************************** */

	/**
	 * The last statement of the fragment
	 */
	public Statement lastStatement;

	/**
	 * The start location of the fragment
	 */
	public Location startLocation;

	/* **************************** Constructors *************************** */

	/**
	 * create an empty fragment
	 */
	public CommonFragment() {
	}

	/**
	 * 
	 * @param startLocation
	 *            the start location
	 * @param lastStatement
	 *            the last statement
	 */
	public CommonFragment(Location startLocation, Statement lastStatement) {
		this.startLocation = startLocation;
		this.lastStatement = lastStatement;
	}

	/**
	 * 
	 * @param statement
	 *            use <code>statement</code> to create a new fragment, with the
	 *            start location being the source location of
	 *            <code>statement</code> and the last statement being
	 *            <code>statement</code>
	 */
	public CommonFragment(Statement statement) {
		this.startLocation = statement.source();
		this.lastStatement = statement;
	}

	/* *********************** Methods from Fragment *********************** */

	@Override
	public void addGuardToStartLocation(Expression guard, ModelFactory factory) {
		int statementCount = this.startLocation.getNumOutgoing();

		for (int i = 0; i < statementCount; i++) {
			Statement statement = this.startLocation().getOutgoing(i);
			Expression oldGuard = statement.guard();

			if (factory.isTrue(oldGuard)) {
				statement.setGuard(guard);
			} else if (!factory.isTrue(guard)) {
				Expression newGuard = factory.binaryExpression(
						factory.sourceOfSpan(guard.getSource(),
								oldGuard.getSource()), BINARY_OPERATOR.AND,
						guard, oldGuard);

				statement.setGuard(newGuard);
			}
		}
	}

	@Override
	public Fragment combineWith(Fragment next) {
		if (next == null || next.isEmpty())
			return this;
		if (this.isEmpty())
			return next;
		this.lastStatement.setTarget(next.startLocation());
		return new CommonFragment(this.startLocation, next.lastStatement());
	}

	@Override
	public boolean isEmpty() {
		if (startLocation == null && lastStatement == null)
			return true;
		return false;
	}

	@Override
	public Statement lastStatement() {
		return lastStatement;
	}

	@Override
	public Fragment parallelCombineWith(Fragment parallel) {
		StatementSet newLastStatement = new StatementSet();

		if (parallel == null || parallel.isEmpty())
			return this;
		if (this.isEmpty())
			return parallel;
		assert this.startLocation.id() == parallel.startLocation().id();
		if (lastStatement instanceof StatementSet) {
			Set<Statement> statements = ((StatementSet) lastStatement)
					.statements();

			for (Statement s : statements) {
				newLastStatement.add(s);
			}
		} else {
			newLastStatement.add(lastStatement);
		}
		if (parallel.lastStatement() instanceof StatementSet) {
			Set<Statement> statements = ((StatementSet) parallel
					.lastStatement()).statements();

			for (Statement s : statements) {
				newLastStatement.add(s);
			}
		} else {
			newLastStatement.add(parallel.lastStatement());
		}
		return new CommonFragment(this.startLocation, newLastStatement);
	}

	@Override
	public void Print(PrintStream out) {
		out.println(this.toString());
	}

	@Override
	public void setLastStatement(Statement statement) {
		this.lastStatement = statement;
	}

	@Override
	public void setStartLocation(Location location) {
		this.startLocation = location;
	}

	@Override
	public Location startLocation() {
		return startLocation;
	}

	@Override
	public void updateStartLocation(Location newLocation) {
		int oldLocationId;
		int number;
		Stack<Location> workings;
		Set<Integer> locationIds;

		if (isEmpty())
			return;
		oldLocationId = this.startLocation.id();
		number = startLocation.getNumOutgoing();
		workings = new Stack<Location>();
		locationIds = new HashSet<Integer>();
		workings.push(startLocation);
		locationIds.add(startLocation.id());

		// For each statement in the fragment, update its source or target
		// location accordingly if it happens to be the previous start location
		while (!workings.isEmpty()) {
			Location location = workings.pop();

			if (location.getNumOutgoing() > 0) {
				number = location.getNumOutgoing();
				for (int i = 0; i < number; i++) {
					Statement s = location.getOutgoing(i);

					if (s.source().id() == oldLocationId) {
						s.setSource(newLocation);
					}
					if (s.target() != null) {
						if (s.target().id() == oldLocationId) {
							s.setTarget(newLocation);
						}
						if (!locationIds.contains(s.target().id())) {
							workings.push(s.target());
							locationIds.add(s.target().id());
						}
					}
				}
			}
		}
		this.startLocation = newLocation;
	}

	/* ************************ Methods from Object ************************ */

	@Override
	public String toString() {
		if (isEmpty())
			return "========Empty=========\r\n";
		String result = "=================\r\n";
		Stack<Location> workings = new Stack<Location>();
		Set<Integer> locationIds = new HashSet<Integer>();

		workings.push(this.startLocation);
		locationIds.add(this.startLocation.id());
		while (!workings.isEmpty()) {
			Location location = workings.pop();

			result += "Location " + location.id() + "\r\n";
			if (location.getNumOutgoing() > 0) {
				for (Statement s : location.outgoing()) {
					result += "when(" + s.guard() + ") " + s + " goto ";
					if (s.target() == null) {
						result += "null";
					} else {
						result += "Location " + s.target().id();
						if (!locationIds.contains(s.target().id())) {
							workings.push(s.target());
							locationIds.add(s.target().id());
						}
					}
				}
				result += "\r\n";
			}
		}
		result += "last statement: " + this.lastStatement + " at Location "
				+ this.lastStatement.source().id() + " "
				+ this.lastStatement.getSource() + "\r\n";
		return result;
	}

	@Override
	public void addLastStatement(Statement statement) {
		if (this.lastStatement == null)
			this.lastStatement = statement;
		if (this.lastStatement instanceof StatementSet) {
			((StatementSet) lastStatement).add(statement);
		} else {
			StatementSet lastStatementSet = new StatementSet();
			lastStatementSet.add(this.lastStatement);
			lastStatementSet.add(statement);
			this.lastStatement = lastStatementSet;
		}
	}

	@Override
	public void addNewStatement(Statement statement) {
		if(this.lastStatement != null)
			this.lastStatement.setTarget(statement.source());
		else
			this.startLocation = statement.source();
		this.lastStatement = statement;
	}

}
