// Copyright 2024 The Bazel Authors. All rights reserved.
//
// 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 "src/tools/singlejar/log4j2_plugin_dat_combiner.h"

#include <cstdint>
#include <map>
#include <sstream>
#include <string>
#include <vector>

#include "src/tools/singlejar/diag.h"
#include "src/tools/singlejar/transient_bytes.h"
#include "src/tools/singlejar/zip_headers.h"
#include "src/tools/singlejar/zlib_interface.h"
#include <zlib.h>

// Swaps the byte order of the input value to ensure it matches the host
// system's endianness.
//
// In the context of this code, byte order swapping is required because the
// Log4j2 plugin cache file format stores multi-byte values (e.g., integers,
// strings) in big-endian order. Many systems, including most modern desktop and
// server  processors, use little-endian byte order internally.
//
// When reading data from the cache file, the byte order of the stored values
// must be converted to the host system's endianness for correct interpretation.
// Similarly, when writing data to the cache file, the values must be converted
// to big-endian order to maintain compatibility with the file format
// specification.
template <typename T>
inline static T swapByteOrder(const T& val) {
  int totalBytes = sizeof(val);
  T swapped = (T)0;
  for (int i = 0; i < totalBytes; ++i) {
    swapped |= (val >> (8 * (totalBytes - i - 1)) & 0xFF) << (8 * i);
  }
  return swapped;
}

bool readBool(std::istringstream& stream) {
  bool value;
  stream.read(reinterpret_cast<char*>(&value), sizeof(value));
  if (stream.eof()) {
    diag_errx(1, "%s:%d: Log4j2Plugins.dat file is malformed", __FILE__,
              __LINE__);
  }
  return value;
}

uint32_t readInt(std::istringstream& stream) {
  uint32_t value;
  stream.read(reinterpret_cast<char*>(&value), sizeof(value));
  if (stream.eof()) {
    diag_errx(1, "%s:%d: Log4j2Plugins.dat file is malformed", __FILE__,
              __LINE__);
  }
  return swapByteOrder(value);
}

std::string readUTFString(std::istringstream& stream) {
  uint16_t length;
  stream.read(reinterpret_cast<char*>(&length), sizeof(length));
  if (stream.eof()) {
    diag_errx(1, "%s:%d: Log4j2Plugins.dat file is malformed", __FILE__,
              __LINE__);
  }
  length = swapByteOrder(length);  // Convert to host byte order
  std::string result(length, '\0');
  stream.read(&result[0], length);
  if (stream.eof()) {
    diag_errx(1, "%s:%d: Log4j2Plugins.dat file is malformed", __FILE__,
              __LINE__);
  }
  return result;
}

void writeBoolean(std::vector<uint8_t>& buffer, bool value) {
  uint8_t byte = value ? 1 : 0;
  buffer.push_back(byte);
}

void writeInt(std::vector<uint8_t>& buffer, int value) {
  value = swapByteOrder(value);
  const uint8_t* data = reinterpret_cast<const uint8_t*>(&value);
  buffer.insert(buffer.end(), data, data + sizeof(value));
}

void writeUTFString(std::vector<uint8_t>& buffer, const std::string& str) {
  uint16_t length = swapByteOrder(static_cast<uint16_t>(str.size()));
  const uint8_t* lengthData = reinterpret_cast<const uint8_t*>(&length);
  buffer.insert(buffer.end(), lengthData, lengthData + sizeof(length));
  buffer.insert(buffer.end(), str.begin(), str.end());
}

// Write Log4j2 plugin cache file.
//
// Modeled after the Java canonical implementation here:
// https://github.com/apache/logging-log4j2/blob/8573ef778d2fad2bbec50a687955dccd2a616cc5/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java#L66-L85
std::vector<uint8_t> writeLog4j2PluginCacheFile(
    const std::map<std::string, std::map<std::string, PluginEntry>>&
        categories) {
  std::vector<uint8_t> buffer;
  writeInt(buffer, static_cast<int>(categories.size()));
  for (const auto& categoryPair : categories) {
    writeUTFString(buffer, categoryPair.first);
    writeInt(buffer, static_cast<int>(categoryPair.second.size()));
    for (const auto& pluginPair : categoryPair.second) {
      const PluginEntry& plugin = pluginPair.second;
      writeUTFString(buffer, plugin.key);
      writeUTFString(buffer, plugin.className);
      writeUTFString(buffer, plugin.name);
      writeBoolean(buffer, plugin.printable);
      writeBoolean(buffer, plugin.defer);
    }
  }

  return buffer;
}

// Load Log4j2 plugin .cache file.
//
// Modeled after the Java canonical implementation here:
// https://github.com/apache/logging-log4j2/blob/8573ef778d2fad2bbec50a687955dccd2a616cc5/log4j-core/src/main/java/org/apache/logging/log4j/core/config/plugins/processor/PluginCache.java#L93-L124
std::map<std::string, std::map<std::string, PluginEntry>>
loadLog4j2PluginCacheFile(TransientBytes& transientBytes) {
  uint64_t data_size = transientBytes.data_size();
  std::vector<uint8_t> byteData(data_size);
  uint32_t checksum = 0;
  transientBytes.CopyOut(byteData.data(), &checksum);
  std::istringstream buffer(std::string(byteData.begin(), byteData.end()));

  std::map<std::string, std::map<std::string, PluginEntry>> categories;
  uint32_t categoriesCount = readInt(buffer);
  for (uint32_t i = 0; i < categoriesCount; ++i) {
    std::string category = readUTFString(buffer);
    uint32_t entries = readInt(buffer);
    for (uint32_t j = 0; j < entries; ++j) {
      std::string key = readUTFString(buffer);
      std::string className = readUTFString(buffer);
      std::string name = readUTFString(buffer);
      bool printable = readBool(buffer);
      bool defer = readBool(buffer);
      PluginEntry entry(key, className, name, printable, defer, category);
      categories[category].insert({key, entry});
    }
  }

  return categories;
}

Log4J2PluginDatCombiner::~Log4J2PluginDatCombiner() {}

bool Log4J2PluginDatCombiner::Merge(const CDH* cdh, const LH* lh) {
  TransientBytes bytes_;
  if (lh->compression_method() == Z_NO_COMPRESSION) {
    bytes_.ReadEntryContents(cdh, lh);
  } else if (lh->compression_method() == Z_DEFLATED) {
    if (!inflater_) {
      inflater_.reset(new Inflater());
    }
    bytes_.DecompressEntryContents(cdh, lh, inflater_.get());
  } else {
    diag_errx(2, "neither stored nor deflated");
  }

  auto newCategories = loadLog4j2PluginCacheFile(bytes_);
  for (const auto& newCategoryPair : newCategories) {
    auto newCategoryId = newCategoryPair.first;
    auto newPlugins = newCategoryPair.second;

    auto existingCategoryPair = categories_.find(newCategoryId);
    if (existingCategoryPair != categories_.end()) {
      for (const auto& pluginPair : newPlugins) {
        auto newPluginKey = pluginPair.first;
        auto newPlugin = pluginPair.second;

        auto existingPluginKey = categories_[newCategoryId].find(newPluginKey);
        if (existingPluginKey != categories_[newCategoryId].end() &&
            no_duplicates_) {
          diag_errx(1, "%s:%d: Log4J2 plugin %s.%s is present in multiple jars",
                    __FILE__, __LINE__, newCategoryId.c_str(),
                    newPluginKey.c_str());
        }

        categories_[newCategoryId].insert(pluginPair);
      }
    } else {
      categories_[newCategoryId] = newPlugins;
    }
  }

  return true;
}

void* Log4J2PluginDatCombiner::OutputEntry(bool compress) {
  if (categories_.empty()) {
    return nullptr;
  }
  auto buffer = writeLog4j2PluginCacheFile(categories_);
  concatenator_->Append(reinterpret_cast<const char*>(buffer.data()),
                        buffer.size());
  return concatenator_->OutputEntry(compress);
}
