/*
 * Copyright 2020 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_logging.h>
#include <buildboxcommon_stringutils.h>

#include <algorithm>
#include <cctype>
#include <cstdint>
#include <iomanip>
#include <numeric>
#include <optional>
#include <random>
#include <regex>
#include <sstream>
#include <string>
#include <string_view>
#include <uuid/uuid.h>

namespace buildboxcommon {

void StringUtils::ltrim(std::string *s)
{
    s->erase(s->begin(),
             std::find_if(s->begin(), s->end(),
                          [](unsigned char ch) { return !std::isspace(ch); }));
}

void StringUtils::ltrim(std::string *s, const std::function<int(int)> &filter)
{
    s->erase(s->begin(),
             std::find_if(s->begin(), s->end(), [&filter](unsigned char ch) {
                 return !filter(ch);
             }));
}

void StringUtils::rtrim(std::string *s)
{
    s->erase(std::find_if(s->rbegin(), s->rend(),
                          [](unsigned char ch) { return !std::isspace(ch); })
                 .base(),
             s->end());
}

void StringUtils::rtrim(std::string *s, const std::function<int(int)> &filter)
{
    s->erase(std::find_if(s->rbegin(), s->rend(),
                          [&filter](unsigned char ch) { return !filter(ch); })
                 .base(),
             s->end());
}

void StringUtils::trim(std::string *s)
{
    ltrim(s);
    rtrim(s);
}

void StringUtils::trim(std::string *s, const std::function<int(int)> &filter)
{
    ltrim(s, filter);
    rtrim(s, filter);
}

std::string StringUtils::ltrim(const std::string &s)
{
    std::string copy(s);
    ltrim(&copy);
    return copy;
}

std::string StringUtils::ltrim(const std::string &s,
                               const std::function<int(int)> &filter)
{
    std::string copy(s);
    ltrim(&copy, filter);
    return copy;
}

std::string StringUtils::rtrim(const std::string &s)
{
    std::string copy(s);
    rtrim(&copy);
    return copy;
}

std::string StringUtils::rtrim(const std::string &s,
                               const std::function<int(int)> &filter)
{
    std::string copy(s);
    rtrim(&copy, filter);
    return copy;
}

std::string StringUtils::trim(const std::string &s)
{
    std::string copy(s);
    trim(&copy);
    return copy;
}

std::string StringUtils::trim(const std::string &s,
                              const std::function<int(int)> &filter)
{
    std::string copy(s);
    trim(&copy, filter);
    return copy;
}

std::string StringUtils::ordinal(const int n)
{
    // NOLINTBEGIN (cppcoreguidelines-avoid-magic-numbers)
    int ord = n % 100;
    if (ord / 10 == 1) {
        ord = 0;
    }
    ord = ord % 10;
    if (ord > 3) {
        ord = 0;
    }
    // NOLINTEND (cppcoreguidelines-avoid-magic-numbers)

    std::ostringstream oss;
    switch (ord) {
        case 1:
            oss << n << "st";
            break;
        case 2:
            oss << n << "nd";
            break;
        case 3:
            oss << n << "rd";
            break;
        default:
            oss << n << "th";
            break;
    }
    return oss.str();
}

std::string StringUtils::getRandomHexString()
{
    // 2 hex digits per byte
    const int width = sizeof(uint32_t) * 2;

    std::random_device randomDevice;
    std::uniform_int_distribution<uint32_t> randomDistribution;
    std::stringstream stream;
    stream << std::hex << std::setw(width) << std::setfill('0')
           << randomDistribution(randomDevice);
    return stream.str();
}

std::string StringUtils::getUUIDString()
{
    uuid_t uu;
    uuid_generate(uu);
    constexpr int UUID_SIZE = 36;
    std::string uuid = std::string(UUID_SIZE, 0);
    uuid_unparse_lower(uu, &uuid[0]);
    return uuid;
}

int64_t StringUtils::parseSize(const std::string &value)
{
    const char *s = value.c_str();
    char *endptr = nullptr;
    const int BASE = 10;
    int64_t size = strtoll(s, &endptr, BASE);

    if (endptr == s || size < 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::invalid_argument,
                                       "Invalid size `" << value << "`");
        return 0;
    }
    else if (strlen(endptr) == 0) {
        // Valid number without suffix
        return size;
    }
    else if (strlen(endptr) == 1) {
        // Valid number with suffix
        switch (*endptr) {
            case 'K': {
                const int64_t KILO = 1000LL;
                return size * KILO;
            }
            case 'M': {
                const int64_t MEGA = 1000000LL;
                return size * MEGA;
            }
            case 'G': {
                const int64_t GIGA = 1000000000LL;
                return size * GIGA;
            }
            case 'T': {
                const int64_t TERA = 1000000000000LL;
                return size * TERA;
            }
            default:
                break;
        }
    }

    BUILDBOXCOMMON_THROW_EXCEPTION(
        std::invalid_argument, "Invalid suffix for size `" << value << "`");
}

bool StringUtils::parseDigest(const std::string &value, Digest *digest)
{
    // "[hash in hex notation]/[size_bytes]"
    static const std::regex regex("^([0-9a-fA-F]+)/(\\d+)");
    std::smatch matches;
    if (std::regex_search(value, matches, regex) && matches.size() == 3) {
        const std::string hash = matches[1];
        const std::string size = matches[2];

        digest->set_hash(hash);
        digest->set_size_bytes(std::stoll(size));
        return true;
    }
    return false;
}

std::string StringUtils::join(const std::vector<std::string> &stringList,
                              const std::string &delimiter)
{
    if (stringList.empty()) {
        return "";
    }
    return std::accumulate(std::next(stringList.begin()), stringList.end(),
                           stringList[0],
                           [&delimiter](std::string a, const std::string &b) {
                               return std::move(a) + delimiter + b;
                           });
}

grpc::StatusCode StringUtils::parseStatusCode(const std::string &value)
{
    if (s_strToStatusCode.find(value) == s_strToStatusCode.end()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "Invalid gRPC status code to parse: " << value);
    }
    return s_strToStatusCode.at(value);
}

Digest StringUtils::digest_from_string(const std::string &s)
{
    // "[hash in hex notation]/[size_bytes]"

    static std::regex regex("([0-9a-fA-F]+)/(\\d+)");

    std::smatch matches;
    if (std::regex_search(s, matches, regex) && matches.size() == 3) {
        const std::string hash = matches[1];
        const std::string size = matches[2];

        Digest d;
        d.set_hash(hash);
        d.set_size_bytes(std::stoll(size));
        return d;
    }

    throw std::invalid_argument("Could not parse a digest from the string: " +
                                s);
}

} // namespace buildboxcommon
