#include "MultiTuProcessor.hpp"

#include "CgStr.hpp"
#include "SimpleTemplate.hpp"
#include "basicHl.hpp"
#include "xref.hpp"

#include <boost/filesystem.hpp>
#include <boost/variant/variant.hpp>

#include <algorithm>
#include <climits>
#include <iostream>

using namespace synth;

static fs::path normalAbsolute(fs::path const& p)
{
    fs::path r = fs::absolute(p).lexically_normal();
    if (r.filename() == ".")
        r.remove_filename();
    return r;
}

// Idea from http://stackoverflow.com/a/15549954/2128694, user Rob Kennedy
static bool isPathSuffix(fs::path const& dir, fs::path const& p)
{
    return std::mismatch(dir.begin(), dir.end(), p.begin(), p.end()).first
        == dir.end();
}

static fs::path commonPrefix(fs::path const& p1, fs::path const& p2)
{
    auto it1 = p1.begin(), it2 = p2.begin();
    fs::path r;
    while (it1 != p1.end() && it2 != p2.end()) {
        if (*it1 != *it2)
            break;
        r /= *it1;
        ++it1;
        ++it2;
    }
    return r;
}

MultiTuProcessor::MultiTuProcessor(
    PathMap const& dirs, ExternalRefLinker&& refLinker)
    : m_refLinker(std::move(refLinker))
{
    if (dirs.empty())
        return;
    m_rootInDir = dirs.begin()->first;
    for (auto const& kv : dirs) {
        fs::path inDir = normalAbsolute(kv.first);
        m_dirs.push_back({inDir, normalAbsolute(kv.second)});
        m_rootInDir = commonPrefix(std::move(m_rootInDir), inDir);
    }
}

bool MultiTuProcessor::isFileIncluded(fs::path const& p) const
{
    return getFileMapping(p) != nullptr;
}

// Returns nullptr if references to f should be ignored.
// Pass 0 for lineno and UINT_MAX for offset if referencing
// the file as a whole.

SymbolDeclaration const* synth::MultiTuProcessor::referenceSymbol(
    CXFile f, unsigned lineno, unsigned offset)
{
    FileEntry* fentry = obtainFileEntry(f);
    if (!fentry)
        return nullptr;
    return &createSymbol(fentry->hlFile, lineno, offset);
}

SymbolDeclaration& synth::MultiTuProcessor::createSymbol(
    HighlightedFile const& hlFile, unsigned lineno, unsigned offset)
{
    std::pair<SymbolMap::iterator, bool> inserted;
    {
        std::lock_guard<std::mutex> lock(m_mut);
        inserted = m_syms.insert({
            SymbolId{ &hlFile, offset },
            SymbolDeclaration{ &hlFile, lineno, std::string() } });
    }

    return inserted.first->second;
}

PathMap::value_type const* MultiTuProcessor::getFileMapping(
    fs::path const& p) const
{
    fs::path cleanP = normalAbsolute(p);
    if (!isPathSuffix(m_rootInDir, cleanP))
        return nullptr;

    for (auto const& kv : m_dirs) {
        if (isPathSuffix(kv.first, cleanP))
            return &kv;
    }
    return nullptr;
}

HighlightedFile* MultiTuProcessor::prepareToProcess(CXFile f)
{
    FileEntry* fentry = obtainFileEntry(f);
    if (!fentry || fentry->processed.test_and_set())
        return nullptr;
    return &fentry->hlFile;
}

void synth::MultiTuProcessor::registerDef(
    std::string && usr, SymbolDeclaration const* def)
{
    std::lock_guard<std::mutex> lock(m_mut);
    m_defs.insert({ std::move(usr), std::move(def) });
}

FileEntry* MultiTuProcessor::obtainFileEntry(CXFile f)
{
    CXFileUniqueID fuid;
    if (!f || clang_getFileUniqueID(f, &fuid) != 0)
        return nullptr;

    std::lock_guard<std::mutex> lock(m_mut);
    auto it = m_processedFiles.find(fuid);
    if (it != m_processedFiles.end())
        return &it->second;
    fs::path fname(CgStr(clang_getFileName(f)).gets());
    if (fname.empty())
        return nullptr;
    auto mapping = getFileMapping(fname);
    if (!mapping)
        return nullptr;
    fname = fs::relative(std::move(fname), mapping->first);
    FileEntry& e = m_processedFiles.emplace(
            std::piecewise_construct,
            std::forward_as_tuple(std::move(fuid)),
            std::forward_as_tuple())
        .first->second;
    e.hlFile.fname = std::move(fname);
    e.hlFile.inOutDir = mapping;
    return &e;
}

void MultiTuProcessor::writeOutput(SimpleTemplate const& tpl)
{
    if (m_dirs.empty())
        return;
    auto it = m_dirs.begin();
    fs::path rootOutDir = it->second;
    for (++it; it != m_dirs.end(); ++it)
        rootOutDir = commonPrefix(rootOutDir, it->second);
    bool commonRoot = !rootOutDir.empty() && isPathSuffix(
        normalAbsolute(fs::current_path()), rootOutDir);
    if (commonRoot && rootOutDir.empty())
        rootOutDir = ".";
    SimpleTemplate::Context ctx;
    std::clog << "Writing " << m_processedFiles.size() << " HTML files...\n";
    for (auto& fentry : m_processedFiles) {
        auto& hlFile = fentry.second.hlFile;
        auto dstPath = hlFile.dstPath();
        auto hldir = dstPath.parent_path();
        if (hldir != "." && !hldir.empty())
            fs::create_directories(hldir);
        sortMarkups(hlFile.markups);
        fs::ifstream srcfile(hlFile.srcPath(), std::ios::binary);
        fs::ofstream outfile;
        try {
            srcfile.exceptions(std::ios::badbit);
            std::vector<Markup> suppMarkups;
            basicHighlightFile(srcfile, suppMarkups);
            sortMarkups(suppMarkups);
            hlFile.supplementMarkups(suppMarkups);
            srcfile.clear();
            srcfile.seekg(0);
            outfile.open(dstPath, std::ios::binary);
            outfile.exceptions(std::ios::badbit | std::ios::failbit);
            ctx["code"] = SimpleTemplate::ValCallback(std::bind(
                &HighlightedFile::writeTo,
                &hlFile,
                std::placeholders::_1,
                std::ref(*this),
                std::ref(srcfile)));
            ctx["filename"] = hlFile.fname.string();
            fs::path rootpath = fs::relative(
                    commonRoot ? rootOutDir : hlFile.inOutDir->second, hldir)
                .lexically_normal();
            ctx["rootpath"] = rootpath.empty() ? "." : rootpath.string();
            tpl.writeTo(outfile, ctx);
        } catch (std::ios::failure const& e) {
            if (!srcfile) {
                throw std::runtime_error(
                    "Error reading from or opening "
                    + hlFile.srcPath().string()
                    + ": " + e.what());
            }
            if (!outfile) {
                throw std::runtime_error(
                    "Error writing to or opening "
                    + hlFile.dstPath().string()
                    + ": " + e.what());
            }
            assert("ios::failure but no file with badbit or failbit" && false);
            throw;
        }
    }
}