/*
 * This file is part of Soprano Project
 *
 * Copyright (C) 2007-2011 Sebastian Trueg <trueg@kde.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include <QtCore/QCoreApplication>
#include <QtCore/QTime>
#include <QtCore/QTextStream>
#include <QtCore/QDebug>
#include <QtCore/QList>
#include <QtCore/QDir>
#include <QtCore/QRegExp>

#include "../soprano/statementiterator.h"
#include "../soprano/queryresultiterator.h"
#include "../soprano/nodeiterator.h"
#include "../soprano/version.h"
#include "../soprano/pluginmanager.h"
#include "../soprano/parser.h"
#include "../soprano/node.h"
#include "../soprano/graph.h"
#include "../soprano/global.h"
#include "../soprano/vocabulary/rdfs.h"
#include "../soprano/vocabulary/rdf.h"
#include "../soprano/vocabulary/nrl.h"


using namespace Soprano;

static const char* LGPL_HEADER = "/*\n"
                                 " * This file has been generated by the onto2vocabularyclass tool\n"
                                 " * copyright (C) 2007-2010 Sebastian Trueg <trueg@kde.org>\n"
                                 " *\n"
                                 " * This library is free software; you can redistribute it and/or\n"
                                 " * modify it under the terms of the GNU Library General Public\n"
                                 " * License as published by the Free Software Foundation; either\n"
                                 " * version 2 of the License, or (at your option) any later version.\n"
                                 " *\n"
                                 " * This library is distributed in the hope that it will be useful,\n"
                                 " * but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
                                 " * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n"
                                 " * Library General Public License for more details.\n"
                                 " *\n"
                                 " * You should have received a copy of the GNU Library General Public License\n"
                                 " * along with this library; see the file COPYING.LIB.  If not, write to\n"
                                 " * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n"
                                 " * Boston, MA 02110-1301, USA.\n"
                                 " */\n";

#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
// In Qt4, QUrl is not threadsafe, even for "readonly" concurrent accesses from multiple threads,
// because the parsing happens on demand, internally in QUrl.
#define USE_THREAD_STORAGE 1
#else
#define USE_THREAD_STORAGE 0
#endif

#define VERSION "1.2"

int version()
{
    QTextStream s( stderr );
    s << "onto2vocabularyclass " << VERSION << " (using Soprano " << Soprano::versionString() << ")" << endl;
    s << "   Copyright (C) 2007-2011 Sebastian Trueg <trueg@kde.org>" << endl;
    s << "   This program is free software; you can redistribute it and/or modify" << endl
      << "   it under the terms of the GNU General Public License as published by" << endl
      << "   the Free Software Foundation; either version 2 of the License, or" << endl
      << "   (at your option) any later version." << endl;

    return 0;
}


int usage( const QString& error = QString() )
{
    version();

    QTextStream s( stderr );
    s << endl;
    s << "Usage:" << endl
      << "   onto2vocabularyclass --name <name> --encoding <encoding> [--namespace <ns>] [--export-module <module>] [--no-visibility-export] <ontologyfile>" << endl;

    if ( !error.isEmpty() ) {
        s << endl << error << endl;
        return 0;
    }
    else {
        return 1;
    }
}


QString createIndent( int indent )
{
    QString s;
    for ( int i = 0; i < indent; ++i ) {
        s += "    ";
    }
    return s;
}


QString writeComment( const QString& comment, int indent )
{
    static const int maxLine = 50;

    QString s;

    if( !comment.isEmpty() ) {
        s += createIndent( indent );
        s += "/**\n";

        QStringList paragraphs = comment.split( '\n', QString::KeepEmptyParts );
        Q_FOREACH( const QString &paragraph, paragraphs ) {
            s += createIndent( indent ) + " * ";
            QStringList words = paragraph.split( QRegExp("\\s"), QString::SkipEmptyParts );
            int cnt = 0;
            for( int i = 0; i < words.count(); ++i ) {
                if( cnt >= maxLine ) {
                    s += '\n'
                         + createIndent( indent ) + " * ";
                    cnt = 0;
                }

                s += words[i] + ' ';
                cnt += words[i].length();
            }
            s += '\n';
        }


        s += createIndent( indent ) + " */";
    }

    return s;
}


QString normalizeName( const QString& name )
{
    QString normalized(name);
    // TODO: add more invalid characters here
    normalized.replace( '-', QString() );
    normalized.replace( '.', QLatin1String("_") );
    return normalized;
}

QString disambiguateKeyword( const QString& name, const QString &className )
{
    Q_ASSERT( !name.isEmpty() );
    // TODO: add more C++ keywords that are not usable as method names
    const QStringList keywords
        = QStringList() << QLatin1String( "class" )
                        << QLatin1String( "long" );
    if ( keywords.contains( name ) )
        return className.toLower() + name.left( 1 ).toUpper() + name.mid( 1 );
    return name;
}

QList<Soprano::Node> extractRelevantResources( const Soprano::Graph& graph )
{
    // we are still using redland for memory storage. Thus, we cannot use graph queries since redland does not support them fully:
    QSet<Node> resources;
    StatementIterator it = graph.listStatements( Node(), Vocabulary::RDF::type(), Node() );
    while ( it.next() ) {
        Statement s( *it );
        if ( !s.context().isValid() ||
             !graph.containsStatement( s.context(), Vocabulary::RDF::type(), Vocabulary::NRL::GraphMetadata() ) ) {
            resources << s.subject();
        }
    }
    return resources.toList();
}


int main( int argc, char *argv[] )
{
    QCoreApplication app( argc, argv );

    QStringList args = app.arguments();
    QString fileName;
    QString className;
    QString namespaceName;
    QString encoding;
    QString exportModule = "soprano";
    bool visibilityExport = true;
    int i = 1;
    while ( i < args.count() ) {
        if ( args[i] == "--encoding" ) {
            ++i;
            if ( i < args.count() ) {
                encoding = args[i];
            }
            else {
                return usage();
            }
        }
        else if ( args[i] == "--name" ) {
            ++i;
            if ( i < args.count() ) {
                className = args[i];
            }
            else {
                return usage();
            }
        }
        else if ( args[i] == "--namespace" ) {
            ++i;
            if ( i < args.count() ) {
                namespaceName = args[i];
            }
            else {
                return usage();
            }
        }
        else if ( args[i] == "--export-module" ) {
            ++i;
            if ( i < args.count() ) {
                exportModule = args[i];
            }
            else {
                return usage();
            }
        }
        else if ( args[i] == "--no-visibility-export" ) {
            visibilityExport = false;
        }
        else if ( i == args.count()-1 ) {
            fileName = args[i];
        }
        else {
            QTextStream s( stderr );
            s << "Unknown argument:" << args[i] << endl << endl;
            return usage();
        }
        ++i;
    }

    if ( fileName.isEmpty() ) {
        return usage();
    }
    if ( encoding.isEmpty() ) {
        return usage();
    }

    if ( !QFile::exists( fileName ) ) {
        QTextStream s( stderr );
        s << "Could not find file " << fileName << endl;
        return 1;
    }

    const Parser* parser = PluginManager::instance()->discoverParserForSerialization( mimeTypeToSerialization( encoding ), encoding );
    if ( !parser ) {
        QTextStream s( stderr );
        s << "Could not find parser plugin for encoding " << encoding << endl;
        return 1;
    }

    StatementIterator it = parser->parseFile( fileName, QUrl( "http://dummybaseuri.org" ), mimeTypeToSerialization( encoding ), encoding );
    if ( parser->lastError() ) {
        QTextStream s( stderr );
        s << "Failed to parse file" << fileName << "(" << parser->lastError() << ")" << endl;
        return 1;
    }

    Graph graph;
    while ( it.next() ) {
        graph.addStatement( *it );
    }

    QFile headerFile( className.toLower() + ".h" );
    QFile sourceFile( className.toLower() + ".cpp" );

    if ( !headerFile.open( QIODevice::WriteOnly ) ) {
        QTextStream s( stderr );
        s << "Failed to open file" << headerFile.fileName() << endl;
        return 1;
    }

    if ( !sourceFile.open( QIODevice::WriteOnly ) ) {
        QTextStream s( stderr );
        s << "Failed to open file" << sourceFile.fileName() << endl;
        return 1;
    }

    QTextStream headerStream( &headerFile );
    QTextStream sourceStream( &sourceFile );

    // select all relevant resource, try to be intelligent about it...
    QList<Node> allResources = extractRelevantResources( graph );

    // create entries
    // ----------------------------------------------------
    QMap<QString, QPair<QString, QString> > normalizedResources;
    QStringList done;
    foreach( const Node &resource, allResources ) {
        QString uri = resource.uri().toString();
        if ( !normalizedResources.contains( uri ) ) {
            QString name = resource.uri().fragment();
            if ( name.isEmpty() && !uri.contains( '#' ) ) {
                name = resource.uri().path().section( "/", -1 );
            }

            if ( !name.isEmpty() && !done.contains( name ) ) {
                normalizedResources.insert( uri, qMakePair( name, QString() ) );
                done += name;
            }
        }
    }

    // extract comments
    foreach( const Node &resource, allResources ) {
        StatementIterator it = graph.listStatements( resource, Soprano::Vocabulary::RDFS::comment(), Node() );
        if ( it.next() ) {
            if ( normalizedResources.contains( resource.toString() ) ) {
                normalizedResources[resource.toString()].second = it.current().object().literal().toString();
            }
        }
    }

    if ( normalizedResources.isEmpty() ) {
        QTextStream s( stderr );
        s << "Nothing found to export." << endl;
        return 1;
    }

    QString ontoNamespace;
    // We simplify and take it as granted that all resources have the same NS
    QUrl namespaceUri( normalizedResources.constBegin().key() );
    if ( namespaceUri.hasFragment() ) {
        namespaceUri.setFragment( QString() );
        ontoNamespace = namespaceUri.toString() + '#';
    }
    else {
        ontoNamespace = namespaceUri.toString().section( "/", 0, -2 ) + '/';
    }
    qDebug() << "namespace: " << ontoNamespace;

    QUrl nrlGraph;
    it = graph.listStatements( Node(), Vocabulary::RDF::type(), Vocabulary::NRL::Ontology() );
    if ( it.next() ) {
        nrlGraph = it.current().subject().uri();
        it.close();
    }
    // ----------------------------------------------------


    // write the header
    // ----------------------------------------------------
    headerStream << LGPL_HEADER << endl;

    headerStream << "#ifndef _SOPRANO_" << className.toUpper() << "_H_" << endl
                 << "#define _SOPRANO_" << className.toUpper() << "_H_" << endl << endl;

    headerStream << "#include <QtCore/QUrl>" << endl;

    if ( visibilityExport )
        headerStream << QString( "#include \"%1_export.h\"").arg(exportModule.toLower()) << endl;
    headerStream << endl;

    int indent = 0;
    if ( !namespaceName.isEmpty() ) {
        QStringList tokens = namespaceName.split( "::" );
        for ( int i = 0; i < tokens.count(); ++i ) {
            headerStream << createIndent( indent++ ) << "namespace " << tokens[i] << " {" << endl;
        }
    }

    // the class name
    headerStream << createIndent( indent++ ) << "namespace " << className << " {" << endl;

    // the onto namespace
    headerStream << createIndent( indent ) << "/**" << endl
                 << createIndent( indent ) << " * " << ontoNamespace << endl
                 << createIndent( indent ) << " */" << endl;

    headerStream << createIndent( indent );
    if ( visibilityExport )
        headerStream << QString( "%1_EXPORT " ).arg(exportModule.toUpper());
    headerStream << "QUrl " << className.toLower() << "Namespace();" << endl << endl;

    // the NRL graph
    if ( !nrlGraph.isEmpty() ) {
        headerStream << createIndent( indent ) << "/**" << endl
                     << createIndent( indent ) << " * " << nrlGraph.toString() << endl
                     << createIndent( indent ) << " */" << endl;
        headerStream << createIndent( indent );
        if ( visibilityExport )
            headerStream << QString( "%1_EXPORT " ).arg(exportModule.toUpper());
        headerStream << "QUrl nrlOntologyGraph();" << endl << endl;
    }

    for( QMap<QString, QPair<QString, QString> >::const_iterator it = normalizedResources.constBegin();
         it != normalizedResources.constEnd(); ++it ) {
        QString uri = it.key();
        QString name = normalizeName( it.value().first );
        QString comment = it.value().second;

        if ( comment.isEmpty() ) {
            headerStream << writeComment( uri, indent ) << endl;
        }
        else {
            headerStream << writeComment( uri + "\n\n" + comment, indent ) << endl;
        }
        headerStream << createIndent( indent );
        if ( visibilityExport )
            headerStream << QString( "%1_EXPORT " ).arg(exportModule.toUpper());
        headerStream << "QUrl " << disambiguateKeyword( name, className ) << "();" << endl;

        ++it;
        if ( it != normalizedResources.constEnd() ) {
            headerStream << endl;
        }
        --it;
    }

    // close the namespaces
    while ( indent > 0 ) {
        headerStream << createIndent( --indent ) << "}" << endl;
    }

    headerStream << endl << "#endif" << endl;
    // ----------------------------------------------------


    // write source
    // ----------------------------------------------------
    sourceStream << LGPL_HEADER << endl;
    sourceStream << "#include \"" << headerFile.fileName() << "\"" << endl;
#if USE_THREAD_STORAGE
    sourceStream << "#include <QThreadStorage>" << endl;
#endif
    sourceStream << endl;

    QString privateClassName = className[0].toUpper() + className.mid( 1 ).toLower() + "Private";
    QString singletonName = "s_" + className.toLower();

    sourceStream << "class " << privateClassName << endl
                 << "{" << endl
                 << "public:" << endl
                 << createIndent( 1 ) << privateClassName << "()" << endl
                 << createIndent( 2 ) << ": ";

    sourceStream << className.toLower() << "_namespace( QUrl::fromEncoded( \"" << ontoNamespace << "\", QUrl::StrictMode ) )," << endl;

    if ( !nrlGraph.isEmpty() )
        sourceStream << createIndent( 2 ) << "  nrlOntologyGraph( QUrl::fromEncoded( \"" << nrlGraph.toString() << "\", QUrl::StrictMode ) )," << endl;

    for( QMap<QString, QPair<QString, QString> >::const_iterator it = normalizedResources.constBegin();
         it != normalizedResources.constEnd(); ++it ) {
        QString uri = it.key();
        QString name = normalizeName( it.value().first );

        sourceStream << createIndent( 2 ) << "  " << className.toLower() << "_" << name << "( QUrl::fromEncoded( \"" << uri << "\", QUrl::StrictMode ) )";

        ++it;
        if ( it != normalizedResources.constEnd() ) {
            sourceStream << "," << endl;
        }
        --it;
    }

    sourceStream << " {" << endl
                 << createIndent( 1 ) << "}" << endl << endl;

    sourceStream << createIndent( 1 ) << "QUrl " << className.toLower() << "_namespace;" << endl;

    if ( !nrlGraph.isEmpty() )
        sourceStream << createIndent( 1 ) << "QUrl nrlOntologyGraph;" << endl;

    for( QMap<QString, QPair<QString, QString> >::const_iterator it = normalizedResources.constBegin();
         it != normalizedResources.constEnd(); ++it ) {
        QString name = normalizeName( it.value().first );
        sourceStream << createIndent( 1 ) << "QUrl " << className.toLower() << "_" << name << ";" << endl;
    }
    sourceStream << "};" << endl << endl;

#if USE_THREAD_STORAGE
    const QString qtsName = "qt" + singletonName;
    sourceStream << "QThreadStorage<" << privateClassName << " *> " << qtsName << ";" << endl;
    sourceStream << privateClassName << "* " << singletonName << "()" << endl
                 << "{" << endl
                 << createIndent( 1 ) << "if (!" << qtsName << ".hasLocalData())" << endl
                 << createIndent( 2 ) << qtsName << ".setLocalData(new " << privateClassName << ");" << endl
                 << createIndent( 1 ) << "return " << qtsName << ".localData();" << endl
                 << "}" << endl;
#else
    sourceStream << "Q_GLOBAL_STATIC( " << privateClassName << ", " << singletonName << " )" << endl;
#endif
    sourceStream << endl;

    sourceStream << "QUrl ";
    if ( !namespaceName.isEmpty() ) {
        sourceStream << namespaceName << "::";
    }
    sourceStream << className << "::" << className.toLower() << "Namespace()" << endl
                 << "{" << endl
                 << createIndent( 1 ) << "return " << singletonName << "()->" << className.toLower() << "_namespace;" << endl
                 << "}" << endl << endl;

    if ( !nrlGraph.isEmpty() ) {
        sourceStream << "QUrl ";
        if ( !namespaceName.isEmpty() ) {
            sourceStream << namespaceName << "::";
        }
        sourceStream << className << "::nrlOntologyGraph()" << endl
                     << "{" << endl
                     << createIndent( 1 ) << "return " << singletonName << "()->nrlOntologyGraph;" << endl
                     << "}" << endl << endl;
    }

    for( QMap<QString, QPair<QString, QString> >::const_iterator it = normalizedResources.constBegin();
         it != normalizedResources.constEnd(); ++it ) {
        QString name = normalizeName( it.value().first );

        sourceStream << "QUrl ";

        if ( !namespaceName.isEmpty() ) {
            sourceStream << namespaceName << "::";
        }

        sourceStream << className << "::" << disambiguateKeyword( name, className ) << "()" << endl
                     << "{" << endl
                     << createIndent( 1 ) << "return " << singletonName << "()->" << className.toLower() << "_" << name << ";" << endl
                     << "}" << endl;

        ++it;
        if ( it != normalizedResources.constEnd() ) {
            sourceStream << endl;
        }
        --it;
    }

    // ----------------------------------------------------


    return 0;
}
