/*******************************************************************************
 mod_unified_transcode.cpp

 mod_unified_transcode - An Apache module for Unified Transcode

 Copyright (C) 2016-2026 CodeShop B.V.

 For licensing see the LICENSE file
******************************************************************************/

#include <httpd.h>
#include <http_core.h>
#include <http_config.h>
#include <http_protocol.h>
#include <http_log.h>
#include <http_request.h>
#include <apr_version.h>
#include <apr_strings.h>
#include <apr_buckets.h>
#include <apr_base64.h>
#include <apr_md5.h>

#include <mod_streaming_export.h>
#include <mp4_process.h>
#include <output_bucket.h>
#include <mp4_options.h>
#include <transcode_process.h>
#include <buckets_t.h>
#include <mp4_error.h>

#include <apr_buckets_usp.h>
#include <log_util.h>

static const apr_size_t http_chunk_size = 16384;
static const char transcode_handler[] = "unified_transcode";

typedef struct
{
  mp4_global_context_t const* global_context;
  char const* license;
} server_config_t;

typedef struct
{
  int usp_handle_transcode;
  char const* usp_transcoders_file;
} dir_config_t;

static void* create_server_config(apr_pool_t* pool, server_rec* svr);
static void* merge_server_config(apr_pool_t* pool, void* basev, void* addv);
static void* create_dir_config(apr_pool_t* pool, char* dummy);
static void* merge_dir_config(apr_pool_t* pool, void* basev, void* addv);

static char const* set_usp_license_key(cmd_parms* cmd, void* cfg,
                                       char const* arg);

static char const* set_handle_transcode(cmd_parms* cmd, void* cfg,
                                        int arg);

static char const* set_transcoders_file(cmd_parms* cmd, void* cfg,
                                        char const* arg);

#if defined(__GNUC__)
// Suppress unavoidable gcc and clang warnings in command_rec initialization.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif // __GNUC__

static command_rec const transcode_cmds[] =
{
  AP_INIT_RAW_ARGS("UspLicenseKey",
    reinterpret_cast<cmd_func>(set_usp_license_key),
    NULL,
    RSRC_CONF | ACCESS_CONF,
    "Set to USP license key"),

  AP_INIT_FLAG("UspHandleTranscode",
    reinterpret_cast<cmd_func>(set_handle_transcode),
    NULL,
    ACCESS_CONF,
    "Set to 'on' to enable mod_unified_transcode functionality"),

  AP_INIT_TAKE1("UspTranscodersFile",
    reinterpret_cast<cmd_func>(set_transcoders_file),
    NULL,
    ACCESS_CONF,
    "Set URL of transcoders file"),

  { NULL }
};

#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif // __GNUC__

static void register_hooks(apr_pool_t* p);

extern "C" {

struct module_struct MOD_STREAMING_DLL_EXPORT unified_transcode_module =
{
  STANDARD20_MODULE_STUFF,
  create_dir_config,
  merge_dir_config,
  create_server_config,
  merge_server_config,
  transcode_cmds,
  register_hooks,
  AP_MODULE_FLAG_ALWAYS_MERGE
};

#if defined(APLOG_USE_MODULE)
APLOG_USE_MODULE(unified_transcode);
#endif

} // extern C definitions

static void* create_dir_config(apr_pool_t* pool, char* /* dummy */)
{
  auto* config = static_cast<dir_config_t*>(
    apr_pcalloc(pool, sizeof(dir_config_t)));

  config->usp_handle_transcode = 0;
  config->usp_transcoders_file = NULL;

  return config;
}

static void* merge_dir_config(apr_pool_t* pool, void* basev, void* addv)
{
  auto* base = static_cast<dir_config_t*>(basev);
  auto* add = static_cast<dir_config_t*>(addv);
  auto* res = static_cast<dir_config_t*>(
    apr_pcalloc(pool, sizeof(dir_config_t)));

  res->usp_handle_transcode = add->usp_handle_transcode == 0 ?
    base->usp_handle_transcode : add->usp_handle_transcode;
  res->usp_transcoders_file = add->usp_transcoders_file == NULL ?
    base->usp_transcoders_file : add->usp_transcoders_file;

  return res;
}

static void* create_server_config(apr_pool_t* pool, server_rec* /* svr */)
{
  auto* config = static_cast<server_config_t*>(
    apr_pcalloc(pool, sizeof(server_config_t)));

  config->global_context = NULL;
  config->license = NULL;

  return config;
}

static void* merge_server_config(apr_pool_t* pool, void* basev, void* addv)
{
  auto* base = static_cast<server_config_t*>(basev);
  auto* add = static_cast<server_config_t*>(addv);
  auto* res = static_cast<server_config_t*>(
    apr_pcalloc(pool, sizeof(server_config_t)));

  res->license = add->license == NULL ? base->license : add->license;

  return res;
}

static apr_status_t cleanup_libfmp4(void* data)
{
  auto const* global_context = static_cast<mp4_global_context_t const*>(data);
  libfmp4_global_exit(global_context);

  return APR_SUCCESS;
}

static apr_status_t cleanup_process_context(void* data)
{
  auto const* context = static_cast<mp4_process_context_t const*>(data);
  mp4_process_context_exit(context);

  return APR_SUCCESS;
}

static int post_config_handler(apr_pool_t* p,
                               apr_pool_t* /* plog */,
                               apr_pool_t* /* ptemp */,
                               server_rec* s)
{
  char const* src = "apache mod_unified_transcode";
  char const* version = X_MOD_SMOOTH_STREAMING_VERSION;

  mp4_global_context_t* global_context = NULL;
  server_config_t* conf = NULL;
  char const* license = NULL;
  void* tmp = NULL;
  char const* key = "mp4_global_context";

  // prevent double initialization
  apr_pool_userdata_get(&tmp, key, s->process->pool);
  if(tmp == NULL)
  {
    apr_pool_userdata_set(reinterpret_cast<void const*>(1), key,
                          apr_pool_cleanup_null, s->process->pool);
    return OK;
  }

  global_context = libfmp4_global_init();
  apr_pool_cleanup_register(p, reinterpret_cast<void const*>(global_context),
                            cleanup_libfmp4, apr_pool_cleanup_null);

  for (; s; s = s->next)
  {
    conf = static_cast<server_config_t*>(
      ap_get_module_config(s->module_config, &unified_transcode_module));

    conf->global_context = global_context; // set pointer

    if (conf->license) // find license key
    {
      license = conf->license;
    }
  }

  char const* policy_check_result =
    libfmp4_load_license(global_context, src, version, license);

  if(policy_check_result != NULL)
  {
    ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, "%s", policy_check_result);
  }
  else if(license)
  {
    ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, s,
                 "License key found: %s", license);
  }

  return OK;
}

static void log_error(request_rec* request, fmp4_log_level_t level,
                      char const* message, size_t size)
{
  int log_level = fmp4_log_level_to_apache_log_level(level);
  int clamped_size = size < INT_MAX ? static_cast<int>(size) : INT_MAX;
  ap_log_rerror(APLOG_MARK, log_level, 0, request, "%.*s", clamped_size,
                message);
}

static void log_error(request_rec* request, fmp4_log_level_t level,
                      char const* message)
{
  log_error(request, level, message, strlen(message));
}

static void log_error_callback(void* context, fmp4_log_level_t level,
                               char const* message, size_t size)
{
  auto* request = static_cast<request_rec*>(context);
  log_error(request, level, message, size);
}

static void log_error_printf(request_rec* request,
                             fmp4_log_level_t level,
                             char const* fmt, ...)
{
  va_list args;
  va_start(args, fmt);
  char* message = apr_pvsprintf(request->pool, fmt, args);
  va_end(args);

  log_error(request, level, message);
}
  
static void log_error_status(request_rec* request,
                             fmp4_log_level_t level,
                             char const* function,
                             apr_status_t status)
{
  char description[256];
  apr_strerror(status, description, sizeof description);
  log_error_printf(request, level, "%s returned %s", function, description);
}
  
static int translate_handler(request_rec* r)
{
  auto* conf = static_cast<dir_config_t*>(
    ap_get_module_config(r->per_dir_config, &unified_transcode_module));

  if(conf->usp_handle_transcode == 1)
  {
    r->handler = transcode_handler;
    r->filename = NULL;
    return OK;
  }

  return DECLINED;
}

static int map_to_storage_handler(request_rec* r)
{
  if(r->handler == NULL || strcmp(r->handler, transcode_handler) != 0)
  {
    return DECLINED;
  }

  // Avoid complaints by Apache's default map_to_storage handler
  // ("Module bug?") triggered by setting r->filename to NULL in our
  // translate handler.
  return OK;
}

static char const* get_pipeline_config(request_rec* r)
{
  // The pipeline config is in the last segment of the URI.
  // An optional ".mp4" at the end of the last segment is stripped.
  char const* begin_config = r->uri;
  char const* last_dot = NULL;

  char const* p;
  for(p = r->uri; *p != '\0'; ++p)
  {
    switch(*p)
    {
    case '/' :
      begin_config = p + 1;
      last_dot = NULL;
      break;
    case '.' :
      last_dot = p;
      break;
    default :
      break;
    }
  }

  char const* end_config = p;
  if(last_dot != NULL && strcmp(last_dot, ".mp4") == 0)
  {
    end_config = last_dot;
  }

  return begin_config == end_config ? "" :
    apr_pstrmemdup(r->pool,
      begin_config, (apr_size_t)(end_config - begin_config));
}

typedef struct 
{
  request_rec *request;
  apr_bucket_brigade* input_brigade;
} transcode_reader_context_t;

static void transcode_reader_context_init(
  transcode_reader_context_t* context,
  request_rec* request,
  apr_bucket_brigade* input_brigade)
{
  context->request = request;
  context->input_brigade = input_brigade;
}
  
static int transcode_reader(void *arg, void* buf, int max_bytes)
{
  auto* context = static_cast<transcode_reader_context_t*>(arg);
  if(max_bytes <= 0)
  {
    log_error(context->request, FMP4_LOG_ERROR,
      "illegal max_bytes value passed to transcode_reader");
    return -1;
  }
  apr_size_t max_len = max_bytes;
    
  for(;;)
  {
    while(APR_BRIGADE_EMPTY(context->input_brigade))
    {
      // Get next brigade
      apr_status_t rv = ap_get_brigade(
        context->request->input_filters, context->input_brigade,
        AP_MODE_READBYTES, APR_BLOCK_READ, http_chunk_size);
      if(rv != APR_SUCCESS)
      {
        log_error_status(context->request, FMP4_LOG_ERROR,
          "ap_get_brigade", rv);
        return -1;
      }
    }

    apr_bucket* bucket = APR_BRIGADE_FIRST(context->input_brigade);
    if(APR_BUCKET_IS_EOS(bucket))
    {
      // At end of stream; keep final bucket
      return 0;
    }

    const char* bucket_data = NULL;
    apr_size_t bucket_len = 0;
    if(!APR_BUCKET_IS_METADATA(bucket))
    {
      apr_status_t rv = apr_bucket_read(bucket,
        &bucket_data, &bucket_len, APR_BLOCK_READ);
      if(rv != APR_SUCCESS)
      {
        log_error_status(context->request, FMP4_LOG_ERROR,
          "apr_bucket_read", rv);
        return -1;
      }
    }

    if(bucket_len > max_len)
    {
      // Return and delete partial bucket
      memcpy(buf, bucket_data, max_len);
      apr_bucket_split(bucket, max_len);
      apr_bucket_delete(bucket);
      return (int) max_len;
    }
    if(bucket_len > 0)
    {
      // Return and delete full bucket
      memcpy(buf, bucket_data, bucket_len);
      apr_bucket_delete(bucket);
      return (int) bucket_len;
    }

    // No data here, to next bucket
    apr_bucket_delete(bucket);
  }
}

typedef struct 
{
  request_rec* request;
  apr_bucket_brigade* output_brigade;
  apr_size_t chunk_space_left;
  int did_flush;
} transcode_writer_context_t;

static void transcode_writer_context_init(
  transcode_writer_context_t* context,
  request_rec* request,
  apr_bucket_brigade* output_brigade)
{
  context->request = request;
  context->output_brigade = output_brigade;
  context->chunk_space_left = http_chunk_size;
  context->did_flush = 0;
}

static void writer_log_error(void* context, fmp4_result result,
                             char const* error)
{
  request_rec* request = static_cast<request_rec*>(context);
  log_error_printf(request, FMP4_LOG_ERROR, "bucket_read error: %s %s",
                   fmp4_result_to_string(result), error);
}

static int transcode_writer(void* arg, buckets_t* output)
{
  auto* context = static_cast<transcode_writer_context_t*>(arg);

  bucket_t* head = output->bucket_;
  bucket_t* bucket;
  for(bucket = bucket_next(head); bucket != head; bucket = bucket_next(head))
  {
    uint8_t const* ignored;
    size_t bucket_size;
    fmp4_result result = bucket_read(bucket, &ignored, &bucket_size,
                                     writer_log_error, context->request);
    if(result != FMP4_OK)
    {
      return -1;
    }
    if(bucket_size == 0)
    {
      // Nothing to see here. Carry on.
      bucket_delete(bucket);
      continue;
    }

    if(bucket_size > context->chunk_space_left)
    {
      // Split bucket to fit
      result = bucket_split(bucket, context->chunk_space_left);
      if(result != FMP4_OK)
      {
        log_error_printf(context->request, FMP4_LOG_ERROR,
          "bucket_split returned: %s",
          fmp4_result_to_string(result));
        return -1;
      }
      bucket_size = context->chunk_space_left; 
    }

    bucket_remove(bucket);

    // Convert usp bucket to apache bucket and append to output brigade
    apr_bucket* apache_bucket = apr_bucket_usp_create(
       bucket, context->output_brigade->bucket_alloc, context->request);
    APR_BRIGADE_INSERT_TAIL(context->output_brigade, apache_bucket);
    context->chunk_space_left -= bucket_size;
      
    if(context->chunk_space_left == 0)
    {
      // Flush output brigade
      apr_bucket* flush_bucket = apr_bucket_flush_create(
        context->output_brigade->bucket_alloc);
      APR_BRIGADE_INSERT_TAIL(context->output_brigade, flush_bucket);
      apr_status_t rv = ap_pass_brigade(
        context->request->output_filters, context->output_brigade);
      if(rv != APR_SUCCESS)
      {
        log_error_status(context->request, FMP4_LOG_ERROR,
          "ap_pass_brigade", rv);
        return -1;
      }
      apr_brigade_cleanup(context->output_brigade);
      context->chunk_space_left = http_chunk_size;
      context->did_flush = 1;
    }
  }

  return 0;
}

static int request_handler(request_rec* r)
{
  if(r->handler == NULL || strcmp(r->handler, transcode_handler) != 0)
  {
    return DECLINED;
  }

  // Set module version info
  apr_table_setn(r->headers_out, "X-TRANSCODE", fmp4_version_string());

  if(r->header_only)
  {
    return OK;
  }

  if(r->method_number != M_POST)
  {
    return HTTP_METHOD_NOT_ALLOWED;
  }

  auto* server_conf = static_cast<server_config_t*>(
    ap_get_module_config(r->server->module_config, &unified_transcode_module));
  auto* dir_conf = static_cast<dir_config_t*>(
    ap_get_module_config(r->per_dir_config, &unified_transcode_module));

  // Set up process context
  mp4_process_context_t* context = mp4_process_context_init(server_conf->global_context);
  apr_pool_cleanup_register(
    r->pool, context, cleanup_process_context, apr_pool_cleanup_null);

  mp4_process_context_set_log_error_callback(context, log_error_callback, r);
  mp4_process_context_set_verbose(context,
    apache_log_level_to_fmp4_log_level(
      ap_get_request_module_loglevel(r, APLOG_MODULE_INDEX)));
  mp4_process_context_set_transcoders_file(context,
    dir_conf->usp_transcoders_file);
  auto const* pipeline_config = get_pipeline_config(r);

  // parse remaining url queries
  if(r->args && !mp4_options_set(mp4_process_context_get_options(context),
                                 r->args, strlen(r->args)))
  {
    return HTTP_BAD_REQUEST;
  }

  apr_bucket_brigade* input_brigade =
    apr_brigade_create(r->pool, r->connection->bucket_alloc);
  transcode_reader_context_t reader_context;
  transcode_reader_context_init(&reader_context, r, input_brigade);

  apr_bucket_brigade* output_brigade =
    apr_brigade_create(r->pool, r->connection->bucket_alloc);
  transcode_writer_context_t writer_context;
  transcode_writer_context_init(&writer_context, r, output_brigade);

  char const* function_name = "transcode_process_streaming";
  int http_status =
    transcode_process_streaming(context, pipeline_config,
      transcode_reader, &reader_context,
      transcode_writer, &writer_context);

  fmp4_result result = mp4_process_context_get_result(context);
  if(result != FMP4_OK)
  {
    char const* result_text = mp4_process_context_get_result_text(context);
    log_error_printf(r, FMP4_LOG_ERROR, 
      "%s(%s) returned: %s %s",
      function_name,
      r->uri,
      fmp4_result_to_string(result),
      result_text);
    r->status = http_status;

    if(writer_context.did_flush)
    {
      /* Assuming the status line has been sent, we force a protocol
       * error to ensure the client at least notices *some* kind
       * of failure.
       * See modules/http/chunk_filter.c in the Apache source.
       */
     log_error(r, FMP4_LOG_ERROR, "forcing protocol error");
     APR_BRIGADE_INSERT_TAIL(output_brigade, ap_bucket_error_create(
        HTTP_BAD_GATEWAY, NULL, r->pool, output_brigade->bucket_alloc));
    }
  }
       
  // add eos bucket
  APR_BRIGADE_INSERT_TAIL(output_brigade, apr_bucket_eos_create(
    output_brigade->bucket_alloc));

  // pass on to any output filters
  apr_status_t rv = ap_pass_brigade(r->output_filters, output_brigade);
  if(rv != APR_SUCCESS)
  {
    log_error_status(r, FMP4_LOG_ERROR, "ap_pass_brigade", rv);
    return AP_FILTER_ERROR;
  }

  return OK;
}

static char const* set_usp_license_key(cmd_parms* cmd,
                                       void* /* cfg */,
                                       char const* arg)
{
  server_rec* s = cmd->server;
  auto* conf = static_cast<server_config_t*>(
    ap_get_module_config(s->module_config, &unified_transcode_module));

  conf->license = arg;

  return NULL;
}

static char const* set_handle_transcode(cmd_parms* cmd, void *cfg,
                                        int arg)
{
  char const* error =
    ap_check_cmd_context(cmd, NOT_IN_DIRECTORY | NOT_IN_FILES);
  if(error != NULL)
  {
    return error;
  }

  auto* dir_config = static_cast<dir_config_t*>(cfg);
  dir_config->usp_handle_transcode = arg;
  
  return NULL;
}

static char const* set_transcoders_file(cmd_parms* cmd, void *cfg,
                                        char const* arg)
{
  char const* error =
    ap_check_cmd_context(cmd, NOT_IN_DIRECTORY | NOT_IN_FILES);
  if(error != NULL)
  {
    return error;
  }

#if 0
  if(!mp4_is_absolute(arg))
  {
    return "UspTranscodersFile requires an absolute URI";
  }
#endif

  auto* dir_config = static_cast<dir_config_t*>(cfg);
  dir_config->usp_transcoders_file = arg;
  
  return NULL;
}

static void register_hooks(apr_pool_t* /* p */)
{
  /* module initializer */
  ap_hook_post_config(post_config_handler, NULL, NULL, APR_HOOK_MIDDLE);
  ap_hook_translate_name(translate_handler, NULL, NULL, APR_HOOK_MIDDLE);
  ap_hook_map_to_storage(map_to_storage_handler, NULL, NULL, APR_HOOK_MIDDLE);
  ap_hook_handler(request_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

// End Of File

