/*******************************************************************************
 * Copyright (c) 2015-2016 University of York.
 * 
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License, v. 2.0 are satisfied: GNU General Public License, version 3.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0
 *
 * Contributors:
 *    Antonio Garcia-Dominguez - initial API and implementation
 *******************************************************************************/
package org.eclipse.hawk.service.api;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;

/**
 * In-memory representation of an effective metamodel as a list of
 * include/exclude rules.
 */
public class EffectiveMetamodelRuleset {

	/** Wildcard for types or slots */
	public static final String WILDCARD = "*";

	/** @see #getInclusionRules() */
	private final Table<String, String, ImmutableSet<String>> inclusions;

	/** @see #getExclusionRules() */
	private final Table<String, String, ImmutableSet<String>> exclusions;

	/**
	 * Default constructor.
	 */
	public EffectiveMetamodelRuleset() {
		this.inclusions = HashBasedTable.create();
		this.exclusions = HashBasedTable.create();
	}

	/** Copy constructor. */
	public EffectiveMetamodelRuleset(EffectiveMetamodelRuleset toCopy) {
		this.inclusions = HashBasedTable.create(toCopy.inclusions);
		this.exclusions = HashBasedTable.create(toCopy.exclusions);
	}

	/** Constructor from two raw maps (e.g. from Thrift). <code>null</code> values are safely ignored. */
	public EffectiveMetamodelRuleset(Map<String, Map<String, Set<String>>> inclusionRules, Map<String, Map<String, Set<String>>> exclusionRules) {
		this();
		loadMapIntoTable(inclusionRules, this.inclusions);
		loadMapIntoTable(exclusionRules, this.exclusions);
	}

	/**
	 * Brings all the exclusions and inclusion rules from another effective
	 * metamodel into this one, potentially overriding some of the existing
	 * rules.
	 */
	public void importRules(EffectiveMetamodelRuleset source) {
		exclusions.putAll(source.exclusions);
		inclusions.putAll(source.inclusions);
	}

	protected void loadMapIntoTable(Map<String, Map<String, Set<String>>> rawMap, final Table<String, String, ImmutableSet<String>> table) {
		if (rawMap != null) {
			for (final Entry<String, Map<String, Set<String>>> mmEntry : rawMap.entrySet()) {
				final String mmURI = mmEntry.getKey();
				for (final Entry<String, Set<String>> typeEntry : mmEntry.getValue().entrySet()) {
					final String typeName = typeEntry.getKey();
					final ImmutableSet<String> slots = ImmutableSet.copyOf(typeEntry.getValue());
					table.put(mmURI, typeName, slots);
				}
			}
		}
	}

	public boolean isEverythingIncluded() {
		return inclusions.isEmpty() && exclusions.isEmpty();
	}

	public boolean isIncluded(String mmURI) {
		final boolean included = inclusions.isEmpty() || inclusions.containsRow(mmURI);
		final boolean excluded = !included || exclusions.contains(mmURI, WILDCARD);
		return included && !excluded;
	}

	public boolean isFullyIncluded(String mmURI) {
		final boolean included = inclusions.isEmpty() || inclusions.contains(mmURI, WILDCARD);
		final boolean excluded = !included || exclusions.containsRow(mmURI);
		return included && !excluded;
	}

	public boolean isIncluded(String mmURI, String typeName) {
		final boolean included = inclusions.isEmpty() || inclusions.contains(mmURI, WILDCARD) || inclusions.contains(mmURI, typeName);
		final Set<String> excludedSlots = exclusions.get(mmURI, typeName);
		final boolean excluded = !included || exclusions.contains(mmURI, WILDCARD) || excludedSlots != null && excludedSlots.contains(WILDCARD);
		return included && !excluded;
	}

	public boolean isFullyIncluded(String mmURI, String typeName) {
		final Set<String> includedSlots = inclusions.get(mmURI, typeName);
		final boolean included = inclusions.isEmpty() || inclusions.contains(mmURI, WILDCARD) || (includedSlots != null && includedSlots.contains(WILDCARD));
		final boolean excluded = !included || exclusions.contains(mmURI, WILDCARD) || exclusions.contains(mmURI, typeName);
		return included && !excluded;
	}

	public boolean isIncluded(String mmURI, String typeName, String slot) {
		final Set<String> includedSlots = inclusions.get(mmURI, typeName);
		final boolean included = inclusions.isEmpty() || inclusions.contains(mmURI, WILDCARD) || (includedSlots != null && (includedSlots.contains(WILDCARD) || includedSlots.contains(slot)));
		final Set<String> excludedSlots = exclusions.get(mmURI, typeName);
		final boolean excluded = !included || exclusions.contains(mmURI, WILDCARD) || (excludedSlots != null && (excludedSlots.contains(WILDCARD) || excludedSlots.contains(slot)));
		return included && !excluded;
	}

	public void include(String mmURI) {
		inclusions.put(mmURI, WILDCARD, ImmutableSet.of(WILDCARD));
	}

	public void include(String mmURI, String type) {
		inclusions.put(mmURI, type, ImmutableSet.of(WILDCARD));
	}

	public void include(String mmURI, String type, ImmutableSet<String> slots) {
		inclusions.put(mmURI, type, slots);
	}

	public void exclude(String mmURI) {
		exclusions.put(mmURI, WILDCARD, ImmutableSet.of(WILDCARD));
	}

	public void exclude(String mmURI, String type) {
		exclusions.put(mmURI, type, ImmutableSet.of(WILDCARD));
	}

	public void exclude(String mmURI, String type, ImmutableSet<String> slots) {
		if (slots == null || slots.isEmpty()) {
			exclusions.remove(mmURI, type);
		} else {
			exclusions.put(mmURI, type, slots);
		}
	}

	/**
	 * Undoes the effects of a {@link #include(String)} or {@link #exclude(String)} call.
	 */
	public void reset(String mmURI) {
		inclusions.remove(mmURI, WILDCARD);
		exclusions.remove(mmURI, WILDCARD);
	}

	/**
	 * Undoes the effects of a {@link #include(String, String)} or {@link #exclude(String, String)} call.
	 */
	public void reset(String mmURI, String type) {
		ImmutableSet<String> oldInclusions = inclusions.remove(mmURI, type);
		if (oldInclusions != null && !oldInclusions.contains(WILDCARD)) {
			inclusions.put(mmURI, type, oldInclusions);
		}

		ImmutableSet<String> oldExclusions = exclusions.remove(mmURI, type);
		if (oldExclusions != null && !oldExclusions.contains(WILDCARD)) {
			exclusions.put(mmURI, type, oldExclusions);
		}
	}

	/**
	 * Undoes the effects of a {@link #include(String, String, ImmutableSet)} or {@link #exclude(String, String, ImmutableSet)} call.
	 */
	public void reset(String mmURI, String type, String slot) {
		ImmutableSet<String> oldInclusions = inclusions.remove(mmURI, type);
		if (oldInclusions != null) {
			if (!oldInclusions.contains(slot)) { 
				inclusions.put(mmURI, type, oldInclusions);
			} else {
				inclusions.put(mmURI, type, copyWithout(slot, oldInclusions));
			}
		}
		
		ImmutableSet<String> oldExclusions = inclusions.remove(mmURI, type);
		if (oldExclusions != null) {
			if (!oldExclusions.contains(slot)) { 
				inclusions.put(mmURI, type, oldExclusions);
			} else {
				inclusions.put(mmURI, type, copyWithout(slot, oldExclusions));
			}
		}
	}

	protected ImmutableSet<String> copyWithout(String slot, ImmutableSet<String> oldInclusions) {
		ImmutableSet.Builder<String> builder = new ImmutableSet.Builder<>();
		for (String s : oldInclusions) {
			if (!slot.equals(s)) {
				builder.add(s);
			}
		}
		final ImmutableSet<String> newSet = builder.build();
		return newSet;
	}

	/**
	 * Returns an unmodifiable view of the table of inclusion rules: rows are
	 * metamodel URIs, columns are type names or WILDCARD (meaning all types in
	 * the metamodel) and cells are either sets of slot names or a singleton set
	 * with WILDCARD (meaning "all"). An empty table means "include everything".
	 */
	public Table<String, String, ImmutableSet<String>> getInclusionRules() {
		return Tables.unmodifiableTable(inclusions);
	}

	/**
	 * Returns an unmodifiable view of the table of exclusion rules: rows are
	 * metamodel URIs, columns are type names and cells are either sets of slot
	 * names or a singleton set with WILDCARD (meaning "all"). An empty table
	 * means "exclude nothing".
	 */
	public Table<String, String, ImmutableSet<String>> getExclusionRules() {
		return Tables.unmodifiableTable(exclusions);
	}

	/**
	 * Returns all the slots explicitly included (after filtering), or a singleton
	 * set with {@link #WILDCARD} if all slots are included.
	 */
	public ImmutableSet<String> getIncludedSlots(String mmURI, String typeName) {
		if (isEverythingIncluded()) {
			return ImmutableSet.of(WILDCARD);
		} else if (exclusions.contains(mmURI, WILDCARD)) {
			return ImmutableSet.of();
		}

		final ImmutableSet<String> included = inclusions.get(mmURI, typeName);
		if (included == null) {
			return ImmutableSet.of();
		}

		final ImmutableSet<String> excluded = exclusions.get(mmURI, typeName);
		if (excluded == null) {
			return included;
		}

		ImmutableSet.Builder<String> result = new ImmutableSet.Builder<>();
		for (String slot : included) {
			if (!excluded.contains(slot)) {
				result.add(slot);
			}
		}
		return result.build();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((exclusions == null) ? 0 : exclusions.hashCode());
		result = prime * result + ((inclusions == null) ? 0 : inclusions.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		EffectiveMetamodelRuleset other = (EffectiveMetamodelRuleset) obj;
		if (exclusions == null) {
			if (other.exclusions != null)
				return false;
		} else if (!exclusions.equals(other.exclusions))
			return false;
		if (inclusions == null) {
			if (other.inclusions != null)
				return false;
		} else if (!inclusions.equals(other.inclusions))
			return false;
		return true;
	}
}
