/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2020 SAP SE. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 *
 */

#include "precompiled.hpp"
#include "jvm.h"
#include "runtime/arguments.hpp"
#include "runtime/mutexLocker.hpp"
#include "runtime/os.hpp"
#include "runtime/thread.inline.hpp"
#include "services/heapDumperCompression.hpp"


char const* FileWriter::open_writer() {
  assert(_fd < 0, "Must not already be open");

  _fd = os::create_binary_file(_path, _overwrite);

  if (_fd < 0) {
    return os::strerror(errno);
  }

  return NULL;
}

FileWriter::~FileWriter() {
  if (_fd >= 0) {
    os::close(_fd);
    _fd = -1;
  }
}

char const* FileWriter::write_buf(char* buf, ssize_t size) {
  assert(_fd >= 0, "Must be open");
  assert(size > 0, "Must write at least one byte");

  if (!os::write(_fd, buf, (size_t)size)) {
    return os::strerror(errno);
  }

  return NULL;
}


typedef char const* (*GzipInitFunc)(size_t, size_t*, size_t*, int);
typedef size_t(*GzipCompressFunc)(char*, size_t, char*, size_t, char*, size_t,
                                  int, char*, char const**);

static GzipInitFunc gzip_init_func;
static GzipCompressFunc gzip_compress_func;

void* GZipCompressor::load_gzip_func(char const* name) {
  char path[JVM_MAXPATHLEN];
  char ebuf[1024];
  void* handle;
  MutexLocker locker(Zip_lock, Monitor::_no_safepoint_check_flag);

  if (os::dll_locate_lib(path, sizeof(path), Arguments::get_dll_dir(), "zip")) {
    handle = os::dll_load(path, ebuf, sizeof ebuf);

    if (handle != NULL) {
      return os::dll_lookup(handle, name);
    }
  }

  return NULL;
}

char const* GZipCompressor::init(size_t block_size, size_t* needed_out_size,
                                 size_t* needed_tmp_size) {
  _block_size = block_size;
  _is_first = true;

  if (gzip_compress_func == NULL) {
    gzip_compress_func = (GzipCompressFunc) load_gzip_func("ZIP_GZip_Fully");

    if (gzip_compress_func == NULL) {
      return "Cannot get ZIP_GZip_Fully function";
    }
  }

  if (gzip_init_func == NULL) {
    gzip_init_func = (GzipInitFunc) load_gzip_func("ZIP_GZip_InitParams");

    if (gzip_init_func == NULL) {
      return "Cannot get ZIP_GZip_InitParams function";
    }
  }

  char const* result = gzip_init_func(block_size, needed_out_size,
                                      needed_tmp_size, _level);
  *needed_out_size += 1024; // Add extra space for the comment in the first chunk.

  return result;
}

char const* GZipCompressor::compress(char* in, size_t in_size, char* out, size_t out_size,
                                     char* tmp, size_t tmp_size, size_t* compressed_size) {
  char const* msg = NULL;

  if (_is_first) {
    char buf[128];
    // Write the block size used as a comment in the first gzip chunk, so the
    // code used to read it later can make a good choice of the buffer sizes it uses.
    jio_snprintf(buf, sizeof(buf), "HPROF BLOCKSIZE=" SIZE_FORMAT, _block_size);
    *compressed_size = gzip_compress_func(in, in_size, out, out_size, tmp, tmp_size, _level,
                                          buf, &msg);
    _is_first = false;
  } else {
    *compressed_size = gzip_compress_func(in, in_size, out, out_size, tmp, tmp_size, _level,
                                          NULL, &msg);
  }

  return msg;
}

WorkList::WorkList() {
  _head._next = &_head;
  _head._prev = &_head;
}

void WorkList::insert(WriteWork* before, WriteWork* work) {
  work->_prev = before;
  work->_next = before->_next;
  before->_next = work;
  work->_next->_prev = work;
}

WriteWork* WorkList::remove(WriteWork* work) {
  if (work != NULL) {
    assert(work->_next != work, "Invalid next");
    assert(work->_prev != work, "Invalid prev");
    work->_prev->_next = work->_next;;
    work->_next->_prev = work->_prev;
    work->_next = NULL;
    work->_prev = NULL;
  }

  return work;
}

void WorkList::add_by_id(WriteWork* work) {
  if (is_empty()) {
    add_first(work);
  } else {
    WriteWork* last_curr = &_head;
    WriteWork* curr = _head._next;

    while (curr->_id < work->_id) {
      last_curr = curr;
      curr = curr->_next;

      if (curr == &_head) {
        add_last(work);
        return;
      }
    }

    insert(last_curr, work);
  }
}



CompressionBackend::CompressionBackend(AbstractWriter* writer,
     AbstractCompressor* compressor, size_t block_size, size_t max_waste) :
  _active(false),
  _err(NULL),
  _nr_of_threads(0),
  _works_created(0),
  _work_creation_failed(false),
  _id_to_write(0),
  _next_id(0),
  _in_size(block_size),
  _max_waste(max_waste),
  _out_size(0),
  _tmp_size(0),
  _written(0),
  _writer(writer),
  _compressor(compressor),
  _lock(new (std::nothrow) PaddedMonitor(Mutex::leaf, "HProf Compression Backend",
    true, Mutex::_safepoint_check_never)) {
  if (_writer == NULL) {
    set_error("Could not allocate writer");
  } else if (_lock == NULL) {
    set_error("Could not allocate lock");
  } else {
    set_error(_writer->open_writer());
  }

  if (_compressor != NULL) {
    set_error(_compressor->init(_in_size, &_out_size, &_tmp_size));
  }

  _current = allocate_work(_in_size, _out_size, _tmp_size);

  if (_current == NULL) {
    set_error("Could not allocate memory for buffer");
  }

  _active = (_err == NULL);
}

CompressionBackend::~CompressionBackend() {
  assert(!_active, "Must not be active by now");
  assert(_nr_of_threads == 0, "Must have no active threads");
  assert(_to_compress.is_empty() && _finished.is_empty(), "Still work to do");

  free_work_list(&_unused);
  free_work(_current);
  assert(_works_created == 0, "All work must have been freed");

  delete _compressor;
  delete _writer;
  delete _lock;
}

void CompressionBackend::deactivate() {
  assert(_active, "Must be active");

  MonitorLocker ml(_lock, Mutex::_no_safepoint_check_flag);

  // Make sure we write the last partially filled buffer.
  if ((_current != NULL) && (_current->_in_used > 0)) {
    _current->_id = _next_id++;
    _to_compress.add_last(_current);
    _current = NULL;
    ml.notify_all();
  }

  // Wait for the threads to drain the compression work list and do some work yourself.
  while (!_to_compress.is_empty()) {
    do_foreground_work();
  }

  _active = false;
  ml.notify_all();
}

void CompressionBackend::thread_loop() {
  {
    MonitorLocker ml(_lock, Mutex::_no_safepoint_check_flag);
    _nr_of_threads++;
  }

  WriteWork* work;
  while ((work = get_work()) != NULL) {
    do_compress(work);
    finish_work(work);
  }

  MonitorLocker ml(_lock, Mutex::_no_safepoint_check_flag);
  _nr_of_threads--;
  assert(_nr_of_threads >= 0, "Too many threads finished");
}

void CompressionBackend::set_error(char const* new_error) {
  if ((new_error != NULL) && (_err == NULL)) {
    _err = new_error;
  }
}

WriteWork* CompressionBackend::allocate_work(size_t in_size, size_t out_size,
                                             size_t tmp_size) {
  WriteWork* result = (WriteWork*) os::malloc(sizeof(WriteWork), mtInternal);

  if (result == NULL) {
    _work_creation_failed = true;
    return NULL;
  }

  _works_created++;
  result->_in = (char*) os::malloc(in_size, mtInternal);
  result->_in_max = in_size;
  result->_in_used = 0;
  result->_out = NULL;
  result->_tmp = NULL;

  if (result->_in == NULL) {
    goto fail;
  }

  if (out_size > 0) {
    result->_out = (char*) os::malloc(out_size, mtInternal);
    result->_out_used = 0;
    result->_out_max = out_size;

    if (result->_out == NULL) {
      goto fail;
    }
  }

  if (tmp_size > 0) {
    result->_tmp = (char*) os::malloc(tmp_size, mtInternal);
    result->_tmp_max = tmp_size;

    if (result->_tmp == NULL) {
      goto fail;
    }
  }

  return result;

fail:
  free_work(result);
  _work_creation_failed = true;
  return NULL;
}

void CompressionBackend::free_work(WriteWork* work) {
  if (work != NULL) {
    os::free(work->_in);
    os::free(work->_out);
    os::free(work->_tmp);
    os::free(work);
    --_works_created;
  }
}

void CompressionBackend::free_work_list(WorkList* list) {
  while (!list->is_empty()) {
    free_work(list->remove_first());
  }
}

void CompressionBackend::do_foreground_work() {
  assert(!_to_compress.is_empty(), "Must have work to do");
  assert(_lock->owned_by_self(), "Must have the lock");

  WriteWork* work = _to_compress.remove_first();
  MutexUnlocker mu(_lock, Mutex::_no_safepoint_check_flag);
  do_compress(work);
  finish_work(work);
}

WriteWork* CompressionBackend::get_work() {
  MonitorLocker ml(_lock, Mutex::_no_safepoint_check_flag);

  while (_active && _to_compress.is_empty()) {
    ml.wait();
  }

  return _to_compress.remove_first();
}

void CompressionBackend::get_new_buffer(char** buffer, size_t* used, size_t* max) {
  if (_active) {
    MonitorLocker ml(_lock, Mutex::_no_safepoint_check_flag);

    if (*used > 0) {
      _current->_in_used += *used;

      // Check if we do not waste more than _max_waste. If yes, write the buffer.
      // Otherwise return the rest of the buffer as the new buffer.
      if (_current->_in_max - _current->_in_used <= _max_waste) {
        _current->_id = _next_id++;
        _to_compress.add_last(_current);
        _current = NULL;
        ml.notify_all();
      } else {
        *buffer = _current->_in + _current->_in_used;
        *used = 0;
        *max = _current->_in_max - _current->_in_used;

        return;
      }
    }

    while ((_current == NULL) && _unused.is_empty() && _active) {
      // Add more work objects if needed.
      if (!_work_creation_failed && (_works_created <= _nr_of_threads)) {
        WriteWork* work = allocate_work(_in_size, _out_size, _tmp_size);

        if (work != NULL) {
          _unused.add_first(work);
        }
      } else if (!_to_compress.is_empty() && (_nr_of_threads == 0)) {
        do_foreground_work();
      } else {
        ml.wait();
      }
    }

    if (_current == NULL) {
      _current = _unused.remove_first();
    }

    if (_current != NULL) {
      _current->_in_used = 0;
      _current->_out_used = 0;
      *buffer = _current->_in;
      *used = 0;
      *max = _current->_in_max;

      return;
    }
  }

  *buffer = NULL;
  *used = 0;
  *max = 0;

  return;
}

void CompressionBackend::do_compress(WriteWork* work) {
  if (_compressor != NULL) {
    char const* msg = _compressor->compress(work->_in, work->_in_used, work->_out,
                                            work->_out_max,
    work->_tmp, _tmp_size, &work->_out_used);

    if (msg != NULL) {
      MutexLocker ml(_lock, Mutex::_no_safepoint_check_flag);
      set_error(msg);
    }
  }
}

void CompressionBackend::finish_work(WriteWork* work) {
  MonitorLocker ml(_lock, Mutex::_no_safepoint_check_flag);

  _finished.add_by_id(work);

  // Write all finished works as far as we can.
  while (!_finished.is_empty() && (_finished.first()->_id == _id_to_write)) {
    WriteWork* to_write = _finished.remove_first();
    size_t size = _compressor == NULL ? to_write->_in_used : to_write->_out_used;
    char* p = _compressor == NULL ? to_write->_in : to_write->_out;
    char const* msg = NULL;

    if (_err == NULL) {
      _written += size;
      MutexUnlocker mu(_lock, Mutex::_no_safepoint_check_flag);
      msg = _writer->write_buf(p, (ssize_t) size);
    }

    set_error(msg);
    _unused.add_first(to_write);
    _id_to_write++;
  }

  ml.notify_all();
}
