/* Proof of History: the host half of the graphics card miner.
 *
 * It reads the open block from the site, hands the template to the OpenCL kernel in sha256d.cl,
 * and posts back every nonce the card finds. Nothing is signed and no key is ever read: the address
 * you pass on the command line is only where the SOL is sent.
 *
 * Build (Linux):
 *   cc -O2 -o poh-miner miner.c -lOpenCL -lcurl
 * Build (Windows, MSVC, with the vendor SDK and libcurl on the include and lib paths):
 *   cl /O2 miner.c OpenCL.lib libcurl.lib
 *
 * Run:
 *   ./poh-miner --address <your solana address> [--site https://<the site>] [--share 4096]
 *
 * --share makes the miner hand in pool shares: it looks for hashes under target * share instead of
 * under the target itself, which is what the browser pool does. Leave it out to mine solo.
 */

#define CL_TARGET_OPENCL_VERSION 120
#include <CL/cl.h>
#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define BATCH (1u << 22)
#define MAX_FOUND 24

#ifdef _WIN32
#include <windows.h>
static void sleep_s(int s) { Sleep((DWORD)s * 1000); }
#else
#include <unistd.h>
static void sleep_s(int s) { sleep((unsigned)s); }
#endif

static const char *B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

static int b58decode32(const char *in, unsigned char out[32]) {
  unsigned char buf[64];
  memset(buf, 0, sizeof buf);
  size_t len = 0;
  for (const char *p = in; *p; p++) {
    const char *at = strchr(B58, *p);
    if (!at) return 0;
    unsigned carry = (unsigned)(at - B58);
    for (size_t i = 0; i < len; i++) {
      carry += (unsigned)buf[i] * 58u;
      buf[i] = (unsigned char)(carry & 0xff);
      carry >>= 8;
    }
    while (carry) {
      if (len >= sizeof buf) return 0;
      buf[len++] = (unsigned char)(carry & 0xff);
      carry >>= 8;
    }
  }
  for (const char *p = in; *p == '1'; p++) {
    if (len >= sizeof buf) return 0;
    buf[len++] = 0;
  }
  if (len != 32) return 0;
  for (size_t i = 0; i < 32; i++) out[i] = buf[31 - i];
  return 1;
}

static int hexdecode(const char *in, unsigned char *out, size_t n) {
  for (size_t i = 0; i < n; i++) {
    unsigned v;
    if (sscanf(in + i * 2, "%2x", &v) != 1) return 0;
    out[i] = (unsigned char)v;
  }
  return 1;
}

struct buf { char *data; size_t len; };

static size_t collect(void *ptr, size_t size, size_t nmemb, void *userdata) {
  struct buf *b = userdata;
  size_t add = size * nmemb;
  char *grown = realloc(b->data, b->len + add + 1);
  if (!grown) return 0;
  b->data = grown;
  memcpy(b->data + b->len, ptr, add);
  b->len += add;
  b->data[b->len] = 0;
  return add;
}

static char *http(const char *url, const char *post) {
  CURL *c = curl_easy_init();
  if (!c) return NULL;
  struct buf b = {NULL, 0};
  struct curl_slist *headers = NULL;
  curl_easy_setopt(c, CURLOPT_URL, url);
  curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, collect);
  curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
  curl_easy_setopt(c, CURLOPT_TIMEOUT, 20L);
  if (post) {
    headers = curl_slist_append(headers, "content-type: application/json");
    curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(c, CURLOPT_POSTFIELDS, post);
  }
  CURLcode r = curl_easy_perform(c);
  curl_slist_free_all(headers);
  curl_easy_cleanup(c);
  if (r != CURLE_OK) {
    free(b.data);
    return NULL;
  }
  return b.data;
}

/* The answers have a known shape, so one field reader is enough and there is no json dependency. */
static int field_str(const char *json, const char *key, char *out, size_t n) {
  char pattern[64];
  snprintf(pattern, sizeof pattern, "\"%s\":\"", key);
  const char *at = strstr(json, pattern);
  if (!at) return 0;
  at += strlen(pattern);
  const char *end = strchr(at, '"');
  if (!end || (size_t)(end - at) >= n) return 0;
  memcpy(out, at, (size_t)(end - at));
  out[end - at] = 0;
  return 1;
}

static int field_num(const char *json, const char *key, long long *out) {
  char pattern[64];
  snprintf(pattern, sizeof pattern, "\"%s\":", key);
  const char *at = strstr(json, pattern);
  if (!at) return 0;
  at += strlen(pattern);
  if (*at == '"') return 0;
  *out = atoll(at);
  return 1;
}

static void mul_target(unsigned char t[32], unsigned factor) {
  /* target = target * factor, saturating at all ones: the share target of a pool miner */
  unsigned long long carry = 0;
  for (int i = 31; i >= 0; i--) {
    unsigned long long v = (unsigned long long)t[i] * factor + carry;
    t[i] = (unsigned char)(v & 0xff);
    carry = v >> 8;
  }
  if (carry) memset(t, 0xff, 32);
}

int main(int argc, char **argv) {
  const char *site = "http://localhost:7244";
  const char *address = NULL;
  unsigned share = 1;
  for (int i = 1; i < argc - 1; i++) {
    if (!strcmp(argv[i], "--site")) site = argv[++i];
    else if (!strcmp(argv[i], "--address")) address = argv[++i];
    else if (!strcmp(argv[i], "--share")) share = (unsigned)strtoul(argv[++i], NULL, 10);
  }
  if (!address) {
    fprintf(stderr, "usage: poh-miner --address <solana address> [--site <url>] [--share <factor>]\n");
    return 1;
  }
  unsigned char pubkey[32];
  if (!b58decode32(address, pubkey)) {
    fprintf(stderr, "that is not a 32 byte base58 address\n");
    return 1;
  }
  curl_global_init(CURL_GLOBAL_DEFAULT);
  srand((unsigned)time(NULL));

  /* OpenCL: first platform, first GPU */
  cl_platform_id platform;
  cl_device_id device;
  cl_int err;
  if (clGetPlatformIDs(1, &platform, NULL) != CL_SUCCESS ||
      clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL) != CL_SUCCESS) {
    fprintf(stderr, "no OpenCL gpu found\n");
    return 1;
  }
  cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err);
  cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err);

  FILE *f = fopen("sha256d.cl", "rb");
  if (!f) { fprintf(stderr, "sha256d.cl is not next to the binary\n"); return 1; }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  char *source = malloc((size_t)size + 1);
  if (fread(source, 1, (size_t)size, f) != (size_t)size) { fprintf(stderr, "sha256d.cl is short\n"); return 1; }
  source[size] = 0;
  fclose(f);

  cl_program program = clCreateProgramWithSource(ctx, 1, (const char **)&source, NULL, &err);
  if (clBuildProgram(program, 1, &device, "", NULL, NULL) != CL_SUCCESS) {
    char log[8192];
    clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, sizeof log, log, NULL);
    fprintf(stderr, "kernel build failed:\n%s\n", log);
    return 1;
  }
  cl_kernel kernel = clCreateKernel(program, "mine", &err);

  cl_mem d_header = clCreateBuffer(ctx, CL_MEM_READ_ONLY, 18 * sizeof(cl_uint), NULL, &err);
  cl_mem d_target = clCreateBuffer(ctx, CL_MEM_READ_ONLY, 8 * sizeof(cl_uint), NULL, &err);
  cl_mem d_out = clCreateBuffer(ctx, CL_MEM_READ_WRITE, (1 + MAX_FOUND * 2) * sizeof(cl_uint), NULL, &err);

  long long mining_height = -1;
  char url[512];
  for (;;) {
    snprintf(url, sizeof url, "%s/api/state", site);
    char *state = http(url, NULL);
    if (!state) { fprintf(stderr, "the site did not answer, waiting\n"); sleep_s(5); continue; }

    char algo[32] = {0}, prev[80] = {0}, target_hex[80] = {0};
    long long height = 0;
    if (!field_str(state, "algo", algo, sizeof algo) ||
        !field_str(state, "prevHash", prev, sizeof prev) ||
        !field_str(state, "target", target_hex, sizeof target_hex) ||
        !field_num(state, "height", &height)) {
      printf("no block is open yet\n");
      free(state);
      sleep_s(10);
      continue;
    }
    free(state);
    if (strcmp(algo, "sha256d") != 0) {
      printf("the open block is %s, this miner is for the SHA-256d era\n", algo);
      sleep_s(20);
      continue;
    }

    unsigned char prev_raw[32], target_raw[32];
    if (!hexdecode(prev, prev_raw, 32) || !hexdecode(target_hex, target_raw, 32)) { sleep_s(5); continue; }
    if (share > 1) mul_target(target_raw, share);

    unsigned char preimage[72];
    memcpy(preimage, prev_raw, 32);
    for (int i = 0; i < 8; i++) preimage[32 + i] = (unsigned char)((unsigned long long)height >> (8 * (7 - i)));
    memcpy(preimage + 40, pubkey, 32);

    cl_uint header[18], target_words[8];
    for (int i = 0; i < 18; i++) {
      header[i] = ((cl_uint)preimage[i * 4] << 24) | ((cl_uint)preimage[i * 4 + 1] << 16) |
                  ((cl_uint)preimage[i * 4 + 2] << 8) | (cl_uint)preimage[i * 4 + 3];
    }
    for (int i = 0; i < 8; i++) {
      target_words[i] = ((cl_uint)target_raw[i * 4] << 24) | ((cl_uint)target_raw[i * 4 + 1] << 16) |
                        ((cl_uint)target_raw[i * 4 + 2] << 8) | (cl_uint)target_raw[i * 4 + 3];
    }
    clEnqueueWriteBuffer(queue, d_header, CL_TRUE, 0, sizeof header, header, 0, NULL, NULL);
    clEnqueueWriteBuffer(queue, d_target, CL_TRUE, 0, sizeof target_words, target_words, 0, NULL, NULL);

    if (height != mining_height) {
      printf("block %lld, target %.16s%s\n", height, target_hex, share > 1 ? " (share target)" : "");
      mining_height = height;
    }

    cl_uint zero[1 + MAX_FOUND * 2] = {0};
    clEnqueueWriteBuffer(queue, d_out, CL_TRUE, 0, sizeof zero, zero, 0, NULL, NULL);

    cl_ulong base = ((cl_ulong)rand() << 32) ^ (cl_ulong)time(NULL);
    cl_uint out_max = MAX_FOUND;
    clSetKernelArg(kernel, 0, sizeof d_header, &d_header);
    clSetKernelArg(kernel, 1, sizeof d_target, &d_target);
    clSetKernelArg(kernel, 2, sizeof base, &base);
    clSetKernelArg(kernel, 3, sizeof d_out, &d_out);
    clSetKernelArg(kernel, 4, sizeof out_max, &out_max);

    size_t global = BATCH;
    clock_t t0 = clock();
    clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global, NULL, 0, NULL, NULL);
    clFinish(queue);
    double seconds = (double)(clock() - t0) / CLOCKS_PER_SEC;

    cl_uint results[1 + MAX_FOUND * 2];
    clEnqueueReadBuffer(queue, d_out, CL_TRUE, 0, sizeof results, results, 0, NULL, NULL);
    cl_uint found = results[0] > MAX_FOUND ? MAX_FOUND : results[0];
    printf("%.2f MH/s, %u found\n", (double)BATCH / seconds / 1e6, found);

    if (found) {
      char body[4096];
      int n = snprintf(body, sizeof body,
                       "{\"address\":\"%s\",\"height\":%lld,\"mode\":\"%s\",\"shareFactor\":%u,\"nonces\":[",
                       address, height, share > 1 ? "pool" : "solo", share);
      for (cl_uint i = 0; i < found; i++) {
        unsigned long long nonce = ((unsigned long long)results[1 + i * 2] << 32) | results[2 + i * 2];
        n += snprintf(body + n, sizeof body - (size_t)n, "%s%llu", i ? "," : "", nonce);
      }
      snprintf(body + n, sizeof body - (size_t)n, "]}");
      snprintf(url, sizeof url, "%s/api/submit", site);
      char *answer = http(url, body);
      printf("submitted: %s\n", answer ? answer : "no answer");
      free(answer);
    }
  }
}
