/* 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/.
 *
 * ExternLayer.cpp
 *
 *  Created on: 4/21/15
 *      Author: pgess <v.melnychenko@xreate.org>
 *
 * \file    ExternLayer.h
 * \brief   Support of external C code. Wrapper over Clang
 */

#include "ExternLayer.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Frontend/ASTUnit.h"
#include "clang/Frontend/CodeGenOptions.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/CodeGen/ModuleBuilder.h"
#include "clang/CodeGen/CodeGenABITypes.h"
#include <llvm/Support/DynamicLibrary.h>
#include <boost/format.hpp>
#include <boost/algorithm/string/join.hpp>
#include <cstdio>
#include <iostream>
#include <clang/Frontend/CompilerInstance.h>

using namespace std;
using namespace clang;
using namespace clang::driver;
using namespace clang::tooling;
using namespace clang::ast_matchers;
using namespace llvm;

namespace xreate{

class FinderCallbackTypeDecl : public MatchFinder::MatchCallback{
public:
    QualType typeResult;

    virtual void
    run(const MatchFinder::MatchResult &Result) {
        if (const TypedefDecl * decl = Result.Nodes.getNodeAs<clang::TypedefDecl>("typename")) {
            typeResult = decl->getUnderlyingType();
        }
    }
} ;

class FinderCallbackFunction : public MatchFinder::MatchCallback{
public:
    QualType typeResult;

    virtual void
    run(const MatchFinder::MatchResult &Result) {
        if (const FunctionDecl * decl = Result.Nodes.getNodeAs<clang::FunctionDecl>("function")) {
            typeResult = decl->getType();
        }
    }
} ;

void
ExternData::addLibrary(Atom<Identifier_t>&& name, Atom<String_t>&& package) {
    __dictLibraries.emplace(name.get(), package.get());
}

void
ExternData::addIncludeDecl(Expression&& e) {
    assert(e.op == Operator::LIST);

    //TODO  ?? implement Expression parsing(Array of Expr as vector<string>);
    for(size_t i = 0, size = e.operands.size(); i < size; ++i) {
        std::string library = e.bindings.at(i);
        assert(__dictLibraries.count(library));
        std::string package = __dictLibraries.at(library);

        Expression listHeaders = e.operands.at(i);
        assert(listHeaders.op == Operator::LIST);

        std::vector<std::string> headers;
        std::transform(listHeaders.operands.begin(), listHeaders.operands.end(), std::inserter(headers, headers.end()),
            [](const Expression & o) {
                assert(o.__state == Expression::STRING);
                return o.getValueString();
            });

        entries.emplace_back(ExternEntry{package, std::move(headers)});
    }
}

void
ExternLayer::addExternalData(const std::vector<ExternEntry>& data) {
    entries.insert(entries.end(), data.begin(), data.end());
}

ExternLayer::ExternLayer(LLVMLayer *llvm)
: __llvm(llvm) { }

std::vector<string>
ExternLayer::fetchPackageFlags(const ExternEntry& entry) {
    std::vector<string> args;
    FILE* flags = popen((string("pkg-config --cflags ") + entry.package).c_str(), "r");
    size_t linesize = 0;
    char* linebuf = 0;
    ssize_t linelen = 0;
    while ((linelen = getdelim(&linebuf, &linesize, ' ', flags)) > 0) {
        if (linebuf[0] == '\n') continue;
        if (linelen == 1 && linebuf[0] == ' ') continue;

        //cut unwanted symbols at the end
        char symbLast = linebuf[linelen - 1 ];
        if (symbLast == ' ' || symbLast == '\n') linebuf[linelen - 1] = 0;

        //print header for debug purposes
        llvm::outs() << '<' << linebuf << "> ";

        args.push_back(linebuf);
        free(linebuf);
        linebuf = 0;
    }

    pclose(flags);
    return (args);
}

std::vector<string>
ExternLayer::fetchPackageLibs(const ExternEntry& entry) {
    std::vector<string> libs;

    FILE* flags = popen((string("pkg-config --libs ") + entry.package).c_str(), "r");
    size_t linesize = 0;
    char* linebuf = 0;
    ssize_t linelen = 0;
    while ((linelen = getdelim(&linebuf, &linesize, ' ', flags)) > 0) {
        if (linebuf[0] == '\n') continue;
        if (linelen == 1 && linebuf[0] == ' ') continue;

        //cut unwanted symbols at the end
        char symbLast = linebuf[linelen - 1 ];
        if (symbLast == ' ' || symbLast == '\n') linebuf[linelen - 1] = 0;

        //cut unwanted symbols at the beginning
        if (linelen > 1 && linebuf[0] == '-' && linebuf[1] == 'l') {
            libs.push_back(linebuf + 2);

        } else {
            libs.push_back(linebuf);
        }

        //print lib name for debug purposes
        llvm::outs() << '<' << linebuf << "> ";
        free(linebuf);
        linebuf = 0;
    }

    pclose(flags);
    return (libs);
}

void
ExternLayer::loadLibraries(vector<string>&& libs) {
    string msgErr;
    for (const string& lib : libs) {
        const string& libName = string("lib") + lib + ".so";
        if (!llvm::sys::DynamicLibrary::LoadLibraryPermanently(libName.c_str(), &msgErr)) {
            llvm::errs() << "\n" << "Loading library " << lib << ". " << msgErr << "\n";
        }
    }
}

void
ExternLayer::init(const AST* root) {
    addExternalData(root->__externdata);

    // TODO -EXTERN01.DIP, use default include path from 'clang -xc++ -E'
    list<string> code;
    std::vector<string> args{
        "-I/usr/include"
        , "-I/usr/local/include"
        , "-I/usr/lib64/clang/5.0.1/include"
        //        ,"-I/usr/lib/gcc/x86_64-linux-gnu/4.9/include"
        //        ,"-I/usr/include/x86_64-linux-gnu"
    };

    std::vector<string> libs;

    boost::format formatInclude("#include \"%1%\"");
    for(const ExternEntry& entry : entries) {
        llvm::outs() << "[ExternC] Processing package: " << entry.package << "\n";
        llvm::outs() << "[ExternC]    args: ";

        vector<string>&& args2 = fetchPackageFlags(entry);
        args.insert(args.end(), args2.begin(), args2.end());
        for(const string arg : args2) {
            llvm::outs() << "<" << arg << "> ";
        }

        llvm::outs() << "\n[ExternC]    libs: ";
        args2 = fetchPackageLibs(entry);
        for(const string arg : args2) {
            llvm::outs() << "<" << arg << "> ";
        }
        libs.insert(libs.end(), args2.begin(), args2.end());


        llvm::outs() << "\n[ExternC]    headers: ";
        std::transform(entry.headers.begin(), entry.headers.end(), std::inserter(code, code.begin()),
            [&formatInclude](const string header ) {
                string line = boost::str(formatInclude % header);
                llvm::outs() << "<" << line << "> ";

                return line;
            });

        llvm::outs() << '\n';
    }

    loadLibraries(move(libs));
    ast = buildASTFromCodeWithArgs(boost::algorithm::join(code, "\n"), args);
    __llvm->module->setDataLayout(ast->getASTContext().getTargetInfo().getDataLayout());
    __clang.reset(new CompilerInstance());
    __clang->createDiagnostics();

    __codegen.reset(CreateLLVMCodeGen(
        __clang->getDiagnostics(),
        __llvm->module->getName(),
        __clang->getHeaderSearchOpts(),
        __clang->getPreprocessorOpts(),
        clang::CodeGenOptions(),
        __llvm->llvmContext
        ));

    __codegen->Initialize(ast->getASTContext());
};

bool
ExternLayer::isPointer(const clang::QualType &t) {
    const clang::Type * tInfo = t.getTypePtr();
    assert(tInfo);

    return tInfo->isAnyPointerType();
}

llvm::Type*
ExternLayer::toLLVMType(const clang::QualType& t) {
    return CodeGen::convertTypeForMemory( __codegen->CGM(), t);
}

std::vector<std::string>
ExternLayer::getStructFields(const clang::QualType& ty) {
    clang::QualType t = ty;
    if (isPointer(ty)) {
        const clang::PointerType* tPtr = ty->getAs<clang::PointerType>();
        t = tPtr->getPointeeType();
    }

    assert(t.getTypePtr()->isRecordType());

    const RecordType *record = t->getAsStructureType();
    assert(record);

    std::vector<std::string> result;
    //FieldDecl* field: record->getDecl()->fields()
    for (auto i = record->getDecl()->field_begin(); i != record->getDecl()->field_end(); ++i) {
        result.push_back(i->getName());
    }

    return result;
}

bool
ExternLayer::isArrayType(const std::string& type){
    clang::QualType typeRaw = lookupType(type);
    if (isPointer(typeRaw)) {
        const clang::PointerType* typePtr = typeRaw->getAs<clang::PointerType>();
        typeRaw = typePtr->getPointeeType();
    }

    return typeRaw->isArrayType();
}

bool
ExternLayer::isRecordType(const std::string& type){
    clang::QualType typeRaw = lookupType(type);
    if (isPointer(typeRaw)) {
        const clang::PointerType* typePtr = typeRaw->getAs<clang::PointerType>();
        typeRaw = typePtr->getPointeeType();
    }

    return typeRaw->isRecordType();
}

clang::QualType
ExternLayer::lookupType(const std::string& id) {
    MatchFinder finder;
    FinderCallbackTypeDecl callbackTypeDecl;
    auto matcherTypeDecl = typedefDecl(hasName(id)).bind("typename");

    finder.addMatcher(matcherTypeDecl, &callbackTypeDecl);
    finder.matchAST(ast->getASTContext());

    assert(! callbackTypeDecl.typeResult.isNull());
    return callbackTypeDecl.typeResult;
}

llvm::Function*
ExternLayer::lookupFunction(const std::string& name) {
    if (__functions.count(name)) {
        return __functions.at(name);
    }

    MatchFinder finder;
    FinderCallbackFunction callback;
    auto matcher = functionDecl(hasName(name)).bind("function");

    finder.addMatcher(matcher, &callback);
    finder.matchAST(ast->getASTContext());

    if (callback.typeResult.isNull()) {
        cout << "[External Layer] "  << "Unknown function: " << name << endl;
        assert(false && "Unknown external function");
    }
    const QualType& tyFuncQual = callback.typeResult;
    llvm::Type *tyRaw =  CodeGen::convertTypeForMemory(__codegen->CGM(), tyFuncQual);
    llvm::FunctionType* tyRawFunc = llvm::dyn_cast<llvm::FunctionType>(tyRaw);

    llvm::Function* function = llvm::Function::Create(
        tyRawFunc,
        llvm::GlobalValue::ExternalLinkage,
        name,
        __llvm->module.get());

    __functions.emplace(name, function);
    return function;
}
}//end of xreate namespace
