#include "CgStr.hpp"
#include "DoxytagResolver.hpp"
#include "MultiTuProcessor.hpp"
#include "SimpleTemplate.hpp"
#include "annotate.hpp"
#include "cgWrappers.hpp"
#include "cmdline.hpp"

#include <boost/filesystem.hpp>
#include <boost/io/ios_state.hpp>

#include <condition_variable>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <thread>

using namespace synth;

static char const kDefaultTemplateText[] =
R"EOT(<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>@@filename@@</title>
        <link rel="stylesheet" href="@@rootpath@@/code.css">
    </head>
    <body>
        <div class="highlight"><pre>@@code@@</pre></div>
        <script src="@@rootpath@@/code.js"></script>
    </body>
</html>)EOT";

namespace {

struct InitialPathResetter {
    ~InitialPathResetter() {
        boost::system::error_code ec;
        fs::current_path(fs::initial_path(), ec);
        if (ec) {
            std::cerr << "Failed resetting working directory: " << ec;
        }
    }
};

struct ThreadSharedState {
    CXIndex cidx;
    MultiTuProcessor& multiTuProcessor;
    std::mutex workingDirMut;
    std::mutex outputMut;
    std::condition_variable workingDirChangedOrFree;
    unsigned nWorkingDirUsers;
    std::atomic_bool cancel;
};

class UIntRef {
public:
    UIntRef(
        unsigned& refd, std::condition_variable& zeroSignal, std::mutex& mut)
        : m_refd(refd)
        , m_zeroSignal(zeroSignal)
        , m_mut(mut)
        , m_acquired(false)
    { }

    // Note: The referenced unsigned must be syncronized by the caller.
    void acquire()
    {
        assert(!m_acquired);
        ++m_refd;
        m_acquired = true;
    }

    ~UIntRef()
    {
        if (m_acquired) {
            bool zeroReached;
            {
                std::lock_guard<std::mutex> lock(m_mut);
                zeroReached = --m_refd == 0;
            }
            if (zeroReached)
                m_zeroSignal.notify_all();
        }
    }

private:
    unsigned& m_refd;
    std::condition_variable& m_zeroSignal;
    std::mutex& m_mut;
    bool m_acquired;
};

} // anonyomous namespace

static std::vector<CgStr> getClArgs(CXCompileCommand cmd)
{
    std::vector<CgStr> result;
    unsigned nArgs = clang_CompileCommand_getNumArgs(cmd);
    result.reserve(nArgs);
    for (unsigned i = 0; i < nArgs; ++i)
        result.push_back(clang_CompileCommand_getArg(cmd, i));
    return result;
}

static bool processCompileCommand(
    CXCompileCommand cmd,
    std::vector<char const*> extraArgs,
    float pct,
    ThreadSharedState& state)
{
    CgStr file(clang_CompileCommand_getFilename(cmd));
    if (!file.empty() && !state.multiTuProcessor.isFileIncluded(file.get()))
        return false;

    std::vector<CgStr> clArgsHandles = getClArgs(cmd);
    std::vector<char const*> clArgs;
    clArgs.reserve(clArgsHandles.size() + extraArgs.size());
    for (CgStr const& s : clArgsHandles)
        clArgs.push_back(s.get());
    clArgs.insert(clArgs.end(), extraArgs.begin(), extraArgs.end());
    CgStr dirStr = clang_CompileCommand_getDirectory(cmd);
    UIntRef dirRef(
        state.nWorkingDirUsers,
        state.workingDirChangedOrFree,
        state.workingDirMut);

    bool dirChanged = false;
    if (!dirStr.empty()) {
        fs::path dir = std::move(dirStr).gets();
        bool dirOk;
        std::unique_lock<std::mutex> lock(state.workingDirMut);
        state.workingDirChangedOrFree.wait(lock, [&]() {
            if (state.cancel)
                return true;
            dirOk = fs::current_path() == dir;
            return dirOk || state.nWorkingDirUsers == 0;
        });
        if (state.cancel)
            return false;
        dirRef.acquire();
        if (!dirOk) {
            fs::current_path(dir);
            dirChanged = true;
        }
    } else {
        std::lock_guard<std::mutex> lock(state.workingDirMut);
        dirRef.acquire();
    }

    {
        std::lock_guard<std::mutex> lock(state.outputMut);

        if (dirChanged)
            std::clog << "Entered directory " << dirStr.get() << '\n';
        boost::io::ios_all_saver saver(std::clog);
        std::clog.flags(std::clog.flags() | std::ios::fixed);
        std::clog.precision(2);
        std::clog << '[' << std::setw(6) << pct << "%]: " << file << "...\n";
    }


    return processTu(
        state.cidx,
        state.multiTuProcessor,
        clArgs.data(),
        static_cast<int>(clArgs.size())) == EXIT_SUCCESS;
}

// Adapted from
// http://insanecoding.blogspot.co.at/2011/11/how-to-read-in-file-in-c.html
static std::string getFileContents(char const* fname)
{
    std::ifstream in(fname, std::ios::in | std::ios::binary);
    in.exceptions(std::ios::badbit);
    std::ostringstream contents;
    contents << in.rdbuf();
    return std::move(contents).str();
}


static int executeCmdLine(CmdLineArgs const& args)
{
    SimpleTemplate tpl("");
    if (args.templateFile) {
        try {
            tpl = SimpleTemplate(getFileContents(args.templateFile));
        } catch (std::ios::failure const& e) {
            std::cerr << "Error reading output template: " << e.what() << '\n';
            return EXIT_FAILURE;
        }
    } else {
        tpl = SimpleTemplate(kDefaultTemplateText); 
    }

    std::vector<ExternalRefLinker> refLinkers;
    for (auto const& doxyMapping : args.doxyTagFiles) {
        refLinkers.push_back(std::bind(&DoxytagResolver::link,
            DoxytagResolver::fromTagFilename(
                doxyMapping.first,
                doxyMapping.second),
            std::placeholders::_1, std::placeholders::_2));
    }

    CgIdxHandle hcidx(clang_createIndex(
            /*excludeDeclarationsFromPCH:*/ true,
            /*displayDiagnostics:*/ true));


    MultiTuProcessor state(
        PathMap(args.inOutDirs.begin(), args.inOutDirs.end()), 
        [&refLinkers](Markup& m, CXCursor c) {
            assert(!m.isRef());
            for (auto const& refLinker : refLinkers) {
                refLinker(m, c);
                if (m.isRef())
                    break;
            }
        });
    state.setMaxIdSz(args.maxIdSz);

    if (args.compilationDbDir) {
        CXCompilationDatabase_Error err;
        CgDbHandle db(clang_CompilationDatabase_fromDirectory(
                args.compilationDbDir, &err));
        if (err != CXCompilationDatabase_NoError) {
            std::cerr << "Failed loading compilation database (code "
                      << static_cast<int>(err)
                      << ")\n";
            return err + 20;
        }
        CgCmdsHandle cmds(
            clang_CompilationDatabase_getAllCompileCommands(db.get()));
        unsigned nCmds = clang_CompileCommands_getSize(cmds.get());
        if (nCmds == 0) {
            std::cerr << "No compilation commands in DB.\n";
            return EXIT_SUCCESS;
        }

        InitialPathResetter pathResetter;
        ThreadSharedState tstate {
            /*cidx=*/ hcidx.get(),
            /*multiTuProcessor=*/ state,
            /*workingDirMut=*/ {},
            /*outputMut=*/ {},
            /*workingDirChangedOrFree=*/ {},
            /*nWorkingDirUsers=*/ 0u,
            /*cancel=*/ {false}};

        // It seems [1] that during creation of the first translation,
        // no others may be created or data races occur inside libclang.
        // [1]: Detected by clang's TSan.
        unsigned idx = 0;
        while (!processCompileCommand(
            clang_CompileCommands_getCommand(cmds.get(), idx++),
            args.clangArgs,
            0,
            tstate)
        ) {
            assert(tstate.nWorkingDirUsers == 0);
        }

        std::atomic_uint sharedCmdIdx(idx);
        std::vector<std::thread> threads;
        threads.reserve(args.nThreads - 1);
        std::clog << "Using " << args.nThreads << " threads.\n";
        auto const worker = [&]() {
            while (!tstate.cancel) {
                unsigned cmdIdx = sharedCmdIdx++;
                if (cmdIdx >= nCmds)
                    return;

                processCompileCommand(
                    clang_CompileCommands_getCommand(cmds.get(), cmdIdx),
                    args.clangArgs,
                    static_cast<float>(cmdIdx) / nCmds * 100,
                    tstate);
            }
        };
        try {
            for (unsigned i = 0; i < args.nThreads - 1; ++i)
                threads.emplace_back(worker);
            worker();
        } catch (...) {
            tstate.cancel = true; // Do before locking to reduce wait time.
            {
                std::lock_guard<std::mutex> lock(tstate.workingDirMut);
                tstate.cancel = true; // Repeat for condition variable.
            }
            tstate.workingDirChangedOrFree.notify_all();
            for (auto& th : threads)
                th.join();
            assert(tstate.nWorkingDirUsers == 0);
            throw;
        }
        for (auto& th : threads)
            th.join();
        assert(tstate.nWorkingDirUsers == 0);
    } else {
        int r = synth::processTu(
            hcidx.get(),
            state,
            args.clangArgs.data(),
            args.nClangArgs);
        if (r)
            return r;
    }
    state.writeOutput(tpl);
    return EXIT_SUCCESS;
}

int main(int argc, char* argv[])
{
    try {
        fs::initial_path(); // Save initial path.
        return executeCmdLine(CmdLineArgs::parse(argc, argv));
    } catch (std::exception const& e) {
        std::cerr << e.what() << '\n';
        return EXIT_FAILURE;
    }
}