#include "LibLsp/lsp/utils.h"

#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstring>
#include <fstream>
#include <functional>

#include <queue>
#include <sstream>
#include <string>
#include <unordered_map>
#include <sys/stat.h>

#include "LibLsp/lsp/lsPosition.h"
#include "utf8.h"
#ifdef _WIN32
#include <Windows.h>
#else
#include <unistd.h>
#endif

// DEFAULT_RESOURCE_DIRECTORY is passed with quotes for non-MSVC compilers, ie,
// foo vs "foo".
#if defined(_MSC_VER)
#define _STRINGIFY(x) #x
#define ENSURE_STRING_MACRO_ARGUMENT(x) _STRINGIFY(x)
#else
#define ENSURE_STRING_MACRO_ARGUMENT(x) x
#endif
#include "LibLsp/JsonRpc/filesystemVersion.h"
//#include <boost/utility.hpp>
//#include <boost/filesystem.hpp>
namespace lsp
{

// See http://stackoverflow.com/a/2072890
bool EndsWith(std::string value, std::string ending)
{
    if (ending.size() > value.size())
    {
        return false;
    }
    return std::equal(ending.rbegin(), ending.rend(), value.rbegin());
}

bool StartsWith(std::string value, std::string start)
{
    if (start.size() > value.size())
    {
        return false;
    }
    return std::equal(start.begin(), start.end(), value.begin());
}

bool AnyStartsWith(std::vector<std::string> const& values, std::string const& start)
{
    return std::any_of(
        std::begin(values), std::end(values), [&start](std::string const& value) { return StartsWith(value, start); }
    );
}

bool StartsWithAny(std::string const& value, std::vector<std::string> const& startings)
{
    return std::any_of(
        std::begin(startings), std::end(startings),
        [&value](std::string const& starting) { return StartsWith(value, starting); }
    );
}

bool EndsWithAny(std::string const& value, std::vector<std::string> const& endings)
{
    return std::any_of(
        std::begin(endings), std::end(endings), [&value](std::string const& ending) { return EndsWith(value, ending); }
    );
}

bool FindAnyPartial(std::string const& value, std::vector<std::string> const& values)
{
    return std::any_of(
        std::begin(values), std::end(values),
        [&value](std::string const& v) { return value.find(v) != std::string::npos; }
    );
}

std::string GetDirName(std::string path)
{

    ReplaceAll(path, "\\", "/");
    if (path.size() && path.back() == '/')
    {
        path.pop_back();
    }
    size_t last_slash = path.find_last_of('/');
    if (last_slash == std::string::npos)
    {
        return "./";
    }
    return path.substr(0, last_slash + 1);
}

std::string GetBaseName(std::string const& path)
{
    size_t last_slash = path.find_last_of('/');
    if (last_slash != std::string::npos && (last_slash + 1) < path.size())
    {
        return path.substr(last_slash + 1);
    }
    return path;
}

std::string StripFileType(std::string const& path)
{
    size_t last_period = path.find_last_of('.');
    if (last_period != std::string::npos)
    {
        return path.substr(0, last_period);
    }
    return path;
}

// See http://stackoverflow.com/a/29752943
std::string ReplaceAll(std::string const& source, std::string const& from, std::string const& to)
{
    std::string result;
    result.reserve(source.length()); // avoids a few memory allocations

    std::string::size_type last_pos = 0;
    std::string::size_type find_pos;

    while (std::string::npos != (find_pos = source.find(from, last_pos)))
    {
        result.append(source, last_pos, find_pos - last_pos);
        result += to;
        last_pos = find_pos + from.length();
    }

    // Care for the rest after last occurrence
    result += source.substr(last_pos);

    return result;
}

std::vector<std::string> SplitString(std::string const& str, std::string const& delimiter)
{
    // http://stackoverflow.com/a/13172514
    std::vector<std::string> strings;

    std::string::size_type pos = 0;
    std::string::size_type prev = 0;
    while ((pos = str.find(delimiter, prev)) != std::string::npos)
    {
        strings.emplace_back(str.substr(prev, pos - prev));
        prev = pos + 1;
    }

    // To get the last substring (or only, if delimiter is not found)
    strings.emplace_back(str.substr(prev));

    return strings;
}

void EnsureEndsInSlash(std::string& path)
{
    if (path.empty() || path[path.size() - 1] != '/')
    {
        path += '/';
    }
}

std::string EscapeFileName(std::string path)
{
    if (path.size() && path.back() == '/')
    {
        path.pop_back();
    }
    std::replace(path.begin(), path.end(), '\\', '@');
    std::replace(path.begin(), path.end(), '/', '@');
    std::replace(path.begin(), path.end(), ':', '@');
    return path;
}

// http://stackoverflow.com/a/6089413
std::istream& SafeGetline(std::istream& is, std::string& t)
{
    t.clear();

    // The characters in the stream are read one-by-one using a std::streambuf.
    // That is faster than reading them one-by-one using the std::istream. Code
    // that uses streambuf this way must be guarded by a sentry object. The sentry
    // object performs various tasks, such as thread synchronization and updating
    // the stream state.

    std::istream::sentry se(is, true);
    std::streambuf* sb = is.rdbuf();

    for (;;)
    {
        int c = sb->sbumpc();
        if (c == EOF)
        {
            // Also handle the case when the last line has no line ending
            if (t.empty())
            {
                is.setstate(std::ios::eofbit);
            }
            return is;
        }

        t += (char)c;

        if (c == '\n')
        {
            return is;
        }
    }
}

bool FileExists(std::string const& filename)
{
    std::ifstream cache(filename);
    return cache.is_open();
}

optional<std::string> ReadContent(AbsolutePath const& filename)
{

    std::ifstream cache;
    cache.open(filename.path);

    try
    {
        return std::string(std::istreambuf_iterator<char>(cache), std::istreambuf_iterator<char>());
    }
    catch (std::ios_base::failure&)
    {
        return {};
    }
}

std::vector<std::string> ReadLinesWithEnding(AbsolutePath const& filename)
{
    std::vector<std::string> result;

    std::ifstream input(filename.path);
    for (std::string line; SafeGetline(input, line);)
    {
        result.emplace_back(line);
    }

    return result;
}

bool WriteToFile(std::string const& filename, std::string const& content)
{
    std::ofstream file(filename, std::ios::out | std::ios::trunc | std::ios::binary);
    if (!file.good())
    {

        return false;
    }

    file << content;
    return true;
}

std::string FormatMicroseconds(long long microseconds)
{
    long long milliseconds = microseconds / 1000;
    long long remaining = microseconds - milliseconds;

    // Only show two digits after the dot.
    while (remaining >= 100)
    {
        remaining /= 10;
    }

    return std::to_string(milliseconds) + "." + std::to_string(remaining) + "ms";
}

std::string UpdateToRnNewlines(std::string output)
{
    size_t idx = 0;
    while (true)
    {
        idx = output.find('\n', idx);

        // No more matches.
        if (idx == std::string::npos)
        {
            break;
        }

        // Skip an existing "\r\n" match.
        if (idx > 0 && output[idx - 1] == '\r')
        {
            ++idx;
            continue;
        }

        // Replace "\n" with "\r|n".
        output.replace(output.begin() + idx, output.begin() + idx + 1, "\r\n");
    }

    return output;
}

bool IsAbsolutePath(std::string const& path)
{
    return IsUnixAbsolutePath(path) || IsWindowsAbsolutePath(path);
}

bool IsUnixAbsolutePath(std::string const& path)
{
    return !path.empty() && path[0] == '/';
}

bool IsWindowsAbsolutePath(std::string const& path)
{
    auto is_drive_letter = [](char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); };

    return path.size() > 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') && is_drive_letter(path[0]);
}

bool IsDirectory(std::string const& path)
{
    struct stat path_stat;

    if (stat(path.c_str(), &path_stat) != 0)
    {
        perror("cannot access path");
        return false;
    }

    return path_stat.st_mode & S_IFDIR;
}

std::string ws2s(std::wstring const& wstr)
{
    // BOOST_IF_CONSTEXPR (we'll just compile it, compiler probably will optimize it away)
    if (sizeof(wchar_t) == 2)
    {
        std::string narrow;
        utf8::utf16to8(wstr.begin(), wstr.end(), std::back_inserter(narrow));
        return narrow;
    }
    else
    {
        std::string narrow;
        utf8::utf32to8(wstr.begin(), wstr.end(), std::back_inserter(narrow));
        return narrow;
    }
}
std::wstring s2ws(std::string const& str)
{
    std::wstring wide;
    // BOOST_IF_CONSTEXPR (we'll just compile it, compiler probably will optimize it away)
    if (sizeof(wchar_t) == 2)
    {
        utf8::utf8to16(str.begin(), str.end(), std::back_inserter(wide));
        return wide;
    }
    else
    {
        utf8::utf8to32(str.begin(), str.end(), std::back_inserter(wide));
        return wide;
    }
}

#ifndef _WIN32
// Returns the canonicalized absolute pathname, without expanding symbolic
// links. This is a variant of realpath(2), C++ rewrite of
// https://github.com/freebsd/freebsd/blob/master/lib/libc/stdlib/realpath.c
AbsolutePath RealPathNotExpandSymlink(std::string path, bool ensure_exists)
{
    if (path.empty())
    {
        errno = EINVAL;
        return {};
    }
    if (path[0] == '\0')
    {
        errno = ENOENT;
        return {};
    }

    // Do not use PATH_MAX because it is tricky on Linux.
    // See https://eklitzke.org/path-max-is-tricky
    char tmp[1024];
    std::string resolved;
    size_t i = 0;
    struct stat sb;
    if (path[0] == '/')
    {
        resolved = "/";
        i = 1;
    }
    else
    {
        if (!getcwd(tmp, sizeof tmp) && ensure_exists)
        {
            return {};
        }
        resolved = tmp;
    }

    while (i < path.size())
    {
        auto j = path.find('/', i);
        if (j == std::string::npos)
        {
            j = path.size();
        }
        auto next_token = path.substr(i, j - i);
        i = j + 1;
        if (resolved.back() != '/')
        {
            resolved += '/';
        }
        if (next_token.empty() || next_token == ".")
        {
            // Handle consequential slashes and "."
            continue;
        }
        else if (next_token == "..")
        {
            // Strip the last path component except when it is single "/"
            if (resolved.size() > 1)
            {
                resolved.resize(resolved.rfind('/', resolved.size() - 2) + 1);
            }
            continue;
        }
        // Append the next path component.
        // Here we differ from realpath(3), we use stat(2) instead of
        // lstat(2) because we do not want to resolve symlinks.
        resolved += next_token;
        if (stat(resolved.c_str(), &sb) != 0 && ensure_exists)
        {
            return {};
        }
        if (!S_ISDIR(sb.st_mode) && j < path.size() && ensure_exists)
        {
            errno = ENOTDIR;
            return {};
        }
    }

    // Remove trailing slash except when a single "/".
    if (resolved.size() > 1 && resolved.back() == '/')
    {
        resolved.pop_back();
    }
    return AbsolutePath(resolved, true /*validate*/);
}
#endif

AbsolutePath NormalizePath(std::string const& path0, bool ensure_exists, bool force_lower_on_windows)
{
#ifdef _WIN32

    std::wstring path = lsp::s2ws(path0);

    wchar_t buffer[MAX_PATH] = (L"");

    // Normalize the path name, ie, resolve `..`.
    unsigned long len = GetFullPathNameW(path.c_str(), MAX_PATH, buffer, nullptr);
    if (!len)
    {
        return {};
    }
    path = std::wstring(buffer, len);

    // Get the actual casing of the path, ie, if the file on disk is `C:\FooBar`
    // and this function is called with `c:\fooBar` this will return `c:\FooBar`.
    // (drive casing is lowercase).
    if (ensure_exists)
    {
        len = GetLongPathNameW(path.c_str(), buffer, MAX_PATH);
        if (!len)
        {
            return {};
        }
        path = std::wstring(buffer, len);
    }

    // Empty paths have no meaning.
    if (path.empty())
    {
        return {};
    }

    // We may need to normalize the drive name to upper-case; at the moment
    // vscode sends lower-case path names.
    /*
        path[0] = toupper(path[0]);
        */
    // Make the path all lower-case, since windows is case-insensitive.
    if (force_lower_on_windows)
    {
        for (size_t i = 0; i < path.size(); ++i)
        {
            path[i] = (wchar_t)tolower(path[i]);
        }
    }

    // cquery assumes forward-slashes.
    std::replace(path.begin(), path.end(), '\\', '/');

    return AbsolutePath(lsp::ws2s(path), false /*validate*/);
#else

    return RealPathNotExpandSymlink(path0, ensure_exists);

#endif
}

// VSCode (UTF-16) disagrees with Emacs lsp-mode (UTF-8) on how to represent
// text documents.
// We use a UTF-8 iterator to approximate UTF-16 in the specification (weird).
// This is good enough and fails only for UTF-16 surrogate pairs.
int GetOffsetForPosition(lsPosition position, std::string const& content)
{
    size_t i = 0;
    // Iterate lines until we have found the correct line.
    while (position.line > 0 && i < content.size())
    {
        if (content[i] == '\n')
        {
            position.line--;
        }
        i++;
    }
    // Iterate characters on the target line.
    while (position.character > 0 && i < content.size())
    {
        if (uint8_t(content[i++]) >= 128)
        {
            // Skip 0b10xxxxxx
            while (i < content.size() && uint8_t(content[i]) >= 128 && uint8_t(content[i]) < 192)
            {
                i++;
            }
        }
        position.character--;
    }
    return int(i);
}

lsPosition GetPositionForOffset(size_t offset, std::string const& content)
{
    lsPosition result;
    for (size_t i = 0; i < offset && i < content.length(); ++i)
    {
        if (content[i] == '\n')
        {
            result.line++;
            result.character = 0;
        }
        else
        {
            result.character++;
        }
    }
    return result;
}

lsPosition CharPos(std::string const& search, char character, int character_offset)
{
    lsPosition result;
    size_t index = 0;
    while (index < search.size())
    {
        char c = search[index];
        if (c == character)
        {
            break;
        }
        if (c == '\n')
        {
            result.line += 1;
            result.character = 0;
        }
        else
        {
            result.character += 1;
        }
        ++index;
    }
    assert(index < search.size());
    result.character += character_offset;
    return result;
}

void scanDirsUseRecursive(std::wstring const& rootPath, std::vector<std::wstring>& ret)
{
    namespace fs = filesystem;
    fs::path fullpath(rootPath);
    if (!fs::exists(fullpath))
    {
        return;
    }
    fs::recursive_directory_iterator end_iter;
    for (fs::recursive_directory_iterator iter(fullpath); iter != end_iter; iter++)
    {
        try
        {
            if (fs::is_directory(*iter))
            {
                ret.push_back(iter->path().wstring());
            }
        }
        catch (std::exception const&)
        {
            continue;
        }
    }
}

void scanDirsNoRecursive(std::wstring const& rootPath, std::vector<std::wstring>& ret)
{
    namespace fs = filesystem;
    fs::path myPath(rootPath);
    if (!fs::exists(rootPath))
    {
        return;
    }
    fs::directory_iterator endIter;
    for (fs::directory_iterator iter(myPath); iter != endIter; iter++)
    {
        if (fs::is_directory(*iter))
        {
            ret.push_back(iter->path().wstring());
        }
    }
}

void to_lower(std::wstring& input, std::locale const& loc = std::locale())
{
    std::ctype<wchar_t> const& facet = std::use_facet<std::ctype<wchar_t>>(loc);
    for (std::size_t i = 0; i < input.size(); ++i)
    {
        input[i] = facet.tolower(input[i]);
    }
}

void scanFilesUseRecursive(std::wstring const& rootPath, std::vector<std::wstring>& ret, std::wstring suf)
{
    namespace fs = filesystem;
    to_lower(suf);

    fs::path fullpath(rootPath);
    if (!fs::exists(fullpath))
    {
        return;
    }
    fs::recursive_directory_iterator end_iter;
    for (fs::recursive_directory_iterator iter(fullpath); iter != end_iter; iter++)
    {
        try
        {
            if (!fs::is_directory(*iter) && fs::is_regular_file(*iter))
            {
                auto temp_path = iter->path().wstring();
                auto size = suf.size();
                if (!size)
                {
                    ret.push_back(std::move(temp_path));
                }
                else
                {

                    if (temp_path.size() < size)
                    {
                        continue;
                    }
                    auto suf_temp = temp_path.substr(temp_path.size() - size);
                    to_lower(suf_temp);
                    if (suf_temp == suf)
                    {
                        ret.push_back(std::move(temp_path));
                    }
                }
            }
        }
        catch (std::exception const&)
        {
            continue;
        }
    }
}

void scanFileNamesUseRecursive(std::wstring const& rootPath, std::vector<std::wstring>& ret, std::wstring strSuf)
{
    scanFilesUseRecursive(rootPath, ret, strSuf);
    std::vector<std::wstring> names;
    for (auto& it : ret)
    {
        if (it.size() >= rootPath.size())
        {
            names.push_back(it.substr(rootPath.size()));
        }
    }
    ret.swap(names);
}

void scanFileNamesUseRecursive(std::string const& rootPath, std::vector<std::string>& ret, std::string strSuf)
{
    std::vector<std::wstring> out;
    scanFileNamesUseRecursive(s2ws(rootPath), out, s2ws(strSuf));
    for (auto& it : out)
    {
        ret.push_back(ws2s(it));
    }
}

void scanFilesUseRecursive(std::string const& rootPath, std::vector<std::string>& ret, std::string strSuf)
{
    std::vector<std::wstring> out;
    scanFilesUseRecursive(s2ws(rootPath), out, s2ws(strSuf));
    for (auto& it : out)
    {
        ret.push_back(ws2s(it));
    }
}

} // namespace lsp
