/**
 * 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 "QDDocument.h"

#include <QRegExp>
#include <QStringList>

#include "QDSceneIOTasks.h"

namespace U2 {

#define STRING_MAX_LEN 80
#define TEXT_OFFSET "    "
#define DOUBLE_TEXT_OFFSET "        "

static const QString GRAPH_KEYWORD = "query";
static const QString IMPORT_KEYWORD = "import";
static const QString BLOCK_START = "{";
static const QString BLOCK_END = "}";
static const QString META_KEYWORD = ".meta";
static const QString VISUAL_KEYWORD = "visual";

QString QDDocStatement::getAttribute(const QString& name) const {
    foreach (const StringAttribute& attr, attributes) {
        if (attr.first == name) {
            return attr.second;
        }
    }
    return QString();
}

void QDDocStatement::setAttribute(const QString& name, const QString& value) {
    assert(!name.contains(' '));
    for (int i = 0; i < attributes.size(); i++) {
        StringAttribute& attr = attributes[i];
        if (attr.first == name) {
            attr.second = value;
            return;
        }
    }
    QPair<QString, QString> newAttr = qMakePair(name, value);
    attributes.append(newAttr);
}

QString QDDocStatement::toString() const {
    int strLen = evalStringLen();
    QString res;
    foreach (const StringAttribute& attr, attributes) {
        if (strLen > STRING_MAX_LEN) {
            res += "\n";
            res += DOUBLE_TEXT_OFFSET;
        }
        res += " " + attr.first + ": ";
        QString val;
        if (attr.second.contains(';') || attr.second.contains('#')) {
            val = "\"" + attr.second + "\"";
        } else {
            val = attr.second;
        }
        res += val + ';';
    }
    return res;
}

int QDDocStatement::evalStringLen() const {
    int res = 0;
    foreach (const StringAttribute& attr, attributes) {
        res += attr.first.length() + attr.second.length() + 2;
    }
    return res;
}

const QString QDElementStatement::GEOMETRY_ATTR_NAME("geometry");
const QString QDElementStatement::ALGO_ATTR_NAME("type");

QString QDElementStatement::toString() const {
    QString res = QDDocStatement::toString();
    if (evalStringLen() > STRING_MAX_LEN) {
        res = BLOCK_START + res + "\n" + TEXT_OFFSET + BLOCK_END;
    } else {
        res = BLOCK_START + res + " " + BLOCK_END + " ";
    }

    res = TEXT_OFFSET + id + " " + res;
    return res;
}

QString QDElementStatement::definedIn() const {
    return QDDocument::definedIn(id);
}

const QString QDLinkStatement::TYPE_ATTR_NAME("type");
QString QDLinkStatement::toString() const {
    QString res = " " + BLOCK_START + QDDocStatement::toString() + " " + BLOCK_END;
    QString prefix = elementIds.at(0);
    for (int i = 1; i < elementIds.size(); i++) {
        prefix += "--" + elementIds.at(i);
    }
    res = TEXT_OFFSET + prefix + res;
    return res;
}

// QDDocument
//////////////////////////////////////////////////////////////////////////
static const QString COMMENT_PATTERN = "\\\\\\\\|#([^\n]*)\n";
static const QString SINGLE_ID_PATTERN = "(?:[a-zA-Z]+)(?:[a-zA-Z0-9]|_|(?:-(?!-)))*";
const QString QDDocument::ID_PATTERN = "(" + SINGLE_ID_PATTERN + "(?:\\." + SINGLE_ID_PATTERN + ")*)";
// static const QString VAL_PATTERN = "((?:\"[^\"]+\")|[^\\s\"]+)";
static const QString VAL_PATTERN = "((?:\"[^\"]+\")|[^\\s;]+)";
static const QString IMPORT_PATTERN = IMPORT_KEYWORD + "\\s+" + VAL_PATTERN;
static const QString DOC_NAME_PATTERN = GRAPH_KEYWORD + "\\s*" + QDDocument::ID_PATTERN;  // + "\\s*\\{{1,1}" + "[^\\{]*(?:\\{{1,1}[^\\{\\}]\\}{1,1})*" + "}{1,1}";
static const QString ELEMENT_ATTRS_PATTERN = "\\" + BLOCK_START + "{1,1}([^\\" + BLOCK_START + "\\" + BLOCK_END + "]*)\\" + BLOCK_END + "{1,1}";
// static const QString GROUP_ATTRS_PATTERN = "\\{{1,1}([^\\{\\}]*)\\}{1,1}";
static const QString ELEMENT_STATEMENT_PATTERN = "[^--](?:\n|\\s)+" + QDDocument::ID_PATTERN + "\\s*" + ELEMENT_ATTRS_PATTERN;
// static const QString GROUP_STATEMENT_PATTERN ="[^--]" + ID_PATTERN + "\\s*" + GROUP_ATTRS_PATTERN;
static const QString LINK_STATEMENT_PATTERN = "(" + QDDocument::ID_PATTERN + "(?:\\s*--\\s*" + QDDocument::ID_PATTERN + ")+)\\s*" + ELEMENT_ATTRS_PATTERN;

static const QString ORDER_KEYWORD = "order";
static const QString ORDER_PATTERN = ORDER_KEYWORD + "\\{{1,1}([^\\{\\}]*)\\}{1,1}";

static const QString SCHEMA_STRAND_ATTR = "schema-strand";
static const QString SCHEMA_STRAND_PATTERN = SCHEMA_STRAND_ATTR + "\\s*:\\s*(direct|complement|both)\\s*;";

const QString QDDocument::HEADER_LINE("#@UGENE_QUERY");
const QString QDDocument::DEPRECATED_HEADER_LINE("#!UGENE_QUERY");
const QString QDDocument::GROUPS_SECTION("groups");

QDDocument::~QDDocument() {
    qDeleteAll(elements);
    qDeleteAll(links);
}

QDElementStatement* QDDocument::getElement(const QString& id) const {
    foreach (QDElementStatement* stmt, elements) {
        if (stmt->getId() == id) {
            return stmt;
        }
    }
    return nullptr;
}

QList<QDElementStatement*> QDDocument::getElements(QDStatementType type) const {
    QList<QDElementStatement*> res;
    foreach (QDElementStatement* stmt, elements) {
        if (stmt->getType() == type) {
            res.append(stmt);
        }
    }
    return res;
}

QByteArray QDDocument::toByteArray() const {
    QString content;
    foreach (QDElementStatement* el, getElements(Group)) {
        if (el->getId() == GROUPS_SECTION) {
            continue;
        }
        content += el->toString() + "\n";
    }
    content += "\n";
    foreach (QDLinkStatement* lnk, getLinks()) {
        content += lnk->toString() + "\n";
    }
    content += "\n";
    QString txtOffset = QString(TEXT_OFFSET);
    content += txtOffset + META_KEYWORD + BLOCK_START + "\n";
    content += txtOffset + txtOffset + VISUAL_KEYWORD + BLOCK_START + "\n";
    foreach (QDElementStatement* el, getElements(Element)) {
        content += txtOffset + txtOffset + txtOffset;
        content += el->toString() + "\n";
    }
    content += txtOffset + txtOffset + BLOCK_END + "\n";

    if (!order.isEmpty()) {
        content += txtOffset + txtOffset + ORDER_KEYWORD + " " + BLOCK_START + " ";
        foreach (QString s, order) {
            content += s + ';';
        }
        content += " " + BLOCK_END + "\n";
    }

    if (schemaStrand != QDStrand_Both) {
        content += txtOffset + txtOffset + SCHEMA_STRAND_ATTR + " : ";
        content += QDSchemeSerializer::STRAND_MAP.value(schemaStrand) + ';';
        content += " \n";
    }

    QDElementStatement* grEl = getElement(GROUPS_SECTION);
    if (grEl) {
        content += txtOffset + grEl->toString() + "\n";
    }

    content += txtOffset + BLOCK_END + "\n";
    QString desc = docDesc;
    desc = "#" + desc.replace("\n", "\n#") + "\n\n";
    content = HEADER_LINE + "\n" + desc + GRAPH_KEYWORD + " " + docName + " {\n\n\n" + content + "}";
    return content.toUtf8();
}

bool QDDocument::setContent(const QString& content) {
    QRegExp reg(DOC_NAME_PATTERN);
    reg.indexIn(content);
    docName = reg.cap(1);
    findImportedUrls(content);
    findComments(content);
    parseSchemaStrand(content);
    if (!findElementStatements(content)) {
        return false;
    }
    if (!findLinkStatements(content)) {
        return false;
    }
    return true;
}

void QDDocument::parseSchemaStrand(const QString& str) {
    QRegExp reg(SCHEMA_STRAND_PATTERN);
    int pos = reg.indexIn(str);
    if (pos >= 0) {
        QString strandStr = reg.cap(1);
        if (QDSchemeSerializer::STRAND_MAP.values().contains(strandStr)) {
            schemaStrand = QDSchemeSerializer::STRAND_MAP.key(strandStr);
        }
    }
}

void QDDocument::findComments(const QString& str) {
    QRegExp reg("((?:#[^\n]*\n{1,1})+)\\s*" + GRAPH_KEYWORD);
    int pos = reg.indexIn(str);
    if (pos >= 0) {
        QString coms = reg.cap(1);
        coms.remove(HEADER_LINE);
        docDesc = coms.replace("#", "");
        int lastIdx = docDesc.length() - 1;
        assert(QString(docDesc.at(lastIdx)) == "\n");
        docDesc.remove(lastIdx, 1);
    }
}

void QDDocument::parseOrder(const QString& str) {
    order.clear();
    order = str.trimmed().split(QRegExp("\\s*;\\s*"));
}

bool QDDocument::addElement(QDElementStatement* el) {
    QDStatementType elType = el->getType();
    const QString& elId = el->getId();
    foreach (QDElementStatement* stmt, elements) {
        if (stmt->getId() == elId && stmt->getType() == elType) {
            return false;
        }
    }
    elements.append(el);
    assert(el->document == nullptr);
    el->document = this;
    return true;
}

void QDDocument::findImportedUrls(const QString& str) {
    QRegExp reg(IMPORT_PATTERN);
    int pos = 0;
    while (pos >= 0) {
        pos = reg.indexIn(str, pos);
        if (pos >= 0) {
            pos += reg.matchedLength();
            QString toImport = reg.cap(1);
            importedUrls.append(toImport);
        }
    }
}

bool QDDocument::findElementStatements(const QString& str) {
    QRegExp reg;
    reg.setPattern(ELEMENT_STATEMENT_PATTERN);
    int pos = 0;
    while (pos >= 0) {
        pos = reg.indexIn(str, pos);
        if (pos >= 0) {
            /*QString ch1 = QString(str.at(pos));
            QString ch2 = QString(str.at(pos-1));
            QString ch3 = QString(str.at(pos+1));*/
            pos += reg.matchedLength();
            const QString& id = reg.cap(1);
            const QString& attrs = reg.cap(2);
            if (id == ORDER_KEYWORD) {
                parseOrder(attrs);
                continue;
            }
            const QMap<QString, QString>& attrsMap = string2attributesMap(attrs);
            QDStatementType type;
            if (id.contains('.')) {
                type = Element;
            } else {
                type = Group;
            }
            QDElementStatement* element = new QDElementStatement(id, type);
            foreach (const QString& attrName, attrsMap.keys()) {
                const QString& val = attrsMap.value(attrName);
                element->setAttribute(attrName, val);
            }
            addElement(element);
        }
    }
    return true;
}

bool QDDocument::findLinkStatements(const QString& str) {
    QRegExp reg(LINK_STATEMENT_PATTERN);
    int pos = 0;
    while (pos >= 0) {
        pos = reg.indexIn(str, pos);
        if (pos >= 0) {
            pos += reg.matchedLength();
            const QString& elemS = reg.cap(1);
            const QList<QString>& elIds = idsFromString(elemS);
            QDLinkStatement* link = new QDLinkStatement(elIds);
            int capCount = reg.captureCount();
            const QString& attrs = reg.cap(capCount);
            const QMap<QString, QString>& attrsMap = string2attributesMap(attrs);
            foreach (const QString& attrName, attrsMap.keys()) {
                const QString& val = attrsMap.value(attrName);
                link->setAttribute(attrName, val);
            }
            addLink(link);
        }
    }
    return true;
}

QMap<QString, QString> QDDocument::string2attributesMap(const QString& str) {
    QMap<QString, QString> res;
    QRegExp reg(ID_PATTERN + "\\s*:\\s*" + VAL_PATTERN);
    int pos = 0;
    while (pos >= 0) {
        pos = reg.indexIn(str, pos);
        if (pos >= 0) {
            const QString& attrName = reg.cap(1);
            QString attrVal = reg.cap(2);
            attrVal.remove('"');
            res[attrName] = attrVal;
            pos += reg.matchedLength();
        }
    }
    return res;
}

QList<QString> QDDocument::idsFromString(const QString& str) {
    return str.split(QRegExp("\\s*--\\s*"));
}

QString QDDocument::definedIn(const QString& id) {
    const QStringList& chunks = id.split('.');
    if (chunks.size() < 3) {
        return QString();
    } else {
        assert(chunks.size() == 3);
        QString defIn = chunks.at(0);
        return defIn;
    }
}

QString QDDocument::getLocalName(const QString& id) {
    if (id.contains('.')) {
        int dotPos = id.indexOf('.');
        return id.mid(dotPos + 1);
    }
    return id;
}

QDElementStatement* QDDocument::findElementByUnitName(QDElementStatement* parent, const QString& unitName) const {
    return getElement(parent->getId() + "." + unitName);
}

void QDDocument::saveOrder(const QList<QDActor*>& actors) {
    foreach (QDActor* a, actors) {
        order.append(a->getParameters()->getLabel());
    }
}

bool QDDocument::isHeaderLine(const QString& line) {
    return (line.startsWith(HEADER_LINE) ||
            line.startsWith(DEPRECATED_HEADER_LINE));
}

// Mapper
//////////////////////////////////////////////////////////////////////////
QString QDIdMapper::distance2string(QDDistanceType type) {
    switch (type) {
        case E2S:
            return "end-to-start";
        case E2E:
            return "end-to-end";
        case S2S:
            return "start-to-start";
        case S2E:
            return "start-to-end";
        default:
            return QString();
    }
}

int QDIdMapper::string2distance(const QString& str) {
    if (str == "end-to-start") {
        return 0;
    }
    if (str == "end-to-end") {
        return 1;
    }
    if (str == "start-to-start") {
        return 2;
    }
    if (str == "start-to-end") {
        return 3;
    }
    return -1;
}

QDConstraintType QDIdMapper::string2constraintType(const QString& str) {
    if (str == "distance") {
        return QDConstraintTypes::DISTANCE;
    }
    return QString();
}

QString QDIdMapper::constraintType2string(const QDConstraintType& type) {
    if (type == QDConstraintTypes::DISTANCE) {
        return "distance";
    }
    return QString();
}

}  // namespace U2
