package bin;

import java.util.ArrayList;

import fileIO.ByteStreamWriter;
import ml.CellNet;
import shared.Tools;
import structures.ByteBuilder;
import structures.FloatList;
import tax.TaxTree;

public class Oracle implements Cloneable {

	public Oracle(float maxGCDif_, float maxDepthRatio_, float maxKmerDif_, 
			float maxProduct_, float maxCovariance_, float minKmerProb_, int minEdgeWeight_) {
		maxGCDif0=maxGCDif_;
		maxDepthRatio0=maxDepthRatio_;
		maxKmerDif0=maxKmerDif_;
		maxProduct0=maxProduct_;
		maxCovariance0=maxCovariance_;
		minEdgeWeight=minEdgeWeight_;
		stringency0=1;
		minKmerProb0=minKmerProb_;
		if(BinObject.net0!=null) {network=BinObject.net0.copy(false);}
	}
	
	public Oracle(float stringency, int minEdgeWeight_) {
		maxKmerDif0=Binner.maxKmerDif2*stringency;
		maxDepthRatio0=1+((Binner.maxDepthRatio2-1)*stringency);
		maxGCDif0=Binner.maxGCDif2*stringency;
		maxProduct0=maxKmerDif0*maxDepthRatio0*Binner.productMult;
		maxCovariance0=Binner.maxCovariance2*stringency;
		minKmerProb0=Binner.minKmerProb2;
		minEdgeWeight=minEdgeWeight_;
		stringency0=stringency;
		if(BinObject.net0!=null) {network=BinObject.net0.copy(false);}
	}
	
	void clear() {
		best=null;
		bestIdx=-1;
		score=topScore=-1;
	}
	
	/** Higher is more similar */
	static final float similarity(float ratio_, float gcDif_, 
			float simDif_, float covariance_, float kmerProb_, long edges_) {
		final float ratio=ratio_;
		final float gcDif=gcDif_+1f;
		final float simDif=simDif_*0.5f+1f;
		final float covariance=1+covariance_*32;
		float product=simDif*ratio*gcDif*covariance/kmerProb_;
		if(BinObject.verbose) {
			System.err.println(product+"="+simDif+"*"+ratio+"*"+gcDif+"*"+covariance+"/"+kmerProb_);
		}
		return 1f/product;
	}
	
	public final float similarity(Bin a, Bin b, float stringency0) {
		long size=Tools.min(a.size(), b.size());
		float sizeMult=Binner.sizeAdjustMult(size);
		float stringency=stringency0*sizeMult;
		
		float maxKmerDif=maxKmerDif0*stringency;
		float maxDepthRatio=1+((maxDepthRatio0-1)*stringency);
		float maxGCDif=maxGCDif0*stringency;
		float maxProduct=maxKmerDif*maxDepthRatio*Binner.productMult;
		maxProduct=Math.max(maxProduct, maxProduct0*sizeMult);
		float maxCovariance=maxCovariance0*stringency;
		float minProb=1-stringency0*(1-minKmerProb0);
		score=similarity(a, b, maxGCDif, maxDepthRatio, maxKmerDif, maxProduct, maxCovariance, minProb);
		return score;
	}
	
	public static float edgeMult(long e1, long e2, long eT, float d1, float d2) {
		long minEdges=Tools.min(e1, e2);
		if(minEdges<Binner.minEdgeWeight) {return eT<Binner.minEdgeWeight ? 1f : Binner.goodTransEdgeMult;}
		long maxEdges=Tools.max(e1, e2);
		float depth=0.5f*(d1+d2);
		if(minEdges>Binner.lowDepthEdgeRatio*depth && maxEdges<Binner.highDepthEdgeRatio*depth) {
			return Binner.goodEdgeMult;
		}
		return eT<Binner.minEdgeWeight ? 1f : Binner.goodTransEdgeMult;
	}
	
	/** Higher is more similar */
	public final float similarity(Bin a, Bin b, float maxGCDif, float maxDepthRatio, 
			float maxKmerDif, float maxProduct, float maxCovariance, float minKmerProb) {
		if(bsw!=null) {return similarityB(a, b, maxGCDif, maxDepthRatio, maxKmerDif, maxProduct, maxCovariance);}
		fastComparisons++;
		
		if(BinObject.verbose) {
			System.err.println("Comparing to "+b.id()+": "+
					"maxKmerDif="+maxKmerDif+", maxDepthRatio="+maxDepthRatio+
					", maxProduct="+maxProduct+", maxGCDif="+maxGCDif+
					", maxCovariance="+maxCovariance);
		}
		if(Binner.PERFECT_ORACLE) {
			if(a.labelTaxid<1 || b.labelTaxid<1 || a.labelTaxid!=b.labelTaxid) {return -1;}
			return 1-1f/b.size();
		}
		
		final float gcDif=Math.abs(a.gc()-b.gc());
		final float depthRatio=a.depthRatio(b);
		final long minlen=Math.min(a.size(), b.size());
		if(gcDif>maxGCDif*Binner.goodEdgeMult || 
				depthRatio>maxDepthRatio*Binner.goodEdgeMult) {
			return -1;
		}//Early exit before edge-tracking
		
		final long edges1=useEdges ? a.countEdgesTo(b) : 0;
		final long edges2=useEdges ? b.countEdgesTo(a) : 0;
		final long minEdges=Tools.min(edges1, edges2);
		if(BinObject.verbose) {System.err.println("A: "+edges1+", "+edges2+", "+minEdgeWeight);}
		if(minEdges<minEdgeWeight) {return -1;}
		final long edgesT=(minEdges>=Binner.minEdgeWeight ? minEdges : a.transEdgesTo(b));
		final float mult=edgeMult(edges1, edges2, edgesT, a.depthTotal(), b.depthTotal());
		
		if(BinObject.verbose) {System.err.println("B: "+gcDif+", "+(maxGCDif*mult));}
		if(gcDif>maxGCDif*mult) {return -1;}
		float covariance=a.covariance(b);
		if(covariance<0 || !Float.isFinite(covariance)) {covariance=0;}
		if(BinObject.verbose) {
			System.err.println("C: "+depthRatio+", "+(maxDepthRatio*mult)+
					", "+covariance+", "+(maxCovariance*mult));
		}
		if(depthRatio>maxDepthRatio*mult || covariance>maxCovariance*mult) {return -1;}
		if(!taxaOK(a.taxid(), b.taxid())) {return -1;}
		
		slowComparisons++;
		float kmerDif=SimilarityMeasures.calculateDifferenceAverage(a.counts, b.counts);
		if(kmerDif<0 || !Float.isFinite(kmerDif)) {kmerDif=0;}
		final float product=kmerDif*depthRatio;
		float kmerProb=KmerProb.prob(minlen, kmerDif);
		kmerProb=1-(1-kmerProb)/mult;
		if(BinObject.verbose) {
			System.err.println("D: "+kmerDif+", "+(maxKmerDif*mult)+
					", "+product+", "+(maxProduct*mult));
		}
		if(kmerDif>maxKmerDif*mult || product>maxProduct*mult || kmerProb<minKmerProb) {return -1;}
		float similarity=similarity(depthRatio, gcDif, kmerDif, covariance, kmerProb, Tools.min(edges1, edges2));
		if(network!=null) {
			if(vector==null) {vector=new FloatList();}
			toVector(a, b, vector, false);
			network.applyInput(vector);
			float result=network.feedForward();
			if(result<network.cutoff && minlen>=Binner.minNetSize) {return -1;}
			return result;
		}
		return similarity;
	}
	
	/** Higher is more similar */
	public final float similarityB(Bin a, Bin b, float maxGCDif, float maxDepthRatio, 
			float maxKmerDif, float maxProduct, float maxCovariance) {
		fastComparisons++;
		
		boolean flag=false;
		
		if(BinObject.verbose) {
			System.err.println("Comparing to "+b.id()+": "+
					"maxKmerDif="+maxKmerDif+", maxDepthRatio="+maxDepthRatio+
					", maxProduct="+maxProduct+", maxGCDif="+maxGCDif+
					", maxCovariance="+maxCovariance);
		}
		long edges1=useEdges ? a.countEdgesTo(b) : 0;
		long edges2=useEdges ? b.countEdgesTo(a) : 0;
		long minEdges=Tools.min(edges1, edges2);
		if(BinObject.verbose) {System.err.println("A: "+edges1+", "+edges2+", "+minEdgeWeight);}
		if(minEdges<minEdgeWeight) {flag=true;}
		long edgesT=(minEdges>=Binner.minEdgeWeight ? minEdges : a.transEdgesTo(b));
		float mult=edgeMult(edges1, edges2, edgesT, a.depthTotal(), b.depthTotal());
		long minlen=Math.min(a.size(), b.size());
		
		float gcDif=Math.abs(a.gc()-b.gc());
		if(BinObject.verbose) {System.err.println("B: "+gcDif+", "+(maxGCDif*mult));}
		if(gcDif>maxGCDif*mult) {flag=true;}
		final float depthRatio=a.depthRatio(b);
		float covariance=a.covariance(b);

		if(covariance<0 || !Float.isFinite(covariance)) {covariance=0;}
		if(BinObject.verbose) {
			System.err.println("C: "+depthRatio+", "+(maxDepthRatio*mult)+
					", "+covariance+", "+(maxCovariance*mult));
		}
		if(depthRatio>maxDepthRatio*mult || covariance>maxCovariance*mult) {flag=true;}
		if(!taxaOK(a.taxid(), b.taxid())) {flag=true;}
		
		slowComparisons++;
		float kmerDif=SimilarityMeasures.calculateDifferenceAverage(a.counts, b.counts);
		if(kmerDif<0 || !Float.isFinite(kmerDif)) {kmerDif=0;}
		final float product=kmerDif*depthRatio;
		float kmerProb=KmerProb.prob(minlen, kmerDif);
		kmerProb=1-(1-kmerProb)/mult;
		if(BinObject.verbose) {
			System.err.println("D: "+kmerDif+", "+(maxKmerDif*mult)+
					", "+product+", "+(maxProduct*mult));
		}
		if(kmerDif>maxKmerDif*mult || product>maxProduct*mult) {flag=true;}
		float f=similarity(depthRatio, gcDif, kmerDif, covariance, kmerProb, Tools.min(edges1, edges2));
		
		if(bsw!=null && a.labelTaxid>0 && b.labelTaxid>0) {
			float minSize=Tools.min(a.size(),  b.size());
			float prob=(minSize/4000f)*(minSize/2000f);
			if(a.pure() && b.pure() && Math.random()<=prob) {
				if(vector==null) {vector=new FloatList();}
				toVector(a, b, vector, true);
				ByteBuilder bb=new ByteBuilder();
				for(int i=0; i<vector.size(); i++) {
					if(i>0) {bb.tab();}
					bb.append(vector.get(i), 7, true);
				}
				synchronized(bsw) {
					bsw.print(bb.nl());
				}
			}
		}
		
		return flag ? -1 : f;
	}
	
	static String header() {
		ByteBuilder bb=new ByteBuilder();
//		bb.append('#').append("aSize").tab().append("bSize");//0 1
//		bb.tab().append("aGC").tab().append("bGC");//2 3
//		bb.tab().append("gcDif");//4
//		bb.tab().append("depthRatio").tab().append("covariance");//5 6
//		bb.tab().append("aDepth").tab().append("bDepth");//7 8
//		bb.tab().append("numDepth").tab().append("kmerDif");//9 10
//		bb.tab().append("aEntrop").tab().append("bEntrop");//11 12
//		bb.tab().append("entDif");//13
//		bb.tab().append("aSpec").tab().append("bSpec");//14 15
//		bb.tab().append("specDif");//16
//		bb.tab().append("aContigs").tab().append("bContigs");//17 18
//		bb.tab().append("aEdgeW").tab().append("bEdgeW");//19 20
//		bb.tab().append("aEdges").tab().append("bEdges");//21 22
//		bb.tab().append("similarity");//23
//		bb.tab().append("sameTax");//24
		

		bb.append('#').append("aSize");//0
		bb.tab().append("bGC");//1
		bb.tab().append("gcDif");//2
		bb.tab().append("depthRatio").tab().append("covariance");//3 4
		bb.tab().append("bDepth");//5
		bb.tab().append("numDepth").tab().append("kmerDif");//6 7
		bb.tab().append("bEntrop");//8
		bb.tab().append("entDif");//9
		bb.tab().append("bSpec");//10
		bb.tab().append("specDif");//11
		bb.tab().append("minEdge");//12
		bb.tab().append("similarity");//13
		bb.tab().append("sameTax");//14
		
		return bb.toString();
	}
	
	FloatList toVector(Bin a, Bin b, FloatList list, boolean includeAnswer) {
		if(a.size()>b.size()) {return toVector(b, a, list, includeAnswer);}
		if(list==null) {list=new FloatList();}
		list.clear();
		long edges1=a.countEdgesTo(b);
		long edges2=b.countEdgesTo(a);
		long minEdges=Tools.min(edges1, edges2);
		float depth=0.5f*(a.depthTotal()+b.depthTotal()+0.5f);
		float gcDif=Math.abs(a.gc()-b.gc());
		float depthRatio=a.depthRatio(b);
		float covariance=a.covariance(b);
		float kmerDif=SimilarityMeasures.calculateDifferenceAverage(a.counts, b.counts);
		
		if(covariance<0 || !Float.isFinite(covariance)) {covariance=0;}
		if(kmerDif<0 || !Float.isFinite(kmerDif)) {kmerDif=0;}
		long minlen=Math.min(a.size(), b.size());
		final float kmerProb=KmerProb.prob(minlen, kmerDif);
		float similarity=similarity(depthRatio, gcDif, kmerDif, covariance, kmerProb, Tools.min(edges1, edges2));
		
//		list.add(a.size());//-1
		list.add(0.1f*(float)Tools.log2(a.size()));//0
//		list.add(0.1f*(float)Tools.log2(b.size()));
//		list.add(a.gc());
		list.add(b.gc());//1
		list.add(gcDif);//2
		list.add(kmerDif<0 || !Float.isFinite(kmerDif) ? 0 : kmerDif);//3
		list.add((float)Tools.log2(1+minEdges/depth));//4
		list.add((float)(0.25f*Tools.log2(depthRatio)));//5
		list.add(covariance<0 || !Float.isFinite(covariance) ? 0 : (float)Math.sqrt(covariance));//6
//		list.add((float)(0.1f*Tools.log2(1+a.depthTotal())));
		list.add((float)(0.1f*Tools.log2(1+b.depthTotal())));//7
		list.add(a.numDepths()>1 ? 1 : 0);//8
		list.add(a.numDepths()>2 ? 1 : 0);
		list.add(a.numDepths()>3 ? 1 : 0);
		list.add(a.numDepths()>4 ? (0.2f*(a.numDepths()-4)) : 0);
//		list.add(a.entropy);
//		list.add(b.entropy);
//		list.add(Tools.absdif(a.entropy, b.entropy));
//		list.add(a.strandedness-1);
//		list.add(b.strandedness-1);
//		list.add(Tools.absdif(a.strandedness, b.strandedness));
//		list.add((float)(0.5f*Tools.log2(a.numContigs())));
//		list.add((float)(0.5f*Tools.log2(b.numContigs())));
//		list.add(edges1/depth);
//		list.add(edges2/depth);
//		list.add(0.1f*a.numEdges()/(float)(Tools.max(b.numContigs(), 1)));
//		list.add(0.1f*b.numEdges()/(float)(Tools.max(b.numContigs(), 1)));
		list.add(1-similarity);//12
		if(includeAnswer) {
			assert(a.labelTaxid>0 && b.labelTaxid>0) : a.labelTaxid+", "+b.labelTaxid+", "+a.name();
			list.add(a.labelTaxid==b.labelTaxid ? 1 : 0);
		}
		list.shrink();
		for(float f : list.array) {
			assert(Float.isFinite(f)) : list;
		}
		return list;
	}
	
	private boolean taxaOK(int aTaxid, int bTaxid) {
		if(!allowHalfTaxID && (aTaxid<1 || bTaxid<1)) {return false;}
		if(!allowNoTaxID && aTaxid<1 && bTaxid<1) {return false;}
		if(taxlevel<0 || BinObject.tree==null || aTaxid==bTaxid || aTaxid<1 || bTaxid<1) {return true;}
		int commonAncestorLevel=BinObject.tree.commonAncestorLevel(aTaxid, bTaxid);
		return (commonAncestorLevel<=taxlevel);
	}
	
	
	protected Oracle clone() {
		try {
			Oracle clone=(Oracle) super.clone();
			clone.best=null;
			clone.vector=null;
			clone.network=(network==null ? null : network.copy(false));
			return clone;
		} catch (CloneNotSupportedException e) {
			throw new RuntimeException(e);
		}
	}
	
	Bin best=null;
	float score=-1;
	float topScore=-1;
	int bestIdx=-1;
	
	long fastComparisons=0;
	long slowComparisons=0;
	
	final float maxKmerDif0;
	final float maxDepthRatio0;
	final float maxGCDif0;
	final float maxProduct0;
	final float maxCovariance0;
	final float minKmerProb0;
	final int minEdgeWeight;
	
	final float stringency0;
	private FloatList vector;
	private CellNet network;
	
	int taxlevel=TaxTree.SPECIES;
	boolean allowNoTaxID=true;
	boolean allowHalfTaxID=true;
	boolean useEdges=true;
	
	static ByteStreamWriter bsw;
	
}
