/*
 * ReachedSet.java - This file is part of the Jakstab project.
 * Copyright 2007-2015 Johannes Kinder <jk@jakstab.org>
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, see <http://www.gnu.org/licenses/>.
 */
package org.jakstab.analysis;

import java.util.*;

import org.jakstab.analysis.composite.CompositeState;
import org.jakstab.analysis.location.LocationState;
import org.jakstab.cfa.Location;
import org.jakstab.cfa.RTLLabel;
import org.jakstab.util.Logger;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;

/**
 * A set of reached states for use in the CPA algorithm. It can generate lightweight views on 
 * parts of its contents much like an SQL database using select(row) and where(row, hasValue).
 * 
 * @author Johannes Kinder
 */
public class ReachedSet extends AbstractSet<AbstractState> implements Collection<AbstractState> {

	private static final Logger logger = Logger.getLogger(ReachedSet.class);
	
	private final SetMultimap<AbstractState, CompositeState> compositeMap;
	private static final int indexComponent = 0; 
	private final int selectedRow;
	private final int whereRow;
	private final AbstractState whereState;
	
	public ReachedSet() {
		this(-1, -1, null, HashMultimap.<AbstractState, CompositeState>create(1000, 5));
	}
	
	private ReachedSet(int selectedRow, int whereRow, AbstractState whereState, 
			SetMultimap<AbstractState, CompositeState> compositeMap) {
		this.compositeMap = compositeMap;
		this.selectedRow = selectedRow;
		this.whereRow = whereRow;
		this.whereState = whereState;
	}
	
	public ReachedSet select(int component) {
		assert this.selectedRow == -1;
		return new ReachedSet(component, this.whereRow, this.whereState, this.compositeMap);
	}
	
	public ReachedSet where(int row, AbstractState state) {
		assert this.whereRow == -1 && this.whereState == null;
		return new ReachedSet(this.selectedRow, row, state, this.compositeMap);
	}
	
	public ReachedSet where(Location l) {
		assert this.whereRow == -1 && this.whereState == null;
		return where(0, new LocationState(l));
	}

	public boolean add(AbstractState s) {
		if (s instanceof CompositeState) return add((CompositeState)s);
		else throw new UnsupportedOperationException();
	}

	public boolean remove(AbstractState s) {
		if (s instanceof CompositeState) return remove((CompositeState)s);
		else throw new UnsupportedOperationException();
	}

	public boolean add(CompositeState s) {
		assert selectedRow == -1;
		return compositeMap.put(s.getComponent(indexComponent), s);
	}
	
	public boolean remove(CompositeState s) {
		assert selectedRow == -1;
		return compositeMap.remove(s.getComponent(indexComponent), s);
	}
	
	public int size() {
		if (whereRow < 0)
			return compositeMap.size();
		if (whereRow == indexComponent)
			return compositeMap.get(whereState).size();
		throw new UnsupportedOperationException("Cannot determine reached set size for non-index component restrictions!");
	}

	@Override
	public boolean contains(Object o) {
		if (selectedRow < 0) { 
			if (o instanceof CompositeState) 
				return compositeMap.containsEntry(((CompositeState)o).getComponent(indexComponent), o);
			else throw new UnsupportedOperationException();
		} else {
			for (AbstractState as : this) {
				if (as.equals(o)) return true;
			}
			return false;
		}
	}
	
	public void logHighestStateCounts(int count) {
		AbstractState[] stateArray = compositeMap.keySet().toArray(new AbstractState[compositeMap.keySet().size()]);
		Arrays.sort(stateArray, new Comparator<AbstractState>() {
			@Override
			public int compare(AbstractState o1, AbstractState o2) {
				Integer size1 = compositeMap.get(o1).size();
				Integer size2 = compositeMap.get(o2).size();
				return size2.compareTo(size1);
			}
		});
		
		logger.fatal("==========================================");
		logger.fatal("The " + count + " locations with highest state count:");
		logger.fatal("==========================================");
		for (int i=0; i < Math.min(count, stateArray.length); i++) {
			logger.fatal(stateArray[i] + ":\t" + compositeMap.get(stateArray[i]).size());
		}
		logger.fatal("-------------");
		logger.fatal("Top Location:");
		logger.fatal("-------------");
		for (AbstractState s : compositeMap.get(stateArray[0])) {
			logger.fatal(s);
		}
	}
	
	public void logStates(RTLLabel loc) {
		for (CompositeState c : compositeMap.get(new LocationState(loc))) {
			logger.fatal(c);
		}
	}

	@Override
	public Iterator<AbstractState> iterator() {
		return new Iterator<AbstractState>() {
			CompositeState next;
			private Iterator<CompositeState> compIter;
			// Select either generic iterator or refine by indexed map
			{
				if (whereRow == indexComponent) {
					compIter = compositeMap.get(whereState).iterator();
				} else {
					compIter = compositeMap.values().iterator();
				}
			}
			
			@Override
			public boolean hasNext() {
				while (next == null) {
					if (!compIter.hasNext()) return false;
					CompositeState c = compIter.next();
					
					// If we're filtering by indexed component (or not at all), no need for an extra comparison
					if (whereRow < 0 || whereRow == indexComponent) {
						next = c;
					} else {
						if (c.getComponent(whereRow).equals(whereState)) {
							next = c;
						}
					}
				}
				return true;
			}

			@Override
			public AbstractState next() {
				AbstractState retVal;
				if (hasNext()) {
					if (selectedRow < 0) {
						retVal = next;
					} else {
						retVal = next.getComponent(selectedRow);
					}
					next = null;
					return retVal;
				}
				else throw new NoSuchElementException();
			}

			@Override
			public void remove() {
				compIter.remove();
			}
		};
	}


}
