package assemble;

import java.util.ArrayList;
import java.util.HashMap;

import dna.AminoAcid;
import shared.Tools;
import structures.ByteBuilder;
import ukmer.Kmer;

/**
 * Contig generated by Tadpole.
 * @author Brian Bushnell
 * @date July 12, 2018
 *
 */
public class Contig {
	
	public Contig(byte[] bases_){
		this(bases_, null, 0);
	}
	
	public Contig(byte[] bases_, int id_){
		this(bases_, null, id_);
	}
	
	public Contig(byte[] bases_, String name_, int id_){
		bases=bases_;
		name=name_;
		id=id_;
	}
	
	public ByteBuilder toFasta(int wrap, ByteBuilder sb){
		if(wrap<1){wrap=Integer.MAX_VALUE;}
		sb.append('>');
		toHeader(sb);
		if(bases!=null){
			int pos=0;
			while(pos<bases.length-wrap){
				sb.append('\n');
				sb.append(bases, pos, wrap);
				pos+=wrap;
			}
			if(pos<bases.length){
				sb.append('\n');
				sb.append(bases, pos, bases.length-pos);
			}
		}
		return sb;
	}
	
	@Override
	public String toString() {
		return name2();
	}

	public String name() {
		return toHeader(new ByteBuilder()).toString();
	}

	public String name2() {//Extra info
		ByteBuilder bb=toHeader(new ByteBuilder());
		bb.append(" flipped="+flipped+", used="+used+", associate="+associate);
		return bb.toString();
	}
	
	private ByteBuilder toHeader(ByteBuilder bb){
		if(name!=null){return bb.append(name);}
		bb.append("contig_").append(id);
		bb.append(",length=").append(length());
		bb.append(",cov=").append(coverage, 1);
		bb.append(",min=").append(minCov);
		bb.append(",max=").append(maxCov);
		bb.append(",gc=").append(gc(), 3);
		
		bb.append(",left=").append(Tadpole.codeStrings[leftCode]);
		if(leftBranch()){
			bb.append('_').append(leftRatio, 1);
		}
		bb.append(",right=").append(Tadpole.codeStrings[rightCode]);
		if(rightBranch()){
			bb.append('_').append(rightRatio, 1);
		}
		if(leftEdges!=null){
			bb.append(' ').append("leftEdges=");
			appendEdges(leftEdges, bb);
		}
		if(rightEdges!=null){
			bb.append(' ').append("rightEdges=");
			appendEdges(rightEdges, bb);
		}
		return bb;
	}
	
	int appendEdges(ArrayList<Edge> edges, ByteBuilder bb){
		int added=0;
		if(edges==null || edges.isEmpty()){return 0;}
		
//		bb.append('_').append('[');
		for(int i=0; i<edges.size(); i++){
			Edge e=edges.get(i);
			if(e!=null){
				if(added>0){bb.append(';');}
				e.appendTo(bb);
				added++;
			}
		}
//		bb.append(']');
		return added;
	}

	/**
	 * @return GC fraction
	 */
	public float gc() {
		if(bases==null || bases.length<1){return 0;}
		int at=0, gc=0;
		for(byte b : bases){
			int x=AminoAcid.baseToNumber[b];
			if(x>-1){
				if(x==0 || x==3){at++;}
				else{gc++;}
			}
		}
		if(gc<1){return 0;}
		return gc*1f/(at+gc);
	}
	
	public int length(){return bases.length;}
	
	long leftKmer(int k){
		assert(k<32);
		long kmer=0;
		for(int i=0; i<k; i++){
			byte b=bases[i];
			byte x=AminoAcid.baseToNumber[b];
			assert(x>=0);
			kmer=(kmer<<2)|x;
		}
		return kmer;
	}
	
	long rightKmer(int k){
		assert(k<32);
		long kmer=0;
		for(int i=bases.length-k; i<bases.length; i++){
			byte b=bases[i];
			byte x=AminoAcid.baseToNumber[b];
			assert(x>=0);
			kmer=(kmer<<2)|x;
		}
		return kmer;
	}
	
	Kmer leftKmer(Kmer kmer){
		kmer.clearFast();
		for(int i=0; i<kmer.kbig; i++){
			kmer.addRight(bases[i]);
		}
		return kmer;
	}
	
	Kmer rightKmer(Kmer kmer){
		kmer.clearFast();
		for(int i=bases.length-kmer.kbig; i<bases.length; i++){
			kmer.addRight(bases[i]);
		}
		return kmer;
	}
	
	boolean canonical(){
		for(int i=0, j=bases.length-1; i<bases.length; i++, j--){
			final byte a=AminoAcid.baseToComplementExtended[i], b=bases[j];
			if(a<b){return true;}
			else if(b<a){return false;}
		}
		return true;
//		final long leftKmer=leftKmer(k);
//		final long rightKmer=rightKmer(k);
//		final long leftRKmer=AminoAcid.reverseComplementBinaryFast(leftKmer, k);
//		assert((rightKmer!=leftKmer && rightKmer!=leftRKmer) || length()==k /* Palindrome case */);
//		return rightKmer>=leftRKmer;
	}
	
	void rcomp(){
		AminoAcid.reverseComplementBasesInPlace(bases);
		{
			int temp=leftCode;
			leftCode=rightCode;
			rightCode=temp;
		}
		{
			float temp=leftRatio;
			leftRatio=rightRatio;
			rightRatio=temp;
		}
		{
			ArrayList<Edge> temp=leftEdges;
			leftEdges=rightEdges;
			rightEdges=temp;
		}
	}
	
	boolean leftBranch(){return Tadpole.isBranchCode(leftCode);}
	boolean rightBranch(){return Tadpole.isBranchCode(rightCode);}
	
	boolean leftForwardBranch(){return leftCode==Tadpole.F_BRANCH;}
	boolean rightForwardBranch(){return rightCode==Tadpole.F_BRANCH;}
	
	boolean leftBackwardBranch(){return leftCode==Tadpole.B_BRANCH;}
	boolean rightBackwardBranch(){return rightCode==Tadpole.B_BRANCH;}
	
	final int leftEdgeCount(){
		return leftEdges==null ? 0 : leftEdges.size();
	}
	
	final int rightEdgeCount(){
		return rightEdges==null ? 0 : rightEdges.size();
	}
	
	public void addLeftEdge(Edge e) {
		assert(!hasLeftEdge(e.destination, e.bases, e.orientation)) : e+"\n"+leftEdges;
		Edge old=getLeftEdge(e.destination, e.orientation);
		if(old!=null){
			if(e.depth>=old.depth && (old.depth==1 || old.length==e.length)){//Merge edges
				old.bases=e.bases;
				old.length=e.length;
				old.depth+=e.depth;
				return;
			}
		}
		if(leftEdges==null){leftEdges=new ArrayList<Edge>(4);}
		leftEdges.add(e);
	}
	
	public void addRightEdge(Edge e) {
		assert(!hasRightEdge(e.destination, e.bases, e.orientation)) : e+"\n"+rightEdges;
		Edge old=getRightEdge(e.destination, e.orientation);
		if(old!=null){
			if(e.depth>=old.depth && (old.depth==1 || old.length==e.length)){//Merge edges
				old.bases=e.bases;
				old.length=e.length;
				old.depth+=e.depth;
				return;
			}
		}
		if(rightEdges==null){rightEdges=new ArrayList<Edge>(4);}
		rightEdges.add(e);
	}
	
//	public boolean hasLeftEdge(int dest) {
//		return hasLeftEdge(dest, null);
//	}
//	
//	public boolean hasRightEdge(int dest) {
//		return hasRightEdge(dest, null);
//	}

	public void removeLeftEdge(int dest, boolean destRight){leftEdges=removeEdge(dest, destRight, leftEdges);}
	public void removeRightEdge(int dest, boolean destRight){rightEdges=removeEdge(dest, destRight, rightEdges);}
	
	public static ArrayList<Edge> removeEdge(int dest, boolean destRight, ArrayList<Edge> edges){
		if(edges==null){return null;}
		int removed=0;
		for(int i=0; i<edges.size(); i++){
			Edge e=edges.get(i);
			assert(e!=null);
			if(e.destination==dest && e.destRight()==destRight){
				edges.set(i, null);
				removed++;
			}
		}
		if(removed>0){Tools.condenseStrict(edges);}
		return (edges.isEmpty() ? null : edges);
	}
	
	public Edge getLeftEdge(int dest, int orientation){
		if(leftEdges==null){return null;}
		for(Edge e : leftEdges){
			if(e.destination==dest && (orientation<0 || orientation==e.orientation)){
				return e;
			}
		}
		return null;
	}
	
	public Edge getRightEdge(int dest, int orientation){
		if(rightEdges==null){return null;}
		for(Edge e : rightEdges){
			if(e.destination==dest && (orientation<0 || orientation==e.orientation)){
				return e;
			}
		}
		return null;
	}
	
	public boolean hasLeftEdge(int dest, byte[] path, int orientation) {
		if(leftEdges!=null){
			for(Edge e : leftEdges){
				if(e.destination==dest && e.orientation==orientation){
					if(path==null || Tools.equals(path, e.bases)){
						return true;
					}
				}
			}
		}
		return false;
	}
	
	public boolean hasRightEdge(int dest, byte[] path, int orientation) {
		if(rightEdges!=null){
			for(Edge e : rightEdges){
				if(e.destination==dest && e.orientation==orientation){
					if(path==null || Tools.equals(path, e.bases)){
						return true;
					}
				}
			}
		}
		return false;
	}
	
	public final boolean flipped(){return flipped;}
	
	final void flip(ArrayList<Edge> inbound){
		if(Tadpole.verbose){System.err.println("Flipping contig "+name());}
		flipped=!flipped;
		AminoAcid.reverseComplementBasesInPlace(bases);
		{
			int temp=leftCode;
			leftCode=rightCode;
			rightCode=temp;
		}
		{
			float temp=leftRatio;
			leftRatio=rightRatio;
			rightRatio=temp;
		}
		{
			ArrayList<Edge> temp=leftEdges;
			leftEdges=rightEdges;
			rightEdges=temp;
			if(leftEdges!=null){
				for(Edge e : leftEdges){
					e.flipSource();
				}
			}
			if(rightEdges!=null){
				for(Edge e : rightEdges){
					e.flipSource();
				}
			}
		}
		if(inbound!=null){
			for(Edge e : inbound){
				assert(e.destination==id) : id+", "+e.destination+"\n"+e+"\n"+name()+"\n";
				e.flipDest();
			}
		}
		if(Tadpole.verbose){System.err.println("Flipped contig "+name());}
	}
	
	public final boolean used(){return used;}
	
	final void setUsed(HashMap<Integer, ArrayList<Edge>> map, ArrayList<Contig> allContigs){setUsed(map.get(id), allContigs);}
	
	final void setUsed(ArrayList<Edge> inbound, ArrayList<Contig> allContigs){
		assert(!used);
		assert(!associate);
		used=true;
		removeAllEdges(inbound, allContigs);
	}
	
	public final boolean associate(){return associate;}
	
	final void setAssociate(HashMap<Integer, ArrayList<Edge>> map, ArrayList<Contig> allContigs){setAssociate(map.get(id), allContigs);}
	
	final void setAssociate(ArrayList<Edge> inbound, ArrayList<Contig> allContigs){
		assert(!used);
		assert(!associate);
		associate=true;
		removeAllEdges(inbound, allContigs);
	}
	
	void removeAllEdges(ArrayList<Edge> inbound, ArrayList<Contig> allContigs){
		leftEdges=null;
		rightEdges=null;
		
		if(inbound!=null){
			for(Edge e : inbound){
				if(e.destination==id){
					Contig source=allContigs.get(e.origin);
					if(source!=this && !source.used() && !source.associate()){
						source.removeLeftEdgesTo(id);
						source.removeRightEdgesTo(id);
					}
				}
			}
		}
	}
	
	private void removeLeftEdgesTo(int destNode) {
		leftEdges=removeEdgesTo(leftEdges, destNode);
	}
	
	private void removeRightEdgesTo(int destNode) {
		rightEdges=removeEdgesTo(rightEdges, destNode);
	}
	
	private static ArrayList<Edge> removeEdgesTo(ArrayList<Edge> edges, final int destNode) {
		if(edges==null || edges.isEmpty()){return null;}
		int cnt=0;
		for(int i=0; i<edges.size(); i++){
			Edge e=edges.get(i);
			if(e.destination==destNode){
				edges.set(i, null);
				cnt++;
			}
		}
		if(cnt==edges.size()){return null;}
		Tools.condenseStrict(edges);
		return edges;
	}

	final void renumber(final int newID, ArrayList<Edge> inbound){
		if(id==newID){return;}
		if(leftEdges!=null){
			for(Edge e : leftEdges){
				assert(e.origin==id);
				e.origin=newID;
			}
		}
		if(rightEdges!=null){
			for(Edge e : rightEdges){
				assert(e.origin==id);
				e.origin=newID;
			}
		}
		if(inbound!=null){
			for(Edge e : inbound){
				assert(e.destination==id);
				e.destination=newID;
			}
		}

		id=newID;
	}
	
	public String name;
	public byte[] bases;
	public float coverage;
	public int minCov;
	public int maxCov;
	int leftCode;
	int rightCode;
	float leftRatio;
	float rightRatio;
	public int id;
	
	private boolean flipped=false;
	private boolean used=false;
	private boolean associate=false;//The unused path in a collapsed bubble
	
//	public Edge[] leftEdges;
//	public Edge[] rightEdges;

	ArrayList<Edge> leftEdges;
	ArrayList<Edge> rightEdges;
}
