#ifndef NPROOFS

#include "allocate.h"
#include "error.h"
#include "file.h"
#include "inline.h"

#include <string.h>

#include <limits.h>

#define size_buffer (4u << 20)

struct write_buffer {
  unsigned char *chars;
  size_t pos;
};

typedef struct write_buffer write_buffer;

struct proof {
  write_buffer buffer;
  kissat *solver;
  bool binary;
  file *file;
  ints line;
  uint64_t added;
  uint64_t deleted;
  uint64_t lines;
  uint64_t literals;
#ifndef NDEBUG
  bool empty;
  char *units;
  size_t size_units;
#endif
#if !defined(NDEBUG) || defined(LOGGING)
  unsigneds imported;
#endif
};

#undef LOGPREFIX
#define LOGPREFIX "PROOF"

#define LOGIMPORTED3(...) \
  LOGLITS3 (SIZE_STACK (proof->imported), BEGIN_STACK (proof->imported), \
            __VA_ARGS__)

#define LOGLINE3(...) \
  LOGINTS3 (SIZE_STACK (proof->line), BEGIN_STACK (proof->line), \
            __VA_ARGS__)

// clang-format off

static inline void write_char (proof *, unsigned char)
  ATTRIBUTE_ALWAYS_INLINE;

static void write_bytes (proof *, const char *, size_t);
static void write_string (proof *, const char *);
static void print_proof_footer (proof *);

// clang-format on

void kissat_init_proof (kissat *solver, file *file, bool binary) {
  assert (file);
  assert (!solver->proof);
  proof *proof = kissat_calloc (solver, 1, sizeof (struct proof));
  proof->binary = false;
  proof->file = file;
  proof->solver = solver;
  proof->buffer.chars = kissat_malloc (solver, size_buffer);
  solver->proof = proof;
  write_string (proof, "pseudo-Boolean proof version 3.0\n");
  LOG ("starting to trace VeriPB proof");
  (void) binary;
}

static void flush_buffer (proof *proof) {
  size_t bytes = proof->buffer.pos;
  if (!bytes)
    return;
  size_t written = kissat_write (proof->file, proof->buffer.chars, bytes);
  if (bytes != written)
    kissat_fatal ("flushing %zu bytes in proof write-buffer failed", bytes);
  proof->buffer.pos = 0;
}

void kissat_release_proof (kissat *solver) {
  proof *proof = solver->proof;
  assert (proof);
  LOG ("stopping to trace proof");
  print_proof_footer (proof);
  flush_buffer (proof);
  kissat_flush (proof->file);
  kissat_free (solver, proof->buffer.chars, size_buffer);
  RELEASE_STACK (proof->line);
#ifndef NDEBUG
  kissat_free (solver, proof->units, proof->size_units);
#endif
#if !defined(NDEBUG) || defined(LOGGING)
  RELEASE_STACK (proof->imported);
#endif
  kissat_free (solver, proof, sizeof (struct proof));
  solver->proof = 0;
}

#ifndef QUIET

#include <inttypes.h>

#define PERCENT_LINES(NAME) kissat_percent (proof->NAME, proof->lines)

void kissat_print_proof_statistics (kissat *solver, bool verbose) {
  proof *proof = solver->proof;
  PRINT_STAT ("proof_added", proof->added, PERCENT_LINES (added), "%",
              "per line");
  PRINT_STAT ("proof_bytes", proof->file->bytes,
              proof->file->bytes / (double) (1 << 20), "MB", "");
  PRINT_STAT ("proof_deleted", proof->deleted, PERCENT_LINES (deleted), "%",
              "per line");
  if (verbose)
    PRINT_STAT ("proof_lines", proof->lines, 100, "%", "");
  if (verbose)
    PRINT_STAT ("proof_literals", proof->literals,
                kissat_average (proof->literals, proof->lines), "",
                "per line");
}

#endif

static inline void import_external_proof_literal (kissat *, proof *, int)
  ATTRIBUTE_ALWAYS_INLINE;

static inline void
import_internal_proof_literal (kissat *, proof *, unsigned)
  ATTRIBUTE_ALWAYS_INLINE;

static inline void write_char (proof *proof, unsigned char ch) {
  write_buffer *buffer = &proof->buffer;
  if (buffer->pos == size_buffer)
    flush_buffer (proof);
  buffer->chars[buffer->pos++] = ch;
}

static void write_bytes (proof *proof, const char *str, size_t len) {
  write_buffer *buffer = &proof->buffer;
  while (len) {
    size_t available = size_buffer - buffer->pos;
    if (!available) {
      flush_buffer (proof);
      available = size_buffer;
    }
    const size_t chunk = len < available ? len : available;
    memcpy (buffer->chars + buffer->pos, str, chunk);
    buffer->pos += chunk;
    str += chunk;
    len -= chunk;
  }
}

static void write_string (proof *proof, const char *str) {
  write_bytes (proof, str, strlen (str));
}

static inline void import_internal_proof_literal (kissat *solver,
                                                  proof *proof,
                                                  unsigned ilit) {
  int elit = kissat_export_literal (solver, ilit);
  assert (elit);
  PUSH_STACK (proof->line, elit);
  proof->literals++;
#if !defined(NDEBUG) || defined(LOGGING)
  PUSH_STACK (proof->imported, ilit);
#endif
}

static inline void import_external_proof_literal (kissat *solver,
                                                  proof *proof, int elit) {
  assert (elit);
  PUSH_STACK (proof->line, elit);
  proof->literals++;
#ifndef NDEBUG
  assert (EMPTY_STACK (proof->imported));
#endif
}

static void import_internal_proof_binary (kissat *solver, proof *proof,
                                          unsigned a, unsigned b) {
  assert (EMPTY_STACK (proof->line));
  import_internal_proof_literal (solver, proof, a);
  import_internal_proof_literal (solver, proof, b);
}

static void import_internal_proof_literals (kissat *solver, proof *proof,
                                            size_t size,
                                            const unsigned *ilits) {
  assert (EMPTY_STACK (proof->line));
  assert (size <= UINT_MAX);
  for (size_t i = 0; i < size; i++)
    import_internal_proof_literal (solver, proof, ilits[i]);
}

static void import_external_proof_literals (kissat *solver, proof *proof,
                                            size_t size, const int *elits) {
  assert (EMPTY_STACK (proof->line));
  assert (size <= UINT_MAX);
  for (size_t i = 0; i < size; i++)
    import_external_proof_literal (solver, proof, elits[i]);
}

static void import_proof_clause (kissat *solver, proof *proof,
                                 const clause *c) {
  import_internal_proof_literals (solver, proof, c->size, c->lits);
}

static unsigned external_variable (int elit) {
  assert (elit);
  assert (elit != INT_MIN);
  return elit < 0 ? -elit : elit;
}

static char *format_unsigned (char *out, unsigned number) {
  char buffer[16];
  char *p = buffer + sizeof buffer;
  do {
    *--p = '0' + (number % 10);
    number /= 10;
  } while (number);
  const size_t size = (buffer + sizeof buffer) - p;
  memcpy (out, p, size);
  return out + size;
}

static void print_external_pb_literal (proof *proof, int elit) {
  char buffer[20];
  char *p = buffer;
  *p++ = '+';
  *p++ = '1';
  *p++ = ' ';
  if (elit < 0)
    *p++ = '~';
  *p++ = 'x';
  p = format_unsigned (p, external_variable (elit));
  *p++ = ' ';
  write_bytes (proof, buffer, p - buffer);
}

static void print_pseudo_boolean_constraint (proof *proof) {
  for (all_stack (int, elit, proof->line))
    print_external_pb_literal (proof, elit);
  write_bytes (proof, ">= 1", 4);
}

static void print_redundance_witness (proof *proof) {
  assert (!EMPTY_STACK (proof->line));
  const int pivot = PEEK_STACK (proof->line, 0);
  char buffer[32];
  char *p = buffer;
  *p++ = ' ';
  *p++ = ':';
  *p++ = ' ';
  *p++ = 'x';
  p = format_unsigned (p, external_variable (pivot));
  *p++ = ' ';
  *p++ = '-';
  *p++ = '>';
  *p++ = ' ';
  *p++ = pivot < 0 ? '0' : '1';
  write_bytes (proof, buffer, p - buffer);
}

static void clear_proof_line (proof *proof) {
  CLEAR_STACK (proof->line);
#if !defined(NDEBUG) || defined(LOGGING)
  CLEAR_STACK (proof->imported);
#endif
}

static bool normalize_proof_line (proof *proof) {
  int *const begin = BEGIN_STACK (proof->line);
  int *const end = END_STACK (proof->line);
  int *q = begin;
  for (int *p = begin; p != end; p++) {
    const int lit = *p;
    assert (lit);
    assert (lit != INT_MIN);
    bool duplicated = false;
    for (int *r = begin; r != q; r++) {
      if (*r == lit) {
        duplicated = true;
        break;
      }
      if (*r == -lit) {
        clear_proof_line (proof);
        return false;
      }
    }
    if (!duplicated)
      *q++ = lit;
  }
  proof->line.end = q;
  return true;
}

static void finish_proof_line (proof *proof) {
  proof->lines++;
  write_char (proof, ';');
  write_char (proof, '\n');
  clear_proof_line (proof);
#ifndef NOPTIONS
  kissat *solver = proof->solver;
#endif
  if (GET_OPTION (flushproof)) {
    flush_buffer (proof);
    kissat_flush (proof->file);
  }
}

static void print_proof_footer (proof *proof) {
  write_string (proof, "output NONE;\n");
  if (proof->solver->inconsistent)
    write_string (proof, "conclusion UNSAT;\n");
  else
    write_string (proof, "conclusion NONE;\n");
  write_string (proof, "end pseudo-Boolean proof;\n");
}

#ifndef NDEBUG

static unsigned external_to_proof_literal (int elit) {
  assert (elit);
  assert (elit != INT_MIN);
  return 2u * (abs (elit) - 1) + (elit < 0);
}

static void resize_proof_units (proof *proof, unsigned plit) {
  kissat *solver = proof->solver;
  const size_t old_size = proof->size_units;
  size_t new_size = old_size ? old_size : 2;
  while (new_size <= plit)
    new_size *= 2;
  char *new_units = kissat_calloc (solver, new_size, 1);
  if (old_size)
    memcpy (new_units, proof->units, old_size);
  kissat_dealloc (solver, proof->units, old_size, 1);
  proof->units = new_units;
  proof->size_units = new_size;
}

static void check_repeated_proof_lines (proof *proof) {
  size_t size = SIZE_STACK (proof->line);
  if (!size) {
    assert (!proof->empty);
    proof->empty = true;
  } else if (size == 1) {
    const int eunit = PEEK_STACK (proof->line, 0);
    const unsigned punit = external_to_proof_literal (eunit);
    assert (punit != INVALID_LIT);
    if (!proof->size_units || proof->size_units <= punit)
      resize_proof_units (proof, punit);
    proof->units[punit] = 1;
  }
}

#endif

static void print_added_proof_line (proof *proof) {
  proof->added++;
#ifdef LOGGING
  struct kissat *solver = proof->solver;
  assert (SIZE_STACK (proof->imported) == SIZE_STACK (proof->line));
  LOGIMPORTED3 ("added proof line");
  LOGLINE3 ("added proof line");
#endif
#ifndef NDEBUG
  check_repeated_proof_lines (proof);
#endif
  assert (!proof->binary);
  if (EMPTY_STACK (proof->line))
    write_bytes (proof, "rup ", 4);
  else
    write_bytes (proof, "red ", 4);
  print_pseudo_boolean_constraint (proof);
  if (!EMPTY_STACK (proof->line))
    print_redundance_witness (proof);
  finish_proof_line (proof);
}

static void print_delete_proof_line (proof *proof) {
  if (!normalize_proof_line (proof))
    return;
  proof->deleted++;
#ifdef LOGGING
  struct kissat *solver = proof->solver;
  if (SIZE_STACK (proof->imported) == SIZE_STACK (proof->line))
    LOGIMPORTED3 ("deleted internal proof line");
  LOGLINE3 ("deleted external proof line");
#endif
  assert (!proof->binary);
  write_bytes (proof, "del spec ", 9);
  print_pseudo_boolean_constraint (proof);
  finish_proof_line (proof);
}

void kissat_add_binary_to_proof (kissat *solver, unsigned a, unsigned b) {
  proof *proof = solver->proof;
  assert (proof);
  import_internal_proof_binary (solver, proof, a, b);
  print_added_proof_line (proof);
}

void kissat_add_clause_to_proof (kissat *solver, const clause *c) {
  proof *proof = solver->proof;
  assert (proof);
  import_proof_clause (solver, proof, c);
  print_added_proof_line (proof);
}

void kissat_add_empty_to_proof (kissat *solver) {
  proof *proof = solver->proof;
  assert (proof);
  assert (EMPTY_STACK (proof->line));
  print_added_proof_line (proof);
}

void kissat_add_lits_to_proof (kissat *solver, size_t size,
                               const unsigned *ilits) {
  proof *proof = solver->proof;
  assert (proof);
  import_internal_proof_literals (solver, proof, size, ilits);
  print_added_proof_line (proof);
}

void kissat_add_unit_to_proof (kissat *solver, unsigned ilit) {
  proof *proof = solver->proof;
  assert (proof);
  assert (EMPTY_STACK (proof->line));
  import_internal_proof_literal (solver, proof, ilit);
  print_added_proof_line (proof);
}

void kissat_shrink_clause_in_proof (kissat *solver, const clause *c,
                                    unsigned remove, unsigned keep) {
  proof *proof = solver->proof;
  const value *const values = solver->values;
  assert (EMPTY_STACK (proof->line));
  const unsigned *ilits = c->lits;
  const unsigned size = c->size;
  for (unsigned i = 0; i != size; i++) {
    const unsigned ilit = ilits[i];
    if (ilit == remove)
      continue;
    if (ilit != keep && values[ilit] < 0 && !LEVEL (ilit))
      continue;
    import_internal_proof_literal (solver, proof, ilit);
  }
  print_added_proof_line (proof);
  import_proof_clause (solver, proof, c);
  print_delete_proof_line (proof);
}

void kissat_delete_binary_from_proof (kissat *solver, unsigned a,
                                      unsigned b) {
  proof *proof = solver->proof;
  assert (proof);
  import_internal_proof_binary (solver, proof, a, b);
  print_delete_proof_line (proof);
}

void kissat_delete_clause_from_proof (kissat *solver, const clause *c) {
  proof *proof = solver->proof;
  assert (proof);
  import_proof_clause (solver, proof, c);
  print_delete_proof_line (proof);
}

void kissat_delete_external_from_proof (kissat *solver, size_t size,
                                        const int *elits) {
  proof *proof = solver->proof;
  assert (proof);
  LOGINTS3 (size, elits, "explicitly deleted");
  import_external_proof_literals (solver, proof, size, elits);
  print_delete_proof_line (proof);
}

void kissat_delete_internal_from_proof (kissat *solver, size_t size,
                                        const unsigned *ilits) {
  proof *proof = solver->proof;
  assert (proof);
  import_internal_proof_literals (solver, proof, size, ilits);
  print_delete_proof_line (proof);
}

#else
int kissat_proof_dummy_to_avoid_warning;
#endif
