/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.broad.igv.sam;

import org.broad.igv.logging.*;
import org.broad.igv.Globals;
import org.broad.igv.feature.LocusScore;
import org.broad.igv.feature.Strand;
import org.broad.igv.track.WindowFunction;

import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author jrobinso
 */
public class ReducedMemoryAlignment implements Alignment {

    private static Logger log = LogManager.getLogger(ReducedMemoryAlignment.class);
    private final String cigarString;

    private String readName;
    private String chromosome;
    private int start;
    private int end;
    boolean negativeStrand;
    AlignmentBlock[] blocks;
    AlignmentBlock[] insertions;
    List<Gap> gaps;

    public ReducedMemoryAlignment(Alignment al, int indelLimit) {


        this.negativeStrand = al.isNegativeStrand();
        this.readName = al.getReadName();
        this.chromosome = al.getChr();
        this.start = al.getStart();
        this.end = al.getEnd();
        this.cigarString = al.getCigarString();

        AlignmentBlock[] blocks = al.getAlignmentBlocks();
        if (blocks != null) {


            List<AlignmentBlock> rmBlocks = new ArrayList<AlignmentBlock>(blocks.length);
            int start = blocks[0].getStart();
            int end = blocks[0].getEnd();
            boolean softClip = blocks[0].isSoftClip();

            for (int i = 1; i < blocks.length; i++) {

                if (blocks[i].getStart() - end < indelLimit && blocks[i].isSoftClip() == softClip) {
                    end = blocks[i].getEnd();
                } else {
                    rmBlocks.add(new ReducedMemoryAlignmentBlock(start, end - start, softClip));
                    start = blocks[i].getStart();
                    end = blocks[i].getEnd();
                    softClip = blocks[i].isSoftClip();
                }
            }

            // Last one
            rmBlocks.add(new ReducedMemoryAlignmentBlock(start, end - start, softClip));

            this.blocks = rmBlocks.toArray(new AlignmentBlock[rmBlocks.size()]);
        }

        AlignmentBlock[] insertions = al.getInsertions();
        if (insertions != null) {

            List<AlignmentBlock> rmInsertions = new ArrayList<AlignmentBlock>();
            for (AlignmentBlock b : insertions) {
                if (b.getLength() >= indelLimit) {
                    rmInsertions.add(b);
                }
            }
            this.insertions = rmInsertions.toArray(new AlignmentBlock[rmInsertions.size()]);
        }

        List<Gap> gaps = al.getGaps();
        if (gaps != null) {
            List<Gap> rmGaps = new ArrayList<Gap>();
            for (Gap gap : gaps) {
                if (gap.getnBases() >= indelLimit) {
                    rmGaps.add(gap);
                }
            }
            this.gaps = rmGaps;
        }

    }

    public void setMateSequence(String sequnce) {
        // Ignore
    }

    public String getPairOrientation() {
        return "";
    }

    public boolean isSmallInsert() {
        return false;
    }

    public boolean isVendorFailedRead() {
        return false;  //To change body of implemented methods use File | Settings | File Templates.
    }


    public Color getYcColor() {
        return null;
    }


    public String getChromosome() {
        return chromosome;
    }

    public String getChr() {
        return chromosome;
    }

    @Override
    public String getContig() {
        return chromosome;
    }

    public int getAlignmentStart() {
        return getStart();
    }

    public boolean contains(double location) {
        return location >= getStart() && location < getEnd();
    }

    public AlignmentBlock[] getAlignmentBlocks() {
        return blocks;
    }

    public AlignmentBlock[] getInsertions() {
        return insertions;
    }

    public int getInferredInsertSize() {
        return 0;
    }

    public int getMappingQuality() {
        return 255;
    }

    public boolean isProperPair() {
        return true;
    }

    public boolean isMapped() {
        return true;
    }

    public boolean isPaired() {
        return false;
    }

    public boolean isNegativeStrand() {
        return negativeStrand;
    }

    public boolean isDuplicate() {
        return false;
    }

    public float getScore() {
        return 1.0f;
    }

    public LocusScore copy() {
        return this;
    }

    public String getClipboardString(double location, int mouseX) {
        return getValueString(location, mouseX, (WindowFunction) null);
    }

    public String getValueString(double position, int mouseX, WindowFunction ignored) {

        StringBuffer buf = new StringBuffer();

        buf.append("Read name = " + readName + "<br>");

        String sample = getSample();
        if (sample != null) {
            buf.append("Sample = " + sample + "<br>");
        }
        String readGroup = getReadGroup();
        if (sample != null) {
            buf.append("Read group = " + readGroup + "<br>");
        }

        buf.append("----------------------" + "<br>");
        buf.append("Alignment start = " + Globals.DECIMAL_FORMAT.format(getAlignmentStart() + 1) + " (" + (isNegativeStrand() ? "-" : "+") + ")<br>");
        buf.append("Cigar = " + cigarString + "<br>");

        return buf.toString();
    }


    /**
     * @return the start
     */
    public int getStart() {
        return start;
    }

    /**
     * @param start the start to set
     */
    public void setStart(int start) {
        this.start = start;
    }

    /**
     * @return the end
     */
    public int getEnd() {
        return end;
    }

    public int getAlignmentEnd() {
        return end;
    }

    /**
     * @param end the end to set
     */
    public void setEnd(int end) {
        this.end = end;
    }

    public byte getBase(double position) {
        return 0;
    }

    public byte getPhred(double position) {
        return 0;
    }

    @Override
    public List<Gap> getGaps() {
        return gaps;
    }

    public boolean isFirstOfPair() {
        return false;
    }

    public boolean isSecondOfPair() {
        return false;
    }

    public Strand getFirstOfPairStrand() {
        return isNegativeStrand() ? Strand.NEGATIVE : Strand.POSITIVE;
    }


    public Strand getSecondOfPairStrand() {
        return Strand.NONE;
    }

    public Strand getReadStrand() {
        return isNegativeStrand() ? Strand.NEGATIVE : Strand.POSITIVE;
    }

    @Override
    public boolean isPrimary() {
        return true;
    }

    @Override
    public boolean isSupplementary() {
        return false;
    }

    public static class ReducedMemoryAlignmentBlock implements AlignmentBlock {

        private int pixelStart;
        private int pixelEnd;
        private char cigarOperator;

        ReducedMemoryAlignmentBlock(int start, int length, boolean softClipped) {
            this.start = start;
            this.length = length;
            this.softClipped = softClipped;
            this.cigarOperator = cigarOperator;
        }

        int start;
        int length;
        boolean softClipped;


        @Override
        public void setPixelRange(int s, int e) {
            this.pixelStart = s;
            this.pixelEnd = e;
        }

        @Override
        public boolean containsPixel(int x) {
            return x >= this.pixelStart && x <= this.pixelEnd;
        }

        @Override
        public char getCigarOperator() {
            return cigarOperator;
        }


        @Override
        public boolean contains(int position) {
            int offset = position - start;
            return offset >= 0 && offset < getLength();
        }

        @Override
        public int getBasesLength() {
            return length;
        }

        @Override
        public int getBasesOffset() {
            return 0;
        }


        @Override
        public int getLength() {
            return length;
        }

        @Override
        public byte getBase(int offset) {
            return 0;
        }

        @Override
        public ByteSubarray getBases() {
            return null;
        }

        @Override
        public int getStart() {
            return start;
        }

        @Override
        public byte getQuality(int offset) {
            return 0;
        }

        @Override
        public ByteSubarray getQualities() {
            return null;
        }

        @Override
        public int getEnd() {
            return start + length;
        }

        @Override
        public boolean isSoftClip() {
            return softClipped;
        }


        @Override
        public boolean hasBases() {
            return false;
        }
    }


    // TODO -- why is this class a "Feature"?

    public static class ReducedMemoryAlignmentCounts implements AlignmentCounts {

        private int start;
        private int end;
        private int bucketSize;
        private int nBuckets;
        double[] total;
        private int maxCount;


        public ReducedMemoryAlignmentCounts(int start, int end, int bucketSize) {
            this.start = start;
            this.bucketSize = bucketSize;

            this.nBuckets = (end - start) / bucketSize;
            if (nBuckets == 0 || (end - start) % bucketSize != 0) nBuckets++;

            total = new double[nBuckets];
            Arrays.fill(total, 0);

            // Adjust end to avoid fractional bucket
            this.end = start + nBuckets * bucketSize;

        }

        @Override
        public int getStart() {
            return start;
        }

        @Override
        public int getEnd() {
            return end;
        }

        @Override
        public int getBucketSize() {
            return bucketSize;
        }


        @Override
        public int getNumberOfPoints() {
            return nBuckets;
        }


        @Override
        public int getTotalCount(int pos) {
            int offset = (pos - start) / bucketSize;
            if (offset < 0 || offset >= total.length) {
                if (log.isDebugEnabled()) {
                    log.debug("Position out of range: " + pos + " (valid range - " + start + "-" + end);
                }
                return 0;
            } else {
                return (int) Math.round(total[offset]);
            }
        }

        @Override
        public int getTotalPositiveCount(int pos) {
            throw new RuntimeException(" Method getTotalPositiveCount not implemented");
        }

        @Override
        public int getTotalNegativeCount(int pos) {
            throw new RuntimeException(" Method getTotalNegativeCount not implemented");
        }

        @Override
        public void incCounts(Alignment alignment) {

            AlignmentBlock[] blocks = alignment.getAlignmentBlocks();
            if (blocks != null) {
                for (AlignmentBlock b : blocks) {
                    if (!b.isSoftClip()) {
                        incrementBuckets(b.getStart(), b.getEnd());
                    }
                }

            } else {
                incrementBuckets(alignment.getAlignmentStart(), alignment.getAlignmentEnd());
            }
        }

        private void incrementBuckets(int blockStart, int blockEnd) {

            int startBucket = Math.max(0, (blockStart - this.start) / bucketSize);
            int endBucket = Math.min(nBuckets - 1, (blockEnd - this.start) / bucketSize);

            for (int b = startBucket; b <= endBucket; b++) {
                int bucketStart = this.start + b * bucketSize;
                int bucketEnd = bucketStart + bucketSize;
                if (blockEnd >= bucketStart && blockStart <= bucketEnd) {
                    double s = Math.max(blockStart, bucketStart);
                    double e = Math.min(blockEnd, bucketEnd);
                    double f = (e - s) / bucketSize;
                    total[b] += f;

                    if (total[b] > maxCount) maxCount = (int) Math.round(total[b]);
                }

            }

        }


        @Override
        public int getMaxCount(int origin, int end) {
            return maxCount;
        }

        @Override
        public String getValueStringAt(int pos) {
            int idx = (pos - start) / bucketSize;
            return idx > 0 && idx < total.length ? String.valueOf((int) Math.round(total[idx])) : "";
        }

        // Rest is needed for the interface, but NA for reduced memory alignments
        @Override
        public String getChr() {
            return null;
        }

        @Override
        public String getContig() {
            return null;
        }


        @Override
        public int getTotalQuality(int pos) {
            return 0;
        }

        @Override
        public int getCount(int pos, byte b) {
            return 0;
        }

        @Override
        public int getNegCount(int pos, byte b) {
            return 0;
        }

        @Override
        public int getPosCount(int pos, byte b) {
            return 0;
        }

        @Override
        public int getDelCount(int pos) {
            return 0;
        }

        @Override
        public int getInsCount(int pos) {
            return 0;
        }

        @Override
        public int getQuality(int pos, byte b) {
            return 0;
        }


        @Override
        public boolean isConsensusMismatch(int pos, byte ref, String chr, float snpThreshold) {
            return false;
        }

        @Override
        public boolean isConsensusDeletion(int start, int end, float snpThreshold) {
            return false;
        }

        @Override
        public boolean isConsensusInsertion(int pos, float snpThreshold) {
            return false;
        }

        @Override
        public BisulfiteCounts getBisulfiteCounts() {
            return null;
        }

        @Override
        public boolean hasBaseCounts() {
            return false;
        }

        @Override
        public void finish() {

        }

    }
}
