/*
 * Copyright (c) 2012-2017 The ANTLR Project. All rights reserved.
 * Use of this file is governed by the BSD 3-clause license that
 * can be found in the LICENSE.txt file in the project root.
 */

package org.antlr.v4.runtime.atn;

import org.antlr.v4.runtime.misc.AbstractEqualityComparator;
import org.antlr.v4.runtime.misc.Array2DHashSet;
import org.antlr.v4.runtime.misc.DoubleKeyMap;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * Specialized {@link Set}{@code <}{@link ATNConfig}{@code >} that can track
 * info about the set, with support for combining similar configurations using a
 * graph-structured stack.
 */
public class ATNConfigSet implements Set<ATNConfig> {
	/**
	 * The reason that we need this is because we don't want the hash map to use
	 * the standard hash code and equals. We need all configurations with the same
	 * {@code (s,i,_,semctx)} to be equal. Unfortunately, this key effectively doubles
	 * the number of objects associated with ATNConfigs. The other solution is to
	 * use a hash table that lets us specify the equals/hashcode operation.
	 */
	public static class ConfigHashSet extends AbstractConfigHashSet {
		public ConfigHashSet() {
			super(ConfigEqualityComparator.INSTANCE);
		}
	}

	public static final class ConfigEqualityComparator extends AbstractEqualityComparator<ATNConfig> {
		public static final ConfigEqualityComparator INSTANCE = new ConfigEqualityComparator();

		private ConfigEqualityComparator() {
		}

		@Override
		public int hashCode(ATNConfig o) {
			int hashCode = 7;
			hashCode = 31 * hashCode + o.state.stateNumber;
			hashCode = 31 * hashCode + o.alt;
			hashCode = 31 * hashCode + o.semanticContext.hashCode();
	        return hashCode;
		}

		@Override
		public boolean equals(ATNConfig a, ATNConfig b) {
			if ( a==b ) return true;
			if ( a==null || b==null ) return false;
			return a.state.stateNumber==b.state.stateNumber
				&& a.alt==b.alt
				&& a.semanticContext.equals(b.semanticContext);
		}
	}

	/** Indicates that the set of configurations is read-only. Do not
	 *  allow any code to manipulate the set; DFA states will point at
	 *  the sets and they must not change. This does not protect the other
	 *  fields; in particular, conflictingAlts is set after
	 *  we've made this readonly.
 	 */
	protected boolean readonly = false;

	/**
	 * All configs but hashed by (s, i, _, pi) not including context. Wiped out
	 * when we go readonly as this set becomes a DFA state.
	 */
	public AbstractConfigHashSet configLookup;

	/** Track the elements as they are added to the set; supports get(i) */
	public final ArrayList<ATNConfig> configs = new ArrayList<ATNConfig>(7);

	// TODO: these fields make me pretty uncomfortable but nice to pack up info together, saves recomputation
	// TODO: can we track conflicts as they are added to save scanning configs later?
	public int uniqueAlt;
	/** Currently this is only used when we detect SLL conflict; this does
	 *  not necessarily represent the ambiguous alternatives. In fact,
	 *  I should also point out that this seems to include predicated alternatives
	 *  that have predicates that evaluate to false. Computed in computeTargetState().
 	 */
	protected BitSet conflictingAlts;

	// Used in parser and lexer. In lexer, it indicates we hit a pred
	// while computing a closure operation.  Don't make a DFA state from this.
	public boolean hasSemanticContext;
	public boolean dipsIntoOuterContext;

	/** Indicates that this configuration set is part of a full context
	 *  LL prediction. It will be used to determine how to merge $. With SLL
	 *  it's a wildcard whereas it is not for LL context merge.
	 */
	public final boolean fullCtx;

	private int cachedHashCode = -1;

	public ATNConfigSet(boolean fullCtx) {
		configLookup = new ConfigHashSet();
		this.fullCtx = fullCtx;
	}
	public ATNConfigSet() { this(true); }

	public ATNConfigSet(ATNConfigSet old) {
		this(old.fullCtx);
		addAll(old);
		this.uniqueAlt = old.uniqueAlt;
		this.conflictingAlts = old.conflictingAlts;
		this.hasSemanticContext = old.hasSemanticContext;
		this.dipsIntoOuterContext = old.dipsIntoOuterContext;
	}

	@Override
	public boolean add(ATNConfig config) {
		return add(config, null);
	}

	/**
	 * Adding a new config means merging contexts with existing configs for
	 * {@code (s, i, pi, _)}, where {@code s} is the
	 * {@link ATNConfig#state}, {@code i} is the {@link ATNConfig#alt}, and
	 * {@code pi} is the {@link ATNConfig#semanticContext}. We use
	 * {@code (s,i,pi)} as key.
	 *
	 * <p>This method updates {@link #dipsIntoOuterContext} and
	 * {@link #hasSemanticContext} when necessary.</p>
	 */
	public boolean add(
		ATNConfig config,
		DoubleKeyMap<PredictionContext,PredictionContext,PredictionContext> mergeCache)
	{
		if ( readonly ) throw new IllegalStateException("This set is readonly");
		if ( config.semanticContext != SemanticContext.Empty.Instance ) {
			hasSemanticContext = true;
		}
		if (config.getOuterContextDepth() > 0) {
			dipsIntoOuterContext = true;
		}
		ATNConfig existing = configLookup.getOrAdd(config);
		if ( existing==config ) { // we added this new one
			cachedHashCode = -1;
			configs.add(config);  // track order here
			return true;
		}
		// a previous (s,i,pi,_), merge with it and save result
		boolean rootIsWildcard = !fullCtx;
		PredictionContext merged =
			PredictionContext.merge(existing.context, config.context, rootIsWildcard, mergeCache);
		// no need to check for existing.context, config.context in cache
		// since only way to create new graphs is "call rule" and here. We
		// cache at both places.
		existing.reachesIntoOuterContext =
			Math.max(existing.reachesIntoOuterContext, config.reachesIntoOuterContext);

		// make sure to preserve the precedence filter suppression during the merge
		if (config.isPrecedenceFilterSuppressed()) {
			existing.setPrecedenceFilterSuppressed(true);
		}

		existing.context = merged; // replace context; no need to alt mapping
		return true;
	}

	/** Return a List holding list of configs */
    public List<ATNConfig> elements() { return configs; }

	public Set<ATNState> getStates() {
		Set<ATNState> states = new HashSet<ATNState>();
		for (ATNConfig c : configs) {
			states.add(c.state);
		}
		return states;
	}

	/**
	 * Gets the complete set of represented alternatives for the configuration
	 * set.
	 *
	 * @return the set of represented alternatives in this configuration set
	 *
	 * @since 4.3
	 */

	public BitSet getAlts() {
		BitSet alts = new BitSet();
		for (ATNConfig config : configs) {
			alts.set(config.alt);
		}
		return alts;
	}

	public List<SemanticContext> getPredicates() {
		List<SemanticContext> preds = new ArrayList<SemanticContext>();
		for (ATNConfig c : configs) {
			if ( c.semanticContext!=SemanticContext.Empty.Instance ) {
				preds.add(c.semanticContext);
			}
		}
		return preds;
	}

	public ATNConfig get(int i) { return configs.get(i); }

	public void optimizeConfigs(ATNSimulator interpreter) {
		if ( readonly ) throw new IllegalStateException("This set is readonly");
		if ( configLookup.isEmpty() ) return;

		for (ATNConfig config : configs) {
//			int before = PredictionContext.getAllContextNodes(config.context).size();
			config.context = interpreter.getCachedContext(config.context);
//			int after = PredictionContext.getAllContextNodes(config.context).size();
//			System.out.println("configs "+before+"->"+after);
		}
	}

	@Override
	public boolean addAll(Collection<? extends ATNConfig> coll) {
		for (ATNConfig c : coll) add(c);
		return false;
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		else if (!(o instanceof ATNConfigSet)) {
			return false;
		}

//		System.out.print("equals " + this + ", " + o+" = ");
		ATNConfigSet other = (ATNConfigSet)o;
		boolean same = configs!=null &&
			configs.equals(other.configs) &&  // includes stack context
			this.fullCtx == other.fullCtx &&
			this.uniqueAlt == other.uniqueAlt &&
			this.conflictingAlts == other.conflictingAlts &&
			this.hasSemanticContext == other.hasSemanticContext &&
			this.dipsIntoOuterContext == other.dipsIntoOuterContext;

//		System.out.println(same);
		return same;
	}

	@Override
	public int hashCode() {
		if (isReadonly()) {
			if (cachedHashCode == -1) {
				cachedHashCode = configs.hashCode();
			}

			return cachedHashCode;
		}

		return configs.hashCode();
	}

	@Override
	public int size() {
		return configs.size();
	}

	@Override
	public boolean isEmpty() {
		return configs.isEmpty();
	}

	@Override
	public boolean contains(Object o) {
		if (configLookup == null) {
			throw new UnsupportedOperationException("This method is not implemented for readonly sets.");
		}

		return configLookup.contains(o);
	}

	public boolean containsFast(ATNConfig obj) {
		if (configLookup == null) {
			throw new UnsupportedOperationException("This method is not implemented for readonly sets.");
		}

		return configLookup.containsFast(obj);
	}

	@Override
	public Iterator<ATNConfig> iterator() {
		return configs.iterator();
	}

	@Override
	public void clear() {
		if ( readonly ) throw new IllegalStateException("This set is readonly");
		configs.clear();
		cachedHashCode = -1;
		configLookup.clear();
	}

	public boolean isReadonly() {
		return readonly;
	}

	public void setReadonly(boolean readonly) {
		this.readonly = readonly;
		configLookup = null; // can't mod, no need for lookup cache
	}

	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder();
		buf.append(elements().toString());
		if ( hasSemanticContext ) buf.append(",hasSemanticContext=").append(hasSemanticContext);
		if ( uniqueAlt!=ATN.INVALID_ALT_NUMBER ) buf.append(",uniqueAlt=").append(uniqueAlt);
		if ( conflictingAlts!=null ) buf.append(",conflictingAlts=").append(conflictingAlts);
		if ( dipsIntoOuterContext ) buf.append(",dipsIntoOuterContext");
		return buf.toString();
	}

	// satisfy interface

	@Override
	public ATNConfig[] toArray() {
		return configLookup.toArray();
	}

	@Override
	public <T> T[] toArray(T[] a) {
		return configLookup.toArray(a);
	}

	@Override
	public boolean remove(Object o) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean containsAll(Collection<?> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		throw new UnsupportedOperationException();
	}

	public static abstract class AbstractConfigHashSet extends Array2DHashSet<ATNConfig> {

		public AbstractConfigHashSet(AbstractEqualityComparator<? super ATNConfig> comparator) {
			this(comparator, 16, 2);
		}

		public AbstractConfigHashSet(AbstractEqualityComparator<? super ATNConfig> comparator, int initialCapacity, int initialBucketCapacity) {
			super(comparator, initialCapacity, initialBucketCapacity);
		}

		@Override
		protected final ATNConfig asElementType(Object o) {
			if (!(o instanceof ATNConfig)) {
				return null;
			}

			return (ATNConfig)o;
		}

		@Override
		protected final ATNConfig[][] createBuckets(int capacity) {
			return new ATNConfig[capacity][];
		}

		@Override
		protected final ATNConfig[] createBucket(int capacity) {
			return new ATNConfig[capacity];
		}

	}
}
