/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 "paimon/common/data/variant/variant_json_utils.h"

#include <charconv>
#include <cmath>
#include <cstdio>
#include <cstdlib>

#include "arrow/vendored/datetime.h"
#include "fmt/format.h"

namespace paimon {

namespace {

constexpr int64_t kMicrosPerSecond = 1000000LL;
constexpr int64_t kMicrosPerDay = 86400LL * kMicrosPerSecond;

// Formats the shortest round-trip decimal digits + decimal exponent into the textual form of
// `java.lang.Double.toString` / `java.lang.Float.toString`: plain notation when
// `1e-3 <= |value| < 1e7`, computerized scientific notation otherwise.
std::string JavaFloatingToString(std::string_view digits, int32_t exp, bool negative) {
    std::string result;
    if (negative) {
        result.push_back('-');
    }
    if (exp >= -3 && exp < 7) {
        if (exp >= 0) {
            size_t int_digits = static_cast<size_t>(exp) + 1;
            if (digits.size() > int_digits) {
                result.append(digits, 0, int_digits);
                result.push_back('.');
                result.append(digits.substr(int_digits));
            } else {
                result.append(digits);
                result.append(int_digits - digits.size(), '0');
                result.append(".0");
            }
        } else {
            result.append("0.");
            result.append(static_cast<size_t>(-exp) - 1, '0');
            result.append(digits);
        }
    } else {
        result.push_back(digits[0]);
        result.push_back('.');
        if (digits.size() > 1) {
            result.append(digits.substr(1));
        } else {
            result.push_back('0');
        }
        result.push_back('E');
        result.append(std::to_string(exp));
    }
    return result;
}

template <typename T>
std::string FloatingToJavaString(T value) {
    if (std::isnan(value)) {
        return "NaN";
    }
    if (std::isinf(value)) {
        return value > 0 ? "Infinity" : "-Infinity";
    }
    if (value == 0) {
        return std::signbit(value) ? "-0.0" : "0.0";
    }
    // The shortest round-trip representation in scientific form, e.g. `1.234e+05`. gcc 8's
    // <charconv> lacks floating-point to_chars, so probe increasing precision until the value
    // round-trips; dropping a trailing zero digit is an exact rounding, so the first precision
    // that round-trips carries no trailing zeros.
    constexpr int kMaxFractionDigits = std::is_same_v<T, float> ? 8 : 16;
    char buf[64];
    int len = 0;
    for (int precision = 0; precision <= kMaxFractionDigits; ++precision) {
        len = std::snprintf(buf, sizeof(buf), "%.*e", precision, static_cast<double>(value));
        T parsed;
        if constexpr (std::is_same_v<T, float>) {
            parsed = std::strtof(buf, nullptr);
        } else {
            parsed = std::strtod(buf, nullptr);
        }
        if (parsed == value) {
            break;
        }
    }
    auto parse = [](std::string_view repr, std::string* digits, int32_t* exp, bool* negative) {
        *negative = repr[0] == '-';
        if (*negative) {
            repr.remove_prefix(1);
        }
        size_t e_pos = repr.find('e');
        std::string_view mantissa = repr.substr(0, e_pos);
        size_t exp_start = e_pos + 1;
        // `std::from_chars` does not accept a leading '+'.
        if (repr[exp_start] == '+') {
            ++exp_start;
        }
        *exp = 0;
        std::from_chars(repr.data() + exp_start, repr.data() + repr.size(), *exp);
        digits->clear();
        digits->push_back(mantissa[0]);
        if (mantissa.size() > 2) {
            digits->append(mantissa.substr(2));
        }
    };
    std::string digits;
    int32_t exp = 0;
    bool negative = false;
    parse(std::string_view(buf, static_cast<size_t>(len)), &digits, &exp, &negative);
    if (digits.size() == 1 && (exp < -3 || exp >= 8)) {
        // Java's FloatingDecimal emits at least two significant digits when the first digit
        // alone would terminate in scientific form; re-render the correctly rounded two-digit
        // representation (e.g. `Double.MIN_VALUE` is `4.9E-324`, not `5.0E-324`).
        len = std::snprintf(buf, sizeof(buf), "%.1e", static_cast<double>(value));
        parse(std::string_view(buf, static_cast<size_t>(len)), &digits, &exp, &negative);
    }
    return JavaFloatingToString(digits, exp, negative);
}

// Appends a year like `java.time.LocalDate.toString`: absolute value padded to at least 4
// digits; years above 9999 get a `+` prefix.
void AppendJavaYear(int64_t year, std::string* out) {
    if (year < 0) {
        out->push_back('-');
        year = -year;
        out->append(fmt::format("{:04}", year));
    } else {
        if (year > 9999) {
            out->push_back('+');
        }
        out->append(fmt::format("{:04}", year));
    }
}

void AppendDate(int64_t days_since_epoch, std::string* out) {
    using arrow_vendored::date::days;
    using arrow_vendored::date::sys_days;
    using arrow_vendored::date::year_month_day;
    year_month_day ymd{sys_days{days{days_since_epoch}}};
    AppendJavaYear(static_cast<int32_t>(ymd.year()), out);
    out->append(fmt::format("-{:02}-{:02}", static_cast<uint32_t>(ymd.month()),
                            static_cast<uint32_t>(ymd.day())));
}

int64_t FloorDiv(int64_t x, int64_t y) {
    int64_t quotient = x / y;
    if ((x % y != 0) && ((x < 0) != (y < 0))) {
        --quotient;
    }
    return quotient;
}

}  // namespace

void VariantJsonUtils::AppendEscapedJson(std::string_view str, std::string* out) {
    out->push_back('"');
    for (char c : str) {
        switch (c) {
            case '"':
                out->append("\\\"");
                break;
            case '\\':
                out->append("\\\\");
                break;
            case '\b':
                out->append("\\b");
                break;
            case '\f':
                out->append("\\f");
                break;
            case '\n':
                out->append("\\n");
                break;
            case '\r':
                out->append("\\r");
                break;
            case '\t':
                out->append("\\t");
                break;
            default:
                if (static_cast<uint8_t>(c) < 0x20) {
                    out->append(fmt::format("\\u{:04x}", static_cast<uint8_t>(c)));
                } else {
                    out->push_back(c);
                }
        }
    }
    out->push_back('"');
}

std::string VariantJsonUtils::JavaDoubleToString(double value) {
    return FloatingToJavaString(value);
}

std::string VariantJsonUtils::JavaFloatToString(float value) {
    return FloatingToJavaString(value);
}

std::string VariantJsonUtils::DateToString(int32_t days_since_epoch) {
    std::string result;
    AppendDate(days_since_epoch, &result);
    return result;
}

std::string VariantJsonUtils::TimestampToString(int64_t micros_since_epoch, int32_t offset_seconds,
                                                bool with_offset) {
    int64_t local_micros = micros_since_epoch + static_cast<int64_t>(offset_seconds) * 1000000;
    int64_t days = FloorDiv(local_micros, kMicrosPerDay);
    int64_t micros_of_day = local_micros - days * kMicrosPerDay;
    std::string result;
    AppendDate(days, &result);
    int64_t seconds_of_day = micros_of_day / kMicrosPerSecond;
    int64_t micros_of_second = micros_of_day % kMicrosPerSecond;
    result.append(fmt::format(" {:02}:{:02}:{:02}", seconds_of_day / 3600,
                              (seconds_of_day / 60) % 60, seconds_of_day % 60));
    if (micros_of_second != 0) {
        std::string fraction = fmt::format("{:06}", micros_of_second);
        while (fraction.back() == '0') {
            fraction.pop_back();
        }
        result.push_back('.');
        result.append(fraction);
    }
    if (with_offset) {
        int32_t abs_offset = offset_seconds >= 0 ? offset_seconds : -offset_seconds;
        result.append(fmt::format("{}{:02}:{:02}", offset_seconds >= 0 ? '+' : '-',
                                  abs_offset / 3600, (abs_offset / 60) % 60));
    }
    return result;
}

Result<int32_t> VariantJsonUtils::GetZoneOffsetSeconds(const std::string& zone_id,
                                                       int64_t micros_since_epoch) {
    std::string_view id = zone_id;
    if (id == "Z" || id == "UTC" || id == "GMT" || id == "UT") {
        return 0;
    }
    // `UTC+08:00` style ids: strip the prefix and parse the remaining fixed offset.
    if (id.size() > 3 && (id.substr(0, 3) == "UTC" || id.substr(0, 3) == "GMT")) {
        id.remove_prefix(3);
    } else if (id.size() > 2 && id.substr(0, 2) == "UT" && (id[2] == '+' || id[2] == '-')) {
        id.remove_prefix(2);
    }
    if (!id.empty() && (id[0] == '+' || id[0] == '-')) {
        bool negative = id[0] == '-';
        id.remove_prefix(1);
        // Accepted forms: H, HH, HH:MM, HHMM, HH:MM:SS, HHMMSS.
        std::string digits;
        for (char c : id) {
            if (c >= '0' && c <= '9') {
                digits.push_back(c);
            } else if (c != ':') {
                return Status::Invalid(fmt::format("Invalid zone offset: {}", zone_id));
            }
        }
        int32_t hours = 0;
        int32_t minutes = 0;
        int32_t seconds = 0;
        if (digits.size() == 1 || digits.size() == 2) {
            hours = std::stoi(digits);
        } else if (digits.size() == 4) {
            hours = std::stoi(digits.substr(0, 2));
            minutes = std::stoi(digits.substr(2, 2));
        } else if (digits.size() == 6) {
            hours = std::stoi(digits.substr(0, 2));
            minutes = std::stoi(digits.substr(2, 2));
            seconds = std::stoi(digits.substr(4, 2));
        } else {
            return Status::Invalid(fmt::format("Invalid zone offset: {}", zone_id));
        }
        if (hours > 18 || minutes > 59 || seconds > 59) {
            return Status::Invalid(fmt::format("Invalid zone offset: {}", zone_id));
        }
        int32_t total = hours * 3600 + minutes * 60 + seconds;
        return negative ? -total : total;
    }
    // IANA region id, resolved at the given instant (honoring DST).
    try {
        const auto* zone = arrow_vendored::date::locate_zone(zone_id);
        std::chrono::time_point<std::chrono::system_clock, std::chrono::microseconds> tp{
            std::chrono::microseconds(micros_since_epoch)};
        auto info = zone->get_info(tp);
        return static_cast<int32_t>(info.offset.count());
    } catch (const std::exception& e) {
        return Status::Invalid(fmt::format("Invalid zone id: {}, {}", zone_id, e.what()));
    }
}

}  // namespace paimon
