/**
 * Copyright (c) 2017-2019, the Alpha Team.
 * All rights reserved.
 *
 * Additional changes made by Siemens.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1) Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2) Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package at.ac.tuwien.kr.alpha.core.solver;

import static at.ac.tuwien.kr.alpha.commons.util.Util.arrayGrowthSize;
import static at.ac.tuwien.kr.alpha.commons.util.Util.oops;
import static at.ac.tuwien.kr.alpha.core.solver.ThriceTruth.MBT;
import static at.ac.tuwien.kr.alpha.core.solver.ThriceTruth.TRUE;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Manages influence of atoms on the activity of certain other atoms. Can be used for either choice points or heuristic atoms.
 *
 * Copyright (c) 2020, the Alpha Team.
 */
public class ChoiceInfluenceManager implements Checkable {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(ChoiceInfluenceManager.class);
	
	// Active choice points and all atoms that influence a choice point (enabler, disabler, choice atom itself).
	private final Set<ChoicePoint> activeChoicePoints = new LinkedHashSet<>();
	private final Set<Integer> activeChoicePointsAtoms = new LinkedHashSet<>();
	private ChoicePoint[] influencers = new ChoicePoint[0];
	private ActivityListener activityListener;

	private final WritableAssignment assignment;

	private boolean checksEnabled;

	/**
	 * @param assignment
	 * 
	 */
	public ChoiceInfluenceManager(WritableAssignment assignment) {
		this.assignment = assignment;
	}

	void addInformation(Pair<Map<Integer, Integer>, Map<Integer, Integer>> choiceAtoms) {
		// Assumption: we get all enabler/disabler pairs in one call.
		Map<Integer, Integer> enablers = choiceAtoms.getLeft();
		Map<Integer, Integer> disablers = choiceAtoms.getRight();
		for (Map.Entry<Integer, Integer> atomToEnabler : enablers.entrySet()) {
			addInformation(atomToEnabler, disablers);
		}
	}

	private void addInformation(Map.Entry<Integer, Integer> atomToEnabler, Map<Integer, Integer> disablers) {
		// Construct and record ChoicePoint.
		Integer atom = atomToEnabler.getKey();
		if (atom == null) {
			throw oops("Incomplete choice point description found (no atom)");
		}
		if (influencers[atom] != null) {
			throw oops("Received choice information repeatedly");
		}
		Integer enabler = atomToEnabler.getValue();
		Integer disabler = disablers.get(atom);
		if (enabler == null || disabler == null) {
			throw oops("Incomplete choice point description found (no enabler or disabler)");
		}
		assignment.registerCallbackOnChange(atom);
		assignment.registerCallbackOnChange(enabler);
		assignment.registerCallbackOnChange(disabler);
		ChoicePoint choicePoint = new ChoicePoint(atom, enabler, disabler);
		influencers[atom] = choicePoint;
		influencers[enabler] = choicePoint;
		influencers[disabler] = choicePoint;
		choicePoint.recomputeActive();
	}

	void checkActiveChoicePoints() {
		HashSet<ChoicePoint> actualActiveChoicePoints = new HashSet<>();
		for (int i = 0; i < influencers.length; i++) {
			ChoicePoint choicePoint = influencers[i];
			if (choicePoint == null) {
				continue;
			}
			if (checkActiveChoicePoint(choicePoint)) {
				actualActiveChoicePoints.add(choicePoint);
			}
		}
		if (!actualActiveChoicePoints.equals(activeChoicePoints)) {
			throw oops("ChoiceInfluenceManager internal checker detected wrong activeChoicePoints");
		}
		LOGGER.trace("Checking internal choice manger: all ok.");
	}
	
	private boolean checkActiveChoicePoint(ChoicePoint choicePoint) {
		ThriceTruth enablerTruth = assignment.getTruth(choicePoint.enabler);
		ThriceTruth disablerTruth = assignment.getTruth(choicePoint.disabler);
		boolean isActive = enablerTruth == TRUE
			&& (disablerTruth == null || !disablerTruth.toBoolean());
		ThriceTruth atomTruth = assignment.getTruth(choicePoint.atom);
		boolean isNotChosen = atomTruth == null || atomTruth == MBT;
		return isActive && isNotChosen;
	}

	boolean isActive(int atom) {
		if (checksEnabled) {
			checkActiveChoicePoints();
		}
		ChoicePoint choicePoint = influencers[atom];
		return choicePoint != null && choicePoint.isActive && choicePoint.atom == atom;
	}

	int getNextActiveAtomOrDefault(int defaultAtom) {
		if (checksEnabled) {
			checkActiveChoicePoints();
		}
		return activeChoicePointsAtoms.size() > 0 ? activeChoicePointsAtoms.iterator().next() : defaultAtom;
	}

	boolean isAtomInfluenced(int atom) {
		ChoicePoint choicePoint = influencers[atom];
		return choicePoint != null && choicePoint.atom == atom;
	}

	@Override
	public void setChecksEnabled(boolean checksEnabled) {
		this.checksEnabled = checksEnabled;
	}

	void callbackOnChanged(int atom) {
		if (LOGGER.isTraceEnabled()) {
			LOGGER.trace("Callback received on influencer atom: {}", atom);
		}
		ChoicePoint choicePoint = influencers[atom];
		if (choicePoint != null) {
			choicePoint.recomputeActive();
		}
	}

	public void growForMaxAtomId(int maxAtomId) {
		// Grow arrays only if needed.
		if (influencers.length > maxAtomId) {
			return;
		}
		// Grow to default size, except if bigger array is required due to maxAtomId.
		int newCapacity = arrayGrowthSize(influencers.length);
		if (newCapacity < maxAtomId + 1) {
			newCapacity = maxAtomId + 1;
		}
		influencers = Arrays.copyOf(influencers, newCapacity);
	}
	
	void setActivityListener(ActivityListener listener) {
		this.activityListener = listener;
	}

	private class ChoicePoint {
		final Integer atom;
		final int enabler;
		final int disabler;
		boolean isActive;

		private ChoicePoint(Integer atom, Integer enabler, int disabler) {
			this.atom = atom;
			this.enabler = enabler;
			this.disabler = disabler;
		}

		private boolean isActiveChoicePoint() {
			ThriceTruth enablerTruth = assignment.getTruth(enabler);
			ThriceTruth disablerTruth = assignment.getTruth(disabler);
			return  enablerTruth == TRUE
				&& (disablerTruth == null || !disablerTruth.toBoolean());
		}

		private boolean isNotChosen() {
			ThriceTruth atomTruth = assignment.getTruth(atom);
			return atomTruth == null || atomTruth == MBT;
		}

		void recomputeActive() {
			LOGGER.trace("Recomputing activity of atom {}.", atom);
			final boolean wasActive = isActive;
			isActive = isNotChosen() && isActiveChoicePoint();
			boolean changed = false;
			if (isActive && !wasActive) {
				activeChoicePoints.add(this);
				activeChoicePointsAtoms.add(atom);
				changed = true;
				LOGGER.debug("Activating choice point for atom {}", this.atom);
			} else if (wasActive && !isActive) {
				activeChoicePoints.remove(this);
				activeChoicePointsAtoms.remove(atom);
				changed = true;
				LOGGER.debug("Deactivating choice point for atom {}", this.atom);
			}
			if (changed && activityListener != null) {
				activityListener.callbackOnChanged(atom, isActive);
			}
		}

		@Override
		public String toString() {
			return String.valueOf(atom);
		}
	}
	
	public static interface ActivityListener {
		void callbackOnChanged(int atom, boolean active);
	}

}
