/* 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>
 */

#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::addLibAlias(Atom<Identifier_t>&& alias, std::string&& package) {
    __aliases.emplace(alias.get(), package.get());
}

void
ExternData::requireHeaders(const std::list<std::string>& headers){
  __requiredHeaders.insert(
    std::inserter(__requiredHeaders, __requiredHeaders.end()),
    headers.begin(), headers.end());
}

void
ExternData::requireLibPackage(Atom<String_t>&& package){
  __requiredPackages.push_back(package.get());
}

void
ExternData::requireLibAlias(Atom<Identifier_t>&& alias){
  assert(__aliases.count(alias.get());
  __requiredPackages.push_back(__aliases.at(alias.get()));
}

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 std::string& package) {
    std::vector<string> args;
    FILE* flags = popen((string("pkg-config --cflags ") + 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 std::string& package) {
    std::vector<string> libs;

    FILE* flags = popen((string("pkg-config --libs ") + 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* ast) {
  std::vector<string> args{
    "-isystem"
  };

  list<string> code;
  std::vector<string> libs;

  boost::format formatInclude("#include \"%1%\"");
  for(const ExternData& entry : ast->__externdata) {
      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
