/**
 * UGENE - Integrated Bioinformatics Tools.
 * Copyright (C) 2008-2024 UniPro <ugene@unipro.ru>
 * http://ugene.net
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include "SAMFormat.h"

#include <QRegExp>

#include <U2Core/AppContext.h>
#include <U2Core/DNAAlphabet.h>
#include <U2Core/DNAQuality.h>
#include <U2Core/DNASequenceObject.h>
#include <U2Core/DNATranslation.h>
#include <U2Core/GObjectTypes.h>
#include <U2Core/IOAdapter.h>
#include <U2Core/L10n.h>
#include <U2Core/MsaObject.h>
#include <U2Core/TextUtils.h>
#include <U2Core/U2AlphabetUtils.h>
#include <U2Core/U2AssemblyUtils.h>
#include <U2Core/U2OpStatus.h>
#include <U2Core/U2SafePoints.h>

#include <U2Formats/BAMUtils.h>

#include "DocumentFormatUtils.h"

namespace U2 {

const QByteArray SAMFormat::VERSION = "1.0";
const QByteArray SAMFormat::SAM_SECTION_START = "@";
const QByteArray SAMFormat::SECTION_HEADER = "@HD";
const QByteArray SAMFormat::SECTION_SEQUENCE = "@SQ";
const QByteArray SAMFormat::SECTION_READ_GROUP = "@RG";
const QByteArray SAMFormat::SECTION_PROGRAM = "@PG";
const QByteArray SAMFormat::SECTION_COMMENT = "@CO";

const QByteArray SAMFormat::TAG_VERSION = "VN";
const QByteArray SAMFormat::TAG_SORT_ORDER = "SO";
const QByteArray SAMFormat::TAG_GROUP_ORDER = "GO";

const QByteArray SAMFormat::TAG_SEQUENCE_NAME = "SN";
const QByteArray SAMFormat::TAG_SEQUENCE_LENGTH = "LN";
const QByteArray SAMFormat::TAG_GENOME_ASSEMBLY_ID = "AS";
const QByteArray SAMFormat::TAG_SEQUENCE_MD5_SUM = "M5";
const QByteArray SAMFormat::TAG_SEQUENCE_URI = "UR";
const QByteArray SAMFormat::TAG_SEQUENCE_SPECIES = "SP";

const SAMFormat::Field SAMFormat::samFields[] = {  // alignment section fields excluding optional tags
    Field("QNAME", "[ !-?A-~]+"),
    Field("FLAG", "[0-9]+"),
    Field("RNAME", "\\*|[!-()+-<>-~][ !-~]*"),
    Field("POS", "[0-9]+"),
    Field("MAPQ", "[0-9]+"),
    Field("CIGAR", "([0-9]+[MIDNSHP])+|\\*"),
    Field("RNEXT", "\\*|=|[!-()+-<>-~][!-~]*"),
    Field("PNEXT", "[0-9]+"),
    Field("TLEN", "-?[0-9]+"),
    Field("SEQ", "\\*|[A-Za-z=.]+"),
    Field("QUAL", "[!-~]+|\\*")};

bool SAMFormat::validateField(int num, QByteArray& field, U2OpStatus* ti) {
    if (!samFields[num].getPattern().exactMatch(field)) {
        if (ti != nullptr) {
            ti->setError(SAMFormat::tr("Field \"%1\" not matched pattern \"%2\", expected pattern \"%3\"").arg(samFields[num].name).arg(QString(field)).arg(samFields[num].getPattern().pattern()));
        }
        return false;
    }
    return true;
}

SAMFormat::SAMFormat(QObject* p)
    : TextDocumentFormatDeprecated(p, BaseDocumentFormats::SAM, DocumentFormatFlags(DocumentFormatFlag_SupportWriting | DocumentFormatFlag_CannotBeCompressed), QStringList() << "sam") {
    formatName = tr("SAM");
    formatDescription = tr("The Sequence Alignment/Map (SAM) format is a generic alignment format for"
                           "storing read alignments against reference sequence");
    supportedObjectTypes += GObjectTypes::ASSEMBLY;
    skipDetection = false;
    formatFlags |= DocumentFormatFlag_Hidden;
}

FormatCheckResult SAMFormat::checkRawTextData(const QByteArray& rawData, const GUrl&) const {
    if (skipDetection) {
        return FormatDetection_NotMatched;
    }
    QRegExp rx("^@[A-Za-z][A-Za-z](\\t[A-Za-z][A-Za-z]:[ -~]+)");
    // try to find SAM header
    if (rx.indexIn(rawData) != 0) {
        // if no header try to parse first alignment line
        QList<QByteArray> fieldValues = rawData.split('\n')[0].split(SPACE);
        int readFieldsCount = fieldValues.count();
        if (readFieldsCount < 11) {
            return FormatDetection_NotMatched;
        }
        for (int i = 0; i < qMin(11, readFieldsCount); i++) {
            if (!validateField(i, fieldValues[i])) {
                return FormatDetection_NotMatched;
            }
        }
        return FormatDetection_HighSimilarity;
    }
    return FormatDetection_Matched;
}

// static void prepareRead(const QByteArray& core, const QByteArray& quality, QByteArray& seq, QByteArray& qual, QByteArray& cigar) {
//     bool gap = true;
//     int lastCigarPos = 0;
//
//     QList<U2CigarToken> cigarTokens;
//
//     for(int i = 0, coreLen = core.length(); i < coreLen; i++) {
//         char c = core[i];
//         if(c == Msa::GapChar) {
//             if(!gap) {
//                 if(i != lastCigarPos) {
//                     cigarTokens.append(U2CigarToken(U2CigarOp_M, i - lastCigarPos));
//                 }
//                 gap = true;
//                 lastCigarPos = i;
//             }
//         } else {
//             seq.push_back(c);
//             if(!quality.isEmpty()) {
//                 qual.push_back(DNAQuality::encode(quality[i], DNAQualityType_Sanger));
//             }
//             if(gap) {
//                 if(i != lastCigarPos) {
//                     cigarTokens.append(U2CigarToken(U2CigarOp_N, i - lastCigarPos));
//                 }
//                 gap = false;
//                 lastCigarPos = i;
//             }
//         }
//     }
//     if(lastCigarPos != core.length() - 1) {
//         cigarTokens.append(U2CigarToken(gap ? U2CigarOp_N : U2CigarOp_M, core.length() - 1 - lastCigarPos));
//     }
//     if(cigarTokens.isEmpty()) {
//         cigar = QByteArray("*");
//     } else {
//         foreach(U2CigarToken token, cigarTokens) {
//             assert(token.count != 0);
//             cigar.append(QString::number(token.count));
//             switch(token.op) {
//                 case U2CigarOp_M: cigar.append("M"); break;
//                 case U2CigarOp_N: cigar.append("N"); break;
//                 default:;
//             }
//         }
//     }
// }

Document* SAMFormat::loadTextDocument(IOAdapter* /* io */, const U2DbiRef& /* dbiRef */, const QVariantMap& /* _fs */, U2OpStatus& /* os */) {
    FAIL("Not implemented", nullptr);

    // CHECK_EXT(io != NULL   && io->isOpen(), os.setError(L10N::badArgument("IO adapter")), NULL);
    //
    // QList<GObject*> objects;
    // QVariantMap fs = _fs;

    // QString lockReason;

    // QMap<QString, Msa> maMap; //file may contain multiple MA objects
    // Msa defaultMA("Alignment " + io->getURL().baseFileName());

    // QByteArray readBuffer(READ_BUFF_SIZE, '\0');
    // char* buff  = readBuffer.data();
    // bool lineOk = false;

    // QByteArray fields[11];

    // int len = 0;
    // while(!os.isCoR() && (len = io->readLine(buff, READ_BUFF_SIZE, &lineOk)) > 0) {
    //     QByteArray line = QByteArray( buff, len );

    //    if(line.startsWith(SAM_SECTION_START)) { //Parse sections

    //        QList<QByteArray> tags;

    //        if(getSectionTags(line, SECTION_SEQUENCE, tags)) { //Parse sequence section
    //            foreach(QByteArray tag, tags) {
    //                if(tag.startsWith(TAG_SEQUENCE_NAME)) { // Set alignment name
    //                    QString maName = QByteArray(tag.constData() + 3, tag.length() - 3);
    //                    Msa ma;
    //                    ma.setName(maName);
    //                    maMap[maName] = ma;
    //                }
    //            }
    //        } else if(getSectionTags(line, SECTION_HEADER, tags)) { //Parse header section
    //            foreach(QByteArray tag, tags) {
    //                if(tag.startsWith(TAG_VERSION)) { //Check file format version
    //                    QByteArray versionStr = QByteArray(tag.constData() + 3, tag.length() - 3);
    //                    QList<QByteArray> version = versionStr.split('.');
    //                    if (version[0].toInt() != 1 && version[1].toInt() > 3) {
    //                        os.setError(SAMFormat::tr("Unsupported file version \"%1\"").arg(QString(versionStr)));
    //                        return NULL;
    //                    }
    //                }
    //            }
    //        }
    //        // Skip other sections

    //        continue;
    //    }

    //    QList<QByteArray> fieldValues = line.split(SPACE);

    //    int readFieldsCount = fieldValues.count();

    //    //if(readFieldsCount < 11) readFieldsCount--;
    //    QBitArray terminators = TextUtils::WHITES | TextUtils::LINE_BREAKS;
    //    char lastTerminator = lineOk ? '\n' : 0;

    //    while(readFieldsCount < 11 && (len = io->readUntil(buff, READ_BUFF_SIZE, terminators, IOAdapter::Term_Include, &lineOk)) > 0) {
    //        QByteArray addline = QByteArray( buff, len - 1 ).simplified();
    //        fieldValues[readFieldsCount - 1].append(addline);
    //        lastTerminator = buff[len-1];
    //        if(lineOk)
    //            break;
    //        else {
    //            fieldValues[readFieldsCount - 1].append(lastTerminator);
    //        }
    //    }
    //    {
    //        bool merge = readFieldsCount < 11 ? false : true;
    //        /*if(readFieldsCount < 11)*/ {
    //            while(!TextUtils::LINE_BREAKS.at(lastTerminator) && (len = io->readUntil(buff, READ_BUFF_SIZE, terminators, IOAdapter::Term_Include, &lineOk)) > 0) {
    //                if(!lineOk) {
    //                    len++;
    //                }
    //                QByteArray addline = QByteArray( buff, len - 1).simplified();
    //                if(merge) {
    //                    fieldValues[readFieldsCount - 1].append(addline);
    //                } else {
    //                    fieldValues.append(addline);
    //                    readFieldsCount++;
    //                }
    //                lastTerminator = buff[len - 1];
    //                merge = !lineOk;
    //            }
    //        }

    //        // skipping optional tags
    //        if(!TextUtils::LINE_BREAKS.at(lastTerminator))
    //            while((len = io->readLine(buff, READ_BUFF_SIZE, &lineOk)) > 0 && !lineOk);
    //    }

    //    if(readFieldsCount < 11) {
    //        os.setError(SAMFormat::tr("Unexpected end of file"));
    //        return NULL;
    //    }

    //    for(int i=0; i < qMin(11, readFieldsCount); i++) {
    //        fields[i] = fieldValues[i];
    //         if (!validateField(i, fields[i], &os)) {
    //             return NULL;
    //        }
    //    }

    //    QString rname = fields[2];

    //    if(rname != "*" && !maMap.contains(rname)) {
    //        //ioLog.info(SAMFormat::tr("Reference sequence \"%1\" not present in @SQ header").arg(rname));
    //        rname = "*";
    //    }

    //    MsaRow row;

    //    //short flag = fields[1].toShort();

    //    row.setName(fields[0]);
    //    if(fields[9] == "*") {
    //        row.setRowContent("", 0);
    //    } else {
    //            row.setRowContent(fields[9], fields[3].toInt()-1);
    //    }

    //    if(fields[10] != "*") {
    //        row.setQuality(DNAQuality(fields[10]));
    //    }

    //    if(rname == "*") {
    //        defaultMA.addRow(row);
    //    } else {
    //        maMap[rname].addRow(row);
    //    }

    //    os.setProgress(io->getProgress());
    //}

    // foreach(Msa ma, maMap.values()) {
    //     U2AlphabetUtils::assignAlphabet(ma);
    //     CHECK_EXT(ma.getAlphabet() != NULL, os.setError( SAMFormat::tr("Alphabet is unknown")), NULL);
    //     objects.append(new MultipleAlignmentObject(ma));
    // }

    // if (defaultMA.getRows().count() != 0) {
    //     U2AlphabetUtils::assignAlphabet(defaultMA);
    //     CHECK_EXT(defaultMA.getAlphabet() != NULL, os.setError( SAMFormat::tr("Alphabet is unknown")), NULL);
    //     objects.append(new MultipleAlignmentObject(defaultMA));
    // }

    // CHECK_OP_EXT(os, qDeleteAll(objects), NULL);
    //
    // DocumentFormatUtils::updateFormatHints(objects, fs);
    // Document* doc = new Document(this, io->getFactory(), io->getURL(), dbiRef, objects, fs, lockReason);
    // return doc;
}

void SAMFormat::storeDocument(Document* d, IOAdapter* io, U2OpStatus& os) {
    CHECK_EXT(d != nullptr, os.setError(L10N::badArgument("doc")), );
    CHECK_EXT(io != nullptr && io->isOpen(), os.setError(L10N::badArgument("IO adapter")), );

    QList<GObject*> als = d->findGObjectByType(GObjectTypes::ASSEMBLY);
    GUrl url = io->getURL();
    io->close();

    BAMUtils::writeObjects(
        d->findGObjectByType(GObjectTypes::ASSEMBLY),
        url.getURLString(),
        getFormatId(),
        os);
}

void SAMFormat::storeEntry(IOAdapter* /* io */, const QMap<GObjectType, QList<GObject*>>& /* objectsMap */, U2OpStatus& /*os*/) {
    FAIL("Not implemented", );
    // SAFE_POINT(objectsMap.contains(GObjectTypes::MULTIPLE_SEQUENCE_ALIGNMENT), "Clustal entry storing: no alignment", );
    // const QList<GObject*> &als = objectsMap[GObjectTypes::MULTIPLE_SEQUENCE_ALIGNMENT];
    // SAFE_POINT(als.size() > 0, "Clustal entry storing: alignment objects count error", );

    // QList<const MultipleAlignmentObject*> maList;
    // foreach(GObject *obj, als) {
    //     const MultipleAlignmentObject* maObj = qobject_cast<const MultipleAlignmentObject*>(obj);
    //     assert(maObj != NULL);
    //     maList.append(maObj);
    // }

    // QByteArray tab = "\t";
    // QByteArray block;

    ////Writing header
    // block.append(SECTION_HEADER).append("\t").append("VN:").append(VERSION).append("\n");
    // if (io->writeBlock( block ) != block.length()) {
    //     throw 0;
    // }

    ////Writing sequence section
    // foreach(const MultipleAlignmentObject* maObj, maList) {
    //     const Msa &ma = maObj->getMAlignment();
    //     block.clear();
    //     block.append(SECTION_SEQUENCE).append(tab).append(TAG_SEQUENCE_NAME).append(":").append(ma.getName().replace(QRegExp("\\s|\\t"), "_"))
    //         .append(tab).append(TAG_SEQUENCE_LENGTH).append(":").append(QByteArray::number(ma.getLength())).append("\n");
    //     if (io->writeBlock( block ) != block.length()) {
    //         throw 0;
    //     }
    // }

    ////Writing alignment section
    // foreach(const MultipleAlignmentObject* maObj, maList) {
    //     const Msa &ma = maObj->getMAlignment();
    //     QByteArray rname(ma.getName().replace(QRegExp("\\s|\\t"), "_").toLatin1());
    //     foreach(MsaRow row, ma.getRows()) {
    //         block.clear();
    //         //const QByteArray &core = row.getCore();
    //         QByteArray qname = QString(row.getName()).replace(QRegExp("\\s|\\t"), "_").toLatin1();
    //         QByteArray flag("0"); // can contains strand, mapped/unmapped, etc.
    //         QByteArray pos = QByteArray::number(row.getCoreStart()+1);
    //         QByteArray mapq("255"); //255 indicating the mapping quality is not available
    //         QByteArray mrnm("*");
    //         QByteArray mpos("0");
    //         QByteArray isize("0");
    //         QByteArray seq;
    //         QByteArray qual;
    //         QByteArray cigar;

    //        prepareRead(row.getCore(), row.getCoreQuality().qualCodes, seq, qual, cigar);
    //        if (row.hasQuality()) {
    //            row.getCoreQuality().qualCodes;
    //        } else {
    //            qual.reserve(seq.length());
    //            qual.fill('I', seq.length());
    //        }
    //
    //        block = qname + tab + flag + tab+ rname + tab + pos + tab + mapq + tab + cigar + tab + mrnm
    //            + tab + mpos + tab + isize + tab + seq + tab + qual + "\n";
    //        if (io->writeBlock( block ) != block.length()) {
    //            throw 0;
    //        }
    //    }
    //}
}

bool SAMFormat::getSectionTags(QByteArray& line, const QByteArray& sectionName, QList<QByteArray>& tags) {
    if (!line.startsWith(sectionName))
        return false;
    QByteArray tagsLine = QByteArray(line.constData() + 3, line.length() - 3);
    tags = tagsLine.split(SPACE);
    tags.removeAll("");
    return true;
}

bool SAMFormat::storeHeader(IOAdapter* io, const QVector<QByteArray>& names, const QVector<int>& lengths, bool coordinateSorted) {
    assert(names.size() > 0);
    assert(names.size() == lengths.size());
    io->setFormatMode(IOAdapter::TextMode);
    static const QByteArray TAB = "\t";
    QByteArray block;
    block.append(SECTION_HEADER).append(TAB).append(TAG_VERSION).append(":").append(VERSION);
    if (coordinateSorted) {
        block.append(TAB).append(TAG_SORT_ORDER).append(":").append("coordinate");
    }
    block.append("\n");
    for (int i = 0; i < names.size(); i++) {
        block.append(SECTION_SEQUENCE).append(TAB).append(TAG_SEQUENCE_NAME).append(":");
        block.append(names[i]).append(TAB);
        block.append(TAG_SEQUENCE_LENGTH).append(":").append(QByteArray::number(lengths[i])).append("\n");
    }
    if (io->writeBlock(block) != block.length()) {
        return false;
    }
    block.clear();

    return true;
}

bool SAMFormat::storeAlignedRead(int offset, const DNASequence& read, IOAdapter* io, const QByteArray& refName, int refLength, bool first, bool useCigar, const QByteArray& cigar) {
    static const QByteArray TAB = "\t";
    static const QByteArray flag("0");  // can contains strand, mapped/unmapped, etc.
    static const QByteArray mapq("255");  // 255 indicating the mapping quality is not available
    static const QByteArray mrnm("*");
    static const QByteArray mpos("0");
    static const QByteArray isize("0");
    static const QString rowDataNotCigar = "%1" + TAB + flag + TAB + "%2" + TAB + "%3" + TAB + mapq + TAB + "%4M" + TAB + mrnm + TAB + mpos + TAB + isize + TAB + "%5" + TAB + "%6" + "\n";
    static const QString rowDataCigar = "%1" + TAB + flag + TAB + "%2" + TAB + "%3" + TAB + mapq + TAB + "%4" + TAB + mrnm + TAB + mpos + TAB + isize + TAB + "%5" + TAB + "%6" + "\n";

    if (io == nullptr || !io->isOpen()) {
        return false;
    }
    io->setFormatMode(IOAdapter::TextMode);

    if (first) {
        QByteArray block;
        block.append(SECTION_HEADER).append(TAB).append("VN:").append(VERSION).append("\n");
        block.append(SECTION_SEQUENCE).append(TAB).append(TAG_SEQUENCE_NAME).append(":");
        block.append(refName).append(TAB);
        block.append(TAG_SEQUENCE_LENGTH).append(":").append(QByteArray::number(refLength)).append("\n");
        if (io->writeBlock(block) != block.length()) {
            return false;
        }
        block.clear();
    }

    QByteArray qname = QString(read.getName()).replace(QRegExp("\\s|\\t"), "_").toLatin1();
    if (qname.isEmpty()) {
        qname = "contig";
    }

    QByteArray qual;
    if (read.hasQualityScores()) {
        qual = read.quality.qualCodes;
    } else {
        qual.fill('I', read.seq.length());
    }
    QString row;
    if (useCigar) {
        row = rowDataCigar.arg(qname.constData())
                  .arg(refName.constData())
                  .arg(offset + 1)
                  .arg(cigar.constData())
                  .arg(read.seq.constData())
                  .arg(qual.constData());
    } else {
        row = rowDataNotCigar.arg(qname.constData())
                  .arg(refName.constData())
                  .arg(offset + 1)
                  .arg(read.seq.length())
                  .arg(read.seq.constData())
                  .arg(qual.constData());
    }

    if (io->writeBlock(row.toLatin1()) != row.length()) {
        return false;
    }

    return true;
}

}  // namespace U2
