/**
 * CertainTrust SDK
 * 
 * Implements the computational trust model "CertainTrust"
 * in Java.
 * See <http://www.tk.informatik.tu-darmstadt.de/de/research/smart-security-and-trust/> for further details.
 * 
 * 
 * Telecooperation Department, Technische Universit�t Darmstadt
 * <http://www.tk.informatik.tu-darmstadt.de/>
 * 
 * Prof. Dr. Max Mühlhäuser <max@informatik.tu-darmstadt.de>
 * Florian Volk <florian.volk@cased.de>
 * 
 * 
 * @author	Maria Pelevina
 * @author	David Kalnischkies
 * @version	1.1
 */

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


package CertainTrust;
import java.util.Observable;

/**
 * 
 * t - average rating value, [0; 1], from very negative to very positive
 * c - certainty value, [0; 1] from low certainty (no evidence) to the maximal maximal certainty.
 * f - initial trust value
 * w - weight
 * r - number of positive evidence
 * s - number of negative evidence
 * n - maximal number of expected evidence
 * doc - degree of conflict
 * 
 */
public class CertainTrust extends Observable {
	
	private double c, t, f, r, s, doc;
	private int n, weight = 2;

//=========== Constructors =================
	/**
	 * Constructs a CertainTrust object with predefined n value.
	 * @param n - maximal number of expected evidence
	 */
	public CertainTrust(int n) {
		if (n > 0) {
			this.n = n;
			c = 0;
			t = 0.5;
			f = 0.5;
			r = 0;
			s = 0;
		}
		else throw new IllegalArgumentException("N should be greater than 0. Entered n = " + n + "\n");
	}
	
	/**
	 * Constructs a CertainTrust object with predefined r, s, n values. 
	 * @param r - number of positive evidence
	 * @param s - number of negative evidence
	 * @param n - maximal number of expected evidence
	 */
	public CertainTrust(double r, double s, int n) {
		if (n > 0) {
			this.n = n;
			this.c = 0;
			this.t = 0.5;
			this.f = 0.5;
			this.doc = 0;
			setRS(r, s);
		}
		else throw new IllegalArgumentException("N should be greater than 0. Entered n = " + n + "\n");
	}
	
	/**
	 * Constructs a CertainTrust object with predefined c, t, f, n values. 
	 * @param c - certainty
	 * @param t - average rating
	 * @param f - initial trust value
	 * @param n - maximal number of expected evidence
	 */
	public CertainTrust(double t, double c, double f, int n) {
		this(t,c,f,n,0);
	}
	
	/**
	 * Constructs a CertainTrust object with predefined c, t, f, n values. 
	 * @param c - certainty
	 * @param t - average rating
	 * @param f - initial trust value
	 * @param n - maximal number of expected evidence
	 * @param doc - initial degree of conflict
	 */
	private CertainTrust(double t, double c, double f, int n, double doc) {
		if (n > 0) {
			if (f <= 1 && f >= 0) {
				this.n = n;
				this.f = f;
				this.r = 0;
				this.s = 0;
				this.doc = doc;
				setTC(t, c);
			}
			else throw new IllegalArgumentException("f should lie within [0;1]. Entered f = " + f + "\n");
		}
		else throw new IllegalArgumentException("N should be greater than 0. Entered n = " + n + "\n");
	}
	
	//=========== Setters =================
	/**
	 * Resets N value. Renormalises r and s values, recalculates c and t accordingly.  
	 * @param n - new maximal number of expected evidence
	 */
	public void setN(int n) {
		if (n > 0) {
			this.n = n;
			normaliseRS();
			calculateRStoTC();
			
			setChanged();
			notifyObservers();
		}
		else throw new IllegalArgumentException("N should be greater than 0. Entered n = " + n + "\n");
	}
	
	/**
	 * Sets f value.
	 * @param f - initial trust value.
	 */
	public void setF(double f) {
		if (f >= 0 && f <= 1) {
			this.f = f;
			
			setChanged();
			notifyObservers();
		}
		
		else throw new IllegalArgumentException("f should lie within [0;1]. Entered f = " + f + "\n");
	}

	/**
	  * Sets Distance of Conflict value.
	  * @param doc is the new value for DoC
	  */
	public void setDoC(double doc) {
		if (doc > 0) {
			this.doc = doc;
		}
		else throw new IllegalArgumentException("DoC should be greater than 0. Entered DoC = " + doc + "\n");
	}
	
	/**
	 * Sets c and t values. Recalculates r and s values accordingly.
	 * @param t - new average trust value
	 * @param c - new certainty value
	 */
	public void setTC(double t, double c) {
		if (c >= 0 && c <= 1) {
			if (t >= 0 && t <= 1) {
				this.c = c;
				this.t = t;
				calculateTCtoRS();
				
				setChanged();
				notifyObservers();
			}
			else throw new IllegalArgumentException("t should be greater than 0. Entered t = " + t + "\n");
		}
		else throw new IllegalArgumentException("c should lie within [0;1]. Entered c = " + c + "\n");
	}
	/**
	 * Sets r and s values. Recalculates c and t values accordingly.
	 * @param r - new number of positive evidence
	 * @param s - new number of negative evidence
	 */
	public void setRS(double r, double s) {
		if (r >= 0) {
			if (s >= 0) {
				this.r = r;
				this.s = s;
				normaliseRS();
				calculateRStoTC();
				
				setChanged();
				notifyObservers();
			}
			else throw new IllegalArgumentException("s should be positive. Entered s = " + s + "\n");
		}
		else throw new IllegalArgumentException("r should be positive. Entered r = " + r + "\n");
	}
	
	/**
	 * Add some positive evidence to r.
	 * @param posEvidence - number of new positive evidences
	 */
	public void addR(int posEvidence) {
		if (posEvidence >= 0) {
			r += posEvidence;
			normaliseRS();
			calculateRStoTC();
			
			setChanged();
			notifyObservers();
		}
		else throw new IllegalArgumentException("Number of positive evidences should be positive. Entered " + posEvidence + "\n");
	}
	
	/**
	 * Add some negative evidence to s.
	 * @param negEvidence - number of new negative evidences
	 */
	public void addS(int negEvidence) {
		if (negEvidence >= 0) {
			s += negEvidence;
			normaliseRS();
			calculateRStoTC();
			
			setChanged();
			notifyObservers();
		}
		else throw new IllegalArgumentException("Number of negative evidences should be positive. Entered " + negEvidence + "\n");
	}
	
//=========== Internal Calculations ==========
	/**
	 * Normalises r and s values according to n - maximal number of expected evidence
	 * Important! Doesn't notify observers.
	 */
	private void normaliseRS() {
		if (r + s > n){
			double initR = r;
			r = r*n / (initR + s);
			s = s*n / (initR + s);
		}
	}
	/**
	 * Calculates c and t values based on existing r and s values
	 * Important! Doesn't notify observers.
	 */
	private void calculateRStoTC() {
		c = n * (r + s) / ( weight*(n-r-s) + n*(r+s));
		if (c == 0)
			t = 0.5;
		else t = r / (r + s);
	}
	
	/**
	 * Calculates r and s values based on existing c and t values
	 * Important! Doesn't notify observers.
	 */
	private void calculateTCtoRS() {
		if (c == 0) {
			r = 0;
			s = 0;
			t = 0.5;
		}
		else {
			r = (2*c*weight*n*t) / (2*c*weight + n - c*n);
			s = (2*c*weight*n - 2*c*weight*n*t) / (2*c*weight + n - c*n);
		}
	}
	
//=========== Getters =================
	public double getC() {
		return c;
	}

	public double getT() {
		return t;
	}

	public double getF() {
		return f;
	}

	public double getR() {
		return r;
	}

	public double getS() {
		return s;
	}

	public int getN() {
		return n;
	}
	
	public double getDoC() {
		return doc;
	}
	
	public double getExpectation() {
		return t*c + (1-c)*f;
	}
	
//=========== Logic =================
	/**
	 * Computes OR function for this CertainTrust object and the specified argument. Result is returned as a new object,
	 * argument and this CertainTrust object remain unchanged.
	 * N values of both objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param arg - CertainTrust object
	 * @return - result of OR computation for this object and an argument.
	 */
	
	private CertainTrust OR(CertainTrust arg){
		double c1 = getC();
		double t1 = getT();
		double f1 = getF();
		
		double c2 = arg.getC();
		double t2 = arg.getT();
		double f2 = arg.getF();
		
		double resT = 0.5, resF = 0.5, resC = 0;
		
		if (this.getN() != arg.getN()) 
			throw new IllegalStateException("Different N values. Operation not allowed. \n");
				
		resF = f1 + f2 - f1*f2;
		if (almostEqual(resF, 0))
			resC = c1 + c2 - c1*c2;
		else 
			resC = c1 + c2 - c1*c2 - (c1*f2*(1-c2)*(1-t1)+c2*f1*(1-c1)*(1-t2)) / resF;
		
		if (almostEqual(resC, 0)) 
			resT = 0.5;	
		else resT = (1/resC) * (c1*t1 + c2*t2 - c1*c2*t1*t2);
		
		resT = adjustValue(resT);
		resC = adjustValue(resC);
		resF = adjustValue(resF);
		
		CertainTrust result = new CertainTrust(resT, resC, resF, n, 0); 
			
		return result;	
	}
	/**
	 * Computes OR function for this CertainTrust object and the specified arguments. 
	 * Result is returned as a new object, arguments and this CertainTrust object remain unchanged.
	 * Example: a.OR(b, c, d) returns new CertainTrust object that equals a OR b OR c OR d.
	 * Multiple arguments allowed, but not less than one.
	 * N values of all objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param args - arguments
	 * @return - result of OR computation for this object and all arguments.
	 */
	public CertainTrust OR(CertainTrust ...args) {
		CertainTrust result = clone();
		for (CertainTrust m: args) {
			if (n != m.getN())
				throw new IllegalStateException("Different N values. Operation not allowed. \n");
			result = result.OR(m);
		}
			
		return result;
	}
	
	/**
	 * Computes AND function for this CertainTrust object and the specified argument. Result is returned as a new object,
	 * argument and this CertainTrust object remain unchanged.
	 * N values of both objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param arg - CertainTrust object
	 * @return - result of AND computation for this object and an argument.
	 */
	private CertainTrust AND(CertainTrust arg){
		double c1 = getC();
		double f1 = getF();
		double t1 = getT();
		
		double c2 = arg.getC();
		double f2 = arg.getF();
		double t2 = arg.getT();
		
		double resC = 0, resT = 0.5, resF = 0.5;
		
		if (n != arg.getN()) 
			throw new IllegalStateException("Different N values. Operation not allowed. \n");
		
		resF = f1*f2;
		
		if (almostEqual(resF, 1))		//avoid division by 0
			resC = c1 + c2 - c1*c2;
		else
			resC = c1 + c2 - c1*c2 - (c2*t2*(1-c1)*(1-f1)+c1*t1*(1-c2)*(1-f2)) / (1 - resF);
		
		if (almostEqual(resC, 0)) 
			resT = 0.5;
		else if (almostEqual(resF, 1))	//avoid division by 0
			resT = (1/resC) *  (c1*t1*c2*t2);
		else resT = (1/resC) *  ((c1*t1*c2*t2) + (c1*f2*t1*(1-c2)*(1-f1)+c2*f1*t2*(1-c1)*(1-f2)) / (1 - resF));
		
		resT = adjustValue(resT);
		resC = adjustValue(resC);
		resF = adjustValue(resF);
		
		CertainTrust result = new CertainTrust(resT, resC, resF, n, 0);
			
		return result;
	}
	
	/**
	 * Computes AND function for this CertainTrust object and the specified arguments. 
	 * Result is returned as a new object, arguments and this CertainTrust object remain unchanged.
	 * Example: a.AND(b, c, d) returns new CertainTrust object that equals a AND b AND c AND d.
	 * Multiple arguments allowed, but not less than one.
	 * N values of all objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param args - arguments
	 * @return - result of AND computation for this object and all arguments.
	 */
	public CertainTrust AND(CertainTrust ...args) {
		CertainTrust result = clone();
		for (CertainTrust m: args) {
			if (n != m.getN())
				throw new IllegalStateException("Different N values. Operation not allowed. \n");
			result = result.AND(m);
		}
			
		return result;
	}
	
	/**
	 * Returns NOT of this CertainTrust object.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @return - NOT of this CertainTrust object.
	 */
	public CertainTrust NOT(){
		return new CertainTrust(getC(), adjustValue(1 - getT()), adjustValue(1 - getF()), n, 0);
	}
	
	/**
	 * an internal implementation of fusion function.
	 * Is called by wFusion and cFusion
	 * @param args - an array of CertainTrust objects
	 * @param weights - an integer array of corresponding weights
	 * @param doc - a degree of conflict (always 0 for wFusion)
	 * @return - new CertainTrust object
	 */
	private static CertainTrust internalFusion(CertainTrust[] args, int[] weights, double doc) {
		double resC, resT, resF;
		boolean allOne = true;
		boolean allZero = true;
		boolean allWeightsZero = true;
		boolean atLeastOne1 = false;
		int arrLength = args.length;
		
		// set the flags about C and Weight values
		for (int i = 0; i < arrLength; i++) 
			if (args[i].getC() != 1) {
				allOne = false;
				i = arrLength;
			}
		for (int i = 0; i < arrLength; i++) 
			if (args[i].getC() != 0) {
				allZero = false;
				i = arrLength;
			}
		for (int i = 0; i < arrLength; i++) 
			if (weights[i] != 0) {
				allWeightsZero = false;
				i = arrLength;
			}
		for (int i = 0; i < arrLength; i++)
			if (args[i].getC() == 1) {
				atLeastOne1 = true;
				i = arrLength;
			}
		
		//Calculate T and C
		
		// 1. all C's  = 1
		if (allOne) {
			// set C
			resC = 1 * (1 - doc);
			// set T
			if (allWeightsZero) {// save some calculation time 
				resT = 0;
			}
			else {				// or use the function
				double numeratorT = 0, denominatorT = 0;
				for (int i = 0; i < arrLength; i++) {
					numeratorT += weights[i] * args[i].getT();
					denominatorT += weights[i];
				}
				resT = numeratorT/denominatorT;
			}
		}
		
		else {
			if (atLeastOne1)
				throw new IllegalStateException("Illeagal arguments. Either all C values must equal 1 or none of them. Operation not allowed \n");

			// 2. Any other combination
			if (allWeightsZero) { // save some calculation time
				resT = 0;
				resC = 0;
			}
			else {					// or use the function
				double numeratorT = 0, denominatorT = 0, numeratorC = 0, denominatorC = 0, mult;
				for (int i = 0; i < arrLength; i++) {
					mult = 1;
					for (int j = 0; j < arrLength; j++) // Count the product for each sum element
						if (j != i) 
							mult *= 1 - args[j].getC();	
					numeratorT += weights[i] * args[i].getT() * args[i].getC() * mult;
					denominatorT += weights[i] * args[i].getC() * mult;
					denominatorC += weights[i] * mult;
				}
				numeratorC = denominatorT;
				resC = (numeratorC/denominatorC) * (1 - doc);
				if (allZero)
					resT = 0.5;
				else 		
					resT = numeratorT/denominatorT;
			}
			
			// Special case for T
			if (allZero)
				resT = 0.5;
		}
		
		// Calculate F
		if (allWeightsZero)
			resF = 0;
		else {
			double numerator = 0, denominator = 0;
			for (int i = 0; i < arrLength; i ++) {
				numerator += weights[i] * args[i].getF();
				denominator += weights[i];
			}
			resF = numerator/denominator;
		}
		return new CertainTrust(resT, resC, resF, args[0].getN(), doc);
	}
	
	/**
	 * Performs weighted fusion for an array of CertainTrust objects in correspondence with 
	 * an array of weights. Returns new CertainTrust object.
	 * Requirements: N values of CertainTrust objects must be equal.
	 * Number of weights should equal the number of CertainTrust objects.
	 * Arrays must be non-empty
	 * Either all of CertainTrust must be of certainty 1 or none of them.
	 * @param args - an array of CertainTrust objects
	 * @param weights - an integer array of corresponding weights
	 * @return - new CertainTrust object
	 */
	public static CertainTrust wFusion(CertainTrust[] args, int[] weights) {
		//arrays should be equal
		if (args.length == weights.length) {
			//and not empty
			if (args.length != 0) {
				boolean equalNs = true;
				int N = args[0].getN();
				for (int i = 1; i < args.length; i++)
					if (N != args[i].getN()) {
						equalNs = false;
						i = args.length;
					}
				//and all N's of TC's must be equal
				if (equalNs) {
					return internalFusion(args, weights, 0);
				}
				throw new IllegalStateException("Different N values. Operation not allowed. \n");
			}
			throw new IllegalStateException("Arrays are empty. Operation not allowed. \n");
		}
		throw new IllegalStateException("Different lengths of arrays. Operation not allowed. \n");
	}
	
	/**
	 * Conflicted Fusion is a variation of weighted fusion, which additionally computes the degree of conflict 
	 * between given opinions (CertainTrust objects) and takes it into consideration while performing fusion.
	 * The degree of conflict is then saved in the resulting CertainTrust object and may be checked with getDoC() function.
	 * @param args - an array of CertainTrust objects
	 * @param weights - an integer array of corresponding weights
	 * @return - new CertainTrust object
	 */
	public static CertainTrust cFusion(CertainTrust[] args, int[] weights) {
		//arrays should be equal
		if (args.length == weights.length) {
			//and not empty
			if (args.length != 0) {
				boolean equalNs = true;
				int N = args[0].getN();
				for (int i = 1; i < args.length; i++)
					if (N != args[i].getN()) {
						equalNs = false;
						i = args.length;
					}
				//and all N's of TC's must be equal
				if (equalNs) {
					double denominator = args.length*(args.length - 1) / 2;
					double numerator = 0;
					for (int i = 0; i < args.length; i++) 
						for (int j = i; j < args.length; j++)
							numerator += Math.abs(args[i].getT() - args[j].getT()) * args[i].getC() * args[j].getC()*(1 - Math.abs((double)(weights[i] - weights[j])/(weights[i] + weights[j])));	
					double doc = numerator/denominator;	
					return internalFusion(args, weights, doc);
				}
				throw new IllegalStateException("Different N values. Operation not allowed. \n");
			}
			throw new IllegalStateException("Arrays are empty. Operation not allowed. \n");
		}
		throw new IllegalStateException("Different lengths of arrays. Operation not allowed. \n");
	}
	
	/**
	 * Computes CONSENSUS function for this CertainTrust object and the specified argument. Result is returned as a new object,
	 * argument and this CertainTrust object remain unchanged.
	 * N values of both objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param arg - CertainTrust object
	 * @return - result of CONSENSUS computation for this object and an argument.
	 */
	private CertainTrust CONSENSUS(CertainTrust arg){
		double c1 = getC();
		double f1 = getF();
		double t1 = getT();
		
		double c2 = arg.getC();
		double f2 = arg.getF();
		double t2 = arg.getT();
		
		double resC = 0, resT = 0.5, resF = 0.5;
		
		if (n != arg.getN()) 
			throw new IllegalStateException("Different N values. Operation not allowed. \n");
		
		//resF = f1*f2;
		
		if (almostEqual(resF, 1))		//avoid division by 0
			resC = (c1+c2-2*c1*c2)/(1-c1*c2);
		else
			resC = (c1+c2-2*c1*c2)/(1-c1*c2);
		
		if (almostEqual(resC, 0)) 
			resT = 0.5;
		else if (almostEqual(resF, 1))	//avoid division by 0
			resT = (c1*t1*(1-c2)+c2*t2*(1-c1))/(c1*(1-c2)+c2*(1-c1));
		else resT = (c1*t1*(1-c2)+c2*t2*(1-c1))/(c1*(1-c2)+c2*(1-c1));
		
		resT = adjustValue(resT);
		resC = adjustValue(resC);
		resF = adjustValue(resF);
		
		CertainTrust result = new CertainTrust(resT, resC, resF, n, 0);
			
		return result;
	}
	
	/**
	 * Computes CONSENSUS function for this CertainTrust object and the specified arguments. 
	 * Result is returned as a new object, arguments and this CertainTrust object remain unchanged.
	 * Example: a.CONSENSUS(b, c, d) returns new CertainTrust object that equals a CONSENSUS b CONSENSUS c CONSENSUS d.
	 * Multiple arguments allowed, but not less than one.
	 * N values of all objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param args - arguments
	 * @return - result of CONSENSUS computation for this object and all arguments.
	 */
	public CertainTrust CONSENSUS(CertainTrust ...args) {
		CertainTrust result = clone();
		for (CertainTrust m: args) {
			if (n != m.getN())
				throw new IllegalStateException("Different N values. Operation not allowed. \n");
			result = result.CONSENSUS(m);
		}
			
		return result;
	}

	/**
	 * Computes DISCOUNTING function for this CertainTrust object and the specified argument. Result is returned as a new object,
	 * argument and this CertainTrust object remain unchanged.
	 * N values of both objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param arg - CertainTrust object
	 * @return - result of DISCOUNTING computation for this object and an argument.
	 */
	private CertainTrust DISCOUNTING(CertainTrust arg){
		double c1 = getC();
		double f1 = getF();
		double t1 = getT();
		
		double c2 = arg.getC();
		double f2 = arg.getF();
		double t2 = arg.getT();
		
		double resC = 0, resT = 0.5, resF = 0.5;
		
		if (n != arg.getN()) 
			throw new IllegalStateException("Different N values. Operation not allowed. \n");
		
		//resF = f1*f2;
		
		if (almostEqual(resF, 1))	
			resC = t1*c1*c2;
		else
			resC = t1*c1*c2;
		
		if (almostEqual(resC, 0)) 
			resT = 0.5;
		else if (almostEqual(resF, 1))	
			resT = t2;
		else resT = t2;
		
		resT = adjustValue(resT);
		resC = adjustValue(resC);
		resF = adjustValue(resF);
		
		CertainTrust result = new CertainTrust(resT, resC, resF, n, 0);
			
		return result;
	}
	
	/**
	 * Computes DISCOUNTING function for this CertainTrust object and the specified arguments. 
	 * Result is returned as a new object, arguments and this CertainTrust object remain unchanged.
	 * Example: a.DISCOUNTING(b, c, d) returns new CertainTrust object that equals a DISCOUNTING b DISCOUNTING c DISCOUNTING d.
	 * Multiple arguments allowed, but not less than one.
	 * N values of all objects should be equal.
	 * For detailed information see CertainLogic: A Logic for Modeling Trust and Uncertainty
	 * @param args - arguments
	 * @return - result of DISCOUNTING computation for this object and all arguments.
	 */
	public CertainTrust DISCOUNTING(CertainTrust ...args) {
		CertainTrust result = clone();
		for (CertainTrust m: args) {
			if (n != m.getN())
				throw new IllegalStateException("Different N values. Operation not allowed. \n");
			result = result.DISCOUNTING(m);
		}
			
		return result;
	}
	
	
//=========== Additional Functions =================	
	
	@Override
	public CertainTrust clone() {
		CertainTrust copy = new CertainTrust(n);
		copy.c = this.c;
		copy.t = this.t;
		copy.f = this.f;
		copy.r = this.r;
		copy.s = this.s;
		copy.doc = this.doc;
		return copy;
	}
	
	/**
	 * Adjusts the value of a into the allowed range [0,1]. 
	 * @param a - value to be adjusted
	 */
	private double adjustValue(double a) {
		return Math.max(Math.min(a, 1), 0);
	}
	
	/**
	 * compares two double values using an epsilon
	 * @param value - given value
	 * @param target - expected value
	 * @return
	 */
	private boolean almostEqual(double value, double target) {
		return Math.abs(value - target) < 1E-10;
	}

}




