package org.fda.alignment;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.fda.data.Enums.CoverageType;
import org.fda.data.Enums.Inconsistency;
import org.fda.data.Reference;
import org.fda.data.Utilities;
import java.io.Serializable;
import java.util.TreeMap;
import org.fda.data.Misassembly;
import org.fda.intervaltree.Interval;
import org.fda.intervaltree.IntervalSearchTree;

/**
 *
 * @author Gokhan.Yavas
 */
public class ScaffoldContig implements Comparable<ScaffoldContig>, Serializable{
    private static final long serialVersionUID = 211241001102182489L;
    
    //private Map<Inconsistency, Integer> inconMap;
    private Map<Inconsistency, List<Misassembly>> missassemblyMap;
    private Map<Reference, List<Alignment>> refAlignmentMap = new HashMap<>();
    private final String id;
    private final int contigSize;
    private int nongapcontigSize;
    private IntervalSearchTree gapIntervalTree;
    private int ncount;
    private int gccount;
    private double nratio;
    private double gcratio;    
    private int alignableSizeOnContig=0;
    private int coveredSizeOnReference=0;
    private double contigSizeAligPerc=0;
    private CoverageType coverageType=CoverageType.NONE;    
    private double quality=0;
    private Reference mostlyAlignedRef=null;
    private byte[] hashValue=null;
    private byte[] seqHashValue=null;
    private boolean isMisassambled=false;
    private int totalNumberofIncons=0;
    private boolean containsScaffoldingGap=false;


    public ScaffoldContig(String contigID,int contigSize, int ncount, int gccount, IntervalSearchTree ist){
        this(contigID, contigSize);
        setGCCount(gccount);
        setNCount(ncount);
        this.gapIntervalTree = ist;

    }
    public ScaffoldContig(String contigID,int contigSize){
        this.id = contigID;
        this.contigSize = contigSize;
        this.nongapcontigSize = contigSize;
        
        init();        
    }
    public boolean containsScaffoldingGap() {
        return containsScaffoldingGap;
    }       
    public void setGapIntervalTree(IntervalSearchTree ist){
        this.gapIntervalTree = ist;
    }
    public IntervalSearchTree getGapIntervalTree(){
        return gapIntervalTree;
    }
    public byte[] getSeqHashValue() {
        return seqHashValue;
    }

    public void setSeqHashValue(byte[] seqHashValue) {
        this.seqHashValue = seqHashValue;
    }

    public byte[] getHashValue() {
        return hashValue;
    }

    public void setHashValue(byte[] hashValue) {
        this.hashValue = hashValue;
    }
    
    public Map<Inconsistency, List<Misassembly>> getMisassemblyMap() {
        return missassemblyMap;
    }

    public Map<Reference, List<Alignment>> getRefAlignmentMap() {
        return refAlignmentMap;
    }


    public int getNcount() {
        return ncount;
    }
    public int getGccount() {
        return gccount;
    }
    public double getGcratio() {
        return gcratio;
    }
    public double getContigSizeAligPerc() {
        return contigSizeAligPerc;
    }

    public Reference getMostlyAlignedRef() {
        return mostlyAlignedRef;
    }
    public void setNCount(int n){
        this.ncount =n;
        this.nratio = (double)this.ncount/this.contigSize;
    }
    public void setGCCount(int gc){
        this.gccount= gc;
        this.gcratio = (double)this.gccount /this.contigSize;
    }
    public int getGCCount(){
        return this.gccount;
    }
    public double getNRatio(){
        return this.nratio;
    }
    public double getGCRatio(){
        return this.gcratio;
    }
    
    public int getTotalInconsistencyNumber(){
        return totalNumberofIncons;
    }
    public void setMisassembly(){
        init();
        Alignment a,b;
        List<Alignment> a2ret = getAlignments();
        int i;
        
        for(i=0; i < (a2ret.size()-1); i++){
            a = a2ret.get(i);
            b = a2ret.get(i+1);

            if(a.getReference().equals(b.getReference())){
                if(a.getReferenceInterval().overlap(b.getReferenceInterval())>=Utilities.alignmentDistanceThreshold){
                    //incrementInconsistency(Inconsistency.RELOCATION);
                    addInconsistency(new Misassembly(Inconsistency.RELOCATION, a, b));
                }
                else if(a.getReferenceInterval().getDistance(b.getReferenceInterval()) >= Utilities.alignmentDistanceThreshold){
                    // now check if the gap between the alignments is a scaffolding gap
                    Interval refgapinterval = Interval.getIntervalBetween(a.getReferenceInterval(),b.getReferenceInterval());
                    Interval contiggapinterval = Interval.getIntervalBetween(a.getContigInterval(),b.getContigInterval());
                    
                    if(contiggapinterval!=null)
                    {
                        Reference r = a.getReference();
                        //Interval ref_intersection = r.getGapIntervalTree().search(refgapinterval);
                        List<Interval> ref_intersections = r.getGapIntervalTree().searchAll(refgapinterval);
                        //Interval contig_intersection = getGapIntervalTree().search(contiggapinterval);
                        List<Interval> contig_intersections = getGapIntervalTree().searchAll(contiggapinterval);                        
                        /* This is a scaffolding gap which also exists on reference */                        
//                        if(!ref_intersections.isEmpty() && !contig_intersections.isEmpty() && Interval.MinRO(ref_intersection, refgapinterval) >=0.1
//                                && Interval.MinRO(contig_intersection, contiggapinterval) >=0.1){                        
                        if(!ref_intersections.isEmpty() && !contig_intersections.isEmpty() && Interval.MaxRO(refgapinterval, ref_intersections) >= Utilities.scaffoldinggapoverlapratio
                                && Interval.MaxRO(contiggapinterval, contig_intersections) >= Utilities.scaffoldinggapoverlapratio){                        
                            addInconsistency(new Misassembly(Inconsistency.SCAFFOLD_GAP, a, b));
                            continue;
                        }
                    }
                    addInconsistency(new Misassembly(Inconsistency.RELOCATION, a, b));                    
                }
                else if(a.getContigOrientation()!=b.getContigOrientation()){
                    addInconsistency(new Misassembly(Inconsistency.INVERSION, a, b));
                }
            }
            else{
                addInconsistency(new Misassembly(Inconsistency.TRANSLOCATION, a, b));
            }
        }            
    }
    
    public boolean getIsMisassembled(){
        return isMisassambled;
    }
    public double getContigAlignPerc(){
        return contigSizeAligPerc;
    }    
    public int getCoveredSizeOnReference(){
        return coveredSizeOnReference;
    }
    public int getAlignableSizeOnContig(){
        return alignableSizeOnContig;
    }
    
    public CoverageType getCoverageType(){
        return this.coverageType;
    }
    public void checkAlignmentsForGaps(){
        Alignment a,b;
    
        List<Alignment> a2ret = getAlignments();
        int i;
        
        for(i=0; i < (a2ret.size()-1); i++){
            a = a2ret.get(i);
            b = a2ret.get(i+1);

            if(a.getReference().equals(b.getReference())){
                Interval refgapinterval = Interval.getIntervalBetween(a.getReferenceInterval(),b.getReferenceInterval());
                Interval contiggapinterval = Interval.getIntervalBetween(a.getContigInterval(),b.getContigInterval());
                Reference r = a.getReference();
                if(refgapinterval==null || contiggapinterval==null)
                    continue;
                //Interval ref_intersection = r.getGapIntervalTree().search(refgapinterval);
                List<Interval> ref_intersections = r.getGapIntervalTree().searchAll(refgapinterval);
                //Interval contig_intersection = getGapIntervalTree().search(contiggapinterval);
                List<Interval> contig_intersections = getGapIntervalTree().searchAll(contiggapinterval);
                /* */
                if(!ref_intersections.isEmpty() && !contig_intersections.isEmpty() && Interval.MaxRO(refgapinterval, ref_intersections) >= Utilities.scaffoldinggapoverlapratio
                        && Interval.MaxRO(contiggapinterval, contig_intersections) >= Utilities.scaffoldinggapoverlapratio){                        
                    double sizeratio = (double)refgapinterval.getlength()/contiggapinterval.getlength();
                    if(sizeratio >=(1-Utilities.sizeratiodifference) && sizeratio<=(1+Utilities.sizeratiodifference)){
                        nongapcontigSize=nongapcontigSize- contiggapinterval.getlength();
                    }
                }                
            }            
        }
    }
    public void setCoverage(int asoc, int csor){
        this.alignableSizeOnContig = asoc;
        this.coveredSizeOnReference = csor;
        checkAlignmentsForGaps();
                
        //this.contigSizeAligPerc = 100 * (double)alignableSizeOnContig / (double)contigSize;
        this.contigSizeAligPerc = 100 * (double)alignableSizeOnContig / (double)nongapcontigSize;
        if(this.contigSizeAligPerc==100)
            coverageType = CoverageType.FULL;
        else
            coverageType = CoverageType.PARTIAL;                
    }
    public String getId(){
        return this.id;
    }
    public List<Alignment> getAlignments(){
        List<Alignment> toRet=new ArrayList<>();
        for(List tmp :refAlignmentMap.values())
            toRet.addAll(tmp);
        
        Collections.sort(toRet, new Comparator<Alignment>() {
            @Override
            public int compare(Alignment o1, Alignment o2) {
                return o1.getContSt()-o2.getContSt();
            }
        });

        return toRet;
    }
    public List<Alignment> getAlignments(Reference c){
        return this.refAlignmentMap.get(c);
    }
    private int getSum(List<Alignment> lst){
        int sum=0;
        for(Alignment l: lst)
            sum+=l.getRefAlignmentLength();
        return sum;
        
    }
    private void setMostFavouredReference(){
        Reference mx=null;
        int max=0;

        for(Reference r : refAlignmentMap.keySet()){                        
            if(mx ==null){
                mx = r;
                max = getSum(refAlignmentMap.get(r));
            }
            else {
                int tmp = getSum(refAlignmentMap.get(r));
                if(tmp > max){
                    mx = r;
                    max = tmp;
                }                    
            }            
        }
        this.mostlyAlignedRef = mx;
        
    }
    public void setAlignments(List<Alignment> alignments){
        refAlignmentMap = new  HashMap<>();
        for(Alignment a : alignments){
            addAlignment(a);
        }
        setMostFavouredReference();
    }

    public Set<Reference> getReferences(){
        return refAlignmentMap.keySet();
    }
    public int getContigLength(){
        return this.contigSize;
    }
    private void init(){
        //nongapcontigSize=contigSize;
        totalNumberofIncons=0;
        //inconMap = new HashMap<>();
        missassemblyMap = new TreeMap<>();
        for(Inconsistency i: Inconsistency.values()){
            //inconMap.put(i, 0);
            missassemblyMap.put(i, new ArrayList<>());
        }  
        
    }
    public void addInconsistency(Misassembly m){
        //System.out.println(m);
        if(m.getType() != Inconsistency.SCAFFOLD_GAP){
            totalNumberofIncons++;        
            isMisassambled = true;
        }
        missassemblyMap.get(m.getType()).add(m); 
        containsScaffoldingGap=true;
    }
    
    
    public int getInconsistencyCount(Inconsistency i){
        //return inconMap.get(i);
        return missassemblyMap.get(i).size();
    }
    public void assignQuality(){
        double alignmentRatio = contigSizeAligPerc/100;
        if(alignmentRatio==0)
            quality=0;
        else{
            setMisassembly();
            quality = Utilities.getLengthScalingCoefficient(contigSize) * Utilities.alpha*alignmentRatio / (1+Utilities.beta*Utilities.getPenalty(contigSize, totalNumberofIncons, mostlyAlignedRef));        
            //System.out.println(id+"\t"+alignmentRatio+"\t"+Utilities.getPenalty(contigSize, totalNumberofIncons));
        }
    }
    
    public void setQuality(double c){
        quality = c;
    }
    
    public double getQuality(){
        return quality;
    }
    
    
    public void addAlignments(List<Alignment> aList){        
        for(Alignment a : aList){
            addAlignment(a);
        }        
    }
    
    public void addAlignment(Alignment a){        
        
        if(refAlignmentMap.containsKey(a.getReference())){
            refAlignmentMap.get(a.getReference()).add(a);
        }
        else{
            List<Alignment> tmp = new ArrayList<>();
            tmp.add(a);
            refAlignmentMap.put(a.getReference(), tmp);
        }
    }

    public double value(){
        double sum =0;
        for(Alignment a : getAlignments()){
            sum = sum + a.getIdentity() * a.getRefAlignmentLength();
        }
        return sum;
    }
    @Override
    public int compareTo(ScaffoldContig o) {        
        double value1 = value();
        double value2 = o.value();
        if(value1 > value2)
            return 1;
        else if(value1 < value2)
            return -1;
        else
            return 0;
    }
        
    public String toString(boolean detailed){
        NumberFormat nr = Utilities.numberFormatter;        
        StringBuilder sb = new StringBuilder();
        List<Alignment> alignments = getAlignments();
        if(detailed){
            for(Alignment a: alignments){
                sb.append(this.id+a+Utilities.ls);
            }
        }
        else{
            sb.append(this.id+"\t"+nr.format(this.contigSize)+"\t"+nr.format(this.alignableSizeOnContig)+"\t"+nr.format(this.contigSizeAligPerc)+"\t"
                    +nr.format(getGCRatio())+"\t"+nr.format(getNRatio())+"\t"
                    +nr.format(getQuality()));
            for(Inconsistency i: Inconsistency.values())
                sb.append("\t"+this.getInconsistencyCount(i));
            sb.append("\t"+totalNumberofIncons);
            sb.append(Utilities.ls);
        }
        return sb.toString();
    }
    public String getString4Misassemblies(){
        StringBuilder sb = new StringBuilder();
        
        for(Map.Entry<Inconsistency, List<Misassembly>> ent : getMisassemblyMap().entrySet()){
            if(ent.getValue().size()==0)
                continue;
            
            for(Misassembly m : ent.getValue()){
                sb.append(this.getId()+"\t");
                sb.append(m+Utilities.ls);
            }
        }
        return sb.toString();
    }
}
