/* -*- 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 "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/ADT/StringExtras.h"

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

#include "config_clang.h"
#include "../check.hxx"
#include "../check.cxx"
#include "../compat.hxx"

using namespace clang;
using namespace llvm;

using namespace loplugin;

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

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

static std::set< TraverseFunctionInfo, TraverseFunctionInfoLess > traverseFunctions;

class CheckFileVisitor
    : public RecursiveASTVisitor< CheckFileVisitor >
{
public:
    void setContext(ASTContext const& context) { context_ = &context; }

    bool VisitCXXRecordDecl(CXXRecordDecl *Declaration);

    bool TraverseNamespaceDecl(NamespaceDecl * decl)
    {
        // Skip non-LO namespaces the same way FilteringPlugin does.
        if( !ContextCheck( decl ).Namespace( "loplugin" ).GlobalNamespace()
            && !ContextCheck( decl ).AnonymousNamespace())
        {
            return true;
        }
        return RecursiveASTVisitor<CheckFileVisitor>::TraverseNamespaceDecl(decl);
    }

private:
    ASTContext const* context_ = nullptr;

    QualType unqualifyPointeeType(QualType type)
    {
        assert(context_ != nullptr);
        if (auto const t = type->getAs<clang::PointerType>())
        {
            return context_->getQualifiedType(
                context_->getPointerType(t->getPointeeType().getUnqualifiedType()),
                type.getQualifiers());
        }
        return type;
    }
};

static bool inheritsPluginClassCheck( const Decl* decl )
{
    return bool( DeclCheck( decl ).Class( "FilteringPlugin" ).Namespace( "loplugin" ).GlobalNamespace())
        || bool( DeclCheck( decl ).Class( "FilteringRewritePlugin" ).Namespace( "loplugin" ).GlobalNamespace());
}

static TraverseFunctionInfo findOrCreateTraverseFunctionInfo( StringRef name )
{
    TraverseFunctionInfo info;
    info.name = name.str();
    auto foundInfo = traverseFunctions.find( info );
    if( foundInfo != traverseFunctions.end())
    {
        info = std::move( *foundInfo );
        traverseFunctions.erase( foundInfo );
    }
    return info;
}

static bool foundSomething;

bool CheckFileVisitor::VisitCXXRecordDecl( CXXRecordDecl* decl )
{
    if( !isDerivedFrom( decl, inheritsPluginClassCheck ))
        return true;

    if( decl->getName() == "FilteringPlugin" || decl->getName() == "FilteringRewritePlugin" )
        return true;

    std::cout << "# This file is autogenerated. Do not modify." << std::endl;
    std::cout << "# Generated by compilerplugins/clang/sharedvisitor/analyzer.cxx ." << std::endl;
    std::cout << "InfoVersion:1" << std::endl;
    std::cout << "ClassName:" << decl->getName().str() << std::endl;
    traverseFunctions.clear();
    for( const CXXMethodDecl* method : decl->methods())
    {
        if( !method->getDeclName().isIdentifier())
            continue;
        if( method->isStatic() || method->getAccess() != AS_public )
            continue;
        if( compat::starts_with(method->getName(), "Visit" ))
        {
            if( method->getNumParams() == 1 )
            {
                std::cout << "VisitFunctionStart" << std::endl;
                std::cout << "VisitFunctionName:" << method->getName().str() << std::endl;
                std::cout << "VisitFunctionArgument:"
                    << unqualifyPointeeType(
                        method->getParamDecl( 0 )->getTypeSourceInfo()->getType()).getAsString()
                    << std::endl;
                std::cout << "VisitFunctionEnd" << std::endl;
            }
            else
            {
                std::cerr << "Unhandled Visit* function: " << decl->getName().str()
                     << "::" << method->getName().str() << std::endl;
                abort();
            }
        }
        else if( compat::starts_with(method->getName(), "Traverse" ))
        {
            if( method->getNumParams() == 1 )
            {
                TraverseFunctionInfo traverseInfo = findOrCreateTraverseFunctionInfo( method->getName());
                traverseInfo.argument = method->getParamDecl( 0 )->getTypeSourceInfo()->getType().getAsString();
                traverseFunctions.insert( std::move( traverseInfo ));
            }
            else
            {
                std::cerr << "Unhandled Traverse* function: " << decl->getName().str()
                     << "::" << method->getName().str() << std::endl;
                abort();
            }
        }
        else if( compat::starts_with(method->getName(), "PreTraverse" ))
        {
            TraverseFunctionInfo traverseInfo = findOrCreateTraverseFunctionInfo( method->getName().substr( 3 ));
            traverseInfo.hasPre = true;
            traverseFunctions.insert( std::move( traverseInfo ));
        }
        else if( compat::starts_with(method->getName(), "PostTraverse" ))
        {
                TraverseFunctionInfo traverseInfo = findOrCreateTraverseFunctionInfo( method->getName().substr( 4 ));
                traverseInfo.hasPost = true;
                traverseFunctions.insert( std::move( traverseInfo ));
        }
        else if( method->getName() == "shouldVisitTemplateInstantiations" )
            std::cout << "ShouldVisitTemplateInstantiations:1" << std::endl;
        else if (method->getName() == "shouldVisitImplicitCode")
            std::cout << "ShouldVisitImplicitCode:1" << std::endl;
        else if( compat::starts_with(method->getName(), "WalkUp" ))
        {
            std::cerr << "WalkUp function not supported for shared visitor: " << decl->getName().str()
                 << "::" << method->getName().str() << std::endl;
            abort();
        }
    }

    for( const auto& traverseFunction : traverseFunctions )
    {
        std::cout << "TraverseFunctionStart" << std::endl;
        std::cout << "TraverseFunctionName:" << traverseFunction.name << std::endl;
        std::cout << "TraverseFunctionArgument:" << traverseFunction.argument << std::endl;
        std::cout << "TraverseFunctionHasPre:" << traverseFunction.hasPre << std::endl;
        std::cout << "TraverseFunctionHasPost:" << traverseFunction.hasPost << std::endl;
        std::cout << "TraverseFunctionEnd" << std::endl;
    }

    std::cout << "InfoEnd" << std::endl;
    foundSomething = true;
    return true;
}

class FindNamedClassConsumer
    : public ASTConsumer
{
public:
    void Initialize(ASTContext& context) override
    {
        visitor.setContext(context);
    }
    virtual void HandleTranslationUnit(ASTContext& context) override
    {
        visitor.TraverseDecl( context.getTranslationUnitDecl());
    }
private:
    CheckFileVisitor visitor;
};

class FindNamedClassAction
    : public ASTFrontendAction
    {
public:
    virtual std::unique_ptr<ASTConsumer> CreateASTConsumer( CompilerInstance&, StringRef ) override
    {
        return std::unique_ptr<ASTConsumer>( new FindNamedClassConsumer );
    }
};


std::string readSourceFile( const char* filename )
{
    std::string contents;
    std::ifstream stream( filename );
    if( !stream )
    {
        std::cerr << "Failed to open: " << filename << std::endl;
        exit( 1 );
    }
    std::string line;
    bool hasIfdef = false;
    while( getline( stream, line ))
    {
        // TODO add checks that it's e.g. not "#ifdef" ?
        if( line.find( "#ifndef LO_CLANG_SHARED_PLUGINS" ) == 0 )
            hasIfdef = true;
        contents += line;
        contents += '\n';
    }
    if( stream.eof() && hasIfdef )
        return contents;
    return "";
}

int main(int argc, char** argv)
{
    std::vector< std::string > args;
    int i = 1;
    for( ; i < argc; ++ i )
    {
        constexpr std::size_t prefixlen = 5; // strlen("-arg=");
        if (std::strncmp(argv[i], "-arg=", prefixlen) != 0)
        {
            break;
        }
        args.push_back(argv[i] + prefixlen);
    }
    SmallVector< StringRef, 20 > clangflags;
    SplitString( CLANGFLAGS, clangflags );
    for (auto const & i: clangflags) {
        args.push_back(i.str());
    }
    args.insert(
        args.end(),
        {   // These must match LO_CLANG_ANALYZER_PCH_CXXFLAGS in Makefile-clang.mk .
            "-I" BUILDDIR "/config_host" // plugin sources use e.g. config_global.h
#if LO_CLANG_USE_ANALYZER_PCH
            ,
            "-include-pch", // use PCH with Clang headers to speed up parsing/analysing
            BUILDDIR "/compilerplugins/clang/sharedvisitor/clang.pch"
#endif
        });
    for( ; i < argc; ++ i )
    {
        std::string contents = readSourceFile(argv[i]);
        if( contents.empty())
            continue;
        foundSomething = false;
        if( !tooling::runToolOnCodeWithArgs( std::unique_ptr<FindNamedClassAction>(new FindNamedClassAction), contents, args, argv[ i ] ))
        {
            std::cerr << "Failed to analyze: " << argv[ i ] << std::endl;
            return 2;
        }
        if( !foundSomething )
        {
            // there's #ifndef LO_CLANG_SHARED_PLUGINS in the source, but no class matched
            std::cerr << "Failed to find code: " << argv[ i ] << std::endl;
            return 2;
        }
    }
    return 0;
}
