//  Copyright (2013) Cédric Coussinet (cedric.coussinet@nomoseed.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 3 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, see <http://www.gnu.org/licenses/>

#include <QDir>

#include <libxml/parser.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>

#include <libxslt/functions.h>
#include <libexslt/exslt.h>

#include <QFile>

#include "formulation.h"


QString simplifier(QString txt){
    QString res = txt;
    if (txt.length()!=0)
    {
        QStringList eq = txt.split(":");
        res = "";
        QStringList list = eq[0].split("+");
        for (int i=list.length()-1;i>=0;i--)
        {
            QStringList state = list[i].split(".");
            state.removeDuplicates();
            foreach (const QString &str, state)
            {
               if (!state.filter(str+"^").empty())
                   state.clear();
            }
            if (!state.empty())
            {
                state.sort();
                res = "+" + state.join(".") + res;
            }
        }
        list = res.split('+', QString::SkipEmptyParts);
        list.sort();
        list.removeDuplicates();
        res = list.join("+")+ ":" + eq[1];
    }
    return res;
}

QString nettoyerOr(QString txt){
    QRegExp rx("(^|[\\+\\(])\\([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)*\\)(?![\\^\\.\\*])", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    QString A, B, C;
    int first, last;
    first = rx.indexIn(txt);
    while (first != -1)
    {
        last = rx.matchedLength();
        A = txt.mid(0,first);
        B = rx.cap(0);
        if (B[0]=='(')
            B.remove(0,1);
        else
            B.remove(1,1);
        B.remove(B.length()-1,1);
        C = txt.mid(first+last);
        txt = A + B + C;
        first = rx.indexIn(txt);
    }
    txt.remove("^^");
    return txt;
}

QString resoudreOrAnd(QString txt){
    QRegExp rx("\\(([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)*\\)(\\.[a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)+(?![\\^\\.])", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    QString A, B, C, D;
    int first, last;
    first = rx.indexIn(txt);
    if (first != -1)
    {
        last = rx.matchedLength();
        A = txt.mid(0,first);
        B = rx.cap(0);
        QRegExp rxand("\\)(\\.[a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)+");
        int f = rxand.indexIn(B)+1;
        D = rxand.cap().remove(0,1);
        B.remove(f,rxand.matchedLength());
        B.replace(")",D+")");
        B.replace("+",D+"+");
        C = txt.mid(first+last);
        txt = A + B + C;
    }
    return txt;
}

QString resoudreAndOr(QString txt){
    QRegExp rx("([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?\\.)+\\(([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)*\\)(?![\\^\\.])", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    QString A, B, C, D;
    int first, last;
    first = rx.indexIn(txt);
    if (first != -1)
    {
        last = rx.matchedLength();
        A = txt.mid(0,first);
        B = rx.cap(0);
        QRegExp rxand("([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?\\.)+");
        int f = rxand.indexIn(B);
        D = rxand.cap();
        B.remove(f,rxand.matchedLength());
        B.replace("+","+"+D);
        B.replace("(","("+D);
        C = txt.mid(first+last);
        txt = A + B + C;
    }
    return txt;
}

QString resoudreAndAnd(QString txt){
    QRegExp rx("\\(([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)*\\)\\.\\(([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)*\\)(?![\\^])", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    QString A, B, C, D, E;
    int first, last;
    first = rx.indexIn(txt);
    if (first != -1)
    {
        last = rx.matchedLength();
        A = txt.mid(0,first);
        B = rx.cap(0);
        QRegExp rxand("\\(([a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*[\\^]?)*\\)\\.");
        int f = rxand.indexIn(B);
        D = rxand.cap();
        B.remove(f,rxand.matchedLength());
        B.replace("+","+"+D);
        B = "(" + D + B.remove(0,1);
        C = txt.mid(first+last);
        txt = A + B + C;
    }
    return txt;
}

QString fnd(QString txt){
    QRegExp canon("([a-zA-Z]+[a-zA-Z0-9_]*(\\^)?([\\+\\.][a-zA-Z]+[a-zA-Z0-9_]*(\\^)?)*)?[:][a-zA-Z]+[a-zA-Z0-9_]*(\\^)?(\\,[a-zA-Z]+[a-zA-Z0-9_]*(\\^)?)*", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    int i = 0;
    while (!canon.exactMatch(txt) && i<1000)
    {
        txt = nettoyerOr (txt);
        txt = resoudreAndAnd (txt);
        txt = resoudreAndOr (txt);
        txt = resoudreOrAnd (txt);
        i++;
    }
    txt = simplifier (txt);
    if (canon.exactMatch(txt))
         return txt;
    else
        return "#";
}

QStringList detailler (QString txt, QString inhibitory){
    QStringList answer;
    QStringList list = txt.split(QRegExp("[:,]"));
    QStringList list2 = list[0].split(QRegExp("[+]"));
    if (!inhibitory.isEmpty())
        inhibitory = inhibitory + "!";
    for (int i=1; i < list.length(); i++){
        if (!list2.empty())
            for (int j=0; j < list2.length(); j++) {
                answer.append(inhibitory + list2[j] + ":" + list[i]);
            }
        else
            answer.append(inhibitory + list[0] + ":" + list[i]);
    }
    return answer;
}

QString verifier(QString txt){
    QRegExp rx("([\\(]*[a-zA-Z]+[a-zA-Z0-9_]*[\\)]*[\\+\\.]?[\\)]*)*([\\(]*[a-zA-Z]+[a-zA-Z0-9_]*[\\)]*)?[:][a-zA-Z]+[a-zA-Z0-9_]*(\\,[a-zA-Z]+[a-zA-Z0-9_]?)*", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    if(rx.exactMatch(txt)){
        if(rx.cap(0).count("(") !=  rx.cap(0).count(")")){
            if (rx.cap(0).count("(") >  rx.cap(0).count(")"))
               return "too many opening parenthesis in > " + txt.toUtf8() + "\n";
            else
               return "too many closing parenthesis in > " + txt.toUtf8() + "\n";
        }
        if (rx.cap(0).count("[") !=  rx.cap(0).count("]")){
            if (rx.cap(0).count("[") >  rx.cap(0).count("]"))
                return "too many opening square bracket in > " + txt.toUtf8() + "\n";
            else
                return "too many closing square bracket in > " + txt.toUtf8() + "\n";
        }
    }
    else{
        return "syntax error in > " + txt.toUtf8() + "\n";
    }
    return "";
}

QStringList extraireFormules(QString txt){
    txt.remove(QRegExp("//[^\\n]*[\\n]|$", Qt::CaseSensitive, QRegExp::W3CXmlSchema11));
    QRegExp rx("[^:]*[:]\\s*[a-zA-Z]+[a-zA-Z0-9_]*(\\,[\\s]*[a-zA-Z]+[a-zA-Z0-9_]*)*[\\s]*", Qt::CaseSensitive, QRegExp::W3CXmlSchema11);
    QString eq;
    QString error;
    QStringList list;
    int first, last;
    first = rx.indexIn(txt);
    while (first != -1){
        if (first != 0){
            list.append("#");
            first = -1;
        }
        else {
            eq = rx.cap(0).remove(QRegExp("[\\s$]"));
            QString inhibitory;
            if (eq.contains("!"))
                inhibitory = eq.left(eq.indexOf("!"));
            eq.remove(0, eq.indexOf("!")+1);
            error = verifier(eq);
            if (error == "") {
                list = list + detailler(fnd(eq), inhibitory);
                last = rx.matchedLength();
                txt = txt.mid(first+last);
                first = rx.indexIn(txt);
            }
            else {
                list.append("#"+error);
                first = -1;
            }
        }
    }
    list.removeDuplicates();
    if (list.join("|").replace("^|","|").split("|").removeDuplicates() > 0)
        list.append("#error unknown");
    return list;
}

static xmlNsPtr ns = NULL;

QString getDictionnary (const QStringList list, const xmlXPathObjectPtr xpathResPremises, const xmlXPathObjectPtr xpathResConclusions, QStringList* premises, QStringList* conclusions){
    conclusions->clear();
    premises->clear();
    for (int i = 0; i < list.length(); i++){
        conclusions->append(list[i].mid(list[i].indexOf(':') + 1));
        premises->append(list[i].left(list[i].indexOf(':')).split(QRegExp("[!.+]")));
    }
    conclusions->removeDuplicates();
    premises->removeDuplicates();
    QStringList temp = *premises;
    for (int i=xpathResPremises->nodesetval->nodeNr-1; i >= 0; i--){
        const QString name = QString((const char*)xmlGetProp(xpathResPremises->nodesetval->nodeTab[i], (const xmlChar*) "name"));
        const int pos = premises->indexOf(name);
        if (pos >= 0){
            premises->move(pos, i);
            temp.removeOne(name);
        }
    }
    if (!temp.isEmpty())
        return "undeclared premises are present in the formulas > " + temp.join(",");
    temp = *conclusions;
    for (int i=xpathResConclusions->nodesetval->nodeNr-1; i >= 0; i--){
        const QString name = QString((const char*)xmlGetProp(xpathResConclusions->nodesetval->nodeTab[i], (const xmlChar*) "name"));
        const int pos = conclusions->indexOf(name);
        if (pos >= 0){
            conclusions->move(pos, i);
            temp.removeOne(name);
        }
    }
    if (!temp.isEmpty())
        return "undeclared conclusions are present in the formulas > " + temp.join(",");
    else
        return "";
}

void addRule(const xmlNodePtr formulation, const xmlXPathContextPtr ctxt, const QString formula,  const xmlXPathObjectPtr xpathResPremises, const xmlXPathObjectPtr xpathResConclusions, QStringList* premises, QStringList* conclusions){
    int indexInhibition = formula.indexOf('!');
    if (indexInhibition < 0)
        indexInhibition = 0;
    const QStringList premisesInhibitory = formula.left(indexInhibition).split('+', QString::SkipEmptyParts);
    const QStringList premisesExictatory = formula.mid(formula.indexOf('!')+1).split(QRegExp ("[+.:]"), QString::SkipEmptyParts).mid(0,formula.count(QRegExp ("[+!.:]")) - premisesInhibitory.length());
    xmlNodePtr conclusion = xmlCopyNode(xpathResConclusions->nodesetval->nodeTab[conclusions->indexOf(formula.mid(formula.indexOf(':')+1))], 1);
    xmlNodePtr rule = xmlNewNode(ns, (const xmlChar*) "rule");
    xmlAddChild (formulation, rule);
    xmlChar * value;
    value = xmlGetProp(conclusion, (const xmlChar*)"relevance");
    if (value){
        xmlSetProp(rule, (const xmlChar*)"relevance", value);
        xmlRemoveProp(xmlSetProp(conclusion, (const xmlChar*)"relevance", NULL));
    }
    value = xmlGetProp(conclusion, (const xmlChar*)"fitting_nbr");
    if (value){
        xmlSetProp(rule, (const xmlChar*)"fitting_nbr", value);
        xmlRemoveProp(xmlSetProp(conclusion, (const xmlChar*)"fitting_nbr", NULL));
    }
    xmlRemoveProp(xmlSetProp(conclusion, (const xmlChar*)"name", NULL));
    xmlXPathObjectPtr xpathResID = xmlXPathEvalExpression((const xmlChar*)"generate-id(formulation:formulation/formulation:rule[position()=last()])", ctxt);
    xmlSetProp(rule, (const xmlChar*)"name", xmlXPathCastToString(xpathResID));
    xmlNodePtr premise;
    for (int i = 0; i < premisesInhibitory.length(); i++){
        premise = xmlCopyNode(xpathResPremises->nodesetval->nodeTab[premises->indexOf(premisesInhibitory[i])], 1);
        xmlSetProp(premise, (const xmlChar*)"inhibitor", (const xmlChar*)"true");
        xmlRemoveProp(xmlSetProp(premise, (const xmlChar*)"name", NULL));
        xmlAddChild (rule, premise);
    }
    for (int i = 0; i < premisesExictatory.length(); i++){
        premise = xmlCopyNode(xpathResPremises->nodesetval->nodeTab[premises->indexOf(premisesExictatory[i])], 1);
        xmlRemoveProp(xmlSetProp(premise, (const xmlChar*)"name", NULL));
        xmlAddChild (rule, premise);
    }
    xmlAddChild (rule, conclusion);
}

void macroActualize(const char* file){
    QFile error("macro.log");
    error.open(QFile::WriteOnly);
    xmlSubstituteEntitiesDefault (1);
    xmlLoadExtDtdDefaultValue = 1;
    xmlDocPtr doc;
    doc = xmlReadFile (file, "UTF-8", 0 );
    xmlXPathInit();
    exsltFuncRegister ();
    xmlXPathContextPtr ctxt = xmlXPathNewContext(doc);
    xsltRegisterAllFunctions (ctxt);
    xmlXPathRegisterNs(ctxt, (const xmlChar*)"sdk", (const xmlChar*)"http://www.nomoseed.org/sdk");
    xmlXPathRegisterNs(ctxt, (const xmlChar*)"formulation", (const xmlChar*)"http://www.nomoseed.org/formulation");
    xmlXPathObjectPtr xpathResMacros = xmlXPathEvalExpression((const xmlChar*)"/*/*/sdk:macro[@active='true' and formulation:formulation]", ctxt);
    for (int i=0; i < xpathResMacros->nodesetval->nodeNr; i++){
        ctxt->node = xpathResMacros->nodesetval->nodeTab[i];
        xmlXPathObjectPtr xpathRes = xmlXPathEvalExpression((const xmlChar*)"formulation:formulation/formulation:formulas/text()", ctxt);
        QString code = QString((char*) xmlXPathCastToString(xpathRes));
        xmlXPathFreeObject(xpathRes);
        QStringList conjonctions = extraireFormules(code);
        QStringList premises;
        QStringList conclusions;
        if (conjonctions[0][0]=='#'){
            const QString scheme((char*) xmlGetProp(xpathResMacros->nodesetval->nodeTab[i], (const xmlChar*)"scheme"));
            error.write("Macro formulation error in " + scheme.toUtf8() + ": " + conjonctions[0].mid(1).toUtf8());
            error.close();
            xmlXPathFreeObject(xpathResMacros);
            xmlXPathFreeContext(ctxt);
            xmlFreeDoc (doc);
            xmlCleanupParser ();
            return;
        }
        else{
            xmlXPathObjectPtr xpathResConclusions = xmlXPathEvalExpression((const xmlChar*)"formulation:formulation/formulation:declarations/formulation:conclusion", ctxt);
            xmlXPathObjectPtr xpathResPremises = xmlXPathEvalExpression((const xmlChar*)"formulation:formulation/formulation:declarations/formulation:premise", ctxt);
            xmlXPathObjectPtr xpathResFormulation = xmlXPathEvalExpression((const xmlChar*)"formulation:formulation", ctxt);
            ns = xmlSearchNsByHref(doc, xpathResFormulation->nodesetval->nodeTab[0], (const xmlChar*)"http://www.nomoseed.org/formulation");
            const QString result = getDictionnary (conjonctions, xpathResPremises, xpathResConclusions, &premises, &conclusions);
            if (result != ""){
                const QString name((char*) xmlGetProp(xpathResMacros->nodesetval->nodeTab[i], (const xmlChar*)"name"));
                error.write("Macro formulation error in " + name.toUtf8() + ": " + result.toUtf8());
                error.close();
                xmlXPathFreeObject (xpathResMacros);
                xmlXPathFreeContext (ctxt);
                xmlFreeDoc (doc);
                xmlCleanupParser ();
                return;
            }
            else
                for (int j=0; j < conjonctions.length(); j++)
                    addRule (xpathResFormulation->nodesetval->nodeTab[0], ctxt, conjonctions[j], xpathResPremises, xpathResConclusions, &premises, &conclusions);
            xmlXPathFreeObject(xpathResConclusions);
            xmlXPathFreeObject(xpathResPremises);
            xmlXPathFreeObject(xpathResFormulation);
        }
        conjonctions.clear();
  }
    xmlSaveFileEnc (file, doc, "UTF-8");
    xmlXPathFreeObject(xpathResMacros);
    xmlXPathFreeContext(ctxt);
    xmlFreeDoc (doc);
    error.close();
    xmlCleanupParser ();
}
