// Copyright (c) 2014-2018 Zano Project
// Copyright (c) 2014-2018 The Louisdor Project
// Copyright (c) 2012-2013 The Boolberry developers
// Copyright (c) 2017-2025 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
//     https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// The EUPL is a copyleft licence that is compatible with the MIT/X11
// licence used by the original projects; the MIT terms are therefore
// considered “grandfathered” under the EUPL for this code.
//
// SPDX‑License‑Identifier: EUPL-1.2
//


#include "include_base_utils.h"
using namespace epee;

#include "basic_pow_helpers.h"
#include "currency_format_utils.h"
#include "serialization/binary_utils.h"
#include "serialization/stl_containers.h"
#include "config/currency_config.h"
#include "crypto/crypto.h"
#include "crypto/hash.h"
#include "common/int-util.h"

// RandomX PoW
#include "randomx.h"

// Keep ethash headers for stratum/RPC compatibility (epoch seed helpers)
#include "ethereum/libethash/ethash/ethash.hpp"

#include <mutex>
#include <thread>
#include <vector>
#include <algorithm>
#include <cstring>

namespace currency
{
  //--------------------------------------------------------------
  // RandomX global state — full dataset mode for fast hashing
  //--------------------------------------------------------------
  static randomx_cache*   rx_cache   = nullptr;
  static randomx_dataset* rx_dataset = nullptr;
  static randomx_flags    rx_flags   = RANDOMX_FLAG_DEFAULT;
  static std::once_flag   rx_init_flag;

  static void init_randomx()
  {
    rx_flags = randomx_get_flags() | RANDOMX_FLAG_FULL_MEM;
    LOG_PRINT_L0("[RandomX] Initializing with flags: " << static_cast<int>(rx_flags));

    rx_cache = randomx_alloc_cache(rx_flags);
    if (!rx_cache)
    {
      LOG_ERROR("[RandomX] Failed to allocate cache");
      throw std::bad_alloc();
    }

    static const char rx_key[] = "LetheanRandomXv1";
    randomx_init_cache(rx_cache, rx_key, sizeof(rx_key) - 1);
    LOG_PRINT_L0("[RandomX] Cache initialized with key \"LetheanRandomXv1\"");

    rx_dataset = randomx_alloc_dataset(rx_flags);
    if (!rx_dataset)
    {
      LOG_ERROR("[RandomX] Failed to allocate dataset, falling back to light mode");
      rx_flags = rx_flags & ~RANDOMX_FLAG_FULL_MEM;
      return;
    }

    // Multi-threaded dataset init
    unsigned long item_count = randomx_dataset_item_count();
    unsigned int num_threads = std::thread::hardware_concurrency();
    if (num_threads == 0) num_threads = 4;
    if (num_threads > 16) num_threads = 16;

    LOG_PRINT_L0("[RandomX] Initializing dataset (" << item_count << " items, " << num_threads << " threads)...");

    std::vector<std::thread> threads;
    unsigned long per_thread = item_count / num_threads;
    unsigned long remainder  = item_count % num_threads;

    for (unsigned int i = 0; i < num_threads; ++i)
    {
      unsigned long start = i * per_thread + std::min((unsigned long)i, remainder);
      unsigned long count = per_thread + (i < remainder ? 1 : 0);
      threads.emplace_back([start, count]() {
        randomx_init_dataset(rx_dataset, rx_cache, start, count);
      });
    }
    for (auto& t : threads) t.join();

    LOG_PRINT_L0("[RandomX] Dataset initialized OK");
  }

  static randomx_vm* get_thread_vm()
  {
    std::call_once(rx_init_flag, init_randomx);

    static thread_local randomx_vm* vm = nullptr;
    if (!vm)
    {
      if (rx_dataset)
        vm = randomx_create_vm(rx_flags, nullptr, rx_dataset);
      else
        vm = randomx_create_vm(rx_flags & ~RANDOMX_FLAG_FULL_MEM, rx_cache, nullptr);

      if (!vm)
      {
        LOG_ERROR("[RandomX] Failed to create VM");
        throw std::bad_alloc();
      }
    }
    return vm;
  }

  //--------------------------------------------------------------
  // Ethash helpers kept for stratum/RPC backward compat
  //--------------------------------------------------------------
  int ethash_height_to_epoch(uint64_t height)
  {
    return static_cast<int>(height / ETHASH_EPOCH_LENGTH);
  }
  //--------------------------------------------------------------
  crypto::hash ethash_epoch_to_seed(int epoch)
  {
    auto res_eth = ethash_calculate_epoch_seed(epoch);
    crypto::hash result = currency::null_hash;
    memcpy(&result.data, &res_eth, sizeof(res_eth));
    return result;
  }
  //--------------------------------------------------------------
  crypto::hash get_block_longhash(uint64_t height, const crypto::hash& block_header_hash, uint64_t nonce)
  {
    // Combine header hash + nonce into a single input buffer
    struct {
      crypto::hash header;
      uint64_t     nonce;
    } input;
    input.header = block_header_hash;
    input.nonce  = nonce;

    char hash_out[RANDOMX_HASH_SIZE];
    randomx_vm* vm = get_thread_vm();
    randomx_calculate_hash(vm, &input, sizeof(input), hash_out);

    crypto::hash result = currency::null_hash;
    std::memcpy(&result.data, hash_out, sizeof(result.data));
    return result;
  }
  //---------------------------------------------------------------
  crypto::hash get_block_header_mining_hash(const block& b)
  {
    blobdata bd = get_block_hashing_blob(b);

    set_nonce_to_blockblob(bd, 0);
    return crypto::cn_fast_hash(bd.data(), bd.size());
  }
  //---------------------------------------------------------------
  void get_block_longhash(const block& b, crypto::hash& res)
  {
    /*
    Since etherium hash has a bit different approach in minig, to adopt our code we made little hack:
    etherium hash calculates from block's hash and(!) nonce, both passed into PoW hash function.
    To achieve the same effect we make blob of data from block in normal way, but then set to zerro nonce
    inside serialized buffer, and then pass this nonce to ethash algo as a second argument, as it expected.
    */
    crypto::hash bl_hash = get_block_header_mining_hash(b);
    res = get_block_longhash(get_block_height(b), bl_hash, b.nonce);
  }
  //---------------------------------------------------------------
  crypto::hash get_block_longhash(const block& b)
  {
    crypto::hash p = null_hash;
    get_block_longhash(b, p);
    return p;
  }
}