/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstring>
#include <iostream>
#include <fstream>
#include <memory>
#include <set>
#include <vector>

using namespace std;

// Info about a Visit* function in a plugin.
struct VisitFunctionInfo
{
    string name;
    string argument;
};


// Info about a Traverse* function in a plugin.
struct TraverseFunctionInfo
{
    string name;
    string argument;
    bool hasPre = false;
    bool hasPost = false;
};

struct VisitFunctionInfoLess
{
    bool operator()( const VisitFunctionInfo& l, const VisitFunctionInfo& r ) const
    {
        return l.name < r.name;
    }
};

struct TraverseFunctionInfoLess
{
    bool operator()( const TraverseFunctionInfo& l, const TraverseFunctionInfo& r ) const
    {
        return l.name < r.name;
    }
};


// Information about each LO plugin.
struct PluginInfo
{
    string className; // e.g. "BadStatics"
    string variableName; // e.g. "badStatics"
    string lowercaseName;
    bool shouldVisitTemplateInstantiations;
    bool shouldVisitImplicitCode;
    set< VisitFunctionInfo, VisitFunctionInfoLess > visitFunctions;
    set< TraverseFunctionInfo, TraverseFunctionInfoLess > traverseFunctions;
};

// We need separate visitors for shouldVisitTemplateInstantiations and shouldVisitImplicitCode,
// so split plugins into groups by what they should visit.
// It seems that trying to handle the shouldVisit* functionality with just one visitor
// is tricky.
enum PluginType
{
    PluginBasic,
    PluginVisitTemplates,
    PluginVisitImplicit,
    PluginVisitTemplatesImplicit,
};

const int Plugin_Begin = PluginBasic;
const int Plugin_End = PluginVisitTemplatesImplicit + 1;
static const char* const pluginTypeNames[ Plugin_End ]
    = { "Basic", "VisitTemplates", "VisitImplicit", "VisitTemplatesImplicit" };

static vector< PluginInfo > plugins[ Plugin_End ];


void generateVisitor( PluginType type );

void generate()
{
    ostream& output = cout;
    output <<
"// This file is autogenerated. Do not modify.\n"
"// Generated by compilerplugins/clang/sharedvisitor/generator.cxx .\n"
"\n"
"#ifdef LO_CLANG_SHARED_PLUGINS\n"
"\n"
"#include <config_clang.h>\n"
"\n"
"#include <clang/AST/ASTContext.h>\n"
"#include <clang/AST/RecursiveASTVisitor.h>\n"
"\n"
"#include \"plugin.hxx\"\n"
"\n";

    output << "#undef LO_CLANG_SHARED_PLUGINS // to get sources of individual plugins\n";
    for( const auto& pluginGroup : plugins )
        for( const PluginInfo& plugin : pluginGroup )
            output << "#include \"" << plugin.lowercaseName << ".cxx\"" << endl;

    output <<
"\n"
"using namespace clang;\n"
"using namespace llvm;\n"
"\n"
"namespace loplugin\n"
"{\n";

    for( int type = Plugin_Begin; type < Plugin_End; ++type )
        generateVisitor( static_cast< PluginType >( type ));

    output <<
"} // namespace loplugin\n"
"\n"
"#endif // LO_CLANG_SHARED_PLUGINS\n";
}

void generateVisitor( PluginType type )
{
    if( plugins[ type ].empty())
        return;
    ostream& output = cout;
    output <<
"\n"
"class SharedRecursiveASTVisitor" << pluginTypeNames[ type ] << "\n"
"    : public FilteringPlugin< SharedRecursiveASTVisitor" << pluginTypeNames[ type ] << ">\n"
"{\n"
"public:\n"
"    explicit SharedRecursiveASTVisitor" << pluginTypeNames[ type ] << "(const InstantiationData& rData)\n"
"        : FilteringPlugin(rData)\n";
    for( const PluginInfo& plugin : plugins[ type ] )
        output << "        , " << plugin.variableName << "( nullptr )\n";
    output << "        {}\n";

    output <<
"    virtual bool preRun() override\n"
"    {\n";
    for( const PluginInfo& plugin : plugins[ type ] )
    {
        output << "        if( " << plugin.variableName << " && !" << plugin.variableName << "->preRun())\n";
        // This will disable the plugin for the rest of the run.
        output << "            " << plugin.variableName << " = nullptr;\n";
    }
    output <<
"        return anyPluginActive();\n"
"    }\n";

    output <<
"    virtual void postRun() override\n"
"    {\n";
    for( const PluginInfo& plugin : plugins[ type ] )
    {
        output << "        if( " << plugin.variableName << " )\n";
        output << "            " << plugin.variableName << "->postRun();\n";
    }
    output <<
"    }\n";

    output <<
"    virtual void run() override {\n"
"        if (preRun()) {\n"
"            TraverseDecl(compiler.getASTContext().getTranslationUnitDecl());\n"
"            postRun();\n"
"        }\n"
"    }\n"
"    enum { isSharedPlugin = true };\n";

    output <<
"    virtual bool setSharedPlugin( Plugin* plugin, const char* name ) override\n"
"    {\n";
    bool first = true;
    for( const PluginInfo& plugin : plugins[ type ] )
    {
        output << "        ";
        if( !first )
            output << "else ";
        first = false;
        output << "if( strcmp( name, \"" << plugin.lowercaseName << "\" ) == 0 )\n";
        output << "            " << plugin.variableName << " = static_cast< " << plugin.className << "* >( plugin );\n";
    }
    output <<
"        else\n"
"            return false;\n"
"        return true;\n"
"    }\n";

    if( type == PluginVisitTemplates || type == PluginVisitTemplatesImplicit )
        output << "bool shouldVisitTemplateInstantiations() const { return true; }\n";
    if( type == PluginVisitImplicit || type == PluginVisitTemplatesImplicit )
        output << "bool shouldVisitImplicitCode() const { return true; }\n";

    set< VisitFunctionInfo, VisitFunctionInfoLess > visitFunctions;
    for( const PluginInfo& plugin : plugins[ type ] )
        for( const VisitFunctionInfo& visit : plugin.visitFunctions )
            visitFunctions.insert( visit );
    for( const VisitFunctionInfo& visit : visitFunctions )
    {
        output << "    bool " << visit.name << "(" << visit.argument << " arg)\n";
        output <<
"    {\n"
"        if( ignoreLocation( arg ))\n"
"            return true;\n";
        for( const PluginInfo& plugin : plugins[ type ] )
        {
            if( plugin.visitFunctions.find( visit ) == plugin.visitFunctions.end())
                continue;
            output << "        if( " << plugin.variableName << " != nullptr ";
            output << ")\n";
            output << "        {\n";
            output << "            if( !" << plugin.variableName << "->" << visit.name << "( arg ))\n";
            // This will disable the plugin for the rest of the run (as would returning false
            // from Visit* normally do in the non-shared case).
            output << "                " << plugin.variableName << " = nullptr;\n";
            output << "        }\n";
        }
        output <<
"        return anyPluginActive();\n"
"    }\n";
    }

    set< TraverseFunctionInfo, TraverseFunctionInfoLess > traverseFunctions;
    for( const PluginInfo& plugin : plugins[ type ] )
        for( const TraverseFunctionInfo& traverse : plugin.traverseFunctions )
            traverseFunctions.insert( traverse );
    for( const TraverseFunctionInfo& traverse : traverseFunctions )
    {
        output << "    bool " << traverse.name << "(" << traverse.argument << " arg)\n";
        output << "    {\n";
        for( const PluginInfo& plugin : plugins[ type ] )
        {
            auto pluginTraverse = plugin.traverseFunctions.find( traverse );
            if( pluginTraverse == plugin.traverseFunctions.end())
                continue;
            output << "        " << plugin.className << "* save" << plugin.className << " = " << plugin.variableName << ";\n";
            if( pluginTraverse->hasPre )
            {
                output << "        if( " << plugin.variableName << " != nullptr ";
                output << ")\n";
                output << "        {\n";
                output << "            if( !" << plugin.variableName << "->Pre" << traverse.name << "( arg ))\n";
                // This will disable the plugin for the time of the traverse, until restored later,
                // just like directly returning from Traverse* would skip that part.
                output << "                " << plugin.variableName << " = nullptr;\n";
                output << "        }\n";
            }
        }
        output << "        bool ret = RecursiveASTVisitor::" << traverse.name << "( arg );\n";
        for( const PluginInfo& plugin : plugins[ type ] )
        {
            auto pluginTraverse = plugin.traverseFunctions.find( traverse );
            if( pluginTraverse == plugin.traverseFunctions.end())
                continue;
            if( pluginTraverse->hasPost )
            {
                output << "        if( " << plugin.variableName << " != nullptr ";
                output << ")\n";
                output << "        {\n";
                output << "            if( !" << plugin.variableName << "->Post" << traverse.name << "( arg, ret ))\n";
                // This will disable the plugin for the rest of the run.
                output << "                save" << plugin.className << " = nullptr;\n";
                output << "        }\n";
            }
            output << "        " << plugin.variableName << " = save" << plugin.className << ";\n";
        }
        output << "        return ret;\n";
        output << "    }\n";
    }

    output <<
"private:\n";

    output <<
"    bool anyPluginActive() const\n"
"    {\n";
    first = true;
    for( const PluginInfo& plugin : plugins[ type ] )
    {
        if( first )
            output << "        return " << plugin.variableName << " != nullptr";
        else
            output << "\n            || " << plugin.variableName << " != nullptr";
        first = false;
    }
    output << ";\n";
    output << "    }\n";

    for( const PluginInfo& plugin : plugins[ type ] )
        output << "    " << plugin.className << "* " << plugin.variableName << ";\n";

    output <<
"};\n"
"\n"
"loplugin::Plugin::Registration< SharedRecursiveASTVisitor" << pluginTypeNames[ type ]
    << " > registration" << pluginTypeNames[ type ] << "(\"sharedvisitor" << pluginTypeNames[ type ] << "\");\n"
"\n";
}

static string getValue( const string& line, const char* tag )
{
    size_t taglen = strlen( tag );
    if( line.size() < taglen + 2 )
        return string();
    if( line.compare( 0, taglen, tag ) != 0 )
        return string();
    if( line[ taglen ] != ':' )
        return string();
    return line.substr( taglen + 1 );
}

static bool readFile( const string& fileName )
{
    ifstream file( fileName );
    if( !file )
    {
        cerr << "Cannot open file " << fileName << endl;
        return false;
    }
    PluginInfo pluginInfo;
    string line;
    do
    {
        getline( file, line );
    } while( !line.empty() && line[ 0 ] == '#' );
    string version = getValue( line, "InfoVersion" );
    if( version != "1" )
    {
        cerr << "Incorrect version '" << version << "'" << endl;
        return false;
    }
    getline( file, line );
    pluginInfo.className = getValue( line, "ClassName" );
    pluginInfo.variableName = pluginInfo.className;
    assert( pluginInfo.variableName.size() > 0 );
    pluginInfo.variableName[ 0 ] = tolower( pluginInfo.variableName[ 0 ] );
    pluginInfo.lowercaseName = pluginInfo.className;
    for( char& c : pluginInfo.lowercaseName )
        c = tolower( c );
    pluginInfo.shouldVisitTemplateInstantiations = false;
    pluginInfo.shouldVisitImplicitCode = false;
    bool endOk = false;
    for(;;)
    {
        string line;
        getline( file, line );
        if( file.eof() || !file )
        {
            cerr << "Unexpected end of file" << endl;
            return false;
        }
        if( line.empty())
            continue;
        if( line == "InfoEnd" )
        {
            endOk = true;
            break;
        }
        else if( line == "VisitFunctionStart" )
        {
            VisitFunctionInfo visitInfo;
            getline( file, line );
            visitInfo.name = getValue( line, "VisitFunctionName" );
            getline( file, line );
            visitInfo.argument = getValue( line, "VisitFunctionArgument" );
            getline( file, line );
            if( line != "VisitFunctionEnd" )
            {
                cerr << "Missing VisitFunctionEnd" << endl;
                return false;
            }
            pluginInfo.visitFunctions.insert( move( visitInfo ));
        }
        else if( line == "TraverseFunctionStart" )
        {
            TraverseFunctionInfo traverseInfo;
            getline( file, line );
            traverseInfo.name = getValue( line, "TraverseFunctionName" );
            getline( file, line );
            traverseInfo.argument = getValue( line, "TraverseFunctionArgument" );
            getline( file, line );
            traverseInfo.hasPre = getValue( line, "TraverseFunctionHasPre" ) == "1";
            getline( file, line );
            traverseInfo.hasPost = getValue( line, "TraverseFunctionHasPost" ) == "1";
            getline( file, line );
            if( line != "TraverseFunctionEnd" )
            {
                cerr << "Missing TraverseFunctionEnd" << endl;
                return false;
            }
            pluginInfo.traverseFunctions.insert( move( traverseInfo ));
        }
        else
        {
            string value;
            value = getValue( line, "ShouldVisitTemplateInstantiations" );
            if( value == "1" )
                pluginInfo.shouldVisitTemplateInstantiations = true;
            else
            {
                value = getValue( line, "ShouldVisitImplicitCode" );
                if( value == "1" )
                    pluginInfo.shouldVisitImplicitCode = true;
                else
                {
                    cerr << "Unknown line " << line << endl;
                    return false;
                }
            }
        }
    }

    assert( endOk );
    (void)endOk;

    if( pluginInfo.shouldVisitTemplateInstantiations && pluginInfo.shouldVisitImplicitCode )
        plugins[ PluginVisitTemplatesImplicit ].push_back( move( pluginInfo ));
    else if( pluginInfo.shouldVisitTemplateInstantiations )
        plugins[ PluginVisitTemplates ].push_back( move( pluginInfo ));
    else if( pluginInfo.shouldVisitImplicitCode )
        plugins[ PluginVisitImplicit ].push_back( move( pluginInfo ));
    else
        plugins[ PluginBasic ].push_back( move( pluginInfo ));

    return true;
}

int main(int argc, char** argv)
{
    for( int i = 1 ; i < argc; ++i )
    {
        if( !readFile( argv[ i ] ))
        {
            cerr << "Error reading " << argv[ i ] << endl;
            return 1;
        }
    }
    for( int type = Plugin_Begin; type < Plugin_End; ++type )
    {
        sort( plugins[ static_cast< PluginType >( type ) ].begin(), plugins[ static_cast< PluginType >( type ) ].end(),
            []( const PluginInfo& l, const PluginInfo& r ) { return l.className < r.className; } );
    }
    generate();
    return 0;
}
